Deployment
Image tags
The image is psyb0t/chatz on Docker Hub, built for linux/amd64 and linux/arm64.
| Tag | What it is |
|---|---|
latest | The newest build of main. Every push to main that passes CI replaces it. |
sha-<commit> | The build of one main commit. |
v<semver> | A release. The release pipeline points it at the sha-<commit> image that already passed CI on main, without rebuilding. |
Deploy a v<semver> tag, or pin a digest, so a restart never picks up a different build. The CHANGELOG lists what each release changed.
The container must be started with the run command. The image’s default command prints the help text and exits.
SQLite or Postgres
| SQLite | Postgres | |
|---|---|---|
| Set | CHATZ_DB_DRIVER=sqlite | CHATZ_DB_DRIVER=postgres (default) and the CHATZ_DB_* connection settings |
| Data | One file, /data/chatz.sqlite by default, in a Docker volume | Your Postgres server |
| Fits | One chatz container on one host | Several chatz containers, networked storage, an existing Postgres |
SQLite rules, all checked at startup:
CHATZ_DB_SQLITE_PATHmust be an absolute path to a file directly under/data, with no?or#./datamust be a real, writable directory, not a symlink. The image creates it owned by the app user, and a fresh named volume copies that ownership.- An existing database file must be a regular file, not a symlink.
- Use a local volume. chatz opens one connection in WAL mode, which is not safe on networked filesystems or from two containers at once.
Switching drivers starts an empty database. chatz does not move data between them.
Upgrades and migrations
Pull the new tag and recreate the container. On start chatz runs any pending migrations before it serves requests, for both drivers. To run them on their own, for example before switching traffic:
docker run --rm \
--env-file .env \
--volume chatz-data:/data \
psyb0t/chatz:<tag> migrate
The volume is needed only for SQLite. migrate applies the migrations, logs the resulting version and exits. The admin readiness page shows the current migration version and whether the last migration left the database dirty.
Read the CHANGELOG before upgrading across several releases. To go back to an older release after its migrations ran, restore a backup taken before the upgrade.
Backups
chatz does not back itself up. Back up the SQLite file (stop chatz or use SQLite’s online backup) or dump Postgres with your usual tools.
After each successful backup, have the job write a marker file so the admin readiness page can show how fresh the last backup is. The file goes at CHATZ_BACKUP_STATUS_PATH, /data/chatz-backup-status.json by default, and holds exactly one JSON object:
{
"completedAt": "2026-01-31T03:00:00Z",
"driver": "sqlite"
}
completedAtis an RFC 3339 time, not in the future.drivermust matchCHATZ_DB_DRIVER.- No other fields, at most 4 KiB, and a regular file, not a symlink.
| State shown | Meaning |
|---|---|
current | The marker is valid and younger than CHATZ_BACKUP_MAX_AGE (24h). |
stale | The marker is valid but older than that. |
not_recorded | There is no marker file. |
invalid | The file breaks one of the rules above. |
unavailable | The file could not be read, or is a symlink or not a regular file. |
A bad or missing marker never breaks the page or the app.
TLS and reverse proxies
chatz speaks plain HTTP. The session cookie is not marked Secure, so run chatz behind a reverse proxy that terminates TLS, and keep the container port bound to 127.0.0.1 or a private network as the examples do.
The proxy must pass Server-Sent Events through as they arrive. Turn off response buffering for /api/v1/chats (in nginx, proxy_buffering off), and allow long responses: a turn may run up to CHATZ_UPSTREAM_TURN_TIMEOUT (5m) per model request, with tool rounds on top. Serve chatz on its own host name: the web app and API expect to be at the root path.
Hardening
The image runs as a non-root user and needs no write access outside /data, /tmp and the optional /app/chatz.log bind mount. The docker run command in Getting started and the repository’s docker-compose.yml apply:
- a read-only root filesystem with a small
noexectmpfs at/tmp; - all Linux capabilities dropped and
no-new-privileges; - memory, CPU and process limits (512 MB, one CPU, 256 processes);
- ports published on
127.0.0.1only, and Postgres on an internal network.
Keep the metrics (9091) and pprof (6060) ports unpublished, or turn them off by setting their addresses to empty values. Security covers the rest.
Health checks
GET /healthz returns 200 once the HTTP server is up, and is what the Compose health check calls. The image has BusyBox wget for that; it has no curl. Admins get a fuller picture from the readiness page; see Monitoring.