Loading article…

DocsLoading article…
The self-host Compose overlay runs Caddy, API/database owners, maintenance worker, Studio, Docs and one-shot storage-init. Object bytes live in your separately operated S3-compatible provider, such as RustFS. There is no bundled MinIO service or object-storage volume. Mailpit belongs to local development, not production mail.
Deployment images install production dependencies only, run as non-root user qb (UID/GID 10001) with /var/lib/qb-flex as its writable home, and default to NODE_ENV=production. Every long-running service has a healthcheck; the gateway starts after API, Studio and Docs report healthy.
Keep writable PGlite directories on persistent local disk with one API volume owner. Do not start multiple writers against the same volume or put live databases on network filesystems.
Use a Linux AMD64/ARM64 Docker Engine host and Compose v2. Resolve API, Studio and Docs DNS to the host, and expose only gateway ports 80/443 publicly. Copy .env.production.example to a protected, untracked .env and replace placeholders.
| Variable | Purpose |
|---|---|
QB_API_DOMAIN, QB_STUDIO_DOMAIN, QB_DOCS_DOMAIN | Domains used by the overlay/gateway and public URL configuration. |
QB_APP_ORIGINS | Exact additional browser origins; no wildcard. |
QB_MASTER_KEY | Encryption custody; preserve independently offline. |
QB_BOOTSTRAP_TOKEN | One-time first-owner setup. |
QB_WORKER_TOKEN | Private maintenance and metrics credential. |
QB_S3_ENDPOINT, QB_S3_REGION, QB_S3_BUCKET | Reachable object-provider endpoint and physical bucket. |
QB_S3_ACCESS_KEY, QB_S3_SECRET_KEY | Server-only object-provider credentials. |
QB_RESEND_API_KEY, QB_MAIL_FROM | Resend authentication-mail configuration. |
Generate independent secrets locally, for example with openssl rand -hex 32. Never put them in images, source control, browser variables or documentation. Losing the original master key prevents decrypting protected settings/archives. The overlay enables QB_REQUIRE_ADMIN_MFA=true and derives production HTTPS URLs from domains. Runtime QB_ALLOWED_ORIGINS is distinct from the overlay's QB_APP_ORIGINS input.
Email configuration covers explicit QB_MAIL_PROVIDER and the SMTP alternative (QB_SMTP_HOST, port, user/password and TLS). Provider configuration does not certify delivery.
Use an endpoint reachable from containers, not their own 127.0.0.1. For a host-local provider, use http://host.docker.internal:9000; the current Compose includes the Linux host-gateway mapping. Remote production providers should use HTTPS.
The one-shot initializer tries HeadBucket/CreateBucket. Operators can provision the bucket independently when bucket-creation rights are unavailable. The API, worker and Studio wait for storage-init to complete, so a failed initializer stops those services instead of letting them boot with broken storage; inspect its output and verify upload/download on your actual backend. A successful bucket check alone does not prove application upload paths.
docker compose -f compose.yml -f compose.selfhost.yml --profile selfhost config --quiet
docker compose -f compose.yml -f compose.selfhost.yml --profile selfhost build
docker compose -f compose.yml -f compose.selfhost.yml --profile selfhost up -d
docker compose -f compose.yml -f compose.selfhost.yml --profile selfhost psAvoid printing the fully expanded Compose configuration because it may contain secrets. Caddy obtains TLS certificates; check DNS/firewall if issuance fails. Verify public /ready and Studio/Docs over HTTPS, then inspect bucket provisioning:
curl -fsS https://api.example.com/ready
docker compose -f compose.yml -f compose.selfhost.yml --profile selfhost logs --tail 100 storage-initProduction startup requires injected configuration, not an already-created owner. Open Studio to create the first owner using the QB_BOOTSTRAP_TOKEN value, enroll/verify TOTP and securely save recovery codes. Once an owner exists you may remove the variable from the deployment; the API does not require it again. Platform credentials must never become browser application credentials.
The API reads the first-owner token from QB_BOOTSTRAP_TOKEN only — the value in the API environment or .env file. There is no generated token file and no fallback source, so the value you configured is always the value the API compares against. The API trims surrounding whitespace and quotes, and rejects the example placeholder at startup rather than accepting it. Log in as an operator, then use the same value in Studio's setup form; POST /setup returns 403 if it differs.
# print the configured value from the running container
docker compose exec -T api printenv QB_BOOTSTRAP_TOKENOnce an owner exists the variable is no longer required, so you may remove it. Removing it does not break restarts.
Use the staging overlay to validate production-like secrets, S3 and mail configuration on localhost before pointing real DNS at the TLS gateway. Staging runs with NODE_ENV=production, resource limits and security hardening, but exposes the services directly on 127.0.0.1 with no Caddy gateway or TLS.
Copy .env.staging.example to .env, generate local secrets and fill the same required values as production (master key, QB_BOOTSTRAP_TOKEN, worker token, S3 credentials and Resend or SMTP). Then validate, build and start:
docker compose -f compose.yml -f compose.staging.yml config --quiet
docker compose -f compose.yml -f compose.staging.yml build
docker compose -f compose.yml -f compose.staging.yml up -dThe overlay pins public URLs to http://localhost:7612 (API), http://localhost:7610 (Studio) and http://localhost:7611 (Docs), and sets Compose project name qb-flex-staging, so its database volume is isolated from the local qb-flex stack. It also sets QB_ALLOW_INSECURE_HTTP=true, which the API accepts only for loopback URLs; never set that variable on a public host.
Verify readiness after storage-init completes:
curl -fsS http://localhost:7612/ready
docker compose -f compose.yml -f compose.staging.yml logs --tail 100 storage-initUse the bootstrap token from .env to create the first owner at http://localhost:7610, then remove the token from the host. Staging is not a public deployment target; switch to compose.selfhost.yml with real domains and TLS once validation passes.
Dokploy, Coolify, Portainer and similar platforms run a bare docker compose up and add their own reverse proxy. compose.yml therefore starts storage-init, API, worker, Studio and Docs without a profile flag; Mailpit stays opt-in via --profile local or an explicit service name, and the Caddy gateway stays opt-in via --profile selfhost.
Use the platform's environment-variable UI with .env.dokploy.example as the template. Set NODE_ENV=production, the public HTTPS URLs, QB_ALLOWED_ORIGINS, the three secrets, S3 and mail configuration. Studio and Docs bake VITE_API_URL, VITE_STUDIO_URL and VITE_DOCS_URL at image build time, so change them before the first build and rebuild after any public-URL change.
Published container ports default to 127.0.0.1 to avoid bypassing the platform proxy. If your platform's proxy reaches services through host ports instead of the Docker network, set QB_BIND_ADDRESS=0.0.0.0. Route the API to port 7612, Studio to 7610 and Docs to 7611 in the platform's domain settings. Do not add the compose.selfhost.yml overlay there: its Caddy gateway would compete for ports 80/443 with the platform proxy. Keep /internal/* blocked at the platform proxy, because the API metrics and maintenance routes rely on that boundary.
Upload staging reserves local disk before S3 PUT: plan for up to four 100 MiB uploads plus headroom, WALs and archives. Branch backup/restore streams share a two-operation capacity and 120-second deadline. Do not delete live WAL files or scale out writers to solve disk/queue pressure.
Monitor /ready, bounded logs and private /internal/metrics. The gateway blocks /internal/* with 404; scrape only on the trusted private network with the worker credential. Deployment Doctor adds redacted inventory and an optional explicit readiness probe, not automatic provider tests.
Protect three separate recovery assets: encrypted database/control snapshots, S3 object bytes/versioning and the original master key. Branch restore creates an isolated branch; there is no automatic cutover. Sessions, provider tokens, jobs, webhook destinations and Advisor acknowledgments do not resume from a branch restore.
The existing worker runs experimental database-webhook deliveries through private APIs without PGlite writes. Default egress is public HTTPS only. Optional QB_WEBHOOK_INTERNAL_ALLOWLIST must match on API and worker; exact origins map to fixed reviewed IPs with TLS verification retained and known metadata destinations prohibited. Security Advisor provides static operator findings, not automatic fixes or external alert dispatch.
Create and verify a fresh backup before changing versions. Build the reviewed commit/digest and run the same up -d workflow. Named volumes persist. Never use down -v as an upgrade or port-change step. Validate schema compatibility before rolling code back; data rollback uses an isolated restore and explicit alias cutover, not a data merge.
Set QB_DOCS_URL and VITE_DOCS_URL consistently before generation/build for correct sitemap/canonical links. Docs is independently deployable and needs no login; its production build prerenders content. Do not expose provider secrets to it.
Run repository checks/builds, CLI/browser checks and isolated test:compose/Linux gates for the affected boundaries. These do not replace production TLS, mail/OAuth/device/provider checks or sustained load/recovery evidence. Linux AMD64/ARM64 are certification targets; current evidence must be checked per architecture.
Keep QB_PUSH_ENABLED=false. A separately supplied distinct QB_PUSH_WORKER_TOKEN only enables a restricted queue principal in current source; it does not deploy a Node push executor or campaigns. Off-node Recovery Center, alert deliveries, advanced operations and native/push/WASM integrations remain expanded-release work.
The new API refuses a data directory containing control.sqlite. Run the offline engine migration into a separate directory, validate it, then schedule activation through QB_DATA_DIR. Keep the original installation and master key for recovery.
QB_MAX_BRANCH_OWNERS defaults to four and accepts 1–32. Each active branch has an independent PGlite WASM instance; the control catalog is additional. Size memory for the intended active branch count and snapshot export/import. On the local macOS ARM64/Bun 1.4.2 startup measurement, four empty branch owners including authentication reached about 2.7 GB process RSS, with roughly 0.9-second startup per owner. This short local measurement is not a production capacity guarantee; run bun scripts/pglite-benchmark.ts on the deployment host. The raw repository evidence is docs/benchmarks/pglite-macos-arm64.json.
The self-host Compose API memory ceiling defaults to 8 GiB (QB_API_MEMORY=8g) to leave room for the catalog, four branch instances and snapshot work. This is a configuration default, not measured Linux certification; lower the owner count or adjust memory only after measuring the deployment workload. The Compose environment passes QB_MAX_BRANCH_OWNERS to the API.