Pip (SQLite)
Pure Python. No container runtime required.
Best for
Solo developers, homelab, local dev, bare-metal servers, CI
Three deployment paths, one platform. Every tier runs the same dashboard, the same six engines and the same audit surface. Postgres is what adds replicas, full-text search and partitioned history.
Pure Python. No container runtime required.
Best for
Solo developers, homelab, local dev, bare-metal servers, CI
One image. SQLite bundled, the scheduler started beside the brain. No env vars required.
Best for
Homelab, small teams, evaluation, proof-of-concept
Same image, external Postgres. Horizontal-scale ready.
Best for
Teams that already operate Postgres and want central backups, replicas, and retained history
Side by side
| Capability | Pip (SQLite) | Docker (SQLite) | Docker (Postgres) |
|---|---|---|---|
| Container runtime required | None (pure Python) | Docker | Docker + Postgres |
| Database | SQLite | SQLite (persisted volume) | PostgreSQL 17+ |
| Services to run | 1 process | 3 containers (z4j, scheduler-certs, scheduler) | 4 services (5 containers: Postgres, z4j, scheduler-certs, two scheduler replicas) |
| Topology | Single host | Single container | Postgres-backed replicas |
| Capacity guidance | Benchmark workload | Benchmark workload | Benchmark workload |
| Horizontal scaling | |||
| Full-text search | |||
| Range-partitioned events | |||
| Auto-migrations | |||
| Auto-generated secrets | |||
| Central backup and point-in-time recovery |
Lightest
Pure Python. No container runtime required.
# Install z4j + umbrella (dashboard, backend, SQLite, all bundled)
pip install z4j
# Start it. First boot auto-mints HMAC secrets, runs migrations, and prints
# a one-time setup URL to stderr. Open the URL to create the first admin.
# z4j auto-detects the server's hostname, FQDN, and LAN IPs for Host-header
# validation. Dev mode still binds to loopback and refuses a LAN bind.
z4j serve
# Open http://localhost:7700 on that host, or use an SSH port forward.
# For a public reverse proxy, configure production explicitly. The persistent
# allowed-hosts file is only for local SQLite names when the env var is unset.
export Z4J_ENVIRONMENT=production
export Z4J_PUBLIC_URL=https://tasks.example.com
export Z4J_ALLOWED_HOSTS='["tasks.example.com"]'
z4j serve
# Useful CLI commands:
z4j check # config + DB reachable (does NOT verify schema head)
z4j migrate current --check-heads # exits non-zero unless the schema IS at head
z4j status # version, DB URL, user/project/agent counts
z4j createsuperuser # provision an admin without the setup URL
z4j changepassword # reset a user password
z4j migrate upgrade head # run alembic migrations explicitly
z4j audit verify # verify the HMAC-chained audit log
z4j reset-setup --force # clear pending pre-first-boot tokens; restart z4j serve to mint a new URL
# Run it as a service: hardened systemd units for the brain and the standalone
# scheduler ship in the sdist under deploy/systemd/, with example env files. z4j serves the FastAPI API, the WebSocket agent gateway, and the React dashboard from a single process. SQLite lives on local disk.
Default
One image. SQLite bundled, the scheduler started beside the brain. No env vars required.
# The default. One file. SQLite bundled in the image.
git clone https://github.com/z4jdev/z4j.git && cd z4j
docker compose up -d
# Tail logs for the first-boot admin setup URL.
docker compose logs -f z4j
# Or skip interactive setup with bootstrap env vars in .env:
# Z4J_BOOTSTRAP_ADMIN_EMAIL=you@example.com
# Z4J_BOOTSTRAP_ADMIN_PASSWORD=<long random>
# Add Caddy auto-HTTPS on top after putting Z4J_DOMAIN and
# Z4J_ACME_EMAIL in a root .env file:
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d z4jdev/z4j:latest One image. Bundles the FastAPI backend, the React dashboard, and the SQLite driver. Auto-generates secrets on first boot, persists them to the z4j_data volume, auto-runs migrations. Exposes port 7700.
z4jdev/z4j:latest One-shot. Mints a private CA, the brain's server certificate and the scheduler's client certificate into the z4j_scheduler_pki volume, then exits. Idempotent: a valid bundle is left alone and renewed only within 30 days of expiry.
z4jdev/z4j:latest The same image running z4j-scheduler serve. Dials the brain's gRPC channel over mutual TLS on the private Compose network and fires the schedules the brain holds. One replica with the single leader backend.
Production
Same image, external Postgres. Horizontal-scale ready.
# Four 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:
# Z4J_BOOTSTRAP_ADMIN_EMAIL=you@example.com
# 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 z4jdev/z4j:latest Same image as the default. Bundles backend plus dashboard. The shipped Compose file supplies structured Z4J_DATABASE_* connection fields.
postgres:18.6 (digest-pinned) Your primary datastore. Holds events, tasks, schedules, users, HMAC-chained audit log, and partitioned event history.
z4jdev/z4j:latest One-shot. Mints a private CA, the brain's server certificate and the scheduler's client certificate into the z4j_scheduler_pki volume, then exits. Idempotent: a valid bundle is left alone and renewed only within 30 days of expiry.
z4jdev/z4j:latest Two replicas of the same image running z4j-scheduler serve, electing a leader through the Postgres advisory lock; the standby takes over when the leader stops. --scale scheduler=N adds more.
HTTPS setup
z4j does not bundle a reverse proxy. Pick the TLS pattern that matches your existing infra.
One extra compose file. Auto-HTTPS via Let's Encrypt.
Homelab defaultAlready running Traefik? Add seven labels to z4j service.
Zero public ports. TLS terminates at Cloudflare's edge.
Classic, familiar, works everywhere. On-host or in-container.
Next step
After z4j is running, drop the agent package into your Django, Flask, or FastAPI app. One pip install plus three environment variables.
Django AppConfig integration, zero boilerplate.
pip install z4j-django[celery]Learn more
Flask extension pattern. One line to install.
pip install z4j-flask[celery]Learn more
Lifespan-hook integration for async stacks.
pip install z4j-fastapi[arq]Learn more
No web framework? One call at process boot.
pip install z4j-bare z4j-celeryLearn more
Pick your engine
Install one or more engine adapters alongside your framework. Every deployment tier supports all six.
The industry standard, covered end-to-end.
Learn more
Lightweight Redis queue, fully instrumented.
Learn more
Middleware-driven Dramatiq observability.
Learn more
Lightweight Redis/SQLite queue, first-class.
Learn more
Async Redis queue for FastAPI-era Python.
Learn more
Broker-agnostic async task framework.
Learn moreArchitecture FAQ
Answers to the common confusions about z4j's container layout.
No. z4j image bundles the FastAPI backend and the compiled React dashboard together. The backend serves the dashboard as static HTML, CSS, and JS from the same process. There is no second container for the UI.
No. Both Docker tiers use the same z4jdev/z4j image. The runtime picks SQLite or Postgres based on the Z4J_DATABASE_URL environment variable.
Tags pin versions, not modes. Use :latest for the most recent release, or pin the immutable release tag from the release notes for reproducible deploys. Multi-arch images are published for linux/amd64 and linux/arm64.
Four. z4j-postgres runs Postgres, z4j runs the backend and dashboard from one image, scheduler-certs is a one-shot that mints the mTLS material, and scheduler runs two replicas electing a leader through Postgres. The SQLite stack is three: the same without Postgres and with one scheduler replica. No separate frontend.
No. z4j communicates with your agents over a direct WebSocket. Your task queue engine uses whatever broker it already uses (Redis, RabbitMQ, SQS). z4j observes, it does not re-broker.
Usually yes. Run z4j migrate downgrade to the previous head with the newer image, then bump the tag down. Most revisions walk back. A few refuse a downgrade conditionally (a channel config row that no configured secret decrypts, members in a role the older release lacks, an SQLite library without column drops), and the activation revisions refuse outright because the older schema cannot represent that state. The upgrade and rollback page lists them; across a refusal the way back is the backup you took before the upgrade.
You can switch backends, but changing Z4J_DATABASE_URL does not copy data. It starts a new Postgres installation while your users, audit chain, and schedules remain in SQLite. Keep the SQLite volume and use the API or audit export for records you must retain.
Solo developer or homelab: start with pip. Team of 2-20: default z4j compose. Anyone who already runs Postgres, or who needs central backups and replicas: z4j + Postgres.