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:
pearton127.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-portalon127.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-renderwith noports, reachable only from therendernetwork, which has no route out. It runsread_only, with a size-capped tmpfs,cap_drop: ALL, no-new-privileges and a memory limit. This is the same isolation the systemd unit'sPrivateNetwork/IPAddressDenyprovides, 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.