Skip to content

Docker

The official image is z4jdev/z4j on Docker Hub. Multi-arch: linux/amd64 and linux/arm64. Pin to :{pkg.latest} for reproducible deploys; use :latest for “always the newest stable”.

Sources

The z4jdev/z4j GitHub repo ships three compose files in the root. They use the same image - the runtime mode is selected by env vars, not by tag. Pick one:

Compose fileDatabaseTLSUse case
docker-compose.ymlSQLite (bundled in image, persisted to volume)noneEvaluation, homelab, single-team installs
docker-compose.postgres.ymlPostgreSQL 18 (sidecar)noneProduction self-host - small to large teams
docker-compose.caddy.yml(overlay, layer on either of the above)Caddy auto-HTTPSPublic-facing - get a real cert via Let’s Encrypt
Terminal window
git clone https://github.com/z4jdev/z4j.git
cd z4j
cp .env.example .env # fill Z4J_SECRET, Z4J_SESSION_SECRET
docker compose up -d
docker compose logs -f z4j-brain # capture the first-boot setup URL

That’s it. The brain auto-creates ~/.z4j/z4j.db inside the image’s volume, runs alembic to head, and prints the setup URL.

Recipe 2 - Production self-host (PostgreSQL)

Section titled “Recipe 2 - Production self-host (PostgreSQL)”
Terminal window
git clone https://github.com/z4jdev/z4j.git
cd z4j
cp .env.example .env # fill POSTGRES_PASSWORD + Z4J_SECRET, Z4J_SESSION_SECRET, Z4J_PUBLIC_URL, Z4J_ALLOWED_HOSTS
docker compose -f docker-compose.postgres.yml up -d
docker compose -f docker-compose.postgres.yml logs -f z4j-brain

The PostgreSQL sidecar persists data to the z4j-postgres-data named volume. Take regular pg_dump snapshots; see backups.

Layer the Caddy file on top of either compose stack:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
# or:
docker compose -f docker-compose.postgres.yml -f docker-compose.caddy.yml up -d

Caddy reads your domain from Z4J_PUBLIC_URL and provisions a Let’s Encrypt cert automatically. DNS A/AAAA records must point at the host first.

Quick smoke test on a VM with no compose installed:

docker run -d --name z4j-brain \
-p 7700:7700 \
-v z4j-data:/root/.z4j \
-e Z4J_SECRET=$(openssl rand -hex 32) \
-e Z4J_SESSION_SECRET=$(openssl rand -hex 32) \
z4jdev/z4j:1.6.5
docker logs -f z4j-brain   # capture setup URL

For Postgres add:

Terminal window
-e Z4J_DATABASE_URL=postgresql+asyncpg://z4j:pw@db:5432/z4j \

For TLS, set Z4J_PUBLIC_URL=https://... and put a reverse proxy in front (Caddy, nginx, Cloudflare Tunnel, Traefik). The brain speaks plain HTTP internally; X-Forwarded-Proto is respected.

  • Base: python:3.14-slim-trixie (Debian 13).
  • Multi-arch: linux/amd64, linux/arm64. Built on GitHub Actions native runners (no QEMU emulation).
  • Size: ~63 MiB compressed, ~250 MiB uncompressed.
  • Entry point: z4j serve (FastAPI via Uvicorn).
  • Signal handling: SIGTERM triggers graceful shutdown.

Migrations run automatically on container start (idempotent; safe on every boot). To run them manually:

Terminal window
docker exec -it z4j-brain z4j migrate upgrade head
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:7700/api/v1/health"]
interval: 30s
timeout: 5s
retries: 3

Structured JSON to stdout (one event per line). Aggregate with your log shipper (Loki, Vector, Fluentd, CloudWatch).

Watch stdout on first boot to capture the setup banner:

Terminal window
docker compose logs -f z4j-brain | grep -A 10 "first-boot setup"

Or skip the interactive setup entirely with bootstrap env vars:

Z4J_BOOTSTRAP_ADMIN_PASSWORD=<long random>

The brain provisions the admin and the setup banner is suppressed.

In-place. Bump the image tag, redeploy, restart. Migrations auto-run on boot.

Terminal window
docker compose pull
docker compose up -d

To roll back, redeploy the previous version tag. Schema changes are forward-compatible across patch versions but always test in staging first.