📖 This documentation is also published, web-native, at https://valthon.github.io/zigbase/docs/docker — the site is the canonical reading experience.
ZigBase has no native Windows build (the embedded HTTP server depends on facil.io/zap,
Linux/macOS only). The official Docker image, ghcr.io/valthon/zigbase, is the supported
path for Windows-hardware users and for anyone who prefers a Docker-first self-host
workflow on Linux/macOS too. It ships the same static-musl binary as the
release tarballs — no in-image compilation, no extra runtime deps.
docker run -d --name zigbase -p 8090:8090 -v zigbase_data:/data ghcr.io/valthon/zigbase:latestZero arguments beyond the standard docker run flags — the image's default command is
serve. Open http://localhost:8090/_/ and create a superuser:
docker exec zigbase /zigbase superuser create --email you@example.com --password 'change-me' --data-dir /dataA docker-compose.yml quick-start lives at the repo root; docker compose up brings up
the same thing with a named volume pre-wired.
- Base:
gcr.io/distroless/static-debian12:nonroot— no shell, no package manager, just the CA bundle and a numeric non-root user (uid/gid65532). The binary is already statically linked (musl); distroless is chosen overscratchspecifically because ZigBase's outbound TLS clients (SMTP/STARTTLS mail delivery, a Postgres backend withsslmode=require, OAuth2 token exchange, Sentry error reporting) load the system CA bundle from disk at connect time —scratchhas none, and every one of those integrations would fail TLS verification. If you don't use any of those integrations this doesn't affect you either way, but distroless costs nothing over scratch here. - User: runs as non-root uid
65532always. There is no way to run the container as root. - Data directory:
/data, baked in asZIGBASE_DATA_DIR=/dataand declared as aVOLUME. Mount a named volume (-v zigbase_data:/data, auto-created and owned correctly by Docker) or a bind mount — a bind mount from the host must be pre-created andchown 65532:65532'd, or the container will fail to writedata.db(this is the most common non-root-container support question; see below). - Network bind: the image sets
ZIGBASE_HTTP_HOST=0.0.0.0, overriding the binary's own secure-by-default loopback bind (127.0.0.1on bare metal). Inside a container this override is necessary — otherwise nothing outside the container's network namespace could reach it — and it does not weaken security: the container boundary plus whatever you pass to-p/--networkis the real access-control surface here, same as it would be for any other containerized server.
A framework-mode application that builds its own non-root image must reproduce the runtime
contract the official image supplies. Before switching to uid/gid 65532, create /data with
that ownership; otherwise a new named volume is mounted as root-owned and ZigBase cannot create
data.db:
Zig does not document an official registry image. Do not guess a builder such as
ghcr.io/ziglang/zig:<version>: a syntactically plausible registry name can still be private or
nonexistent. Start from a pinned general-purpose Linux builder and download the matching Zig
release archive from https://ziglang.org/download/<version>/, verifying the SHA-256 published in
Zig's official download/index.json, or use an independently maintained builder image only when
its provenance and immutable digest are explicitly accepted. For Zig 0.16.0 on x86_64 Linux, the
official archive SHA-256 is
70e49664a74374b48b51e6f3fdfbf437f6395d42509050588bd49abe52ba3d00.
The final runtime base and the build target must agree. In particular,
gcr.io/distroless/static-debian12:nonroot has no glibc dynamic loader, so build a static musl
artifact (for example, zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe) before
copying it there. A native glibc build copied into that image exits as
exec /path/to/app: no such file or directory even though the file exists. If you intentionally
build for glibc, choose a runtime image containing the matching loader instead. Smoke-test the
assembled image's real serve command and HTTP readiness; a version healthcheck alone does not
exercise its loader, writable data directory, or listener.
RUN install -d -o 65532 -g 65532 /data
ENV ZIGBASE_DATA_DIR=/data \
ZIGBASE_HTTP_HOST=0.0.0.0 \
ZIGBASE_SERVE_BACKGROUND=0
USER 65532:65532
VOLUME ["/data"]
ENTRYPOINT ["/usr/local/bin/my-app"]
CMD ["serve"]The explicit foreground setting prevents an inherited coding-agent environment from detaching
the process inside Docker. Verify the assembled image—not only the build stage—with a named volume,
an HTTP readiness probe, and doctor --production.
mkdir -p ./zb_data && sudo chown 65532:65532 ./zb_data
docker run -d -p 8090:8090 -v "$(pwd)/zb_data:/data" ghcr.io/valthon/zigbase:latestSkip this and use a named volume (-v zigbase_data:/data) instead if you don't need the
data directory visible on the host filesystem — Docker creates and owns named volumes
correctly without a manual chown.
Auth cookies are Secure by default (HTTPS-only), same as the bare-metal binary. For a
plain-HTTP local or LAN deployment, set ZIGBASE_COOKIE_SECURE=false (equivalent to the
bare-metal --insecure-cookies flag):
docker run -d -p 8090:8090 -v zigbase_data:/data \
-e ZIGBASE_COOKIE_SECURE=false \
ghcr.io/valthon/zigbase:latestPut a TLS-terminating reverse proxy (Caddy, Traefik, nginx) in front for anything beyond
local/LAN use, and drop ZIGBASE_COOKIE_SECURE=false once you do.
Every environment variable in the Configuration reference applies
unchanged — pass them with -e / compose's environment:. The two the image itself sets
(ZIGBASE_HTTP_HOST=0.0.0.0, ZIGBASE_DATA_DIR=/data) can be overridden the same way if
you have a reason to (e.g. mounting the data dir somewhere else inside the container).
GET /api/health (see API reference) is the right endpoint to probe, but the
distroless base has no shell/curl/wget to run a classic HEALTHCHECK CMD curl …
instruction from inside the image. Two supported options:
- Orchestrator-native HTTP probes (Kubernetes
httpGetreadiness/liveness probes, ECS/Nomad HTTP health checks) — point them atGET /api/healthon the published port directly; they run outside the container and don't need a shell inside it. - A reverse proxy in front (Caddy/Traefik) that itself health-checks the upstream via
HTTP and can expose its own
HEALTHCHECK.
The bundled docker-compose.yml example uses /zigbase version (an in-image, no-shell
liveness check) as a lightweight default — it confirms the binary starts and runs, but it
is not an HTTP readiness check. Prefer an orchestrator-native HTTP probe against
/api/health for production.
latest— newest tagged release.X.Y.Z— an exact release, e.g.ghcr.io/valthon/zigbase:0.9.0.X.Y/X— rolling aliases that track the newest patch/minor within that line.
Pin to X.Y.Z for reproducible deployments. Images are published to ghcr.io/valthon/zigbase
(GitHub Container Registry) on each tagged release. Maintainer note: GHCR creates a newly
pushed package as private by default — after the first release push, a repo owner must
flip its visibility to Public once (package Settings → Change visibility) before docker pull
works for anonymous/public users.
No shell, no package manager, no Zig toolchain, no build tools — you cannot docker exec … sh into it. Use docker exec … /zigbase <subcommand> for superuser create,
migrate, etc. If you need a debugging shell, docker cp files out or run the same
binary from a release tarball locally instead.
- Deployment — production Compose, TLS proxies, Fly/Railway, backups, upgrades, and rollback.
- Configuration — the full environment-variable reference.
- The README's Security section — what's safe to change and what isn't.
- Known limitations — no native Windows build; Docker is the supported path there.