Running Apache httpd in Docker with the official httpd image
The Docker Official Image httpd contains the Apache HTTP Server built from upstream source with the upstream default configuration, and nothing else: no PHP, no Debian-style a2enmod tools. As of November 2026 it ships httpd 2.4.69 on Debian 13 (trixie) or Alpine 3.24. You add your content and your httpd.conf, and Docker takes care of the rest.
Tags and variants
| Tags | Base | Use it when |
|---|---|---|
2.4.69, 2.4, 2, latest, 2.4-trixie, trixie | Debian 13 slim (glibc) | The default choice, and the easiest base if you need extra packages via apt-get. |
2.4.69-alpine, 2.4-alpine, alpine, 2.4-alpine3.24 | Alpine Linux 3.24 (musl libc) | Smallest image; extra tools are installed with apk, and software that assumes glibc may misbehave. |
Use httpd:2.4 to pick up new 2.4.x releases (including security fixes) whenever you rebuild, or pin httpd:2.4.69 when you want rebuilds to be exactly repeatable, and then bump it on purpose. Avoid latest in production: it is the same image today but says nothing about intent. The image is published for amd64, arm64v8, arm32v5/v6/v7, i386, ppc64le, riscv64 and s390x.
Quick start
Serve the current directory on port 8080 of your machine, read-only:
docker run -d --name web -p 8080:80 \
-v "$PWD":/usr/local/apache2/htdocs/:ro httpd:2.4
curl -I http://localhost:8080/
For something you deploy, copy the files into your own image instead:
# Dockerfile
FROM httpd:2.4
COPY ./public-html/ /usr/local/apache2/htdocs/
docker build -t my-apache2 .
docker run -d --name web -p 8080:80 my-apache2
Where things live
The image installs httpd under /usr/local/apache2 (the HTTPD_PREFIX environment variable), the layout of a source build, not the Debian or Red Hat package layout:
conf/httpd.conf- The single main configuration file, with optional pieces in
conf/extra/(for examplehttpd-ssl.conf,httpd-vhosts.conf). htdocs/- The default
DocumentRoot. modules/- Every module httpd ships, built as loadable modules (
--enable-mods-shared=reallyall). Most are commented out inhttpd.conf: enable one by uncommenting itsLoadModuleline. bin/httpd,apachectl,htpasswdand friends, already on thePATH.logs/- Holds the PID file (and the mod_cgid socket if you enable CGI); the logs themselves go to the console.
Child processes run as www-data (UID 33 on the Debian variant, 82 on Alpine). The container starts the script httpd-foreground, which removes a stale PID file and runs httpd -DFOREGROUND, so httpd is the container's main process.
Your own httpd.conf
Start from the image's own file so you keep the module list that matches the build:
docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf
Edit it (at minimum set ServerName, which silences the AH00558 warning at startup), then test it inside a throwaway container before you build:
docker run --rm -v "$PWD/my-httpd.conf":/usr/local/apache2/conf/httpd.conf:ro \
httpd:2.4 httpd -t
# Syntax OK
# Dockerfile
FROM httpd:2.4
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./public-html/ /usr/local/apache2/htdocs/
To keep site files separate, add a line such as IncludeOptional conf/sites/*.conf at the end of my-httpd.conf and copy or mount your virtual host files into /usr/local/apache2/conf/sites/. IncludeOptional does not fail when the directory is empty. The configuration basics guide explains the directives themselves.
If you bind-mount the configuration, apply changes without restarting the container:
docker exec web httpd -t && docker exec web httpd -k graceful
Logs
When the image is built, ErrorLog is pointed at /proc/self/fd/2 and CustomLog at /proc/self/fd/1, so the error log goes to the container's standard error and the access log to standard output:
docker logs -f web
Keep it that way and let Docker's logging driver (or your cluster's log collector) handle storage and rotation. Writing log files inside the container fills its writable layer, and the files vanish with the container. Note that the default httpd.conf uses the short common log format; switch the CustomLog line to combined if you want referrer and user agent, and see logging for the fields.
Stopping and reloading
The image sets STOPSIGNAL SIGWINCH. For httpd, WINCH means graceful stop: children finish the request they are serving and then exit. So docker stop web lets in-flight requests complete. Docker sends SIGKILL if the container has not exited when the stop timeout runs out, so long downloads may still be cut; raise the timeout with docker stop -t 30 web if needed.
For a graceful restart (re-read configuration, reopen logs), use docker exec web httpd -k graceful as shown above; it sends httpd the USR1 signal.
Health checks
The image defines no HEALTHCHECK, and the Debian variant contains neither curl nor wget. Add curl in your own image and check a cheap URL:
FROM httpd:2.4
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./public-html/ /usr/local/apache2/htdocs/
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS -o /dev/null http://127.0.0.1/ || exit 1
curl -f returns non-zero on HTTP errors, and Docker marks the container unhealthy after three failures in a row. Check the result with docker inspect --format '{{.State.Health.Status}}' web. On the Alpine variant install curl with apk add --no-cache curl instead.
Running as non-root
By default the httpd parent process runs as root inside the container, so it can bind port 80, and only the children drop to www-data. That is the normal httpd model. If your platform requires containers to run as an unprivileged user from the start, three things must change: listen on a port above 1023, make logs/ writable for the PID file, and set USER.
sed -i 's/^Listen 80$/Listen 8080/' my-httpd.conf
# Dockerfile
FROM httpd:2.4
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
COPY ./public-html/ /usr/local/apache2/htdocs/
RUN chown www-data:www-data /usr/local/apache2/logs
USER www-data
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD curl -fsS -o /dev/null http://127.0.0.1:8080/ || exit 1
The User and Group lines in httpd.conf can stay: when httpd is not started as root it simply keeps running as the user that started it. If you forget the port change, startup fails with (13)Permission denied: AH00072: make_sock: could not bind to address [::]:80 and the same line for 0.0.0.0:80, followed by no listening sockets available, shutting down.
Now the container can also run with a read-only root filesystem and no Linux capabilities. The writable logs/ directory becomes a small tmpfs owned by UID 33:
docker run -d --name web -p 8080:8080 \
--read-only --tmpfs /usr/local/apache2/logs:rw,uid=33,gid=33 \
--cap-drop ALL --security-opt no-new-privileges=true \
my-apache2
docker top web # every httpd process should show www-data
The same settings in Compose:
services:
web:
build: .
ports:
- "8080:8080"
read_only: true
tmpfs:
- /usr/local/apache2/logs:uid=33,gid=33
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD", "curl", "-fsS", "-o", "/dev/null", "http://127.0.0.1:8080/"]
interval: 30s
timeout: 3s
retries: 3
HTTPS and PHP
For HTTPS inside the container, the image documentation's route is to copy or mount server.crt and server.key into /usr/local/apache2/conf/, uncomment the LoadModule lines for mod_socache_shmcb and mod_ssl and the Include conf/extra/httpd-ssl.conf line in httpd.conf, and publish port 443 (-p 443:443). It recommends adapting the configuration further for production; see HTTPS and TLS. In many deployments TLS ends at a reverse proxy or load balancer in front of the container instead, and httpd only speaks plain HTTP on the internal network (reverse proxy).
The httpd image has no PHP. The image documentation points to the php image's -apache tags for that, or you can run PHP-FPM in its own container and connect it with mod_proxy_fcgi.
Kubernetes
The same image works in Kubernetes. Build your configuration and content into the image (or mount the configuration from a ConfigMap), run as the non-root variant above, and give the pod probes and a security context. A container fragment for your Deployment's pod template:
containers:
- name: web
image: registry.example.com/my-apache2:1.0
ports:
- containerPort: 8080
securityContext:
runAsUser: 33
runAsGroup: 33
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 20
volumeMounts:
- name: httpd-run
mountPath: /usr/local/apache2/logs
volumes:
- name: httpd-run
emptyDir: {}
For an httpGet probe, the kubelet treats any status from 200 to 399 as success; a container that fails its readiness probe gets no traffic through Kubernetes Services, and the liveness probe restarts the container. Point both at a cheap static URL, not at a page that queries a database. If an Ingress controller already terminates TLS, the httpd container needs no certificates at all.
Sources
- httpd, Docker Official Image, Docker Hub
- httpd image documentation, docker-library/docs on GitHub
- httpd 2.4 Dockerfile (Debian variant), docker-library/httpd
- httpd 2.4 Dockerfile (Alpine variant), docker-library/httpd
- httpd:2.4 image metadata and package list, docker-library/repo-info
- Stopping and restarting Apache HTTP Server (WINCH, USR1)
- mod_unixd (User, Group), Apache HTTP Server 2.4 documentation
- MPM common directives (PidFile), Apache HTTP Server 2.4 documentation
- Dockerfile reference (HEALTHCHECK, STOPSIGNAL), Docker Docs
- docker container run reference, Docker Docs
- docker container stop reference, Docker Docs
- Running containers (capabilities), Docker Docs
- tmpfs mounts, Docker Docs
- Compose file reference: services, Docker Docs
- Configure liveness, readiness and startup probes, Kubernetes documentation
- Configure a security context for a pod or container, Kubernetes documentation