Deployment Staging

Backend Staging Deployment

Staging and production container deployment notes for the hosted stack.

Host
staging-app.bagels.top
Source
documentation/backend-container-deployment.md
Updated
2026-04-09

Backend Container Deployment

The backend now runs as a small compose stack:

The api and worker services share the same Docker image and differ only by command. This keeps the deploy lightweight while separating request handling from background work.

Why this shape

Local

  1. Copy or fill in /Users/kyle/Developer/projects/shared/aspectavy-next/apps/backend/.env.local
  2. Start the stack:
docker compose up -d --build

Useful checks:

docker compose ps
docker compose logs -f api
docker compose logs -f worker
curl http://127.0.0.1:3001/health
curl http://127.0.0.1:3001/livez
curl http://127.0.0.1:3001/readyz

Backup local bundled Postgres:

./scripts/backup-postgres.sh local

Staging / Shared Edge VPS

Use this path when another VPS edge already owns :80 and :443 and should proxy staging domains such as staging-app.example.com or staging-api.example.com.

Files:

- APP_UNIVERSAL_LINK_HOST - APP_ALLOWED_ORIGINS - APPLE_APP_SITE_ASSOCIATION_APP_IDS - APTABASE_APP_KEY for the staging analytics app - SESSION_SECRET - SESSION_COOKIE_DOMAIN - SMTP credentials - optional iOS version gating fields: - IOS_APP_STORE_URL - IOS_MINIMUM_SUPPORTED_VERSION - IOS_RECOMMENDED_VERSION

  1. Copy /Users/kyle/Developer/projects/shared/aspectavy-next/apps/backend/.env.staging.example to .env.staging.local
  2. Fill in:
  3. Start the staging stack behind the shared edge with bundled Postgres:
docker compose \
  -f docker-compose.production.yml \
  -f docker-compose.staging.edge.yml \
  -f docker-compose.staging.bundled-db.yml \
  up -d --build api worker db

The API binds to loopback only on 127.0.0.1:${EDGE_PROXY_API_PORT:-3401} so a separate VPS edge proxy can forward staging-app.* or staging-api.* traffic into the backend without exposing the container directly on the public interface.

For split-host browser staging like staging-app.example.com plus staging-api.example.com, set SESSION_COOKIE_DOMAIN=example.com so hosted auth on app.* and API calls to api.* can share the same browser session cookie.

Useful checks:

docker compose \
  -f docker-compose.production.yml \
  -f docker-compose.staging.edge.yml \
  -f docker-compose.staging.bundled-db.yml \
  ps
curl http://127.0.0.1:${EDGE_PROXY_API_PORT:-3401}/health
curl http://127.0.0.1:${EDGE_PROXY_API_PORT:-3401}/readyz

GitHub Actions staging deploy:

- DEPLOY_STAGING_SSH_HOST - DEPLOY_STAGING_SSH_PORT - DEPLOY_STAGING_SSH_USER - DEPLOY_STAGING_SSH_PRIVATE_KEY - DEPLOY_STAGING_PATH

The workflow runs backend npm run typecheck and npm test, rsyncs the deploy bundle to the VPS, and then runs scripts/deploy-stack.sh with the staging target.

Production / VPS

There are now two production deployment paths:

/Users/kyle/Developer/projects/shared/aspectavy-next/docker-compose.production.yml

/Users/kyle/Developer/projects/shared/aspectavy-next/docker-compose.production.bundled-db.yml

/Users/kyle/Developer/projects/shared/aspectavy-next/docker-compose.production.edge.yml

- APTABASE_APP_KEY for the production analytics app - SESSION_SECRET - DATABASE_URL - production cookie/session values

- CADDY_HOST when production owns :80 and :443

- EDGE_PROXY_API_PORT for shared-edge production - API_PORT - POSTGRES_DB - POSTGRES_USER - POSTGRES_PASSWORD - POSTGRES_VOLUME_NAME

  1. Copy /Users/kyle/Developer/projects/shared/aspectavy-next/apps/backend/.env.production.example to .env.production.local
  2. Fill in:
  3. Fill in or export:
  4. Optionally export top-level compose vars such as:
  5. Start the stack with external/managed Postgres:
docker compose -f docker-compose.production.yml up -d --build

Or start the stack with bundled Postgres on the VPS:

docker compose -f docker-compose.production.yml -f docker-compose.production.bundled-db.yml up -d --build

Or start production behind an existing shared edge on the same VPS:

docker compose \
  -f docker-compose.production.yml \
  -f docker-compose.production.edge.yml \
  -f docker-compose.production.bundled-db.yml \
  up -d --build api worker db

Useful checks:

docker compose -f docker-compose.production.yml ps
docker compose -f docker-compose.production.yml logs -f api
docker compose -f docker-compose.production.yml logs -f worker
curl http://127.0.0.1:${API_PORT:-3001}/health
curl http://127.0.0.1:${API_PORT:-3001}/livez
curl http://127.0.0.1:${API_PORT:-3001}/readyz

GitHub Actions production deploy:

- DEPLOY_PRODUCTION_SSH_HOST - DEPLOY_PRODUCTION_SSH_PORT - DEPLOY_PRODUCTION_SSH_USER - DEPLOY_PRODUCTION_SSH_PRIVATE_KEY - DEPLOY_PRODUCTION_PATH

- ASPECTAVY_PRODUCTION_DEPLOY_MODE with edge or standalone - ASPECTAVY_PRODUCTION_USE_BUNDLED_DB

The production workflow is intentionally guarded: if the production SSH secrets are not configured yet, the job exits cleanly without deploying. Once configured, it uses the same rsync-plus-remote-compose flow as staging and runs scripts/deploy-stack.sh with the production target.

Backup and restore options:

./scripts/backup-postgres.sh production-bundled
DATABASE_URL=postgres://... ./scripts/backup-postgres.sh production-external

FORCE=1 ./scripts/restore-postgres.sh production-bundled ./backups/production-bundled-YYYYMMDD-HHMMSS.dump
FORCE=1 DATABASE_URL=postgres://... ./scripts/restore-postgres.sh production-external ./backups/production-external-YYYYMMDD-HHMMSS.dump

Notes

Later Hardening

Good future upgrades, but not required for the initial VPS deploy: