ByteBite Docs
Deployment

Staging Deployment

Operational staging runbook — deploy, migrate, seed, and validate.

Staging Deployment

ByteBite staging is active on BYTEBITE-APP-01 (88.99.0.118) under Dokploy. This is a concise operational runbook; infrastructure details are in Infrastructure.

Verified: 2026-09-08

Status

ItemValue
Environmentstaging
Host88.99.0.118
Dokploy projectByteBite / staging / "ByteBite Staging"
Composecompose.deploy.yml
Branchmain (On Push trigger)
Indexingnoindex, nofollow (all public apps)

Prerequisites

  • Dokploy access at dokploy.getbytebite.co
  • GitHub connection: GitHub-ByteBiteCo
  • Dokploy-managed PostgreSQL service attached (internal network)
  • Environment variables configured in Dokploy (see below)

Required environment variables

Set in Dokploy — never commit values.

VariableServiceRequired
BYTEBITE_DEPLOY_ENVapi, landing, storefront, docsstaging
DATABASE_URLmigrate, apiYes
NODE_ENVallproduction
API_INTERNAL_URLstorefrontDefault http://api:3001 in compose
LOG_LEVELapiOptional (default info)
SENTRY_DSNapiOptional
STAGING_BASIC_AUTH_USERScompose labels (Traefik)Yes — hashed user:hash for HTTPS BasicAuth
BETTER_AUTH_SECRETapiStaff session signing. Empty → /auth routes are not registered (no silent Better Auth default). /dashboard/* is 401 without a session. Staging-BasicAuth is not a substitute.

Generate with ./infrastructure/scripts/staging-htpasswd.sh bytebite. Never commit the value. Compose interpolation fails the deploy if it is unset (:?). After changing it, redeploy so Traefik labels are rewritten.

Full inventory: Secrets.

Deploy

Pushes to main trigger Dokploy build and deploy automatically.

Manual redeploy: Dokploy UI → ByteBite Staging → Deploy.

Dokploy builds from compose.deploy.yml and starts six services: migrate, api, landing, storefront, dashboard, docs.

Migrations (automatic)

The migrate one-shot runs node db/dist/migrate.js before api starts (depends_on: service_completed_successfully). The API process does not auto-migrate on startup. Seeds are never part of this service.

Exited (0) on migrate is success. A failed migrate keeps api from starting. Image changes that include new SQL under packages/db/migrations/ recreate the one-shot; env-only redeploys without a new image are a no-op (acceptable).

Do not assign a Dokploy domain or Traefik labels to migrate.

Manual fallback (emergency only), inside the api container or from a machine with staging DATABASE_URL:

node db/dist/migrate.js
pnpm deploy:db:migrate

Slice 4 adds orders / order_items. Slice 5 adds eta_minutes and cancellation_reason on orders. Slice 6 writes the existing preparing / ready / completed enum values and needs no new migration. The automatic migrate one-shot applies the schema before api starts. seed-staging is not required for orders unless the restaurant/menu schema is missing. Seeds do not create carts or orders. The acceptance-timeout scheduler runs inside the API process after migrate.

Post-deploy: staging seed (fresh DB only)

Creates Luigi and Mario staging tenants and their isolated menus:

HostnameRestaurant
luigi.staging.getbytebite.coPizzeria Luigi
mario.staging.getbytebite.coMario's Restaurant
node db/dist/seed-staging.js

Safe to re-run (upserts), including menu categories and items. Do not run against production or local dev databases. On a fresh database, wait for automatic migrate to finish, then run seed-staging so Luigi/Mario catalogs exist.

Routing summary

Explicit domains (Dokploy UI)

HostService
staging.getbytebite.colanding
luigi.staging.getbytebite.costorefront
mario.staging.getbytebite.costorefront
dashboard.staging.getbytebite.codashboard
api.staging.getbytebite.coapi
docs.staging.getbytebite.codocs

staging.getbytebite.co is the staging marketing landing host (ADR-022). apps/landing serves Coming Soon, Staging when BYTEBITE_DEPLOY_ENV=staging. Production uses getbytebite.co and www.getbytebite.co for the same app and must set BYTEBITE_DEPLOY_ENV=production (Coming Soon, Production). Those production hosts are not attached to the staging stack. Restaurant storefronts stay on <slug>.staging.getbytebite.co. There is no restaurant_domains row for the apex. The DNS wildcard does not include it. Do not seed a restaurant on staging.getbytebite.co. Compose priority 200 sends the apex to landing; move the Dokploy UI domain onto that service.

Same-origin /api on luigi, mario, and dashboard (StripPrefix /api → api:3001) — configured in Dokploy UI and mirrored with higher-priority BasicAuth routers in compose.deploy.yml.

HTTPS routes for ByteBite staging hosts require HTTP BasicAuth (bytebite-staging-auth). HTTP still only redirects to HTTPS. dokploy.getbytebite.co is not gated by this middleware.

Wildcard tenants (compose.deploy.yml)

<slug>.staging.getbytebite.co → storefront (priority 10) and /api → api (priority 20) via Traefik labels. Uses letsencrypt-cloudflare wildcard TLS.

Details: Traefik, DNS.

Validate

Run the full Validation smoke test suite after every deploy or infrastructure change.

Quick check (after setting STAGING_BASIC_AUTH_USER / STAGING_BASIC_AUTH_PASSWORD locally):

curl -sI "https://api.staging.getbytebite.co/health" | head -5
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://api.staging.getbytebite.co/health"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://luigi.staging.getbytebite.co/api/health"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://luigi.staging.getbytebite.co/api/storefront/menu"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://mario.staging.getbytebite.co/api/storefront/menu"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://luigi.staging.getbytebite.co/api/storefront/cart"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" "https://mario.staging.getbytebite.co/api/storefront/cart"

Checkout isolation (after migrate; seeds do not create orders). Body restaurantId must be ignored:

curl -s -D - -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -H "content-type: application/json" \
  -d '{"menuItemId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5","quantity":2}' \
  "https://luigi.staging.getbytebite.co/api/storefront/cart/items"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -H "content-type: application/json" \
  -H "cookie: bb_cart=<luigi-cart-uuid>" \
  -d '{"name":"Anna","phone":"+491111111111","restaurantId":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"}' \
  "https://luigi.staging.getbytebite.co/api/storefront/checkout"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -o /dev/null -w "%{http_code}" \
  "https://mario.staging.getbytebite.co/api/storefront/orders/<luigi-order-uuid>"

Luigi's order snapshots Margherita at 850 / 1700. Mario returns 404 for that orderId. Unknown hosts return 404 for /api/storefront/checkout.

Accept / reject (after checkout; seeds do not create orders):

LUIGI_ID="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1"
MARIO_ID="bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb1"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -H "content-type: application/json" \
  -d '{"etaMinutes":30}' \
  "https://dashboard.staging.getbytebite.co/api/dashboard/restaurants/${LUIGI_ID}/orders/<luigi-order-uuid>/accept"
curl -s -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -o /dev/null -w "%{http_code}" \
  -H "content-type: application/json" \
  -d '{"etaMinutes":30}' \
  "https://dashboard.staging.getbytebite.co/api/dashboard/restaurants/${MARIO_ID}/orders/<luigi-order-uuid>/accept"

Luigi accept returns accepted / ETA 30. Mario's path returns 404 and does not write.

Cookie isolation (BasicAuth + host-only bb_cart):

curl -s -D - -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -H "content-type: application/json" \
  -d '{"menuItemId":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa5"}' \
  "https://luigi.staging.getbytebite.co/api/storefront/cart/items"
curl -s -D - -u "${STAGING_BASIC_AUTH_USER}:${STAGING_BASIC_AUTH_PASSWORD}" \
  -H "cookie: bb_cart=<luigi-cart-uuid>" \
  "https://mario.staging.getbytebite.co/api/storefront/cart"

Mario must return items: [] for Mario's restaurant and rotate bb_cart. Unknown hosts return 404 for /api/storefront/cart.

Unauthenticated HTTPS should return 401 with WWW-Authenticate: Basic realm="ByteBite Staging".

Rollback

ScenarioAction
Bad application codeRedeploy previous Git commit in Dokploy
Bad migrationRestore from off-host backup or fix forward — see Disaster recovery
Routing/TLS brokenSee Disaster recovery

Backups

Daily PostgreSQL logical backups are automated (systemd timer, 02:15 UTC). Backup architecture, retention, and restore procedures:

TopicPage
Backup architectureBackups
Disaster recoveryDisaster recovery
Backup validationValidation

Staging-only behavior

  • BYTEBITE_DEPLOY_ENV=staging → X-Robots-Tag: noindex, nofollow on landing, storefront, and docs. The landing heading is Coming Soon, Staging
  • Dashboard always sends noindex (nginx config)
  • Unknown wildcard tenants: storefront shows "Restaurant not found" (HTTP 200 — known gap); API returns 404
  • HTTPS requires Traefik BasicAuth (STAGING_BASIC_AUTH_USERS)
  • No object storage, mail, or production integrations

Deep reference

TopicPage
Full infrastructureInfrastructure overview
Dokploy configDokploy
Traefik / wildcardTraefik
TLS / ACMETLS
DatabaseDatabase
SecuritySecurity
Known gapsKnown gaps

On this page