Skip to main content

Docker (Postgres)

Production

For Teams that already operate Postgres and want central backups, replicas, and retained history

Same image, external Postgres. Horizontal-scale ready.

Small business Medium business Platform teams

Stack

Docker image PostgreSQL 17+

Sources

Current release: v1.10.0 (released 2026-08-28)

Image: z4jdev/z4j:1.10.0 (pin version) or z4jdev/z4j:latest (track current)

Multi-arch: linux/amd64, linux/arm64. The SAME image handles SQLite and Postgres. The shipped PostgreSQL Compose file supplies structured Z4J_DATABASE_* fields; custom deployments can use those fields or Z4J_DATABASE_URL.

Architecture

What runs: 2 services

z4j ships one image for z4j. Backend and dashboard are bundled. There is no separate frontend container.

1

z4j

z4jdev/z4j:latest

Same image as the default. Bundles backend plus dashboard. The shipped Compose file supplies structured Z4J_DATABASE_* connection fields.

2

z4j-postgres

postgres:18.6 (digest-pinned)

Your primary datastore. Holds events, tasks, schedules, users, HMAC-chained audit log, and partitioned event history.

Install

Start the deployment

bash
# Two services. Same z4jdev/z4j image as the default compose. The shipped
# file selects Postgres with structured Z4J_DATABASE_* values. No build required.

git clone https://github.com/z4jdev/z4j.git && cd z4j
umask 077
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 48)
Z4J_SECRET=$(openssl rand -hex 48)
Z4J_SESSION_SECRET=$(openssl rand -hex 48)
Z4J_AUDIT_CHAIN_SECRET=$(openssl rand -hex 48)
Z4J_PUBLIC_URL=https://z4j.yourdomain.com
Z4J_ALLOWED_HOSTS=["z4j.yourdomain.com"]
EOF

docker compose -f docker-compose.postgres.yml up -d

# Capture the first-boot setup URL from z4j logs:
docker compose -f docker-compose.postgres.yml logs -f z4j

# Or skip interactive setup entirely with bootstrap env vars in .env:
#   [email protected]
#   Z4J_BOOTSTRAP_ADMIN_PASSWORD=<long random>

# Layer Caddy auto-HTTPS on top:
docker compose -f docker-compose.postgres.yml -f docker-compose.caddy.yml up -d

After z4j is running, open its configured Z4J_PUBLIC_URL. The loopback Compose default is http://localhost:7700.

Verify

Confirm everything is wired up

Each framework adapter ships a `doctor` command that probes brain reachability, TLS, WebSocket upgrade, and the on-disk buffer path. Run it as the same user the service runs under.

Django

bash
python manage.py z4j_doctor

Flask

bash
python -m z4j_flask doctor

FastAPI

bash
python -m z4j_fastapi doctor

Bare / Celery / RQ / etc.

bash
python -m z4j_bare doctor

Exits 0 on all-green, 1 on any failure. Add --no-websocket to skip the WS probe, or --json for scripts. The WebSocket probe returns when the background runtime starts and does not wait for authentication, so confirm the agent's connected log and dashboard state too. The doctor catches the common www-data-can't-write-$HOME startup failure (the agent auto-relocates the buffer to $TMPDIR/z4j-{uid} and logs a WARNING). See service-user deployments.

Hostname / domain access

Configure accepted hostnames

z4j validates ordinary host-bearing HTTP requests. The two health endpoints, requests without Host, and agent WebSockets bypass this middleware, so keep equivalent validation at the reverse proxy.

Pip/SQLite auto-detect (env unset)

  • localhost, 127.0.0.1, [::1]
  • The system hostname + FQDN (incl. Tailscale's <host>.<tailnet>.ts.net)
  • Every LAN IP bound on the host (covers 192.168.x.x, Docker bridges, Tailscale)

Local name versus public domain

The persistent file is only read on SQLite when Z4J_ALLOWED_HOSTS is unset. Use it for a missed local hostname. For a public reverse proxy, Docker, or PostgreSQL deployment, configure the environment instead:

bash
# Pip/SQLite local hostname only, with Z4J_ALLOWED_HOSTS unset:
z4j allowed-hosts add tasks.lan

# Public or reverse-proxied deployment:
export Z4J_ENVIRONMENT=production
export Z4J_PUBLIC_URL=https://tasks.example.com
export Z4J_ALLOWED_HOSTS='["tasks.example.com"]'
# Restart the z4j service after changing its environment.

Precedence (highest first)

  1. Z4J_ALLOWED_HOSTS env - pins the list, replaces auto-detect
  2. --allowed-host CLI flag - additive, repeatable
  3. ~/.z4j/allowed-hosts file - SQLite only when env is unset
  4. Auto-detect - same SQLite/unset-env path only

Security

Rejected requests get a generic 400 by default in every environment. Local dev can opt into response detail with z4j serve --debug-host-errors; production refuses that flag. Operators always get the detail in the server log via request_id correlation.

Requirements

  • Docker Compose v2+ or Kubernetes
  • PostgreSQL 17 or newer (the shipped Compose file uses PostgreSQL 18)
  • Reverse proxy with TLS (Caddy, nginx, Traefik) or cloud load balancer
  • Secrets management (env, Vault, Sealed Secrets, etc.)
Database

PostgreSQL 17+ (the shipped Compose file uses PostgreSQL 18)

Scale envelope

Multiple brain replicas behind a load balancer; benchmark the intended workload

Decision helper

Is this the right tier for you?

Use this when

  • Self-hosted production deployments with audit requirements
  • Central Postgres with point-in-time recovery already in place
  • Teams with dedicated infrastructure or platform engineering
  • Environments where the database has to be a managed Postgres your platform team already backs up and restores
  • Kubernetes stacks (z4j ships no Helm chart for the brain; a plain Deployment + Service + Ingress works)

Not ideal when

  • You are evaluating. Start with the default compose, then migrate.
  • Single-developer homelab where Postgres is overkill
What ships

Capabilities in this tier

HTTPS

Put a TLS terminator in front

z4j image binds HTTP on port 7700. In production, route traffic through a reverse proxy that terminates TLS. z4j does not bundle one because your infrastructure likely already has one.

For a homelab with a public DNS name, the optional Caddy compose overlay shipped in the repo gives you auto-HTTPS via Let's Encrypt in about two minutes. Teams with existing Traefik, Cloudflare, or nginx plug z4j in with a few lines of config.

TLS setup guide
Upgrade path

How to move up a tier

In-place. Bump the z4jdev/z4j image tag. Migrations auto-run on boot. Take a backup first: once the current schema has run and the database holds z4j-native schedules, it cannot be migrated back down, so restoring the pre-upgrade backup is the only way back.

Install adapters

Works with every engine and framework

Framework adapters

Engine adapters

Other deployments

Compare with

Ready to run z4j with Docker (Postgres)?

Copy the install command above, run it, and open the dashboard on port 7700.