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, cronfpm— FastCGI process manager, pair with nginxapache— 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 \
bcmathComposer 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-authoritativeTuning 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.iniThe 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-dataRun 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 1Putting 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
- Containerizing a PHP Application
- Multi-Stage Builds and Optimization
- Docker Compose for Local Stacks
- CI/CD with GitHub Actions