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
| Environment | Fly App | Trigger | Database |
|---|---|---|---|
| Development | renewa-app-development | Push to main | Fly MPG cluster (development) |
| Staging | renewa-app-staging | Manual promotion | Fly MPG cluster (staging) |
| Production | renewa-app-production | Manual promotion | Fly MPG cluster (production) |
| PR Preview | renewa-app-pr-{N} | PR opened/updated | Ephemeral 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
| Workflow | File | Purpose |
|---|---|---|
| Deploy to dev | main-deploy.yml | Build + deploy on push to main |
| Promote image | promote-image.yml | Move image between environments |
| PR preview | pr-preview.yml | Ephemeral app per PR, ci-gate, 3-way test shards |
| PR cleanup | pr-cleanup.yml | Destroy preview on PR close |
| Security scan | security-scan.yml | Trivy, Semgrep, Gitleaks, npm/bun audit (PR gate) |
| Scheduled scan | security-scan-scheduled.yml | Daily 02:00 UTC MEDIUM+ sweep → tracking issues |
| Quality checks | quality-checks.yml | Lint, typecheck, tests, Storybook build gate |
| E2E tests | e2e-tests.yml | Playwright end-to-end tests |
| Migration tests | migration-tests.yml | Validate Atlas migrations |
| Staging DB sync | staging-db-sync.yml | Refresh staging data |
| Backup verify | backup-verify.yml | Periodic 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
| File | Environment |
|---|---|
fly.development.toml | Development |
fly.staging.toml | Staging |
fly.production.toml | Production |
fly.pr-preview.toml | PR previews |
Docker Build
Multi-stage Dockerfile at renewa-one/Dockerfile:
- frontend-builder — Vite production build (FROM a prebuilt deps image)
- backend-builder — Bundle backend (FROM a prebuilt deps image)
- final (
oven/bunAlpine) — supervisord running nginx + backend asnobody; 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):
| Env | Delivery |
|---|---|
| development / staging / production | Infisical → Fly native sync (Auto-Sync + Auto Redeploy ON) |
| pr-preview | CI fetches via OIDC (Infisical/secrets-action) → flyctl secrets set --stage |
| CI itself | GitHub 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
| Context | How Migrations Run |
|---|---|
| Local / Test | On 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 rollbackdoes not exist. - Path B (break-glass):
flyctl mpg restorealways forks a NEW cluster → manual Infisical re-point ofDATABASE_URL/DATABASE_URL_MIGRATION+ redeploy.
Related Pages
- Architecture Overview — System architecture context
- CI-CD Workflows — All 19 GitHub Actions workflows
- Deployment Rollback — Path A / Path B rollback runbook
- Secret Management Operations — Infisical operations runbook
- Docker Setup — Local Docker Compose configuration
- PR Preview Deployments — Ephemeral preview environment details
- Security Scanning — Trivy, Semgrep, Gitleaks, npm audit
- Database Migrations — Atlas migration system
- Database Architecture — PostgreSQL topology
- Git Workflow — Branch rules and PR process