Deployment Pipeline

Image-based promotion through three environments on Fly.io. The same immutable Docker image moves from development to staging to production. See CI-CD Workflows for all workflow details.

Environments

EnvironmentFly AppTriggerDatabase
Developmentrenewa-app-developmentPush to mainFly MPG cluster (development)
Stagingrenewa-app-stagingManual promotionFly MPG cluster (staging)
Productionrenewa-app-productionManual promotionFly MPG cluster (production)
PR Previewrenewa-app-pr-{N}PR opened/updatedEphemeral per-PR database

All clusters are in the Frankfurt (fra) region. The env → MPG cluster-id map lives in renewa-one/scripts/lib/mpg-clusters.sh (cluster IDs change after a Path B restore — see Deployment Rollback).

Deployment Flow

  Push to main
       │
       ▼
  main-deploy.yml
  ├── Quality checks, migration validation, security scanning
  ├── Build Docker image + container security scan
  ├── Snapshot MPG cluster (only when schema/migrations change)
  └── Deploy to development (secrets arrive via Infisical native sync,
      NOT pushed by the workflow)
       │
       ▼ (manual trigger)
  promote-image.yml  (promotion_path = "development → staging")
  └── Re-tag the same immutable image digest, deploy to staging
       │
       ▼ (manual trigger)
  promote-image.yml  (promotion_path = "staging → production")
  └── Deploy to production

Promote command:

gh workflow run promote-image.yml -f promotion_path="development → staging"
# optionally pin a digest (also how Path A rollback re-promotes):
#   -f image_digest=sha256:…

Every mutating deploy records the image digest and pre-deploy backup ID in a deployment-record GitHub issue — the data source for rollbacks.

Key Workflows

WorkflowFilePurpose
Deploy to devmain-deploy.ymlBuild + deploy on push to main
Promote imagepromote-image.ymlMove image between environments
PR previewpr-preview.ymlEphemeral app per PR, ci-gate, 3-way test shards
PR cleanuppr-cleanup.ymlDestroy preview on PR close
Security scansecurity-scan.ymlTrivy, Semgrep, Gitleaks, npm/bun audit (PR gate)
Scheduled scansecurity-scan-scheduled.ymlDaily 02:00 UTC MEDIUM+ sweep → tracking issues
Quality checksquality-checks.ymlLint, typecheck, tests, Storybook build gate
E2E testse2e-tests.ymlPlaywright end-to-end tests
Migration testsmigration-tests.ymlValidate Atlas migrations
Staging DB syncstaging-db-sync.ymlRefresh staging data
Backup verifybackup-verify.ymlPeriodic MPG backup verification

See CI-CD Workflows for the full list of 19 workflows (as of 2026-06).

PR Preview Deployments

Each PR gets an isolated Fly.io app with its own database. See PR Preview Deployments for details.

  • App name: renewa-app-pr-{number}
  • Database: Created on first deploy, recreated only when schema/migration files change
  • Data: Persists across non-schema commits within the same PR
  • Cleanup: Automatic on PR close via pr-cleanup.yml

The ci-gate job in pr-preview.yml is the single required status check for merging. It validates that all upstream jobs passed or were skipped.

Fly.io Configuration

FileEnvironment
fly.development.tomlDevelopment
fly.staging.tomlStaging
fly.production.tomlProduction
fly.pr-preview.tomlPR previews

Docker Build

Multi-stage Dockerfile at renewa-one/Dockerfile:

  1. frontend-builder — Vite production build (FROM a prebuilt deps image)
  2. backend-builder — Bundle backend (FROM a prebuilt deps image)
  3. final (oven/bun Alpine) — supervisord running nginx + backend as nobody; entrypoint runs migrations as root, then drops privileges

See Docker Setup for local development compose stack.

Secret Management

Infisical EU (project renewa-one) is the source of truth for all runtime app secrets — not GitHub. Delivery per environment (PR#1930):

EnvDelivery
development / staging / productionInfisical → Fly native sync (Auto-Sync + Auto Redeploy ON)
pr-previewCI fetches via OIDC (Infisical/secrets-action) → flyctl secrets set --stage
CI itselfGitHub Actions secrets hold CI-infra credentials only (FLY_API_TOKEN, FLY_MPG_TOKEN, …)

Never flyctl secrets set on a synced app — the sync overwrites it. App secrets include DATABASE_URL, DATABASE_URL_MIGRATION, JWT_SECRET, ENCRYPTION_KEY, TIGRIS_*, UPSTASH_REDIS_*, SENTRY_DSN_BACKEND, BIRD_API_KEY, HUBSPOT_CLIENT_SECRET, SEVDESK_API_TOKEN. See Secret Management Operations for operational detail.

Migration Strategy

ContextHow Migrations Run
Local / TestOn container startup
Cloud (dev/staging/prod)Via Fly.io release_command using DATABASE_URL_MIGRATION

Migrations are kept N-1 compatible (expand → migrate → contract, CI-gated by scripts/check-migration-n1.sh, PR#1938) so the previous image keeps working while release_command commits — this is what keeps image rollback (Path A) safe. atlas-migrate.sh enforces the phase split per database and honours in-file renewa:atlas:skip-on directives. See Database Migrations.

Rollback

Two paths via renewa-one/scripts/rollback-deploy.sh (PR#1918) — full procedure in Deployment Rollback:

  • Path A (first resort): re-promote the previous image digest through promote-image.yml — stays on the CI deploy path; flyctl releases rollback does not exist.
  • Path B (break-glass): flyctl mpg restore always forks a NEW cluster → manual Infisical re-point of DATABASE_URL/DATABASE_URL_MIGRATION + redeploy.