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:
infra/env/{environment}.env.enc- scoped GitLab CI variables
Environments
staging- deployed automatically on every push to
main - used to validate the current
mainstate before production rollout
- deployed automatically on every push to
production- deployed manually from the
mainpipeline - intended as an explicit promotion step after staging validation
- deployed manually from the
GitLab jobs
deploy:staging- environment:
staging - resource group:
staging - trigger: automatic on
main
- environment:
deploy:prod- environment:
production - resource group:
production - trigger: manual on
main
- environment:
Both jobs reuse the same deployment flow:
- sync application sources to the target server
- decrypt
infra/env/{environment}.env.encor generate.envfrom CI variables - validate env with
scripts/check-env.sh - run
infra/deploy.sh - 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.yamlinfra/env/staging.env.encinfra/env/production.env.enc
Helper files:
infra/env/README.mdscripts/bootstrap-sops-config.shscripts/render-deploy-env.shscripts/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_HOSTSERVER_USERSERVER_APP_DIRCOMPOSE_PROJECT_NAMESSH_PRIVATE_KEYDOMAINNAMEFRONTEND_URLFRONTEND_BASE_URLAPI_BASE_URLPUBLIC_ROOT_DOMAINALLOWED_ORIGINSGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_CALLBACK_URLPOSTGRES_DBPOSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_HOST_PORTAPI_HOST_PORTWEB_HOST_PORTPROMETHEUS_HOST_PORTGRAFANA_HOST_PORTR2_*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_DIRover legacy aliases in new configuration - keep distinct host ports when
stagingandproductionrun 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:
DOMAINFRONTEND_URLFRONTEND_BASE_URLAPI_BASE_URLPUBLIC_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_DIRCOMPOSE_PROJECT_NAME- Postgres data directory
- published host ports
Recommended staging values:
SERVER_APP_DIR=/var/www/aaperture-stagingCOMPOSE_PROJECT_NAME=aaperture-stagingPOSTGRES_HOST_PORT=15432API_HOST_PORT=18080WEB_HOST_PORT=18081PROMETHEUS_HOST_PORT=19090GRAFANA_HOST_PORT=13001
Recommended production values:
SERVER_APP_DIR=/var/www/aapertureCOMPOSE_PROJECT_NAME=aaperturePOSTGRES_HOST_PORT=5432API_HOST_PORT=8080WEB_HOST_PORT=8081PROMETHEUS_HOST_PORT=9090GRAFANA_HOST_PORT=3001
Rollout expectation
Recommended workflow:
- push to
main - let
deploy:stagingfinish automatically - validate staging manually
- trigger
deploy:prod
Notes
NODE_ENVremainsproductionin both staging and production deployments- migration script receives
ENVIRONMENT=stagingorENVIRONMENT=production - staging and production use different
resource_groupvalues, so they do not block each other unnecessarily
Ongoing maintenance
After SOPS becomes the source of truth:
- keep
infra/env/staging.env.encandinfra/env/production.env.enccommitted - do not keep
*.plain.envor*.from-gitlab.envin 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_B64SERVER_HOSTSERVER_USERSERVER_APP_DIRSSH_PRIVATE_KEY
Everything else should preferably come from:
infra/env/staging.env.encinfra/env/production.env.enc
When adding a new runtime variable:
- update
.env.example - update
scripts/check-env.shif required - update and re-encrypt both environment files