From TYPO3 container hosting to a cluster

Challenges, architecture and security concepts on the way to a highly available TYPO3 system.

What started as simple Docker container hosting with a single TYPO3 instance grows, in this series, step by step into a production-ready cluster solution. In this part we prepare the container for production use — with a focus on security, stability and immutability.

Overview

This series covers the full path from simple container hosting to a production-ready cluster:

  1. Part 1 — Fundamentals: containers and TYPO3
  2. Part 2 — Dockerfile and image build
  3. Part 3 — PHP-FPM configuration
  4. Part 4 — Web server integration (Caddy/Nginx)
  5. Part 5 — Database and persistence
  6. Part 6 — Caching with Redis/Valkey
  7. Part 7 — Secrets management
  8. Part 8 — CI/CD pipeline
  9. Part 9 — Monitoring and logging
  10. Part 10 — Cluster operations with Kubernetes
  11. Part 11 — Production hardening

Container ready for take-off

The central principle when moving into production is: security and stability come first. A container image that works in development is far from production-ready.

The most important rule here is immutability: a container image must not change after release. What was built and tested is exactly what runs in production. No after-the-fact changes, no manual intervention, no surprises.

DevOps fundamentals

DevOps describes the cooperation between development (Dev) and operations (Ops) with the goal of delivering software faster, more reliably and in an automated way. It is not a role, but a culture and methodology that breaks down silos between teams.

SRE (Site Reliability Engineering) is Google's implementation of DevOps principles. SRE treats operations as a software engineering problem and defines measurable goals for availability (SLOs), automated incident response and systematic capacity planning.

Minimalistic systems or not?

For production containers, clear principles apply:

Operating system comparison for containers

The choice of base image directly affects security and maintainability:

RHEL / UBI (Red Hat Universal Base Image):

Alpine Linux:

Wolfi OS (recommended):

Immutability

Immutability is the key to stable and secure containers. The principle:

If a problem occurs, you don't repair the running container — you build a new image and deploy it. That eliminates configuration drift and makes rollbacks trivial.

Preparing the configuration files

Before we create the Dockerfile, we prepare the PHP configuration files. They are copied into the image at build time.

php.ini

The php.ini is optimised for production:

 

[PHP]
; Error display off — errors belong in the log, not on the screen
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /proc/self/fd/2
; Security-relevant settings
expose_php = Off
allow_url_fopen = On
allow_url_include = Off
; Performance settings
max_execution_time = 240
max_input_time = 60
max_input_vars = 1500
memory_limit = 512M
post_max_size = 100M
upload_max_filesize = 100M
; Session configuration
session.cookie_httponly = On
session.cookie_secure = On
session.cookie_samesite = Strict
session.use_strict_mode = On
; OPcache for production
opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.jit = tracing
opcache.jit_buffer_size = 128M
; Time zone
date.timezone = Europe/Berlin
; Realpath cache (important for Symfony/TYPO3)
realpath_cache_size = 4096K
realpath_cache_ttl = 600

 

Important notes on the settings:

www.conf (PHP-FPM pool configuration)

The pool configuration controls how PHP-FPM manages worker processes:

 

[www]
user = www-data
group = www-data
listen = 9000
; Process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
; Status and ping for health checks
pm.status_path = /status
ping.path = /ping
ping.response = pong
; Logging
access.log = /proc/self/fd/2
slowlog = /proc/self/fd/2
request_slowlog_timeout = 5s
; Security
security.limit_extensions = .php
; Pass through environment variables
clear_env = no

 

Key settings explained:

php-fpm.conf

The global PHP-FPM configuration:

 

[global]
error_log = /proc/self/fd/2
log_limit = 1000000

 

log_limit is set to 1,000,000 characters so that even long stack traces appear fully in the log (the default is only 1024 characters).

Building the Dockerfile

The Dockerfile uses Wolfi OS as its base and follows the multi-stage build principle:

 

# Stage 1: build
FROM cgr.dev/chainguard/wolfi-base AS build
# Install PHP and build dependencies
RUN apk add --no-cache \
    php-8.3 \
    php-8.3-fpm \
    php-8.3-pdo \
    php-8.3-pdo_mysql \
    php-8.3-gd \
    php-8.3-intl \
    php-8.3-opcache \
    php-8.3-zip \
    php-8.3-xml \
    php-8.3-mbstring \
    php-8.3-curl \
    php-8.3-tokenizer \
    php-8.3-fileinfo \
    php-8.3-session \
    php-8.3-ctype \
    composer
# Install Composer dependencies
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-scripts
COPY . .
RUN composer dump-autoload --optimize
# Stage 2: production
FROM cgr.dev/chainguard/wolfi-base AS production
RUN apk add --no-cache \
    php-8.3 \
    php-8.3-fpm \
    php-8.3-pdo \
    php-8.3-pdo_mysql \
    php-8.3-gd \
    php-8.3-intl \
    php-8.3-opcache \
    php-8.3-zip \
    php-8.3-xml \
    php-8.3-mbstring \
    php-8.3-curl \
    php-8.3-tokenizer \
    php-8.3-fileinfo \
    php-8.3-session \
    php-8.3-ctype
# Copy configuration files
COPY docker/php/php.ini /etc/php/8.3/php.ini
COPY docker/php/www.conf /etc/php/8.3/php-fpm.d/www.conf
COPY docker/php/php-fpm.conf /etc/php/8.3/php-fpm.conf
# Copy application from build stage
COPY --from=build /app /var/www/html
# Unprivileged user
RUN adduser -D -u 1000 www-data
USER www-data
WORKDIR /var/www/html
EXPOSE 9000
CMD ["php-fpm8.3", "-F"]

 

Important points about the Dockerfile:

Building the image

The image is built with a simple docker build:

 

docker build -t typo3-app:latest -f Dockerfile .

 

For multi-architecture support (e.g. ARM64 for AWS Graviton or Apple Silicon) you can use docker buildx:

 

docker buildx build --platform linux/amd64,linux/arm64 -t typo3-app:latest .

 

After the build, the image should be tagged with RepoDigests. A RepoDigest is a SHA256 hash that uniquely identifies the image — independent of the tag. That ensures exactly this image gets deployed:

 

docker inspect --format='{{index .RepoDigests 0}}' typo3-app:latest

 

In production deployments you should always reference the digest rather than a tag, since tags can be overwritten while digests are immutable.