Deployment Guide
Three deployment shapes are supported, in order of operator complexity:
-
Compose stack —
deploy/docker/docker-compose.yml. Production-shaped: nginx → anubis → backend, plus Zitadel + RustFS sidecars. -
Binary + nginx — see
docs/deployment/binary.mdanddocs/deployment/nginx.md. -
Self-hosted with custom IdP — same as the above but Zitadel federates to your OIDC provider. See
docs/deployment/zitadel-setup.md.
This file is the deployment overview; each docs/deployment/*.md is the detail for one shape.
Components
| Component | Image / artifact | Listens on | Notes |
|---|---|---|---|
Backend |
|
|
Stateless. Reads |
Frontend |
|
|
Static assets only; nginx config at |
Postgres |
|
|
RLS depends on |
Zitadel |
|
|
OIDC issuer; ⚠️ pin version before staging — see follow-up |
Anubis |
|
|
PoW bot challenge in front of nginx; ⚠️ pin version |
RustFS |
|
|
S3-compatible; ⚠️ pin version |
Configuration
All backend config is via env vars. The runtime reads .env if present (dotenvy::dotenv() in main.rs). See .env.example (in repo) for the full list. Key variables:
| Variable | Purpose | Required |
|---|---|---|
|
Postgres connection string |
Yes |
|
Listen port |
No, default 3000 |
|
OIDC issuer; backend fetches |
Yes |
|
OAuth client id for |
Yes |
|
Tracing filter |
No |
|
Dev-only — bypasses JWT validation. Production builds must never set these. |
No |
|
ed25519 public key for license verification — embedded at compile time via |
If non-saas |
|
Path to signed |
If non-saas |
|
Web-Push signing |
If push enabled |
|
RustFS credentials and endpoint for |
If forms enabled |
The frontend has a parallel set of TRUNK_* build-time vars; see frontend/Trunk.toml and docs/deployment/binary.md.
Build modes (saas vs self-hosted)
backend/Cargo.toml defines [features] saas = []. The license check in backend/src/auth/license.rs::check_on_startup is a no-op when --features saas is set; otherwise it requires SHIFTIT_PUBLIC_KEY (compile-time) and SHIFTIT_LICENSE_FILE (runtime). See docs/licensing.md for the operator workflow and the deploy/license/ standalone crate for the signing CLI.
CI/CD
Pipeline lives in .gitlab-ci.yml (GitLab — not GitHub Actions despite the host repository pattern).
Stages:
-
check—cargo fmt --check,cargo clippy --workspace --all-targets --all-features -D warnings -
test—backend-test(withpostgres:16-alpineservice) +license-tool-test(separate workspace) -
build—frontend-build(trunk build --release, artifact =frontend/dist/) +backend-build(artifact =target/release/shiftit-backend)
CI sets SQLX_OFFLINE=true and relies on the committed .sqlx/ cache. cargo install trunk runs every pipeline — known slowness; the fix is a custom CI image with Trunk + wasm-bindgen-cli baked in (P2 follow-up).
Backups & ops
-
Postgres: standard pg_dump / pg_basebackup. RLS is policy-only — dumps include the policies, no special handling needed. Just don’t restore as a superuser without setting the role correctly afterward.
-
RustFS: S3-compatible — use any tooling that speaks S3. The
event_documents.file_keycolumn is the canonical pointer. -
Zitadel: has its own backup model (Postgres-backed); see Zitadel docs.
Known operator footguns
| Issue | Mitigation |
|---|---|
Migrations 0001 and 0005 were edited after first apply |
Operator must drop+recreate dev DBs that applied earlier versions — see |
|
P1 follow-up: pin to known-good versions before staging. |
Process-local rate limiter |
Multi-replica deploys each have their own counter (P2 follow-up: Redis-backed state). |
|
One outbound JWKS fetch per token. P1: single-flight + minimum-interval refresh. |
See also
-
docs/deployment/binary.md— bare binary + nginx without Compose -
docs/deployment/docker-compose.md— annotated Compose walkthrough -
docs/deployment/nginx.md— TLS termination + reverse proxy config -
docs/deployment/zitadel-setup.md— OAuth app config, claim mapping (⚠️ verify v1/v2 action API per current Zitadel) -
docs/licensing.md— license signing & shipping