Invision Community Docker

Invision Community 5 Docker stack using Caddy, FrankenPHP, Valkey (Redis fork), and s6-overlay for high performance and minimal overhead.

Active Pure Nim score 65/100 · last commit 2026-09-02 · 1 stars · tests present · no docs generated

Summary

Latest Version Unknown
License Unknown
CI Status Failing
Stars 1
Forks 1
Open Issues 0
Last Commit 2026-09-02
Downloads 0
Last Indexed 2026-09-06 06:05

Installation

nimble install Invision Community Docker
choosenim install Invision Community Docker
git clone https://gitlab.com/greyxor/invision-community-docker

OS Compatibility

Platform Linux macOS Windows FreeBSD OpenBSD NetBSD Android iOS WASM Embedded
Invision Community Docker - - - - - - -

README

Invision Community Docker

A production-grade, performance-optimized Docker image for running Invision Community 5 with modern, efficient components:

  • Caddy and FrankenPHP: a high-performance PHP web server.
  • Valkey: a drop-in Redis-compatible store for PHP session handling and caching.
  • s6-overlay: for lightweight, reliable process supervision.

Key Features

  • ⚙️ Production-Ready: Ships with a tuned PHP configuration optimized for production workloads.
  • 🧠 Efficient Process Management: Uses s6-overlay for clean process supervision without unnecessary Python-based tools, reducing memory usage and startup time.
  • 🚀 High Performance Stack: Leverages FrankenPHP and Caddy for fast, modern PHP hosting.
  • 🔌 Socket-Based Valkey Integration: Communicates with Valkey via Unix sockets for reduced latency and improved throughput.
  • 🗃️ Session Storage via Valkey: PHP sessions live in Valkey; Invision's own cache and session store can use the same socket.
  • 🕰️ Background Tasks Without Cron: An s6-supervised loop runs Invision Community's task runner every minute; no cron daemon in the image.
  • 👤 Unprivileged Services: FrankenPHP, Valkey and the task runner run as www-data; only the s6 supervisor is root.
  • 🪶 Minimal Image Layers: Streamlined Dockerfile for smaller image size and faster deployment.
  • Version Compatibility: Tested with v5.0.18, should be compatible with any 5.0.x version, and likely future 5.x.x releases.

🐳 Docker Image Overview

Base Image

dunglas/frankenphp:x.x-phpx.x-x: A lightweight, fast, and secure alternative to traditional Apache/Nginx + PHP-FPM stacks.

PHP Extensions

Installed via mlocati/docker-php-extension-installer:

gd, mysqli, zip, redis, exif, gmp, brotli, bcmath, tidy, intl, imagick, igbinary

PHP Configuration

The production php.ini plus rootfs/usr/local/etc/php/conf.d/application.ini: - memory_limit=512M, max_execution_time=120, upload_max_filesize=10M, post_max_size=12M, max_input_vars=5000. - OPcache with a 256M cache, huge code pages, timestamp validation every 60s (Invision invalidates its own datastore files). - The tracing JIT is enabled with a 64M buffer. PHP 8.3 ships opcache.jit=tracing but opcache.jit_buffer_size=0, so the JIT is off unless you opt in; this image opts in. The buffer is virtual address space filled on demand, so it costs about 3MB of resident memory, not 64. Expect a few percent at most - Invision spends its time in MySQL, string building and IO rather than in compute the JIT can accelerate. Size the buffer from opcache_get_status()['jit']['buffer_free'] on your own site. This is a ZTS build under a threaded server, a combination with a history of JIT bugs, so opcache.jit=disable is the first thing to try if FrankenPHP ever segfaults without an obvious cause. - PHP sessions stored in Valkey over the UNIX socket, igbinary-serialised and zstd-compressed. Invision installs its own session handler for the community itself, so this path serves the installer and upgrader. - disable_functions covers every function Invision's ACP flags as dangerous when enabled, expose_php off. - ImageMagick is Invision's default image suite, and it is built with OpenMP: every operation starts a thread pool sized to the CPU count. Inside FrankenPHP, which already runs PHP in many threads, that pool oversubscribes the CPU, and its runtime state is not safe when a call arrives on a thread other than the one that initialised it - hangs and segfaults that take the whole server process down. imagick.set_single_thread=1 caps ImageMagick at one thread per operation; parallelism comes from PHP's threads instead. It is the extension default since imagick 3.7 and is pinned here so it cannot regress. Do not turn it off.

Process Management

  • s6-overlay is PID 1 and supervises three services, started in order: valkey, then frankenphp, then invision-task. Each is listed in the user bundle and each depends on s6-overlay's base bundle, as upstream recommends, so nothing starts before the container is initialised.
  • Privileges: the supervisor runs as root, as s6-overlay requires; each service drops to www-data (uid 33) with s6-setuidgid before starting. FrankenPHP, Valkey and every PHP task run as that user, so all files Invision writes share one owner. The cap_net_bind_service file capability the base image puts on the FrankenPHP binary is removed, since port 8080 does not need it; that is what makes --cap-drop ALL possible.
  • Bounded shutdown: Caddy stops waiting for in-flight connections after 4 seconds and s6 stops waiting for the FrankenPHP process after 5 seconds, so docker stop completes within Docker's default 10-second timeout even while a PHP request is running. Such a request is cut at shutdown. The task runner forwards SIGTERM to the task it is running rather than orphaning it, and every service carries a timeout-down so s6-rc never waits indefinitely for one to stop.
  • Logs: every service writes to the container log. s6-overlay logs each service start and stop; set S6_VERBOSITY=1 to keep only its warnings.
  • Healthcheck: a HEAD /uploads/index.html request against the local web server.

⚙️ Services Configuration

🧭 FrankenPHP (with Caddy)

Protect the /admin directory using HTTP Basic Auth. Configure credentials through environment variables: - ADMIN_DIR_BASICAUTH_USERNAME - ADMIN_DIR_BASICAUTH_PASSWORD_HASH (argon2id hash)

Generate the password hash easily with the caddy hash-password --algorithm argon2id command.

The Caddyfile is written for a container that sits behind a Cloudflare Tunnel (cloudflared) and is not reachable any other way: - X-Forwarded-For is rewritten from the Cloudflare-verified CF-Connecting-IP header, and the other client-controlled IP headers (Client-IP, X-Client-IP, X-Cluster-Client-IP, Proxy-User) are dropped. - Enable Trust IP addresses provided by proxies? in the ACP so Invision uses that address for bans, flood control and logs. Without the rewrite above, that setting would let visitors pick their own IP address. - Do not publish port 8080 beyond the network cloudflared uses: the X-Forwarded-* and CF-* headers are trusted as-is.

🧠 Valkey (Redis)

  • Used for PHP session storage and Invision Community caching.
  • Communicates via UNIX socket (/data/valkey-server.sock), no TCP ports exposed for improved security and performance.
  • No persistence (save ""), 512 MB maxmemory with allkeys-lru eviction: everything in it is a cache and is rebuilt after a restart.
  • The "memory overcommit must be enabled" warning Valkey prints at start is harmless here, since no background save ever happens.

⏱️ Invision Community Background Tasks

  • An s6 service runs Invision's task runner every minute, exactly like the crontab entry Invision documents; its output goes to the container log.
  • Set the INVISION_TASK_TOKEN environment variable using the token provided by the Invision admin panel.
  • In the ACP, set the task method to cron: that is Invision's name for any external scheduler.
  • Each run is bounded at 1 GB of memory and 15 minutes of wall clock. Without a memory ceiling a runaway task pushes the container into the OOM killer, which is free to pick FrankenPHP rather than the task that caused it. The 15-minute ceiling matches the point at which Invision itself treats a task as dead and unlocks it, so nothing that is still making progress is ever cut short - but one wedged run can no longer stop every task forever.

🌐 Pull from Registry

🐙 Docker image available at: GitLab Container Registry

🔨 Build & Run Locally

1️⃣ Build the Docker Image

docker build -t invision-community .

Optional build arguments:

Argument Default Description
LOCALES en_US fr_FR System locales to generate (space-separated, UTF-8), one for each Invision language pack used
LANG C.UTF-8 Default locale of the container, can also be overridden at runtime with -e LANG=fr_FR.UTF-8
docker build --build-arg LOCALES="en_US fr_FR de_DE" --build-arg LANG=fr_FR.UTF-8 -t invision-community .

2️⃣ Run the Container

docker run -d \
  --cap-drop ALL --cap-add SETUID --cap-add SETGID --cap-add KILL \
  --security-opt no-new-privileges \
  -e ADMIN_DIR_BASICAUTH_USERNAME=YourUserName \
  -e ADMIN_DIR_BASICAUTH_PASSWORD_HASH='{your_argon2id_hash}' \
  -e INVISION_TASK_TOKEN='{your_task_token}' \
  -v /your/invision/app:/app/public \
  invision-community

The capability and no-new-privileges flags are verified with this image: SETUID and SETGID let s6 drop each service to www-data, KILL lets it signal them at shutdown, nothing else is needed.

Replace /your/invision/app with the path to your Invision Community installation. /app/public is the path where Invision Community should be installed.

Invision runs as www-data (uid 33) inside the container, so the mounted tree must belong to that uid:

chown -R 33:33 /your/invision/app

Upgrades made through the ACP keep the right owner by themselves. Run the chown again after copying files into the tree from the host, or extract packages inside the container instead: docker exec -u www-data <container> tar -xzf /tmp/package.tar.gz -C /app/public.

3️⃣ Visit the Website

http://IP-OF-CONTAINER:8080

🛡️ Security Considerations

🔐 Disabled PHP Functions

disable_functions = exec,system,passthru,popen,proc_open,shell_exec

🔐 Admin Protection (Basic Auth)

Set via Caddy using basic_auth with secure argon2id-hashed passwords.

🔐 Web Server Hardening

Caddy reads no .htaccess, so rootfs/etc/frankenphp/Caddyfile carries these rules itself: - PHP is never executed under /uploads/; /uploads/logs/ and /datastore/ are never served. - Dotfiles, conf_global.php, error_log files, editor/backup leftovers, logs, SQL dumps, .ini/.yml/.env files, keys, certificates and shell scripts are never served; archives are never served outside /uploads/. - Every response carries X-Content-Type-Options: nosniff. Invision sets its own security headers in PHP, but Caddy serves static files itself, so those responses would otherwise carry none. - Files under /uploads/ additionally get a sandboxing Content-Security-Policy, which neutralises scripts in any HTML or SVG that lands there. - Requests for missing static assets get a plain 404 instead of booting Invision; long-lived cache headers are only sent for files that exist.

👤 Unprivileged Services

Only the s6 supervisor runs as root. FrankenPHP, meaning Caddy and PHP, Valkey and the task runner all run as www-data, so a compromise of PHP is confined to the Invision tree, the Valkey socket and /tmp: no rewriting of the web server configuration or binaries, no raw sockets on the Docker network, no signals to other services. It does not protect the forum itself: the web user must own the whole Invision tree because the ACP upgrader writes code, so database credentials and content remain reachable from PHP.

🧼 Environment Variables

Variable Description
ADMIN_DIR_BASICAUTH_USERNAME Basic Auth username protecting /admin
ADMIN_DIR_BASICAUTH_PASSWORD_HASH Basic Auth argon2id hash protecting /admin
INVISION_TASK_TOKEN Invision cron key used by the background task loop
LANG Container locale, e.g. fr_FR.UTF-8 (must be generated at build time via LOCALES)
S6_VERBOSITY s6-overlay log level, default 2 (service start/stop); 1 keeps only warnings

📚 Advanced Topics

🌍 Locales

Invision calls setlocale() with the locale declared by each language pack (for example fr_FR.UTF-8) for date and number formatting. That locale must exist in the image, so generate every locale you need with the LOCALES build argument. C.UTF-8 is always available.

🔄 Data Storage and Caching (Redis)

Point Invision at Valkey in ACP > System > Advanced Configuration > Data Storage. That page has three independent controls, and all three matter - enabling only one is the common mistake:

ACP field Constant Invision's default What it covers
Data storage method STORE_METHOD FileSystem The datastore: settings, compiled templates, application caches
Caching method CACHE_METHOD None \IPS\Data\Cache, including the guest page cache
Enable Redis REDIS_ENABLED FALSE Sessions and topic view counters

Set the server to the UNIX socket path and the port to 0:

/data/valkey-server.sock

The ACP does not write these itself: it generates a constants.php for you to download and upload to the site root. Check what is actually in effect with:

docker exec <container> grep -E 'STORE_METHOD|CACHE_METHOD|REDIS_ENABLED|REDIS_CONFIG' /app/public/constants.php

STORE_METHOD is the one that matters most here. Left on FileSystem, Invision writes datastore/*.php and reads them back with require(), so they live in OPcache. Invision calls opcache_invalidate() when it rewrites one, but the task runner is a separate CLI process with no OPcache, so its invalidations never reach the web server: anything a background task writes to the datastore is stale for up to opcache.revalidate_freq (60s), and OPcache churns on hundreds of constantly-rewritten entries. On Redis the problem does not exist.

🎛️ Tuning

FrankenPHP starts two PHP threads per CPU core by default, each bounded by memory_limit=512M, and every thread holds one MySQL connection while it serves a request. FrankenPHP's own sizing rule is num_threads x memory_limit < available memory: with the defaults that is 1 GB of RAM per CPU core, so on a small host lower memory_limit or set num_threads explicitly. Otherwise change it only when MySQL's max_connections cannot cover it, or when requests queue while the CPU is idle, via the global frankenphp block at the top of rootfs/etc/frankenphp/Caddyfile (max_threads auto derives its ceiling from memory_limit and the available memory):

{
    frankenphp {
        num_threads 16 # example for 8 cores: the default is already 2 x the CPUs the container may use
        max_threads auto # optional: add threads on demand above num_threads (default: none)
    }
}