Skip to content

About

Flarum container image for PikaPods

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

288 Commits

Folders and files

Repository files navigation

docker-flarum

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

Why this fork

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.yml runs hourly. It rebuilds when Packagist has a newer flarum/core 2.x release than the registry, or when the published image's org.opencontainers.image.base.digest no 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 /data layout, 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.

Quick start

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 OK

Flarum 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:latest

FLARUM_BASE_URL is mandatory — the container refuses to boot without it, and Flarum bakes it into generated asset and redirect URLs at install time.

Environment variables

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.

Database

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.

Flarum

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.

Container

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.

Scheduler

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

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/upload

Admin → 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.

Upgrading from 1.8

The 2.x image is a different container, not only a different Flarum. Before pointing a 1.8 volume at it:

  1. Re-own the volume. The 1.x image ran as flarum (UID 1000). This one runs as www-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.

  2. Change the port mapping from :8000 to :8080.

  3. Remove the scheduler sidecar. The scheduler runs in the main container now.

  4. 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_LIMIT PHP_MEMORY_LIMIT
    UPLOAD_MAX_SIZE PHP_UPLOAD_MAX_FILE_SIZE + PHP_POST_MAX_SIZE + NGINX_CLIENT_MAX_BODY_SIZE
    TZ PHP_DATE_TIMEZONE
    OPCACHE_MEM_SIZE PHP_OPCACHE_MEMORY_CONSUMPTION
    REAL_IP_FROM, REAL_IP_HEADER, LOG_IP_VAR TRUSTED_PROXY
    LISTEN_IPV6=false NGINX_LISTEN_IP_PROTOCOL=ipv4
    CLEAR_ENV removed, always yes
    PUID, PGID removed, see step 1
    SIDECAR_CRON, CRON_SCHEDULE removed, see Scheduler

    The old names are silently ignored. Note the new upload ceiling defaults to 100M (was 16M).

Then on first boot, the image upgrades Flarum in place:

  1. Backup. Before migrating, the boot compares the database's version setting to the image's core. On a major-version jump it writes mariadb-dump --single-transaction output 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.
  2. Extensions. Every entry in /data/extensions/list must 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, remove blomstra/database-queue and blomstra/fontawesome (now in core), replace blomstra/flarum-redis with fof/redis, blomstra/horizon with fof/horizon, and flarum/package-manager with flarum/extension-manager. If unsure why a package won't resolve, run composer why-not flarum/core 2.0.0 in a 1.8 container with the extension installed.
  3. 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=auto selects the mariadb driver 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.

Mounts

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.

Ports

Port Purpose
8080 HTTP. Terminate TLS at a reverse proxy in front.

User & permissions

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.

Releases

.github/workflows/upstream-watch.yml runs hourly and dispatches build.yml when either:

  • new-version: the newest stable flarum/core 2.x.y on 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's org.opencontainers.image.base.digest differs from the current serversideup/php:8.4-fpm-nginx-alpine digest. 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.

Building locally

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 v prefix anywhere in the version handling. Upstream is inconsistent about it — 1.8.16 through 1.8.18 are tagged v1.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.sh is turned into an s6 oneshot by docker-php-serversideup-s6-init at build time. nginx and php-fpm depend on it, so they start only after a successful bootstrap.
  • etc/s6-overlay/s6-rc.d/flarum-scheduler is the scheduler longrun.
  • usr/local/etc/php-fpm.d/zz-flarum.conf re-opens the base's [www] pool to set clear_env = yes. The base ships clear_env = no, which would expose DB_PASSWORD to every request via $_SERVER.
  • etc/nginx/server-opts.d/ adds Flarum's cache headers and replaces the base's security.conf. Its Referrer-Policy would duplicate Flarum's own, and HSTS belongs to the TLS-terminating proxy.

Anything reachable from /var/www/html/composer.json scripts runs in a php-fpm worker. That worker has clear_env=yes (no environment at all), and Composer then sets PATH to <bin-dir>: before dispatching post-install-cmd / post-update-cmd — non-empty, so the shell never falls back to its built-in default, and containing neither /bin nor /usr/bin. A new helper must therefore export its own PATH and be referenced by absolute interpreter path (/bin/sh /usr/local/bin/…, never sh …). tests/test_image.py::test_composer_hook_runs_under_php_fpm_stripped_env reproduces that environment.

Testing

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 MariaDB

The 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).

Credits & license

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.

About

Flarum container image for PikaPods

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages