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
# 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"].git
vendor/
node_modules/
.env
storage/logs/*
storage/framework/cache/*
Dockerfile
.dockerignoreBefore you build
- Commit composer.lock so Composer installs exact versions.
- Put a web server such as Nginx or Caddy in front of PHP-FPM on port 9000.
- Run config caching and migrations as deploy steps, not during the image build.
- Swap pdo_mysql for pdo_pgsql if you use PostgreSQL.
How this Dockerfile works
FROM composer:2 AS vendorThe official Composer image builds the vendor directory, so Composer itself never ends up in the runtime image.
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqsInstalls 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.
RUN composer dump-autoload --optimize --no-dev --no-scriptsGenerates an optimized class map after the application code is copied in, which speeds up class loading in production.
FROM php:8.4-fpm-alpineThe runtime is the official PHP-FPM image on Alpine, which serves PHP through the FastCGI protocol rather than plain HTTP.
RUN docker-php-ext-install pdo_mysql opcacheAdds the MySQL PDO driver Laravel's database layer needs, plus OPcache, which caches compiled PHP and is essential for production performance.
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.
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
docker build -t laravel-app .
docker run --rm -p 9000:9000 laravel-appPort 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.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| PHP-FPM 8.4 Alpine + Composer stage | php:8.4-fpm-alpine | 119.3 MB | Passing |
| PHP-FPM 8.4 Debian + Composer stage | php:8.4-fpm | 527.9 MB | Passing |
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.