Skip to main content

Docker Deployment

PRISM is distributed as a container image with Chrome and all dependencies pre-installed.

Image​

Images are published to the Trident registry:

docker.trident-cache.com/prism:v1.4.0

Tags carry the v prefix, matching the release tag: v1.4.0, not 1.4.0. latest tracks the newest release. Both amd64 and arm64 are in the same multi-arch manifest, so Docker selects the right one for you.

docker pull docker.trident-cache.com/prism:v1.4.0
docker run --rm docker.trident-cache.com/prism:v1.4.0 --version
# trident-prism 1.3.3
note

PRISM is not published to ghcr.io. Earlier revisions of this page pointed there; GHCR answers a repository it does not host with unauthorized rather than not found, so the result looks like a credentials problem. If you see unauthorized, check the registry hostname before your login.

Quick Start​

docker run -d \
--name prism \
-p 4000:4000 \
-v ./config.toml:/etc/prism/config.toml:ro \
-v ./license.lic:/etc/prism/license.lic:ro \
docker.trident-cache.com/prism:v1.4.0

Docker Compose​

version: "3.8"

services:
prism:
image: docker.trident-cache.com/prism:v1.4.0
ports:
- "4000:4000"
- "4001:4001" # Admin API (optional, bind to localhost in production)
volumes:
- ./config.toml:/etc/prism/config.toml:ro
- ./license.lic:/etc/prism/license.lic:ro
environment:
- RUST_LOG=info
restart: unless-stopped
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"
shm_size: "1g" # Chrome needs shared memory for rendering
security_opt:
- no-new-privileges:true

:::info Chrome Sandbox in Docker PRISM runs Chrome with sandbox enabled. The Chrome sandbox requires user namespaces, which need one of these Docker configurations (in order of preference):

  1. Docker 23.0+ with host kernel kernel.unprivileged_userns_clone=1 (most secure — no extra caps needed):

    security_opt:
    - no-new-privileges:true

    Most modern hosts (Ubuntu 18.04+, Fedora 31+, Debian 12+) have this enabled by default.

  2. Custom seccomp profile allowing clone and unshare syscalls (avoids SYS_ADMIN):

    security_opt:
    - no-new-privileges:true
    - seccomp:chrome-seccomp.json

    See the Chromium seccomp profile for reference.

  3. SYS_ADMIN capability (least preferred — broadens container privileges):

    security_opt:
    - no-new-privileges:true
    cap_add:
    - SYS_ADMIN

    Avoid seccomp:unconfined — it disables all seccomp filtering and significantly weakens container isolation. :::

Configuration​

The container's entrypoint is prism --config /etc/prism/config.toml. To use a different path, override the command (e.g. command: ["prism", "--config", "/path/to/config.toml"]).

The license path is set inside the config file ([license] path = "..."), not via an environment variable.

Environment Variables​

VariableDescriptionDefault
RUST_LOGLog level (trace, debug, info, warn, error) — overrides [logging] levelinfo

Volume Mounts​

Container PathPurpose
/etc/prism/config.tomlConfiguration file (path passed via --config)
/etc/prism/license.licLicense file (path set in [license] path of config)

Shared Memory​

Chrome requires shared memory (/dev/shm) for rendering. Docker's default 64MB is insufficient and will cause Chrome to crash. Either:

  • Set shm_size: "1g" in your compose file (recommended)
  • Mount the host's shared memory: -v /dev/shm:/dev/shm

Resource Limits​

Recommended minimums:

ResourceMinimumRecommended
Memory1 GB2 GB
CPU1 core2 cores
Shared Memory512 MB1 GB

Each Chrome tab uses approximately 50-150 MB of memory depending on page complexity. With the default 8 tabs, plan for at least 1.5 GB for Chrome alone.

Health Check​

The shipped image already defines one, probing /ready every 30 seconds. If you override it, keep both of those.

services:
prism:
# ...
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:4001/ready"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s

Probe /ready, not /health. /health does a real CDP round-trip — it creates a page in the browser and closes it — which is a reasonable occasional liveness signal and a poor thing to do on a short timer, because it competes with actual renders for the browser. /ready reads atomics only.

start_period should cover Chrome launch. Sixty seconds is comfortable; a cold or loaded host can exceed the default retry grace and mark a perfectly healthy container unhealthy.

:::warning A health check that hangs can wedge the whole host

This is worth understanding rather than copying, because it is a property of Docker and containerd, not of PRISM — any container with a slow probe hits it.

A health check that answers before its timeout costs nothing: the docker exec exits and is reaped. A health check that outlives its timeout is killed, and on Docker 29.2.1 / containerd 2.2.1 every kill strands that exec's state on the /run tmpfs, where nothing ever reaps it:

Leaked per killed execLocationExhausts
<exec-id>.pid/run/containerd/io.containerd.runtime.v2.task/moby/<container>/space — a 4 KB tmpfs page each
<exec-id>-stdout, <exec-id>-stderr/run/docker/containerd/<container>/inodes — FIFOs use no blocks

Five clean execs leak nothing; five killed execs leak five. So a probe that hangs is self-reinforcing — it times out, the kill strands three inodes, and it repeats on the next interval, forever.

One host reached 198k pid files — at a page each, the whole 776 MB of /run — alongside 396k FIFOs holding 41% of the inode table. Once /run is exhausted, containerd cannot write container init state and no container on the machine can start, including ones with nothing to do with PRISM:

OCI runtime create failed: unable to store init state: no space left on device

That points at a disk which may be half empty, because the exhausted filesystem is the /run tmpfs. Check both dimensions — space and inodes — since the FIFOs fill the second without touching the first:

df -h /run # space: the .pid files
df -i /run # inodes: the FIFOs

The interval is not the fix. A longer one paces the leak; it does not stop it. The fix is a probe that cannot hang, which is why the shipped image polls /ready (atomics only) rather than /health.

To clear an existing backlog, the cheapest and safest move is to recreate the affected container — removing it makes the daemon drop its entire state directory, FIFOs included, with no risk of deleting something live.

Where that is not an option, clean up in place. Both paths must be swept; a script that handles only the .pid files leaves the larger, inode-consuming half behind:

#!/bin/sh
# Reap exec state stranded by killed health checks.
# -mmin +60 leaves any exec still in flight alone.
set -eu

# .pid files — `init.pid` is the live container process, never remove it.
find /run/containerd/io.containerd.runtime.v2.task/moby \
-mindepth 2 -maxdepth 2 -type f -name '*.pid' \
! -name 'init.pid' -mmin +60 -delete 2>/dev/null || true

# stdio FIFOs — the container's own stdio is named after the container ID,
# so skip any FIFO whose name starts with the directory it sits in.
for d in /run/docker/containerd/*/; do
id=$(basename "$d")
find "$d" -maxdepth 1 -type p -mmin +60 \
! -name "$id-*" -delete 2>/dev/null || true
done

Run it hourly from a systemd timer or cron. Note that it deletes by age only — it cannot tell a stranded FIFO from a live one, which is why the container-ID exclusion and the 60-minute floor both matter. :::

Networking​

In production, bind the admin API port (4001) to localhost only:

ports:
- "4000:4000" # Proxy - exposed to reverse proxy
- "127.0.0.1:4001:4001" # Admin API - localhost only

If PRISM and your origin run in the same Docker network:

services:
prism:
image: docker.trident-cache.com/prism:v1.4.0
volumes:
- ./config.toml:/etc/prism/config.toml:ro
- ./license.lic:/etc/prism/license.lic:ro
shm_size: "1g"
networks:
- app

spa:
image: your-spa:latest
networks:
- app

networks:
app:

Then set the origin in your config to the service name:

[server]
origin = "http://spa:3000"