Symfony Dockerfile: PHP-FPM 8.4 Alpine with Composer Stage
This Symfony Dockerfile installs production dependencies in a composer:2 stage with an authoritative class map, then runs the app on php:8.4-fpm-alpine with OPcache and APP_ENV set to prod. PHP-FPM runs as www-data on port 9000, so Nginx or Caddy sits in front to handle HTTP.
- CI status
- Build passing
- Image size (uncompressed)
- 99 MB
- Build time
- 108 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 --classmap-authoritative --no-dev --no-scripts
FROM php:8.4-fpm-alpine
RUN docker-php-ext-install opcache
ENV APP_ENV=prod
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
var/
storage/logs/*
storage/framework/cache/*
Dockerfile
.dockerignoreBefore you build
- Commit composer.lock so Composer installs exact versions.
- Put Nginx or Caddy in front of PHP-FPM on port 9000, with public/ as the document root.
- Run cache:warmup and database migrations as deploy steps.
- Set APP_SECRET and DATABASE_URL as environment variables at runtime.
How this Dockerfile works
FROM composer:2 AS vendorDependencies are installed in Composer's official image, so Composer itself never reaches the runtime image.
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqsInstalls only production packages from the lockfile. Symfony's auto-scripts are skipped because they need the runtime environment.
RUN composer dump-autoload --optimize --classmap-authoritative --no-dev --no-scriptsAn authoritative class map tells the autoloader never to search the filesystem for unknown classes, which speeds up every request in production.
FROM php:8.4-fpm-alpineThe official PHP-FPM image on Alpine serves PHP over FastCGI and keeps the runtime image small.
RUN docker-php-ext-install opcacheOPcache keeps compiled PHP in memory between requests. Add database drivers such as pdo_pgsql to this same line.
ENV APP_ENV=prodMakes Symfony boot in production mode, with optimized caches and no debug toolbar, even if no .env file sets it.
USER www-data EXPOSE 9000 CMD ["php-fpm"]Runs PHP-FPM as the web user and documents the FastCGI port your web server connects to.
Build and run
docker build -t symfony-app .
docker run --rm -p 9000:9000 symfony-appPort 9000 speaks FastCGI rather than HTTP. Point Nginx or Caddy at it with Symfony's public directory as the document root, and pass secrets as environment variables.
Other Symfony Dockerfile variants
PHP-FPM behind a separate web server is the traditional Symfony production setup. FrankenPHP images that bundle the web server, or Debian-based images for glibc-dependent extensions, suit other platforms. Verified variants appear in the table below.
| Variant | Runtime image | Image size | CI |
|---|---|---|---|
| PHP-FPM 8.4 Alpine + Composer stage | php:8.4-fpm-alpine | 99 MB | Passing |
Troubleshooting
- Class not found errors after deploying
- With an authoritative class map, classes added after the build aren't found. Make sure all code is copied before dump-autoload runs, then rebuild.
- Environment variable not found: APP_SECRET
- Symfony expects APP_SECRET at runtime. Pass it with -e or your orchestrator's secret store instead of committing it to .env.
- Unable to write in the var/cache directory
- var/ must be writable by www-data. Keep the chown on the COPY line, and match ownership on any mounted volumes.
FAQ
- Why skip Symfony's auto-scripts during the build?
- The auto-scripts clear and warm the cache using the runtime environment. Running cache:warmup at deploy time, with real environment variables, gives a correct cache.
- Do I need a .env file in production?
- No. Set real environment variables in your platform. Symfony reads them directly, and APP_ENV=prod is already set in the image.
- Can I use FrankenPHP instead?
- Yes. FrankenPHP bundles a web server and PHP in one image, which removes the separate Nginx container. It needs a different Dockerfile.