production deployment #
This runbook describes the instance used to exercise ZDS with real resident accounts and interoperability clients.
production shape #
| setting | value |
|---|---|
| public origin | https://pds.zat.dev |
| Fly app | zds-pds |
| primary region | ord |
| machine | one shared CPU, 1 GiB RAM |
| persistent data | Fly volume zds_data mounted at /data |
| database | /data/zds.sqlite3 |
| blobstore | /data/blobs |
| hosted handles | *.pds.zat.dev |
| account policy | invite required |
| permissioned data | enabled, experimental |
The committed non-secret configuration is in fly.toml.
Secrets are Fly secrets and must not be copied into source, logs, or issue
reports. The repository .env is ignored local operator state; commands that
need one of its values should source it without printing it.
SQLite and blob bytes live on the mounted volume. A new application image must
reuse that volume. Never create a replacement machine with an empty /data and
send production traffic to it as a deployment shortcut.
normal deployment #
Every push to main runs .tangled/workflows/ci.yml,
which gates deployment on formatting, unit tests, both HTTP smoke suites, and
the ARMv6 ReleaseSafe build. After those checks pass, it executes:
flyctl deploy --remote-only
The workflow owns FLY_API_TOKEN. A normal release is therefore:
- Confirm the worktree is the canonical
maincheckout and is based onorigin/main. - Run the required checks from the repository root.
- Commit and push
main. - Watch the Tangled workflow and verify the live service after Fly replaces the machine.
Required checks:
just test
just smoke-all
zig fmt --check build.zig build.zig.zon src bench tools
git diff --check
zig zen
Also run just smoke-permissioned for permissioned-data storage, OAuth space
scope, com.atproto.space.*, com.atproto.simplespace.*, or /account/spaces
changes. Run the relevant just bench ... lane for performance-sensitive
storage, repo, blob, proxy, or permissioned-data work.
manual deployment #
The manual path is for an operator already authenticated to the Fly app.
Push the release first. If no pipeline starts, verify that absence on the
spindle, run the same checks locally (including the ARMv6 command below), and
deploy a clean archive of the pushed revision so local .env and workspace
files are not uploaded to the remote builder:
flyctl auth whoami
release_src=$(mktemp -d)
git archive HEAD | tar -x -C "$release_src"
(cd "$release_src" && flyctl deploy --remote-only --app zds-pds)
Do not create a second configuration path in shell history. fly.toml remains
the source of non-secret deployment configuration, and Fly secrets remain the
source of secret configuration.
October 1 deployment observation #
The v0.4.0 push (8d0deeb2cf5c) reached the knot, but no matching spindle
pipeline appeared. tg pipeline status zat.dev/zds failed resolving the owner
repo record; the spindle still reported f68da7c as its latest run. The release
used the manual path after all local CI-equivalent checks passed. Restoring the
push trigger remains an operations follow-up; a successful Git push alone must
not be treated as a successful deployment.
For this repo, query the spindle directly if the CLI lookup fails:
curl -fsSG https://spindle.tangled.sh/xrpc/sh.tangled.ci.queryPipelines \
--data-urlencode 'repo=did:plc:6atzzmsifkfmfqmeqrl6jwzo' \
--data-urlencode "commits=$(git rev-parse HEAD)" \
--data-urlencode 'limit=5'
verification #
Capture the current health response before deploying so there is a baseline:
curl -fsS https://pds.zat.dev/xrpc/_health | jq .
curl -fsS https://pds.zat.dev/xrpc/com.atproto.server.describeServer |
jq '{did,availableUserDomains,inviteCodeRequired}'
After deployment:
flyctl status --app zds-pds
flyctl logs --app zds-pds --no-tail
curl -fsS https://pds.zat.dev/xrpc/_health | jq .
curl -fsS https://pds.zat.dev/xrpc/com.atproto.server.describeServer |
jq '{did,availableUserDomains,inviteCodeRequired}'
curl -fsS https://pds.zat.dev/api/openapi.json >/dev/null
curl -fsS https://pds.zat.dev/account >/dev/null
Do not stop at an HTTP 200 from the health route when the change touched a resident workflow. Exercise the affected path with a throwaway account or the appropriate smoke client, then inspect recent logs for panics, repeated 5xx responses, migration failures, proxy timeouts, or memory pressure. Never print passwords, admin tokens, OAuth tokens, PLC keys, or mail credentials while probing.
rollback and recovery #
Application rollback and data rollback are different operations. Re-deploying an older image does not undo a SQLite migration, and restoring SQLite without its corresponding blobstore can produce an incoherent account.
Before a risky storage migration, confirm that a current volume snapshot exists and document the exact application revision and schema transition. If a release fails without changing persistent state, deploy the last known-good source revision through the same Fly path. If persistent state changed, stop and assess the migration and volume snapshot together before rolling either side back.
For ordinary account operations after a healthy deployment, use the operator guide, invite-code guide, and account takedown runbook.
32-bit and small boards #
zds builds and runs on 32-bit ARM without native 64-bit atomics (ARMv6, e.g. a Raspberry Pi 1 with 512 MB). Cross-compile; do not build on the board — zig 0.16's build runner does not compile for 32-bit hosts, independent of RAM:
zig build -Dtarget=arm-linux-musleabihf -Dcpu=arm1176jzf_s -Doptimize=ReleaseSafe
CI builds this target on every push. A complete Pi 1 recipe — Cloudflare Tunnel behind CGNAT, systemd units, Wi-Fi watchdog — is Chad Miller's zds-pi-setup.
Set ZDS_SERVER_DID explicitly or leave it unset and let zds derive did:web:<host> from
ZDS_PUBLIC_URL; the one thing not to do is set only a non-localhost public URL on a build
older than 0.3.0, which advertised did:web:localhost.