Laravel Dockerfile: PHP-FPM 8.4 Alpine with a Composer Stage

This Laravel Dockerfile installs production Composer dependencies in a separate composer:2 stage and copies the result into a php:8.4-fpm-alpine runtime with pdo_mysql and OPcache enabled. PHP-FPM runs as www-data on port 9000, so a web server such as Nginx or Caddy sits in front to handle HTTP. It fits the common Laravel plus MySQL setup.

CI status
Build passing
Image size (uncompressed)
119.3 MB
Build time
114 s
Runtime image
php:8.4-fpm-alpine
Port
9000
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqs
COPY . .
RUN composer dump-autoload --optimize --no-dev --no-scripts

FROM php:8.4-fpm-alpine
RUN docker-php-ext-install pdo_mysql opcache
WORKDIR /var/www/html
COPY --from=vendor --chown=www-data:www-data /app ./
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
.dockerignore
.git
vendor/
node_modules/
.env
storage/logs/*
storage/framework/cache/*
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM composer:2 AS vendor

    The official Composer image builds the vendor directory, so Composer itself never ends up in the runtime image.

  2. RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqs

    Installs only production packages from the lockfile. Scripts and autoloader generation wait until the full source is present, and platform checks are skipped because the runtime image provides the extensions.

  3. RUN composer dump-autoload --optimize --no-dev --no-scripts

    Generates an optimized class map after the application code is copied in, which speeds up class loading in production.

  4. FROM php:8.4-fpm-alpine

    The runtime is the official PHP-FPM image on Alpine, which serves PHP through the FastCGI protocol rather than plain HTTP.

  5. RUN docker-php-ext-install pdo_mysql opcache

    Adds the MySQL PDO driver Laravel's database layer needs, plus OPcache, which caches compiled PHP and is essential for production performance.

  6. COPY --from=vendor --chown=www-data:www-data /app ./

    Copies the application and its vendor directory owned by www-data, so PHP-FPM can read the code and write to storage.

  7. USER www-data EXPOSE 9000 CMD ["php-fpm"]

    Runs the FPM master process as the web user and documents the FastCGI port your web server connects to.

Build and run

terminal
docker build -t laravel-app .
docker run --rm -p 9000:9000 laravel-app

Port 9000 speaks FastCGI, not HTTP, so a browser can't open it directly. Point Nginx or Caddy at the container's port 9000 with your public directory as the document root.

Other Laravel Dockerfile variants

PHP-FPM behind a separate web server is the classic, flexible Laravel production setup. All-in-one variants that bundle a web server in the same image, or Octane-based images for long-running workers, suit simpler platforms or high-throughput apps. They appear below once verified.

VariantRuntime imageImage sizeCI
PHP-FPM 8.4 Alpine + Composer stagephp:8.4-fpm-alpine119.3 MBPassing
PHP-FPM 8.4 Debian + Composer stagephp:8.4-fpm527.9 MBPassing

Troubleshooting

could not find driver (SQL: select ...)
The PDO extension for your database is missing. Install pdo_mysql for MySQL or pdo_pgsql for PostgreSQL in the runtime stage, then rebuild.
The stream or file storage/logs/laravel.log could not be opened: Permission denied
storage and bootstrap/cache must be writable by www-data. The COPY step sets ownership, but mounted volumes need the same ownership on the host.
The browser shows a download or a blank response on port 9000
PHP-FPM doesn't speak HTTP. Route requests through Nginx or Caddy using a FastCGI pass to the container's port 9000.

FAQ

Why are Composer scripts skipped?
Laravel's post-install scripts call artisan, which may need environment values that don't exist at build time. Run package discovery and caching in your deploy or entrypoint step instead.
Do I need Node for Vite assets?
If your app compiles front-end assets with Vite, add a Node build stage that runs the build and copy the public build output into the runtime image.
Why use --ignore-platform-reqs?
The Composer image doesn't carry your runtime's PHP extensions, so platform checks would fail falsely. The runtime stage installs the real extensions instead.