Docker (SQLite)
DefaultFor Homelab, small teams, evaluation, proof-of-concept
One image. SQLite bundled, the scheduler started beside the brain. No env vars required.
Stack
Sources
Current release: v1.12.1 (released 2026-10-05)
Image: z4jdev/z4j:1.12.1 (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: 3 services
z4j ships one image for z4j. Backend and dashboard are bundled. There is no separate frontend container.
z4j
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.
scheduler-certs
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.
scheduler
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.
Install
Start the deployment
# 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
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
python manage.py z4j_doctor Flask
python -m z4j_flask doctor FastAPI
python -m z4j_fastapi doctor Bare / Celery / RQ / etc.
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:
# 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)
Z4J_ALLOWED_HOSTSenv - pins the list, replaces auto-detect--allowed-hostCLI flag - additive, repeatable~/.z4j/allowed-hostsfile - SQLite only when env is unset- 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 Engine with Compose v2.24 or newer (the Caddy overlay uses the !reset tag)
- Port 7700 bound to localhost by default (reverse-proxy for public access)
- Persistent volume for SQLite database and persisted secrets
- Persistent volume for the scheduler channel's certificates (z4j_scheduler_pki)
SQLite, stored in the z4j_data named volume
Single-container deployment; capacity depends on workload and host
Decision helper
Is this the right tier for you?
Use this when
- First-time evaluation: clone the repo, docker compose up, done
- Internal tools for a team of 2 to 20 developers
- Homelab Docker Compose stacks (Synology, Unraid, TrueNAS)
- Customer demos and sales engineering POCs
- Single-instance production where ops simplicity wins over scale
Not ideal when
- You already run Postgres and want central backups
- You need brain-side horizontal scaling
- Concurrent write-heavy administration that requires multi-writer scale-out
What ships
Capabilities in this tier
- All 6 engines supported
- All 3 framework adapters
- Full dashboard UI
- RBAC and audit log
- HMAC wire protocol
- Auto-migrations on boot
- Auto-generated + persisted secrets
- Horizontal brain replicas
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.
Upgrade path
How to move up a tier
Start a new Postgres installation when you outgrow SQLite. Changing Z4J_DATABASE_URL selects the backend and runs schema migrations, but it does not transfer settings, projects, or audit history.
Install adapters
Works with every engine and framework
Framework adapters
Django
Django AppConfig integration, zero boilerplate.
Learn more
Flask
Flask extension pattern. One line to install.
Learn more
FastAPI
Lifespan-hook integration for async stacks.
Learn morePlain Python
No web framework? One call at process boot.
Learn moreEngine adapters
Celery
The industry standard, covered end-to-end.
Learn more
RQ
Lightweight Redis queue, fully instrumented.
Learn more
Dramatiq
Middleware-driven Dramatiq observability.
Learn more
Huey
Lightweight Redis/SQLite queue, first-class.
Learn more
arq
Async Redis queue for FastAPI-era Python.
Learn more
taskiq
Broker-agnostic async task framework.
Learn moreOther deployments
Compare with
Ready to run z4j with Docker (SQLite)?
Copy the install command above, run it, and open the dashboard on port 7700.