Skip to content

Container install

Docker Compose is the primary supported path. The compose.yaml in the repository is not a convenience wrapper: it carries security controls that must not be left to the operator, above all the rendering sidecar's network isolation.

Steps

git clone https://github.com/mentesan/peart.git
cd peart
cp config.example.yaml config.yaml
cp .env.example .env               # set PEART_RENDER_TOKEN to `openssl rand -hex 32`
mkdir -p secrets && touch secrets/master.key && sudo chown 65532:65532 secrets/master.key
docker compose up -d

For the container network, config.yaml's render.base_url must read http://peart-render:8090, so Compose's own DNS resolves the sidecar by service name. (config.example.yaml ships the systemd default, 127.0.0.1:8090, because the two topologies genuinely differ here.)

The chown to 65532 is the nonroot user baked into the distroless base image, the same user the peart and peart-portal processes run as. Without it the process cannot write the master key it generates on first boot; without pre-creating the file, Docker mounts an empty directory in its place instead of a file. The master key lives outside the data volume so a backup of that volume never contains it.

On first access PEART requires you to create the administrator and enrol the second factor. There is no default credential.

What the compose file encodes

Three services, deliberately not two:

  • peart on 127.0.0.1:8080, bound to loopback on purpose: put a TLS-terminating reverse proxy in front, or a VPN. It gates startup on the sidecar being healthy, which closes a real race where report generation would otherwise stay refused for the container's life.
  • peart-portal on 127.0.0.1:8081, a separate binary (its entrypoint is overridden so the portal can never reach an internal handler), with the database mounted read-only via the DSN and no mount for evidence or the key. Renditions and reports are mounted read-only.
  • peart-render with no ports, reachable only from the render network, which has no route out. It runs read_only, with a size-capped tmpfs, cap_drop: ALL, no-new-privileges and a memory limit. This is the same isolation the systemd unit's PrivateNetwork/IPAddressDeny provides, and it is not optional.

The one secret the internal process and the sidecar must share, PEART_RENDER_TOKEN, lives in a single .env entry both containers read, rather than being copied into config.yaml by hand.

Updating

See Upgrading for the pull-migrate-restart cycle; migrations are forward-only and the process refuses to start against an outdated database.