Safeguard
Containers

How to Dockerize a PHP Application Without Shipping Vulnerabilities

Dockerizing a PHP application is easy; doing it securely takes a few deliberate choices about base images, users, and dependencies. Here is a hardened, production-ready approach.

Karan Patel
Platform Engineer
6 min read

To dockerize a PHP application securely, the choices that matter most are your base image, running as a non-root user, and a multi-stage build that keeps Composer and build tools out of the final image. A working Dockerfile is easy to write; the gap between "it runs" and "it is safe to ship" is a handful of deliberate decisions. This guide covers them with a complete, hardened example.

Start from the right base image

The base image is the single biggest factor in your container's vulnerability count. The official PHP images come in several flavors, and the wrong one ships hundreds of packages you never use, each a potential CVE.

Prefer the Alpine or slim variants over the full Debian image for smaller attack surface:

# Smaller, fewer packages, fewer CVEs
FROM php:8.3-fpm-alpine

Pin a specific minor version, not latest or a bare 8. latest makes your build non-reproducible and can pull in an untested runtime overnight. As of 2026, PHP 8.3 (supported through November 2027) and 8.4 (through December 2028) are the sensible targets; PHP 8.1 reached end of life on December 31, 2025, so building on it means running an unpatched runtime. Do not build production images on an EOL PHP line.

Use a multi-stage build

Composer, compilers, and dev dependencies belong in a build stage, not in the image you deploy. A multi-stage build lets you install everything you need to build, then copy only the artifacts into a clean runtime image.

# Stage 1: build dependencies
FROM composer:2 AS build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --optimize-autoloader
COPY . .
RUN composer dump-autoload --optimize --no-dev

# Stage 2: lean runtime
FROM php:8.3-fpm-alpine
WORKDIR /var/www/html
COPY --from=build /app /var/www/html

The --no-dev flag is doing real security work here: it excludes development dependencies like test frameworks and debug tooling that have no business in production and only widen the attack surface. --no-scripts during install prevents package lifecycle scripts from running in the build; run them explicitly once you trust the tree.

Never run as root

By default a container process runs as root, and a compromise of your PHP process then means root inside the container, which is a much shorter path to escaping to the host. Create and switch to an unprivileged user:

RUN addgroup -g 1000 app && adduser -u 1000 -G app -S app
RUN chown -R app:app /var/www/html
USER app

Everything after USER app runs unprivileged. This one directive turns a full container takeover into a far more contained event.

Install only the extensions you need

Every PHP extension is more code that can carry a vulnerability. Install the specific extensions your app uses and nothing more, and clean up the build dependencies in the same layer so they do not persist in the image:

RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
    && docker-php-ext-install pdo_mysql opcache \
    && apk del .build-deps

The .build-deps virtual package groups the compilers so apk del removes them cleanly after the extensions are built. The --no-cache flag stops apk from leaving an index behind.

Manage configuration and secrets from outside the image

Never bake secrets into the image. A database password in a Dockerfile or committed .env lives forever in the image layers and your git history. Pass secrets at runtime through environment variables injected by your orchestrator, or through Docker secrets and mounted files.

Ship the production PHP configuration explicitly rather than relying on defaults:

COPY php.ini-production /usr/local/etc/php/php.ini

The production php.ini disables display_errors, which otherwise leaks stack traces and file paths to users, and it sets sane resource limits.

Scan the image before it ships

A hardened Dockerfile still inherits whatever CVEs live in the base image and your Composer dependencies. Two scans close that gap: a container image scan for OS-level packages, and a software composition analysis pass for the PHP dependency tree. An SCA tool reads your composer.lock and flags known-vulnerable packages before the image reaches a registry. Wire both into CI so a build with a critical vulnerability fails instead of deploying:

# Fail the build if the image carries high/critical OS CVEs
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest

Generate an SBOM at the same time so that when the next widespread CVE lands you can query which images are affected in minutes.

A complete hardened Dockerfile

Putting it together, the pattern is: pinned Alpine base on a supported PHP line, multi-stage build with --no-dev, an unprivileged user, only the extensions you need, secrets injected at runtime, and an image scan in CI. Each piece is small; skipping any one of them is how images ship with root processes, dev dependencies, and unpatched runtimes into production.

FAQ

Which PHP base image should I use for Docker?

Use an official php:<version>-fpm-alpine or slim image pinned to a supported minor version. Alpine and slim variants carry far fewer packages than the full Debian image, which means fewer CVEs. Avoid latest, and do not build on an end-of-life PHP line such as 8.1.

Why should a PHP container not run as root?

If your PHP process is compromised and runs as root, the attacker has root inside the container and a much easier path to escaping to the host. Creating an unprivileged user and switching to it with USER contains the blast radius of any application-level compromise.

How do I keep Composer dev dependencies out of my image?

Use a multi-stage build and run composer install --no-dev in the build stage, then copy only the resulting vendor directory and application code into a clean runtime image. Dev dependencies like test frameworks add attack surface with no production value.

Do I still need to scan a hardened PHP image?

Yes. A careful Dockerfile cannot remove vulnerabilities that live in the base image's OS packages or in your Composer dependencies. Run both a container image scan and software composition analysis in CI, and fail the build on high or critical findings.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.