Dokploy deployment (Bondery production)
Canonical production topology — two Compose apps:
Bondery production only — not part of the self-host distribution. Self-hosters follow Installation.
| Host | Service | Stack |
|---|---|---|
app.usebondery.com | webapp :26632 | deploy/bondery Compose |
api.usebondery.com | api :26631 | same Compose file |
| (internal) | redis, db | same Compose — never attach a public domain |
usebondery.com | website :26630 | deploy/ops Compose (Bondery prod only) |
Self-hosters use only deploy/bondery (api + webapp + redis + Postgres). Marketing lives in ops and is not part of the self-host distribution. Guides: deploy/bondery/README.md · docs/deploy/installation.mdx · deploy/ops/README.md.
Product Compose application (deploy/bondery)
| Setting | Value |
|---|---|
| Provider | Docker Compose |
| Compose path | deploy/bondery/docker-compose.yml (includes docker-compose.postgres.yml) |
| Domains | Traefik labels via BONDERY_INFRA_WEBAPP_DOMAIN / BONDERY_INFRA_API_DOMAIN |
Environment
Copy deploy/bondery/.env.example into Dokploy env (or a compose .env):
BONDERY_INFRA_API_DOMAIN=api.usebondery.com
BONDERY_INFRA_WEBAPP_DOMAIN=app.usebondery.com
BONDERY_INFRA_WEBSITE_DOMAIN=usebondery.com
BONDERY_INFRA_CHROME_EXTENSION_ID=lpcmokfekjjejnpobhbkgmjkodfhpmha
BONDERY_PRIVATE_REDIS_URL=redis://redis:6379Plus Postgres password, Better Auth secrets, and OAuth vars from .env.example. The api service runs pre_start init containers before the main process on every deploy: release-migrate (schema + functions + OAuth + platform admins) then ensure-storage-buckets — no separate migrate service or manual first-boot step.
Important:
- Image tags:
production(floating channel) is the default; pin semver when you want a fixed rollback target. - Set hostnames only (
BONDERY_INFRA_*_DOMAIN). Compose deriveshttps://…URLs and TraefikHost()rules. BONDERY_PRIVATE_BETTER_AUTH_SECRETSmust use formatversion:secret(e.g.1:<≥32-char secret>) in.env.- Webapp never receives raw
BONDERY_PRIVATE_*secrets except its own session/OAuth client secrets — compose allowlists public vars for the webapp service. - Compose publishes unique Docker DNS aliases from
BONDERY_INFRA_TRAEFIK_PREFIX(bonderyvsbondery-beta) so production and beta can sharedokploy-network. Lookups: webapp SSR →${prefix}-api:26631; API S3 →${prefix}-storage:8333. Do not use the Compose service namesapiorseaweedfs-s3on that network.
Health checks
| Service | Liveness | Readiness | Notes |
|---|---|---|---|
webapp container | GET /health/live | GET /health/ready | Webapp process + runtime config only |
webapp BFF → API | GET /api/health/live | GET /api/health/ready | Proxies upstream Fastify health |
api | GET /health/live | GET /health/ready | Process up / Postgres, required S3 bucket HeadBucket, Redis, SMTP (critical — 503 when unhealthy); optional integrations degrade only |
api boot | onReady eager verify | Same live checks as /health/ready for Postgres, storage, Redis, SMTP before accepting traffic (skipped in NODE_ENV=test) | |
api bootstrap | api pre_start init containers | Before main API starts: release-migrate (schema + functions + OAuth + platform admins) then ensure-storage-buckets | |
db | Compose healthcheck | api waits for service_healthy |
Redeploy the full Compose app (Dokploy's default) so api pre_start runs when the API image or init definition changes — don't redeploy webapp alone on schema-changing releases.
CI redeploy webhook
After unified vX.Y.Z release tags, GitHub Actions fetches production Infisical and POSTs to BONDERY_OPS_DOKPLOY_SERVICES_DEPLOY_WEBHOOK with refs/heads/release. Set the Dokploy Compose app branch to release. Leave the webhook unset in Infisical to redeploy manually.
Infisical → Dokploy env sync
Product stack secrets and hostnames can be synced via .github/workflows/sync-dokploy-env.yml:
- Production:
deployment: production,target: services— Infisical production; requiresBONDERY_OPS_DOKPLOY_SERVICES_COMPOSE_ID(and optionalBONDERY_OPS_DOKPLOY_SERVICES_DEPLOY_WEBHOOK). - Beta:
deployment: beta,target: services— Infisical staging app secrets + production ops keys; requiresBONDERY_OPS_DOKPLOY_STAGING_SERVICES_COMPOSE_IDand optionalBONDERY_OPS_DOKPLOY_STAGING_SERVICES_DEPLOY_WEBHOOKin Infisical production.
See workflows README.
Not synced (keep in Dokploy UI or compose derives): BONDERY_INFRA_GIT_SHA, BONDERY_INFRA_VERSION, BONDERY_PRIVATE_S3_ENDPOINT, BONDERY_PUBLIC_STORAGE_URL. BONDERY_INFRA_TRAEFIK_PREFIX syncs from Infisical (bondery in production, bondery-beta in staging).
Isolated Deployments
Leave Dokploy Isolated Deployments off. Production and beta share dokploy-network; unique ${BONDERY_INFRA_TRAEFIK_PREFIX}-* aliases keep Docker DNS from colliding. Optional Isolated Deployments are for operators running unrelated Compose apps, not for Bondery beta vs production.
Ops Compose application (deploy/ops — marketing website)
| Setting | Value |
|---|---|
| Provider | Docker Compose |
| Compose path | deploy/ops/docker-compose.yml |
| Image | ghcr.io/usebondery/website:${BONDERY_INFRA_WEBSITE_IMAGE_TAG:-production} (floating :production when unset) |
| Domain | BONDERY_INFRA_WEBSITE_DOMAIN Traefik label → port 26630 |
CI: Path-filtered PRs and pushes to main run website-build in .github/workflows/verify.yml (pruned production next build). Push to release runs .github/workflows/deploy-website.yml (Docker build-push → smoke → optional Dokploy webhook from Infisical production); image tags :production + :sha-<short>. Infisical key BONDERY_OPS_DOKPLOY_WEBSITE_DEPLOY_WEBHOOK triggers redeploy (refs/heads/release).
BONDERY_INFRA_WEBAPP_DOMAIN=app.usebondery.com
BONDERY_INFRA_WEBSITE_DOMAIN=usebondery.comCompose derives BONDERY_PUBLIC_*_URL from those domains. Health: GET /health/live (liveness), GET /health/ready (env valid).
Cutover from Nixpacks: stop the old Nixpacks/Railpack website app before deploying ops Compose (same Traefik Host). Details: deploy/ops/README.md.
Webapp runtime config
The webapp exposes GET /runtime-config.json and injects window.__BONDERY_RUNTIME_CONFIG__ during SSR — build once, deploy many.
After changing domain or BONDERY_PUBLIC_* variables, redeploy the affected service(s) (no image rebuild for env-only changes).
Better Auth URLs
Social login and sessions are handled by the API (/auth/*) — the API is its own issuer/authorization server (baseURL = BONDERY_PUBLIC_API_URL). Webapp, mobile, and the chrome extension are each independent OAuth 2.1 + PKCE clients of that one authorization server.
| Setting | Value |
|---|---|
| API base (issuer) | https://api.usebondery.com (BONDERY_INFRA_API_DOMAIN) |
| Webapp's own OAuth client redirect | https://app.usebondery.com/auth/oauth-callback |
| Chrome extension | OAuth 2.1 via API; redirect https://{BONDERY_INFRA_CHROME_EXTENSION_ID}.chromiumapp.org/ |
GitHub / LinkedIn OAuth apps
Authorization callback URL must be the API's Better Auth callback (basePath is /auth):
https://api.usebondery.com/auth/callback/github
https://api.usebondery.com/auth/callback/linkedin(Replace host with your BONDERY_INFRA_API_DOMAIN.)
Register the webapp and extension as trusted OAuth clients (once per environment)
The webapp is a confidential OAuth client, and the Chrome extension a public OAuth client, of the API's own authorization server. Both are provisioned deterministically from env vars — there is no signed-in-session registration step. Generate values yourself and set them before first deploy:
openssl rand -hex 16 # -> BONDERY_PUBLIC_WEBAPP_OAUTH_CLIENT_ID
openssl rand -hex 32 # -> BONDERY_PRIVATE_WEBAPP_OAUTH_CLIENT_SECRET
openssl rand -hex 16 # -> BONDERY_PUBLIC_OAUTH_CLIENT_ID (extension)Set BONDERY_PUBLIC_WEBAPP_OAUTH_CLIENT_ID (api + webapp), BONDERY_PRIVATE_WEBAPP_OAUTH_CLIENT_SECRET (webapp), BONDERY_PUBLIC_OAUTH_CLIENT_ID (api + extension), plus a BONDERY_PRIVATE_WEBAPP_SESSION_SECRET (≥32 chars) for the webapp's own encrypted session cookie. OAuth clients are upserted automatically when api pre_start runs release-migrate (after prisma migrate deploy).
Environment migration (pre-1.7.x → BONDERY_*)
If the API container crashes with missing NEXT_PUBLIC_* or legacy deploy paths, you are on an old API image.
- Pin images to
1.7.4or newer (or redeploy after:productionhas moved past the env migration). - Rename Dokploy / compose
.envkeys toBONDERY_*— seedeploy/bondery/.env.example. - Redeploy the stack and pull fresh images (
docker compose pullor Dokploy redeploy).
Cutover (API-only Compose + separate webapp image → unified stack)
Preflight
- Record current image digests / env for API Compose and standalone webapp.
- Keep the standalone webapp Dokploy app running as rollback until verified.
- Redis volume continuity is optional (rate-limit / sync-wake / WS tickets only — disposable).
Steps
- Point the existing Compose app at
deploy/bondery/docker-compose.yml(in-place preferred). - Set domains and secrets from
.env.example(image tags optional — unset falls back to:production). - Deploy; wait until
redisanddbhealthy andapi/statusOK. - Confirm Traefik routes:
api.usebondery.com→api:26631,app.usebondery.com→webapp:26632. Do not route Redis or Postgres publicly. - Stop the old standalone webapp app if it conflicts on the domain.
- Smoke:
curl/health/live,/health/ready,/api/health/ready; login; one authenticated mutation; sync/WebSocket if used.
Rollback
- Re-point
app.usebondery.comto the preserved standalone webapp image app. - Restore previous image tags or digests and redeploy.
- A fresh Redis volume on rollback is acceptable.
After 24–48h healthy bake
- Remove the old Dokploy webapp Docker Image application.
API with external Redis
Advanced: run the API image alone with managed Redis — see api-container.md.