Flarum container image — a PikaPods fork of
crazy-max/docker-flarum (MIT),
built on serversideup/php
and rebuilt automatically when Flarum or the base image changes.
This image powers Flarum on PikaPods and is maintained by the PikaPods team. It's published here for our users' reference and the benefit of the wider community. To run your own Flarum pod from $1.9/month, see pikapods.com/pods?run=flarum.
Published to both ghcr.io/pikapods/docker-flarum and
pikapods/docker-flarum (Docker Hub) — pick whichever registry you prefer.
Three tag patterns are pushed per build:
| Tag | Mutability | Use for |
|---|---|---|
latest |
mutable | Most recent build of the highest stable version; never moves back |
2.0.0 |
mutable | Pin to a Flarum version; auto-receive base-image patches |
2.0.0-r1 |
immutable | Byte-for-byte reproducibility; never reused |
Source: https://github.com/pikapods/docker-flarum
Upstream crazymax/flarum publishes in bursts (1.8.10 → 1.8.17 was a
13-month gap), and a versioned tag only appears when the maintainer cuts a
release — crazymax/flarum:1.8.17 sat unrebuilt while Alpine and PHP CVEs
accumulated underneath it. PikaPods needs those picked up on a clock. This fork
keeps upstream's Flarum logic (the bootstrap, the extension helper, the
fail-closed extension replay) and changes the platform underneath:
- A maintained base.
serversideup/php:8.4-fpm-nginx-alpine(s6-overlay v3, nginx, php-fpm, non-root) is rebuilt frequently upstream. Its digest moving is what triggers our security rebuilds. - Automatic rebuilds.
upstream-watch.ymlruns hourly. It rebuilds when Packagist has a newerflarum/core2.x release than the registry, or when the published image'sorg.opencontainers.image.base.digestno longer matches the current base. - Reproducible builds. CI pins the base by digest and records it in that label, so every rebuild can be attributed to a specific base.
- A test suite. An image lane and a runtime lane that boots against
MariaDB, covering the install, the
/datalayout, the scheduler, the extension persistence cycle and the 1.8 → 2.0 upgrade.
One deviation from upstream's runtime stays: the php-fpm pool runs with
clear_env = yes plus an explicit env[PATH], and the extension helper no
longer trusts the caller's PATH. Without it, Flarum's Extension Manager
cannot install or update anything — see Extensions.
This image tracks Flarum 2.x. Pods following latest move from 1.8 to
2.0 on their next pull — read Upgrading from 1.8 first.
The bundled compose.yaml brings up Flarum and MariaDB with zero external
dependencies — the fastest way to try the image:
git clone https://github.com/pikapods/docker-flarum.git
cd docker-flarum
docker compose up -d
# wait ~60s for the first-boot install
curl -I http://localhost:8000/ # → HTTP/1.1 200 OKFlarum installs itself with administrator username flarum and a random
password printed once in docker compose logs flarum. Log in at
http://localhost:8000/ with that password. To supply your own, set
FLARUM_ADMIN_PASSWORD or FLARUM_ADMIN_PASSWORD_FILE before the first boot.
Against an existing database:
docker run -d --name flarum \
-v flarum-data:/data \
-e FLARUM_BASE_URL=https://forum.example.com \
-e DB_HOST=db.internal \
-e DB_NAME=flarum \
-e DB_USER=flarum \
-e DB_PASSWORD=... \
-p 8000:8080 \
ghcr.io/pikapods/docker-flarum:latestFLARUM_BASE_URL is mandatory — the container refuses to boot without it, and
Flarum bakes it into generated asset and redirect URLs at install time.
The database and Flarum variables configure the application. PHP, nginx and proxy settings come from the base image — see serversideup's environment variable reference for the full list.
| Var | Required | Default | Purpose |
|---|---|---|---|
DB_HOST |
yes | — | MySQL/MariaDB hostname. |
DB_DRIVER |
no | auto |
mariadb or mysql. auto asks the server. |
DB_PORT |
no | 3306 |
DB port. |
DB_NAME |
no | flarum |
Database name. |
DB_USER |
no | flarum |
DB user. DB_USER_FILE also supported. |
DB_PASSWORD |
yes | — | DB password. DB_PASSWORD_FILE also supported. |
DB_PREFIX |
no | flarum_ |
Table prefix. |
DB_NOPREFIX |
no | false |
Set true to force an empty prefix. |
DB_TIMEOUT |
no | 60 |
Seconds to wait for the database on boot. |
| Var | Required | Default | Purpose |
|---|---|---|---|
FLARUM_BASE_URL |
yes | — | Public URL, including scheme. Boot fails if unset. |
FLARUM_FORUM_TITLE |
no | Flarum Dockerized |
Forum title (first install only). |
FLARUM_ADMIN_USER |
no | flarum |
Initial administrator username; _FILE also supported. |
FLARUM_ADMIN_EMAIL |
no | flarum@flarum.docker |
Initial administrator email; _FILE also supported. |
FLARUM_ADMIN_PASSWORD |
no | generated | Initial password; _FILE also supported. A generated password is printed once in the container log. |
FLARUM_DEBUG |
no | false |
Debug mode. Leave off in production. |
FLARUM_API_PATH |
no | api |
API path segment. |
FLARUM_ADMIN_PATH |
no | admin |
Admin path segment. |
FLARUM_COOKIE_SAMESITE |
no | lax |
Session cookie SameSite policy. |
FLARUM_REFERRER_POLICY |
no | same-origin |
Referrer-Policy header. |
FLARUM_POWEREDBY_HEADER |
no | true |
Emit X-Powered-By. |
FLARUM_ANNOUNCEMENTS_DISABLED |
no | false |
Suppress in-admin announcements. |
Administrator variables only apply to a new installation. Restarting or
recreating a container keeps existing accounts and passwords. Supply either a
variable or its _FILE counterpart, never both. Use a real administrator email
address to receive password reset messages.
| Var | Default | Purpose |
|---|---|---|
FLARUM_SCHEDULER_ENABLED |
true |
Run php flarum schedule:run every minute. See Scheduler. |
PHP_MEMORY_LIMIT |
256M |
PHP memory_limit. |
PHP_UPLOAD_MAX_FILE_SIZE |
100M |
PHP upload ceiling. Pair with PHP_POST_MAX_SIZE. |
NGINX_CLIENT_MAX_BODY_SIZE |
100M |
nginx request body ceiling. |
PHP_DATE_TIMEZONE |
UTC |
PHP timezone. |
PHP_OPCACHE_MEMORY_CONSUMPTION |
256 |
OPcache size in MB. |
PHP_MAX_EXECUTION_TIME |
10800 |
Also nginx's FastCGI read timeout. The Extension Manager runs Composer inside a request. |
TRUSTED_PROXY |
local |
Whose X-Forwarded-For to trust: local (RFC 1918 + loopback), cloudflare, sucuri, off. |
NGINX_LISTEN_IP_PROTOCOL |
all |
ipv4 drops the IPv6 listener. |
OPcache is on with timestamp validation (PHP_OPCACHE_VALIDATE_TIMESTAMPS=1),
because the Extension Manager installs code at runtime.
Flarum's scheduled tasks (mail digests, extension jobs) are not driven by web
requests. The image runs them itself: an s6 service, flarum-scheduler, calls
php flarum schedule:run once a minute, aligned to the minute like cron. It
starts only after the bootstrap has migrated the database, and its output goes
to the container log. Set FLARUM_SCHEDULER_ENABLED=false if something else
drives the scheduler.
The 1.x SIDECAR_CRON container is gone. Remove it from existing deployments.
Otherwise it boots a second full forum against the same volume.
Extensions are installed with Composer and mirrored into
/data/extensions/list, which is replayed on every boot — so they survive
container recreation and image upgrades:
docker compose exec flarum extension require fof/upload
docker compose exec flarum extension list
docker compose exec flarum extension remove fof/uploadAdmin → Extension Manager is supported and goes through the same
post-install-cmd / post-update-cmd hook as the CLI, so extensions installed
from the web UI are mirrored to /data/extensions/list too.
Editing /data/extensions/list by hand still works, but is normally not
necessary: the Composer post-install hook keeps the file in sync
automatically.
The bundled-package baseline is recorded when the image is built, so restarting an existing container preserves the same extension list as recreating it.
Note that the vendor tree itself lives inside the image, not on the volume. Recreating the container reinstalls everything in the list from scratch.
The replay fails closed. If Composer cannot install the list (a package
with no release for the current Flarum major, a pinned constraint such as
^1.2), the boot aborts with an ERROR: could not install the extensions listed in /data/extensions/list block naming the packages and Composer's
conflict. The list is left untouched and no migrations run. The Extension
Manager is unreachable at that point, so the fix is file-based: edit
/data/extensions/list — relax the constraint to *, or delete or comment
out (#) the package — and restart.
flarum/realtime ships in the 2.0 skeleton but is not supported here: it
needs a realtime:serve daemon and a non-sync queue, neither of which this
image runs. Leave it disabled.
The 2.x image is a different container, not only a different Flarum. Before pointing a 1.8 volume at it:
-
Re-own the volume. The 1.x image ran as
flarum(UID 1000). This one runs aswww-data(UID 82) and does not switch users at boot. Run this once, with the forum stopped:docker run --rm -v flarum-data:/data alpine chown -R 82:82 /data
Skip it and the container exits right away with
ERROR: /data is not writable by www-data. -
Change the port mapping from
:8000to:8080. -
Remove the scheduler sidecar. The scheduler runs in the main container now.
-
Rename the environment. The bootstrap no longer templates PHP and nginx config. The base image renders it from its own variables:
1.x 2.x MEMORY_LIMITPHP_MEMORY_LIMITUPLOAD_MAX_SIZEPHP_UPLOAD_MAX_FILE_SIZE+PHP_POST_MAX_SIZE+NGINX_CLIENT_MAX_BODY_SIZETZPHP_DATE_TIMEZONEOPCACHE_MEM_SIZEPHP_OPCACHE_MEMORY_CONSUMPTIONREAL_IP_FROM,REAL_IP_HEADER,LOG_IP_VARTRUSTED_PROXYLISTEN_IPV6=falseNGINX_LISTEN_IP_PROTOCOL=ipv4CLEAR_ENVremoved, always yesPUID,PGIDremoved, see step 1 SIDECAR_CRON,CRON_SCHEDULEremoved, see Scheduler The old names are silently ignored. Note the new upload ceiling defaults to
100M(was16M).
Then on first boot, the image upgrades Flarum in place:
- Backup. Before migrating, the boot compares the database's
versionsetting to the image's core. On a major-version jump it writesmariadb-dump --single-transactionoutput to/data/backups/flarum-<oldver>-<UTC timestamp>.sql.gz(mode 600) and refuses to migrate if the dump fails. Delete old dumps yourself once the upgrade has proven out. - Extensions. Every entry in
/data/extensions/listmust have a 2.x release. Constraints written by the 1.8 Extension Manager (^1.x) will not resolve — change them to*. Per the upgrade guide, removeblomstra/database-queueandblomstra/fontawesome(now in core), replaceblomstra/flarum-rediswithfof/redis,blomstra/horizonwithfof/horizon, andflarum/package-managerwithflarum/extension-manager. If unsure why a package won't resolve, runcomposer why-not flarum/core 2.0.0in a 1.8 container with the extension installed. - Database. Flarum 2.0 refuses MariaDB below 10.3 and MySQL below 5.7.8,
and recommends MariaDB 11.8 or MySQL 8.4.
DB_DRIVER=autoselects themariadbdriver on MariaDB, as the upgrade guide asks.
A failed extension replay, backup or migration stops the container before
nginx starts (S6_BEHAVIOUR_IF_STAGE2_FAILS=2, the base default), so the
forum never serves 2.x code against an unmigrated database.
To roll back, restore the dump into an empty database, chown -R 1000:1000 a
copy of the pre-upgrade /data, and run a 1.8.x tag against it.
| Path | Purpose |
|---|---|
/data |
Persistent volume: assets/, extensions/, storage/, extensions/list, backups/. |
/var/www/html |
Flarum source + vendor tree. Baked at build time — do not bind-mount. |
| Port | Purpose |
|---|---|
| 8080 | HTTP. Terminate TLS at a reverse proxy in front. |
Everything, s6 included, runs as www-data (UID/GID 82, Alpine's
default). There is no root phase at boot and no UID remapping, so /data must
be writable by UID 82. Named volumes get that automatically on first use. For
a bind mount, chown -R 82:82 the host directory. The bootstrap checks this
first and exits with the fix if it does not hold.
docker exec lands as www-data too, which is what the extension helper
expects. It refuses to run as root: Composer would leave root-owned files in
the vendor tree that php-fpm can no longer update.
.github/workflows/upstream-watch.yml runs hourly and dispatches build.yml
when either:
new-version: the newest stableflarum/core2.x.yon Packagist has no published tag yet. The series is pinned by major (SERIES_MAJOR). A 3.0 release does not auto-build.base-drift: the published tag'sorg.opencontainers.image.base.digestdiffers from the currentserversideup/php:8.4-fpm-nginx-alpinedigest. This is how base-image security fixes reach existing tags.
Packagist, not GitHub releases, is the source: it is what Composer resolves at
build time, and its version strings are normalised, so a v prefix on the
upstream tag cannot hide a release. Each rebuild bumps the -rN revision that
the mutable <version> tag follows.
Bumping PHP_VERSION in the watcher does not rebuild existing tags. For a
one-off build, dispatch build.yml directly.
docker build --build-arg FLARUM_VERSION=2.0.0 -t flarum:test .FLARUM_VERSION is plain semver with no leading v. It is handed to Composer
as-is and flows into org.opencontainers.image.version as
<version>-<revision>. Composer selects the newest stable flarum/flarum
skeleton in the same major no newer than the requested core. This allows older
2.x releases to be rebuilt after a new minor skeleton is published, without
requiring identical skeleton and core patch tags. Set the optional build argument
FLARUM_SKELETON_VERSION (or the workflow input flarum_skeleton_version) to
override the skeleton release when needed.
Do not add a
vprefix anywhere in the version handling. Upstream is inconsistent about it — 1.8.16 through 1.8.18 are taggedv1.8.x, but 1.8.19 is tagged bare. Composer normalises the constraint either way, so passing the plain version always works; a v-prefixed filter, on the other hand, will silently hide releases. That is exactly how 1.8.19 was initially mistaken for unavailable when this fork was set up.
PHP_VERSION (default 8.4) selects the base tag. CI additionally passes
BASE_IMAGE (digest-pinned), BASE_DIGEST, IMAGE_REVISION, GIT_SHA and
BUILD_DATE.
How rootfs/ hooks into the base:
etc/entrypoint.d/50-flarum-bootstrap.shis turned into an s6 oneshot bydocker-php-serversideup-s6-initat build time. nginx and php-fpm depend on it, so they start only after a successful bootstrap.etc/s6-overlay/s6-rc.d/flarum-scheduleris the scheduler longrun.usr/local/etc/php-fpm.d/zz-flarum.confre-opens the base's[www]pool to setclear_env = yes. The base shipsclear_env = no, which would exposeDB_PASSWORDto every request via$_SERVER.etc/nginx/server-opts.d/adds Flarum's cache headers and replaces the base'ssecurity.conf. ItsReferrer-Policywould duplicate Flarum's own, and HSTS belongs to the TLS-terminating proxy.
Anything reachable from
/var/www/html/composer.jsonscripts runs in a php-fpm worker. That worker hasclear_env=yes(no environment at all), and Composer then setsPATHto<bin-dir>:before dispatchingpost-install-cmd/post-update-cmd— non-empty, so the shell never falls back to its built-in default, and containing neither/binnor/usr/bin. A new helper must therefore export its ownPATHand be referenced by absolute interpreter path (/bin/sh /usr/local/bin/…, neversh …).tests/test_image.py::test_composer_hook_runs_under_php_fpm_stripped_envreproduces that environment.
pip install -r tests/requirements.txt
IMAGE=flarum:test pytest -v tests/ -m 'not runtime' # fast image lane
IMAGE=flarum:test pytest -v tests/ # + runtime lane vs MariaDBThe runtime lane boots MariaDB plus the image, installs Flarum, and exercises
the extension install/persistence cycle against a real volume. It also checks
the scheduler, that an unresolvable extension list aborts the boot without
touching the list, that a UID-1000 volume fails fast, and upgrades a fresh
ghcr.io/pikapods/docker-flarum:1.8.19 install to the image under test,
re-owning the volume in between (override with UPGRADE_FROM_IMAGE).
The Flarum bootstrap, the extension helper and the original image design
are the work of CrazyMax in
crazy-max/docker-flarum,
used under the MIT licence and retained here with full git history. The base
image is serversideup/php
(GPL-3.0-or-later), used unmodified as a base layer.
This fork adds the serversideup port, the CI pipeline and the test suite. Both are MIT — see LICENSE. Flarum itself is MIT.