Practical guide
How to deploy a PHP application with Docker
Build one immutable image, test it, and promote the same digest through environments into production.
In short
A container is a release artifact
A Docker image contains the exact code, PHP extensions, and production dependencies. Environment configuration and secrets arrive only at runtime.
A deploy does not overwrite files inside a running container. It starts a new immutable image, verifies readiness, and only then removes the previous version that remains available for rollback.
Prepare
Separate build from operation
The build needs Composer and compilers; runtime needs only files and libraries required to run.
- A Dockerfile and .dockerignore stored with the application, committed composer.lock, and reproducible CI build.
- A registry with immutable tags or digests and controlled deployment access.
- A runtime configuration and secret store that is not copied into the image or build log.
- A strategy for database migrations, health checks, rolling updates, and returning to the previous release.
Steps 1 to 3
Build a small, predictable image
Every layer has a clear purpose and the production stage excludes build-only tools.
1. Use a multi-stage Dockerfile
- The builder takes Composer from an explicit version, copies composer.json and composer.lock first, and installs exactly locked dependencies.
- It then adds source code and creates optimized autoloading and static assets. Run tests and analysis before publishing the image.
- The runtime stage uses a specific supported PHP base image and contains only required extensions, the application, and vendor from the builder.
- Set an unprivileged USER, file ownership, WORKDIR, and exec-form CMD. Restrict writable locations to cache and runtime data.
COPY --from=vendor /app/vendor /app/vendor Official Docker multi-stage build documentation 2. Keep the image immutable and secret-free
- Tag the image with a commit SHA or release version and deploy by digest. Do not overwrite the meaning of latest.
- Never place passwords, tokens, or certificates in ARG, ENV, or COPY. Mount them at runtime from a least-privilege secret store.
- Validate configuration at startup and reject missing required values. Log structured output to stdout and stderr without personal or secret data.
- Keep uploads, sessions, and durable data outside the container filesystem in object storage, a database, or a managed volume.
docker build --pull -t registry.example/app:$GIT_SHA . Official Docker build best practices 3. Deploy with health checks and compatible migrations
- Run migrations as a one-off release job and make them compatible with old and new versions during a rolling update.
- Liveness detects a stuck process; readiness checks dependencies required to serve a request. Neither should make an expensive full-system query.
- New containers receive traffic only after readiness. On termination they stop accepting requests and finish in-flight work during a grace period.
- Rollback restores the previous digest. If a migration is not backward compatible, returning the image alone is insufficient and needs a prepared plan.
docker image inspect registry.example/app@sha256:... Official Dockerfile HEALTHCHECK reference Step 4
Verify the image as a black box
A release must start on a clean host using only the image, runtime configuration, and declared services.
-
Build without local vendor
The image comes from a clean checkout and composer.lock; runtime contains no Composer, tests, or build secrets.
docker build --no-cache -t app:test . -
Run as an unprivileged user
The application passes a smoke test and writes only to declared locations.
docker run --rm --read-only app:test php -v -
Practice rollback
Traffic does not switch after failed readiness; the previous digest starts again with a compatible schema.
Troubleshooting
Common problems
The image is huge
Separate builder and runtime stages, reduce build context with .dockerignore, and exclude development dependencies and caches.
docker image history app:test Build cache reacts incorrectly to composer.lock
Copy manifests before source code and install dependencies in a separate layer.
The container only works as root
Fix ownership during build and explicitly create writable runtime directories; do not weaken the whole process.
Rollback fails after a migration
Use expand–migrate–contract and delay destructive schema changes until old code is disconnected.
Done
The release is reproducible and reversible.
The production image is small, immutable, and secret-free; deployment uses readiness, compatible migrations, and a tested rollback.