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.

30 minutes · Docker deployment

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

  1. The builder takes Composer from an explicit version, copies composer.json and composer.lock first, and installs exactly locked dependencies.
  2. It then adds source code and creates optimized autoloading and static assets. Run tests and analysis before publishing the image.
  3. The runtime stage uses a specific supported PHP base image and contains only required extensions, the application, and vendor from the builder.
  4. 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

  1. Tag the image with a commit SHA or release version and deploy by digest. Do not overwrite the meaning of latest.
  2. Never place passwords, tokens, or certificates in ARG, ENV, or COPY. Mount them at runtime from a least-privilege secret store.
  3. Validate configuration at startup and reject missing required values. Log structured output to stdout and stderr without personal or secret data.
  4. 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

  1. Run migrations as a one-off release job and make them compatible with old and new versions during a rolling update.
  2. Liveness detects a stuck process; readiness checks dependencies required to serve a request. Neither should make an expensive full-system query.
  3. New containers receive traffic only after readiness. On termination they stop accepting requests and finish in-flight work during a grace period.
  4. 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.

  1. 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 .
  2. 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
  3. 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.

Request a call

I will call you on the next working day between 9:00 and 17:00.

You can also call me directly.

+420 605 181 728

Leave your phone number and send a callback request.

By sending, you agree to processing your data in order to handle your request.