Skip to main content

Docker (Postgres)

Production

For Medium and larger businesses, regulated environments, compliance-sensitive teams

Same image, external Postgres. Horizontal-scale ready.

Small business Medium business Enterprise

Sources

Current release: v1.6.5 (released 2026-05-27)

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

Multi-arch: linux/amd64, linux/arm64. The SAME image handles SQLite and Postgres - the database is selected at runtime via Z4J_DATABASE_URL.

Architecture

What runs: 2 services

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

1

z4j-brain

z4jdev/z4j:latest

Same image as the default. Bundles backend plus dashboard. Connects to external Postgres via Z4J_DATABASE_URL.

2

z4j-postgres

postgres:18-trixie

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

Install

Get running in minutes

bash
# Two services. Same z4jdev/z4j image as the default compose; it auto-
# switches to Postgres because Z4J_DATABASE_URL is set. No build required.

git clone https://github.com/z4jdev/z4j.git && cd z4j
cp .env.example .env

# Edit .env and fill in your secrets:
#   POSTGRES_PASSWORD=<long random>
#   Z4J_SECRET=$(openssl rand -hex 32)
#   Z4J_SESSION_SECRET=$(openssl rand -hex 32)
#   Z4J_PUBLIC_URL=https://z4j.yourdomain.com
#   Z4J_ALLOWED_HOSTS=z4j.yourdomain.com

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

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

# 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 the brain is running, open http://localhost:7700 and sign in.

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 round-trip, --json for scripts. Catches the common www-data-can't-write-$HOME startup failure (the agent now auto-relocates the buffer to $TMPDIR/z4j-{uid} and logs a single WARNING). See service-user deployments.

Hostname / domain access

Reach the brain via any name you want

The brain validates the HTTP `Host:` header on every request to defend against cache-poisoning. Auto-detect handles the common cases; the `z4j allowed-hosts` CLI handles the rest.

What's auto-allowed (no config)

  • 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)

Adding a custom domain (one-time, persisted)

For a public DNS name (reverse proxy, Cloudflare Tunnel, internal LB), persist it once via the CLI:

bash
z4j allowed-hosts add tasks.example.com
z4j allowed-hosts list                  # show current set
z4j allowed-hosts remove old.example     # idempotent
z4j allowed-hosts path                   # ~/.z4j/allowed-hosts
# Restart `z4j serve` to pick up the change.

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 - persistent, additive
  4. Auto-detect - localhost + hostname + FQDN + LAN IPs

Security

Rejected requests get a generic 400 in production mode (no internal hostname leakage). Dev mode shows a verbose response with the rejected host and a fix command. Operators always get the verbose detail in the server log via request_id correlation.

Requirements

  • Docker Compose v2+ or Kubernetes
  • PostgreSQL 17 or newer (18+ recommended for 3x I/O improvements)
  • Reverse proxy with TLS (Caddy, nginx, Traefik) or cloud load balancer
  • Secrets management (env, Vault, Sealed Secrets, etc.)
Database

PostgreSQL 17+ (18.3+ recommended)

Scale envelope

1000+ agents, 5000+ events/second, multiple brain replicas behind a load balancer

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
  • Compliance regimes (SOC 2, HIPAA, ISO 27001) that require Postgres
  • Kubernetes stacks with a Helm chart on the roadmap

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

The brain 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.

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.