WoltLab Suite on FrankenPHP

May 8, 2026 ยท View on GitHub

Production-ready Docker image for WoltLab Suite Core on FrankenPHP and Caddy.

This image is intentionally small in scope:

  • FrankenPHP + Caddy in one application container
  • MariaDB as a separate service
  • SSL, HTTP/1.1, HTTP/2, and HTTP/3 through Caddy
  • WoltLab-compatible generic URL rewrites

Supported Images

WoltLab SuitePHPDefault WSC PatchTag
6.28.46.2.36.2-php8.4
6.18.36.1.196.1-php8.3
6.08.36.0.256.0-php8.3

The WoltLab patch version is a build argument and runtime download variable, so newer patch releases do not require a Dockerfile change.

Manual Setup

This repository is primarily a Docker package. For administrators who want to set up the same stack directly on a host, see manual/README.md.

The manual/ directory is intentionally excluded from the Docker build context and is not copied into release images.

Quick Start

Build locally:

cp .env.example .env
docker compose up -d --build

Open:

https://localhost/install.php

Database values for the installer:

Host: db
Database: generated in the wsc-secrets Docker volume
User: generated in the wsc-secrets Docker volume
Password: generated in the wsc-secrets Docker volume

The compose file exposes these values to the WoltLab installer through WCFSETUP_DBHOST, WCFSETUP_DBNAME_FILE, WCFSETUP_DBUSER_FILE, and WCFSETUP_DBPASSWORD_FILE, matching the setup variables used by wsc-dockerized while avoiding a plaintext default password in the compose file.

If MYSQL_DATABASE, MYSQL_USER, or MYSQL_PASSWORD are empty or missing, credential-init generates random values once and stores them in the wsc-secrets Docker volume. To provide fixed values, set them before the first docker compose up. Existing MariaDB volumes keep their initial credentials.

Using Prebuilt GHCR Images

Prebuilt multi-architecture images are published to:

ghcr.io/softcreatrmedia/frankenphp-woltlab-suite

Available tags:

WoltLab SuitePHPTags
6.28.46.2-php8.4, 6.2.3-php8.4
6.18.36.1-php8.3, 6.1.19-php8.3
6.08.36.0-php8.3, 6.0.25-php8.3

Use compose.prebuilt.yaml to disable local builds and pull from GHCR:

cp .env.example .env
docker compose -f compose.yaml -f compose.prebuilt.yaml pull
docker compose -f compose.yaml -f compose.prebuilt.yaml up -d

Select a different prebuilt variant by changing WSC_TAG in .env:

WSC_TAG=6.1-php8.3

Override WSC_PREBUILT_IMAGE only when using a fork or private registry:

WSC_PREBUILT_IMAGE=ghcr.io/your-org/frankenphp-woltlab-suite

Building

Build the default WSC 6.2 / PHP 8.4 image:

docker build \
  --build-arg PHP_VERSION=8.4 \
  --build-arg WSC_REF=6.2.3 \
  -t frankenphp-woltlab-suite:6.2-php8.4 .

Build all supported variants:

docker buildx bake

Build one variant:

docker buildx bake wsc61_php83

Runtime Configuration

Common environment variables:

VariableDefaultPurpose
SERVER_NAMElocalhostCaddy site address. Use your domain in production.
WSC_REF6.2.3WoltLab/WCF tag or branch used when building the installer.
WCFSETUP_DBHOSTdbDatabase host passed to the WoltLab installer.
WCFSETUP_DBNAME_FILE/run/wsc-secrets/db-nameFile containing the database name for the WoltLab installer.
WCFSETUP_DBUSER_FILE/run/wsc-secrets/db-userFile containing the database user for the WoltLab installer.
WCFSETUP_DBPASSWORD_FILE/run/wsc-secrets/db-passwordFile containing the database password for the WoltLab installer.
MYSQL_INNODB_BUFFER_POOL_SIZE1GMariaDB InnoDB buffer pool size. Lower this only on very small servers.
PHP_MEMORY_LIMIT512MPHP memory limit.
PHP_UPLOAD_MAX_FILESIZE64MMaximum upload file size.
PHP_POST_MAX_SIZE64MMaximum POST body size.
PHP_DISABLE_FUNCTIONSexec,passthru,shell_exec,system,proc_open,popenPHP functions disabled by default to reduce command-execution risk. Set to an empty value only if a trusted plugin requires one of them.
PHP_OPCACHE_VALIDATE_TIMESTAMPS0Disable timestamp checks for production performance. Restart wsc after package updates that change PHP files.
PHP_OPCACHE_MEMORY_CONSUMPTION256OPcache shared memory size in MB.
PHP_OPCACHE_MAX_ACCELERATED_FILES20000Maximum number of cached PHP files.
FRANKENPHP_NUM_THREADS1FrankenPHP PHP thread count. Keep this at 1 unless you have tested installation, package updates, and style rebuilds with a higher value.
FRANKENPHP_MAX_THREADS1FrankenPHP maximum PHP thread count. Keep this aligned with FRANKENPHP_NUM_THREADS by default.
FRANKENPHP_CONFIGemptyExtra FrankenPHP global config.
CADDY_GLOBAL_OPTIONSemptyExtra Caddy global options.
CADDY_SERVER_EXTRA_DIRECTIVESemptyExtra Caddy site directives, for example a custom tls directive.
CERTBOT_CERT_NAMEemptyCertificate directory name below /etc/letsencrypt/live when using compose.certbot.yaml.
MYSQL_MAX_CONNECTIONS300MariaDB connection limit.
MYSQL_TABLE_OPEN_CACHE4000MariaDB table cache size.
MYSQL_THREAD_CACHE_SIZE64MariaDB thread cache size.

The application volume is /app/public. The image builds a WoltLab installer from the selected WoltLab/WCF GitHub tag or branch and stores it in /usr/src/woltlab. If /app/public is empty, the entrypoint copies that prebuilt installer into the volume.

Hardened Runtime

The default compose file runs the application container with a hardened profile:

  • non-root www-data user
  • read-only root filesystem
  • no-new-privileges
  • all Linux capabilities dropped except NET_BIND_SERVICE
  • tmpfs-backed /tmp with noexec,nosuid
  • writable mounts only for /app/public, /data, and /config

SSL / Certbot Certificate

The default Caddy behavior is usually enough for public DNS names: set SERVER_NAME to the domain and Caddy will request and renew the certificate itself. Use Certbot only if you want the host to manage certificates, or if you need a certificate type that Caddy cannot request for you.

To request a new certificate on the host, stop anything that is using ports 80 or 443 and run Certbot in standalone mode:

apt-get update
apt-get install -y certbot

docker compose -f compose.yaml down
certbot certonly --standalone -d example.com

Then copy the host-managed certificate into a Docker volume at startup and tell Caddy to use it:

SERVER_NAME=example.com
CERTBOT_CERT_NAME=example.com
CADDY_SERVER_EXTRA_DIRECTIVES=tls /certs/fullchain.pem /certs/privkey.pem
docker compose -f compose.yaml -f compose.certbot.yaml up -d --build

When using GHCR images, include compose.prebuilt.yaml and omit --build:

docker compose -f compose.yaml -f compose.prebuilt.yaml -f compose.certbot.yaml up -d

For an IP address certificate, set both SERVER_NAME and CERTBOT_CERT_NAME to the IP address. Set CADDY_GLOBAL_OPTIONS to default_sni <ip-address> as well, because many clients omit SNI when connecting directly to an IP address.

After Certbot renews a host-managed certificate, refresh the Docker copy and restart Caddy:

docker compose -f compose.yaml -f compose.certbot.yaml run --rm certbot-cert-init
docker compose -f compose.yaml -f compose.certbot.yaml restart wsc

HTTP/3

Expose UDP 443 as well as TCP 443:

ports:
  - "443:443/tcp"
  - "443:443/udp"

Caddy will advertise HTTP/3 automatically when TLS is active.

URL Rewrites

The Caddyfile implements the common WoltLab rewrite pattern:

/app/path -> /app/index.php?path if /app/index.php exists
/foo/bar  -> /index.php?foo/bar

Existing files and directories are served directly.

After installation, enable WoltLab's matching application setting in:

ACP -> Configuration -> Options -> General -> Enable URL rewrite

You can validate the ACP system check and rewritten ACP routes with:

WSC_BASE_URL=https://example.com \
WSC_ADMIN_USER=admin \
WSC_ADMIN_PASSWORD='change-me' \
WSC_ENABLE_URL_REWRITE=1 \
npm run verify:production

Security Notes

The bundled Caddyfile:

  • hides the Server header
  • disables expose_php
  • denies dotfiles except /.well-known/*
  • denies common archive, backup, config, SQL, key, certificate, shell, and template files
  • denies direct access to /vendor/*
  • denies direct access to selected non-public PHP paths
  • sends long-lived immutable cache headers for static assets

Worker mode is not enabled. WoltLab Suite has not been audited for a persistent PHP worker lifecycle, and classic request isolation is the safer production default.

Backups

Create a database, application volume, and generated-secrets backup:

scripts/backup.sh

Restore on a clean deployment:

scripts/restore.sh backups/<timestamp>

If you restore a deployment that uses additional compose overlays, pass the same compose files through COMPOSE_ARGS:

COMPOSE_ARGS="-f compose.yaml -f compose.certbot.yaml" \
scripts/restore.sh backups/<timestamp>

Updating WoltLab

Use WoltLab's built-in package updater for installed communities. The image bootstraps new installations only; it does not overwrite an existing /app/public volume.

For new images on a newer patch release:

WSC_REF=6.2.4 docker compose up -d --build

WSC_REF accepts a tag such as 6.2.3 or a branch such as 6.2.