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
| Item | Value |
|---|---|
| Environment | staging |
| Host | 88.99.0.118 |
| Dokploy project | ByteBite / staging / "ByteBite Staging" |
| Compose | compose.deploy.yml |
| Branch | main (On Push trigger) |
| Indexing | noindex, 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.
| Variable | Service | Required |
|---|---|---|
BYTEBITE_DEPLOY_ENV | api, landing, storefront, docs | staging |
DATABASE_URL | migrate, api | Yes |
NODE_ENV | all | production |
API_INTERNAL_URL | storefront | Default http://api:3001 in compose |
LOG_LEVEL | api | Optional (default info) |
SENTRY_DSN | api | Optional |
STAGING_BASIC_AUTH_USERS | compose labels (Traefik) | Yes — hashed user:hash for HTTPS BasicAuth |
BETTER_AUTH_SECRET | api | Staff 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:migrateSlice 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:
| Hostname | Restaurant |
|---|---|
luigi.staging.getbytebite.co | Pizzeria Luigi |
mario.staging.getbytebite.co | Mario's Restaurant |
node db/dist/seed-staging.jsSafe 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)
| Host | Service |
|---|---|
staging.getbytebite.co | landing |
luigi.staging.getbytebite.co | storefront |
mario.staging.getbytebite.co | storefront |
dashboard.staging.getbytebite.co | dashboard |
api.staging.getbytebite.co | api |
docs.staging.getbytebite.co | docs |
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.
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
| Scenario | Action |
|---|---|
| Bad application code | Redeploy previous Git commit in Dokploy |
| Bad migration | Restore from off-host backup or fix forward — see Disaster recovery |
| Routing/TLS broken | See Disaster recovery |
Backups
Daily PostgreSQL logical backups are automated (systemd timer, 02:15 UTC). Backup architecture, retention, and restore procedures:
| Topic | Page |
|---|---|
| Backup architecture | Backups |
| Disaster recovery | Disaster recovery |
| Backup validation | Validation |
Staging-only behavior
BYTEBITE_DEPLOY_ENV=staging→X-Robots-Tag: noindex, nofollowon landing, storefront, and docs. The landing heading isComing 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
| Topic | Page |
|---|---|
| Full infrastructure | Infrastructure overview |
| Dokploy config | Dokploy |
| Traefik / wildcard | Traefik |
| TLS / ACME | TLS |
| Database | Database |
| Security | Security |
| Known gaps | Known gaps |