Building a Laravel Application Container with FrankenPHP
What is FrankenPHP? #
FrankenPHP is a PHP server that is actually a module of the Caddy web server. Since Caddy is written in Go, FrankenPHP enables a self-contained executable feature, where your Laravel application is wrapped into a single binary to streamline the deployment process.
Preparation #
Make sure your Laravel project is ready. In this post, I’m using this blog which is built with Laravel, with the note that the frontend assets in this blog project are built using Bun.
Creating a Multi-Stage Container File #
FROM docker.io/oven/bun:1 AS asset-builder
COPY package.json bun.lockb /app/
WORKDIR /app
RUN bun install
COPY . .
RUN bun run build
FROM docker.io/serversideup/php:8.4-cli as vendor
COPY --chown=www-data:www-data . /var/www/html
RUN composer install --no-interaction --optimize-autoloader --ignore-platform-reqs --no-dev
FROM docker.io/dunglas/frankenphp:static-builder-musl-1.7.0 AS builder
ENV NO_COMPRESS=1
# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .
# Remove the tests and other unneeded files to save space
# Alternatively, add these files to a .dockerignore file
RUN rm -Rf tests/
COPY --from=vendor /var/www/html /go/src/app/dist/app
COPY --from=asset-builder /app/public/build /go/src/app/dist/app/public/build
# Build the static binary, be sure to select only the PHP extensions you want
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh
FROM gcr.io/distroless/static-debian12
ARG build=dev
ENV BUILD $build
WORKDIR /app
COPY --from=builder /go/src/app/dist/frankenphp-linux-x86_64 blog
ENTRYPOINT ["/app/blog", "php-server"]Explanation of the Multi-Stage Build #
Stage 1: Asset Builder #
This stage uses Bun to build frontend assets (CSS, JavaScript):
- Install dependencies with
bun install - Build assets with
bun run build - The build results will be copied to the next stage
Stage 2: PHP Dependencies #
This stage contains Composer dependency installation, using the base image from serversideup:
- Install Composer dependencies with production optimization
- The
--no-devflag to skip development dependencies - The
--optimize-autoloaderflag for better performance
Stage 3: Static Binary Build #
Creating a static binary with FrankenPHP:
- Using the base image
dunglas/frankenphp:static-builder-musl-1.7.0 - Combining source code, Composer dependencies, and frontend build assets
- Activating the
NO_COMPRESSvariable to avoid the binary compression process as this will increase the container image build time - Running the
build-static.shscript from FrankenPHP to compile into a single executable
Stage 4: Final Runtime #
The final stage that produces the production image
- Using the base image
distroless/static-debian12 - Only contains the compiled static binary
Building the Docker Image #
Run the following command to build the image:
podman build --format=docker \
-t wayanjimmy/blog-10:latest .Running the Container #
After the build is complete, make sure the container runs without issues:
podman container run -p 8080:80 \
--env-file=./.env --rm wayanjimmy/blog-10:latestExtracting the Static Binary #
If you want to get just the static binary:
podman create --name temp-container wayanjimmy/blog-10:latest
podman cp temp-container:/app/blog ./blog-binary
podman rm temp-containerNow the blog-binary file can be run directly:
chmod +x blog-binary
./blog-binary php-serverConclusion #
By using FrankenPHP and multi-stage Container builds, we can transform a Laravel application into a self-contained single binary. This will streamline the deployment process by eliminating several components that need to be run, such as nginx/apache and php-fpm.
Although the binary size is quite large (around 199MB), the deployment ease gained is, in my opinion, worth it. This approach is very suitable for simplifying deployed components, and like this blog, I deploy it to a Fly.io server.