2022-11-03 09:04:29 +01:00
# Building Custom Docker Image
2022-10-21 16:23:23 +03:00
2025-10-13 16:22:11 +02:00
[FrankenPHP Docker images ](https://hub.docker.com/r/dunglas/frankenphp ) are based on [official PHP images ](https://hub.docker.com/_/php/ ).
Debian and Alpine Linux variants are provided for popular architectures.
Debian variants are recommended.
2024-02-27 17:21:48 +01:00
2025-11-21 16:54:07 +04:00
Variants for PHP 8.2, 8.3, 8.4 and 8.5 are provided.
2024-05-13 14:52:13 +02:00
2024-07-27 02:07:52 +04:00
The tags follow this pattern: `dunglas/frankenphp:<frankenphp-version>-php<php-version>-<os>`
2024-05-13 14:52:13 +02:00
2025-04-22 17:16:57 +02:00
- `<frankenphp-version>` and `<php-version>` are version numbers of FrankenPHP and PHP respectively, ranging from major (e.g. `1` ), minor (e.g. `1.2` ) to patch versions (e.g. `1.2.3` ).
2025-08-27 08:32:05 +02:00
- `<os>` is either `trixie` (for Debian Trixie), `bookworm` (for Debian Bookworm), or `alpine` (for the latest stable version of Alpine).
2024-05-13 14:52:13 +02:00
[Browse tags ](https://hub.docker.com/r/dunglas/frankenphp/tags ).
2022-10-21 16:23:23 +03:00
2022-11-03 09:04:29 +01:00
## How to Use The Images
Create a `Dockerfile` in your project:
2023-09-20 14:15:41 +02:00
``` dockerfile
2022-11-03 09:04:29 +01:00
FROM dunglas/frankenphp
COPY . /app/public
2022-10-21 16:23:23 +03:00
```
2024-02-27 17:21:48 +01:00
Then, run these commands to build and run the Docker image:
2022-10-21 16:23:23 +03:00
2023-09-20 14:15:41 +02:00
``` console
2023-12-01 17:26:21 +01:00
docker build -t my-php-app .
docker run -it --rm --name my-running-app my-php-app
2022-10-21 16:23:23 +03:00
```
2025-10-13 16:22:11 +02:00
## How to Tweak the Configuration
For convenience, [a default `Caddyfile` ](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile ) containing
useful environment variables is provided in the image.
2022-11-03 09:04:29 +01:00
## How to Install More PHP Extensions
The [`docker-php-extension-installer` ](https://github.com/mlocati/docker-php-extension-installer ) script is provided in the base image.
2023-11-07 05:13:15 +08:00
Adding additional PHP extensions is straightforward:
2022-10-21 16:23:23 +03:00
2022-11-03 09:04:29 +01:00
``` dockerfile
FROM dunglas/frankenphp
# add additional extensions here:
RUN install-php-extensions \
2024-01-29 14:40:56 +01:00
pdo_mysql \
gd \
intl \
zip \
opcache
2022-10-21 16:23:23 +03:00
```
2022-11-03 09:04:29 +01:00
2023-11-01 00:06:52 +01:00
## How to Install More Caddy Modules
FrankenPHP is built on top of Caddy, and all [Caddy modules ](https://caddyserver.com/docs/modules/ ) can be used with FrankenPHP.
The easiest way to install custom Caddy modules is to use [xcaddy ](https://github.com/caddyserver/xcaddy ):
``` dockerfile
2024-10-28 11:41:36 +01:00
FROM dunglas/frankenphp:builder AS builder
2023-11-01 00:06:52 +01:00
# Copy xcaddy in the builder image
COPY --from= caddy:builder /usr/bin/xcaddy /usr/bin/xcaddy
# CGO must be enabled to build FrankenPHP
2024-11-18 13:45:11 +01:00
RUN CGO_ENABLED = 1 \
XCADDY_SETCAP = 1 \
XCADDY_GO_BUILD_FLAGS = "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
CGO_CFLAGS = $( php-config --includes) \
CGO_LDFLAGS = " $( php-config --ldflags) $( php-config --libs) " \
xcaddy build \
--output /usr/local/bin/frankenphp \
--with github.com/dunglas/frankenphp= ./ \
--with github.com/dunglas/frankenphp/caddy= ./caddy/ \
--with github.com/dunglas/caddy-cbrotli \
# Mercure and Vulcain are included in the official build, but feel free to remove them
--with github.com/dunglas/mercure/caddy \
--with github.com/dunglas/vulcain/caddy
# Add extra Caddy modules here
2023-11-01 00:06:52 +01:00
FROM dunglas/frankenphp AS runner
# Replace the official binary by the one contained your custom modules
COPY --from= builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
```
2024-02-27 17:21:48 +01:00
The `builder` image provided by FrankenPHP contains a compiled version of `libphp` .
[Builders images ](https://hub.docker.com/r/dunglas/frankenphp/tags?name=builder ) are provided for all versions of FrankenPHP and PHP, both for Debian and Alpine.
2023-11-01 00:06:52 +01:00
2023-12-14 22:45:09 +01:00
> [!TIP]
>
> If you're using Alpine Linux and Symfony,
> you may need to [increase the default stack size](compile.md#using-xcaddy).
2023-12-01 17:26:21 +01:00
## Enabling the Worker Mode by Default
2022-11-03 09:04:29 +01:00
Set the `FRANKENPHP_CONFIG` environment variable to start FrankenPHP with a worker script:
2023-09-20 14:15:41 +02:00
``` dockerfile
2022-11-03 09:04:29 +01:00
FROM dunglas/frankenphp
# ...
ENV FRANKENPHP_CONFIG = "worker ./public/index.php"
2022-10-21 16:23:23 +03:00
```
2023-12-01 17:26:21 +01:00
## Using a Volume in Development
2022-11-03 09:04:29 +01:00
To develop easily with FrankenPHP, mount the directory from your host containing the source code of the app as a volume in the Docker container:
2022-10-21 16:23:23 +03:00
2023-09-20 14:15:41 +02:00
``` console
2024-01-20 11:49:16 +01:00
docker run -v $PWD:/app/public -p 80:80 -p 443:443 -p 443:443/udp --tty my-php-app
2022-10-21 16:23:23 +03:00
```
2024-09-23 22:31:04 +02:00
> [!TIP]
2024-01-20 11:49:16 +01:00
>
> The `--tty` option allows to have nice human-readable logs instead of JSON logs.
2022-11-03 09:04:29 +01:00
With Docker Compose:
2022-10-21 16:23:23 +03:00
2022-11-03 09:04:29 +01:00
``` yaml
2023-11-30 08:07:39 +01:00
# compose.yaml
2022-11-03 09:04:29 +01:00
services :
php :
image : dunglas/frankenphp
# uncomment the following line if you want to use a custom Dockerfile
#build: .
2022-11-22 17:41:13 +01:00
# uncomment the following line if you want to run this in a production environment
# restart: always
2022-11-03 09:04:29 +01:00
ports :
2024-01-20 11:49:16 +01:00
- "80:80" # HTTP
- "443:443" # HTTPS
- "443:443/udp" # HTTP/3
2022-11-03 09:04:29 +01:00
volumes :
- ./:/app/public
2023-10-30 22:16:09 +01:00
- caddy_data:/data
- caddy_config:/config
# comment the following line in production, it allows to have nice human-readable logs in dev
tty : true
# Volumes needed for Caddy certificates and configuration
volumes :
caddy_data :
caddy_config :
2022-11-03 09:04:29 +01:00
```
2024-01-29 14:40:56 +01:00
## Running as a Non-Root User
2024-03-04 16:36:40 -05:00
FrankenPHP can run as non-root user in Docker.
2024-01-29 14:40:56 +01:00
Here is a sample `Dockerfile` doing this:
``` dockerfile
FROM dunglas/frankenphp
2024-12-13 19:41:56 -05:00
ARG USER = appuser
2024-01-29 14:40:56 +01:00
2024-02-11 16:03:08 +05:30
RUN \
# Use "adduser -D ${USER}" for alpine based distros
2024-12-13 19:41:56 -05:00
useradd ${ USER } ; \
2024-02-11 16:03:08 +05:30
# Add additional capability to bind to port 80 and 443
setcap CAP_NET_BIND_SERVICE = +eip /usr/local/bin/frankenphp; \
2025-08-12 08:36:46 +01:00
# Give write access to /config/caddy and /data/caddy
chown -R ${ USER } :${ USER } /config/caddy /data/caddy
2024-02-11 16:03:08 +05:30
USER ${USER }
2024-01-29 14:40:56 +01:00
```
2024-01-30 18:32:47 +01:00
2024-05-13 10:42:39 +02:00
### Running With No Capabilities
Even when running rootless, FrankenPHP needs the `CAP_NET_BIND_SERVICE` capability to bind the
web server on privileged ports (80 and 443).
If you expose FrankenPHP on a non-privileged port (1024 and above), it's possible to run
the webserver as a non-root user, and without the need for any capability:
``` dockerfile
FROM dunglas/frankenphp
2024-12-13 19:41:56 -05:00
ARG USER = appuser
2024-05-13 10:42:39 +02:00
RUN \
# Use "adduser -D ${USER}" for alpine based distros
2024-12-13 19:41:56 -05:00
useradd ${ USER } ; \
2024-05-13 10:42:39 +02:00
# Remove default capability
setcap -r /usr/local/bin/frankenphp; \
2025-08-12 08:36:46 +01:00
# Give write access to /config/caddy and /data/caddy
chown -R ${ USER } :${ USER } /config/caddy /data/caddy
2024-05-13 10:42:39 +02:00
USER ${USER }
```
2024-07-27 02:07:52 +04:00
Next, set the `SERVER_NAME` environment variable to use an unprivileged port.
2024-05-13 10:42:39 +02:00
Example: `:8000`
2024-01-30 18:32:47 +01:00
## Updates
The Docker images are built:
2025-04-22 17:16:57 +02:00
- when a new release is tagged
- daily at 4 am UTC, if new versions of the official PHP images are available
2024-01-30 18:32:47 +01:00
2026-01-26 18:15:12 +01:00
## Hardening Images
To further reduce the attack surface and size of your FrankenPHP Docker images, it's also possible to build them on top of a
[Google distroless ](https://github.com/GoogleContainerTools/distroless ) or
[Docker hardened ](https://www.docker.com/products/hardened-images ) image.
> [!WARNING]
> These minimal base images do not include a shell or package manager, which makes debugging more difficult.
> They are therefore recommended only for production if security is a high priority.
When adding additional PHP extensions, you will need an intermediate build stage:
``` dockerfile
FROM dunglas/frankenphp AS builder
# Add additional PHP extensions here
RUN install-php-extensions pdo_mysql pdo_pgsql #...
# Copy shared libs of frankenphp and all installed extensions to temporary location
# You can also do this step manually by analyzing ldd output of frankenphp binary and each extension .so file
RUN apt-get update && apt-get install -y libtree && \
EXT_DIR = " $( php -r 'echo ini_get("extension_dir");' ) " && \
FRANKENPHP_BIN = " $( which frankenphp) " ; \
LIBS_TMP_DIR = "/tmp/libs" ; \
mkdir -p " $LIBS_TMP_DIR " ; \
for target in " $FRANKENPHP_BIN " $( find " $EXT_DIR " -maxdepth 2 -type f -name "*.so" ) ; do \
libtree -pv " $target " | sed 's/.*── \(.*\) \[.*/\1/' | grep -v " ^ $target " | while IFS = read -r lib; do \
[ -z " $lib " ] && continue ; \
base = $( basename " $lib " ) ; \
destfile = " $LIBS_TMP_DIR / $base " ; \
if [ ! -f " $destfile " ] ; then \
cp " $lib " " $destfile " ; \
fi ; \
done ; \
done
# Distroless debian base image, make sure this is the same debian version as the base image
FROM gcr.io/distroless/base-debian13
# Docker hardened image alternative
# FROM dhi.io/debian:13
# Location of your app and Caddyfile to be copied into the container
ARG PATH_TO_APP = "."
ARG PATH_TO_CADDYFILE = "./Caddyfile"
# Copy your app into /app
# For further hardening make sure only writable paths are owned by the nonroot user
COPY --chown= nonroot:nonroot " $PATH_TO_APP " /app
COPY " $PATH_TO_CADDYFILE " /etc/caddy/Caddyfile
# Copy frankenphp and necessary libs
COPY --from= builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp
2026-03-02 08:46:59 +01:00
COPY --from= builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
2026-01-26 18:15:12 +01:00
COPY --from= builder /tmp/libs /usr/lib
# Copy php.ini configuration files
COPY --from= builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from= builder /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini
2026-03-02 08:46:59 +01:00
# Caddy data dirs — must be writable for nonroot, even on a read-only root filesystem
ENV XDG_CONFIG_HOME = /config \
XDG_DATA_HOME = /data
2026-01-26 18:15:12 +01:00
COPY --from= builder --chown= nonroot:nonroot /data/caddy /data/caddy
COPY --from= builder --chown= nonroot:nonroot /config/caddy /config/caddy
USER nonroot
WORKDIR /app
# entrypoint to run frankenphp with the provided Caddyfile
ENTRYPOINT [ "/usr/local/bin/frankenphp" , "run" , "-c" , "/etc/caddy/Caddyfile" ]
```
2024-01-30 18:32:47 +01:00
## Development Versions
Development versions are available in the [`dunglas/frankenphp-dev` ](https://hub.docker.com/repository/docker/dunglas/frankenphp-dev ) Docker repository.
A new build is triggered every time a commit is pushed to the main branch of the GitHub repository.
The `latest*` tags point to the head of the `main` branch.
Tags of the form `sha-<git-commit-hash>` are also available.