Aller au contenu principal

Deployment Pipeline

This document is the source of truth for the GitLab deployment flow.

Secret source

The deployment pipeline supports two modes:

  • preferred: SOPS-encrypted env files committed in the repository
  • fallback: environment-scoped GitLab CI variables rendered into .env

Priority order:

  1. infra/env/{environment}.env.enc
  2. scoped GitLab CI variables

Environments

  • staging
    • deployed automatically on every push to main
    • used to validate the current main state before production rollout
  • production
    • deployed manually from the main pipeline
    • intended as an explicit promotion step after staging validation

GitLab jobs

  • deploy:staging
    • environment: staging
    • resource group: staging
    • trigger: automatic on main
  • deploy:prod
    • environment: production
    • resource group: production
    • trigger: manual on main

Both jobs reuse the same deployment flow:

  1. sync application sources to the target server
  2. decrypt infra/env/{environment}.env.enc or generate .env from CI variables
  3. validate env with scripts/check-env.sh
  4. run infra/deploy.sh
  5. run infra/migrate-improved.sh

SOPS setup

Required in CI when using encrypted env files:

  • preferred: SOPS_AGE_KEY_B64
    • base64-encoded private age key content on one line
    • recommended for GitLab variables
  • fallback: SOPS_AGE_KEY
    • raw multiline private age key content

Generate SOPS_AGE_KEY_B64 with:

base64 < ~/.config/sops/age/aaperture.txt | tr -d '\n'

Repository files:

  • .sops.yaml
  • infra/env/staging.env.enc
  • infra/env/production.env.enc

Helper files:

  • infra/env/README.md
  • scripts/bootstrap-sops-config.sh
  • scripts/render-deploy-env.sh
  • scripts/export-gitlab-env.mjs

Variable strategy

Use GitLab environment-scoped variables with the same variable names for both environments.

Examples of variables that should be scoped separately for staging and production:

  • SERVER_HOST
  • SERVER_USER
  • SERVER_APP_DIR
  • COMPOSE_PROJECT_NAME
  • SSH_PRIVATE_KEY
  • DOMAIN
  • NAME
  • FRONTEND_URL
  • FRONTEND_BASE_URL
  • API_BASE_URL
  • PUBLIC_ROOT_DOMAIN
  • ALLOWED_ORIGINS
  • GOOGLE_CLIENT_ID
  • GOOGLE_CLIENT_SECRET
  • GOOGLE_CALLBACK_URL
  • POSTGRES_DB
  • POSTGRES_USER
  • POSTGRES_PASSWORD
  • POSTGRES_HOST_PORT
  • API_HOST_PORT
  • WEB_HOST_PORT
  • PROMETHEUS_HOST_PORT
  • GRAFANA_HOST_PORT
  • R2_*
  • STRIPE_*
  • SENTRY_*
  • VAPID_*

Recommendation:

  • keep shared secrets global only when they are truly identical across environments
  • scope host/domain/database credentials per environment
  • prefer SERVER_APP_DIR over legacy aliases in new configuration
  • keep distinct host ports when staging and production run on the same server

Expected URLs

Staging should use its own dedicated domains, for example:

  • marketing: https://staging.aaperture.com
  • app: https://app.staging.aaperture.com
  • tenant: https://{slug}.staging.aaperture.com

Production keeps:

  • marketing: https://aaperture.com
  • app: https://app.aaperture.com
  • tenant: https://{slug}.aaperture.com

The exact staging domains are up to infrastructure, but they must be reflected consistently in:

  • DOMAIN
  • FRONTEND_URL
  • FRONTEND_BASE_URL
  • API_BASE_URL
  • PUBLIC_ROOT_DOMAIN
  • OAuth redirect URIs
  • DNS and certificates

Runtime isolation on the same server

If staging and production run on the same host, they must not share:

  • SERVER_APP_DIR
  • COMPOSE_PROJECT_NAME
  • Postgres data directory
  • published host ports

Recommended staging values:

  • SERVER_APP_DIR=/var/www/aaperture-staging
  • COMPOSE_PROJECT_NAME=aaperture-staging
  • POSTGRES_HOST_PORT=15432
  • API_HOST_PORT=18080
  • WEB_HOST_PORT=18081
  • PROMETHEUS_HOST_PORT=19090
  • GRAFANA_HOST_PORT=13001

Recommended production values:

  • SERVER_APP_DIR=/var/www/aaperture
  • COMPOSE_PROJECT_NAME=aaperture
  • POSTGRES_HOST_PORT=5432
  • API_HOST_PORT=8080
  • WEB_HOST_PORT=8081
  • PROMETHEUS_HOST_PORT=9090
  • GRAFANA_HOST_PORT=3001

Rollout expectation

Recommended workflow:

  1. push to main
  2. let deploy:staging finish automatically
  3. validate staging manually
  4. trigger deploy:prod

Notes

  • NODE_ENV remains production in both staging and production deployments
  • migration script receives ENVIRONMENT=staging or ENVIRONMENT=production
  • staging and production use different resource_group values, so they do not block each other unnecessarily

Ongoing maintenance

After SOPS becomes the source of truth:

  • keep infra/env/staging.env.enc and infra/env/production.env.enc committed
  • do not keep *.plain.env or *.from-gitlab.env in the repository
  • use GitLab scoped variables only for:
    • deployment access
    • bootstrap/migration fallback
    • SOPS_AGE_KEY_B64

Practical minimum GitLab set once the migration is complete:

  • SOPS_AGE_KEY_B64
  • SERVER_HOST
  • SERVER_USER
  • SERVER_APP_DIR
  • SSH_PRIVATE_KEY

Everything else should preferably come from:

  • infra/env/staging.env.enc
  • infra/env/production.env.enc

When adding a new runtime variable:

  1. update .env.example
  2. update scripts/check-env.sh if required
  3. update and re-encrypt both environment files