Docker has been the standard way to package and deploy PHP applications for years. But the difference between a Dockerfile that works in development and one that runs reliably in production is significant. Most PHP Docker images carry unnecessary weight, miss security hardening, and skip optimizations that matter at scale.
This guide covers building production-ready PHP Docker images in 2026, with concrete examples for Laravel, Symfony, and framework-agnostic PHP.
The minimal production Dockerfile
1 | # Stage 1: Install dependencies |
Why multi-stage builds matter
The first stage (deps) installs Composer dependencies. The second stage (production) starts from a clean PHP image and copies only the vendor directory. This ensures:
- Composer itself is not in the final image
- Dev dependencies are not included
- Build tools and caches are discarded
- The final image is smaller and has fewer attack surfaces
PHP extension installation
Every PHP extension you install adds to the image size and attack surface. Install only what you need.
1 | # Common extensions for a typical PHP application |
Extension size impact
| Extension | Size added | When you need it |
|---|---|---|
| pdo_mysql | ~2MB | MySQL/MariaDB access |
| pdo_pgsql | ~3MB | PostgreSQL access |
| intl | ~25MB | Internationalization, number/date formatting |
| gd | ~15MB | Image manipulation |
| imagick | ~40MB | Advanced image processing |
| redis | ~1MB | Redis caching |
| opcache | <1MB | Always in production |
The intl and imagick extensions are the largest. Only include them if your application uses them.
OPcache configuration
OPcache is the single most important performance optimization for production PHP:
1 | ; docker/php/opcache.ini |
Critical setting: validate_timestamps=0 tells PHP not to check if files have changed. In Docker, files never change after the image is built, so this check is wasted work. It saves measurable CPU on every request.
The JIT settings enable PHP’s Just-In-Time compiler, which benefits CPU-intensive operations (math, string processing, loops).
php-fpm tuning
1 | ; docker/php/php-fpm.conf |
Calculating max_children
1 | max_children = Available RAM / Average memory per PHP process |
If your container has 512MB RAM limit and each PHP process uses ~30MB:
1 | max_children = 512 / 30 ≈ 15 (leave headroom for OS and Nginx) |
Setting max_children too high causes the container to exceed its memory limit and get OOM-killed.
Nginx as a sidecar
PHP-FPM does not serve HTTP directly. You need a web server in front of it:
1 | # docker/nginx/default.conf |
1 | # docker-compose.yml |
Security hardening
Run as non-root
1 | # Create and use a non-root user |
Never run PHP-FPM as root in production. The www-data user limits the blast radius of a compromised application.
Minimize the image
1 | # Remove unnecessary files |
Scan for vulnerabilities
1 | # Scan the built image with Trivy |
Run this in CI. Fail the build if critical vulnerabilities are found in the base image or installed packages.
Common mistakes
Using latest tag for base images: php:latest may change PHP versions unexpectedly. Always pin to a specific version: php:8.4.2-fpm-bookworm.
Copying .env into the image: Environment variables should come from the runtime environment (Docker secrets, Kubernetes ConfigMaps), not baked into the image.
Not using .dockerignore:
1 | # .dockerignore |
Without .dockerignore, Docker sends your entire project directory (including .git and node_modules) to the build context, slowing builds significantly.
Running Composer as root: Composer warns against this for good reason. Use multi-stage builds to avoid the issue entirely.
FAQ
Should I use Alpine or Debian-based PHP images?
Debian-based images are larger (~150MB vs ~50MB) but have better compatibility with PHP extensions and fewer surprise issues with musl libc. Use Alpine if image size is critical and you have tested all your extensions.
How do I handle file uploads in Docker?
Mount a volume for the upload directory. Do not write to the container filesystem—it is ephemeral and not shared across replicas.
Should I include Nginx in the same container as PHP-FPM?
No. Run them as separate containers. This follows the one-process-per-container principle and allows independent scaling.
Next steps
Start with the multi-stage Dockerfile template above and adapt it to your application. Build the image, run docker image inspect to verify the size, and run a security scan with Trivy. The first build takes time to get right, but once the Dockerfile is solid, it rarely needs changes.
For PHP runtime performance inside Docker containers, the FrankenPHP guide covers an alternative to the php-fpm + Nginx pattern. The Swoole guide covers high-concurrency deployments that use a single container instead of the php-fpm + Nginx pair.