0Pricing
PHP Academy · Lesson

Containerizing a PHP Application

Write a production-ready PHP Dockerfile.

Containerizing a PHP Application is a free PHP Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Containerize PHP

Shipping PHP reproducibly means freezing the exact interpreter version, extensions, and OS libraries alongside your code. A Docker image gives every environment — laptop, CI, prod — the same php -v and the same ext-* set.

In this lesson we build a production-ready image: PHP-FPM, only the extensions you need, tuned configs, a non-root user, and a health check.

FPM vs Apache Base

The official PHP images come in flavors. For a production web app behind nginx/traefik, prefer php:8.3-fpm-alpine (small) or php:8.3-fpm (Debian, glibc — fewer surprises with native libs).

  • cli — workers, queues, cron
  • fpm — FastCGI process manager, pair with nginx
  • apache — bundled Apache, convenient but heavier

Pin the minor version. Never use :latest in prod.

# Base image choice in your Dockerfile
FROM php:8.3-fpm-alpine AS base

# Why alpine? ~30MB base vs ~140MB Debian.
# Tradeoff: musl libc, occasional native-extension friction.

Installing Extensions

Never apt install php-xxx inside these images — use the bundled helpers docker-php-ext-install, docker-php-ext-configure, and pecl. The install-php-extensions script (mlocati) is the de-facto shortcut that pulls the right dev headers for you.

FROM php:8.3-fpm-alpine

# Grab the helper that resolves build deps automatically
ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/

RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions \
        pdo_mysql \
        opcache \
        intl \
        zip \
        redis \
        bcmath

Composer in the Image

Copy the Composer binary from its official image rather than curling an installer. Run composer install with --no-dev and --optimize-autoloader for production, and copy only composer.json/composer.lock first so the dependency layer caches independently of source changes.

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app

# Cache-friendly: deps layer invalidates only when lock changes
COPY composer.json composer.lock ./
RUN composer install \
        --no-dev \
        --no-scripts \
        --no-autoloader \
        --prefer-dist

COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative

Tuning php.ini

The base image ships php.ini-production and php.ini-development templates. Activate the production one, then drop your own overrides into conf.d — that directory is merged last, so it wins.

Key production values: opcache.enable=1, opcache.validate_timestamps=0 (immutable code in image), and a sane memory_limit.

# Activate production ini
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

# Custom overrides win because conf.d loads last
COPY docker/php/zz-app.ini $PHP_INI_DIR/conf.d/zz-app.ini

The OPcache Override File

This is the single biggest production win. With validate_timestamps=0 PHP never stats files on each request — but that means you MUST rebuild the image to deploy changes (which is exactly what we want for immutable containers).

; docker/php/zz-app.ini
memory_limit = 256M
expose_php = Off

opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.preload = /app/preload.php
opcache.preload_user = www-data

Run as Non-Root

The image already defines www-data. Running FPM as root is a needless attack-surface increase. Set ownership of writable paths (cache, logs) and switch user with USER before CMD.

FPM's master still binds privileged ports? No — FPM listens on 9000 (unprivileged), so non-root is straightforward.

# Make runtime-writable dirs owned by the runtime user
RUN chown -R www-data:www-data /app/var /app/storage 2>/dev/null || true

USER www-data

EXPOSE 9000
CMD ["php-fpm"]

Health Checks

Orchestrators need a signal that FPM is actually alive, not just that the process exists. cgi-fcgi can ping the FPM /status or /ping endpoint. Enable pm.status_path and ping.path in the FPM pool first.

# In www.conf pool config:
;   ping.path = /ping
;   ping.response = pong

# Dockerfile HEALTHCHECK using cgi-fcgi
RUN install-php-extensions @composer >/dev/null 2>&1 || true

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \
      cgi-fcgi -bind -connect 127.0.0.1:9000 || exit 1

Putting the Dockerfile Together

Here is a coherent single-stage production Dockerfile. In the next lesson we split it into multi-stage to drop build tools. Note the order: deps → config → source → autoload → user switch.

FROM php:8.3-fpm-alpine

ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/
RUN chmod +x /usr/local/bin/install-php-extensions && \
    install-php-extensions pdo_mysql opcache intl zip redis

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app

RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php/zz-app.ini $PHP_INI_DIR/conf.d/

COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --classmap-authoritative && \
    chown -R www-data:www-data /app/var

USER www-data
EXPOSE 9000
CMD ["php-fpm"]

Verifying the Build

After building, sanity-check what actually landed in the image: PHP version, loaded extensions, and that OPcache validation is off. A quick CLI script confirms the runtime contract your app depends on.

<?php
// Run inside the container: php verify.php
echo 'PHP ' . PHP_VERSION . PHP_EOL;

$required = ['pdo_mysql', 'opcache', 'intl', 'zip'];
foreach ($required as $ext) {
    printf("%-12s %s\n", $ext, extension_loaded($ext) ? 'OK' : 'MISSING');
}

var_dump((bool) ini_get('opcache.enable'));
?>

Build Context Hygiene

A .dockerignore keeps your build context small and prevents secrets/vendor bloat from leaking into the image and busting cache. Exclude vendor, VCS, env files, and local tooling.

# .dockerignore
.git
.gitignore
vendor/
node_modules/
.env
.env.*
tests/
*.md
docker-compose*.yml
storage/logs/*
var/cache/*

Quick Check

Why set opcache.validate_timestamps=0 in a production image?

Recap

You built a production PHP-FPM image: pinned 8.3-fpm-alpine base, installed only needed extensions via the installer script, copied Composer and cached deps in their own layer, activated the production php.ini with an OPcache override, switched to www-data, and added an FPM health check.

Key habits: pin versions, cache the deps layer, run non-root, disable timestamp validation, and keep the build context lean with .dockerignore.

Frequently asked questions

Is the “Containerizing a PHP Application” lesson free?

Yes — the full text of “Containerizing a PHP Application” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “Containerizing a PHP Application”?

Write a production-ready PHP Dockerfile. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Containerizing a PHP Application” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Containerizing a PHP Application
  2. Multi-Stage Builds and Optimization
  3. Docker Compose for Local Stacks
  4. CI/CD with GitHub Actions
← Back to PHP Academy