8.3 KiB
Deployment Runbook
This document explains how deployments work for the EDR platform using Docker, GitHub Actions, and self-hosted runners.
Overview
- Monorepo contains 6 deployable services:
freight-apifreight-portalfreight-backofficepassenger-apipassenger-portalpassenger-backoffice
- Deployments run through one workflow:
.github/workflows/deploy.yml - Each service is built/deployed independently in parallel (matrix jobs).
- Docker Compose project names are branch-aware to avoid environment collisions on the same host.
Prerequisites
- Docker Engine with Compose plugin on the self-hosted runner.
- GitHub self-hosted runner registered for this repository.
- Repository secret configured:
NPM_TOKEN(for private@tria-plc/*package install during Docker build)
- Server-side env files created for each branch/environment.
Server Environment Files
sync-env-from-server.sh reads env files from:
/home/<DEPLOY_USER>/environment/edr/<branch-slug>/<project>/
Where:
<DEPLOY_USER>defaults totria(overridable byDEPLOY_USER)<branch-slug>is derived from Git branch (lowercase, non-alphanumeric replaced with-)<project>isedr-freightoredr-passenger
Required files per project
For edr-freight:
freight-api.envfreight-portal.envfreight-backoffice.env- optional:
freight-web.build.env
For edr-passenger:
passenger-api.envpassenger-portal.envpassenger-backoffice.env- optional:
passenger-web.build.env
Required env key
Each service env file must contain:
PORT=<number>
The sync script validates this and fails if missing.
Build env files (optional)
Used for additional build-time variables (example: Vite API URL for freight web), with export syntax:
export FREIGHT_VITE_API_URL=https://freight-api.example.com/api
These are injected into GITHUB_ENV during workflow execution.
Passenger web:
NEXT_PUBLIC_API_URLdoes not need a separate build env file. Place it directly in the service runtime env file (passenger-portal.env/passenger-backoffice.env) and the sync script will forward it to the build automatically.
Docker Compose Port Mapping
docker-compose.yaml uses per-service env variables for host/container port mappings:
FREIGHT_API_PORTPASSENGER_API_PORTFREIGHT_PORTAL_PORTFREIGHT_BACKOFFICE_PORTPASSENGER_PORTAL_PORTPASSENGER_BACKOFFICE_PORT
scripts/deploy/sync-env-from-server.sh extracts PORT from each synced .env and exports the corresponding *_PORT variable to GITHUB_ENV.
Passenger Web Docker Configuration
The passenger web apps (portal and backoffice) are deployed as Next.js applications using a dedicated Dockerfile:
- Dockerfile:
infrastructure/docker/Dockerfile.passenger-web - Apps:
apps/edr-passenger-web/portalandapps/edr-passenger-web/backoffice
Key differences from freight-web
| Aspect | Freight Web | Passenger Web |
|---|---|---|
| Framework | Vite (SPA) | Next.js (SSR/SSG) |
| Deployment | Static export + nginx | Node.js server |
| Dockerfile | Dockerfile.web |
Dockerfile.passenger-web |
| Final port (container) | 80 (nginx) | driven by PORT in service .env |
| Build arg | TURBO_FILTER |
APP_PACKAGE + APP_PATH + NEXT_PUBLIC_API_URL |
Build arguments
The Dockerfile accepts the following build args:
APP_PACKAGE: Turbo package filter (e.g.,@edr/passenger-portal)APP_PATH: App directory path (e.g.,apps/edr-passenger-web/portal)NEXT_PUBLIC_API_URL: API URL visible to browser — sourced fromNEXT_PUBLIC_API_URLin the service.envfile
Port mapping
Both host and container ports are driven by PORT in the service env file. The sync script reads PORT, exports PASSENGER_PORTAL_PORT / PASSENGER_BACKOFFICE_PORT to GITHUB_ENV, and docker-compose.yaml uses those variables for both sides of the mapping:
${PASSENGER_PORTAL_PORT:-5174}:${PASSENGER_PORTAL_PORT:-5174}
This ensures docker ps shows 0.0.0.0:<port>-><port>/tcp with matching ports.
Runtime
The final image uses Next.js output: 'standalone' and runs:
node server.js
Next.js reads PORT from the runtime environment (supplied via env_file in docker-compose). The standalone output bundles only the required node_modules, producing a significantly smaller image than a full pnpm deploy.
Rollback Procedure
Each build is tagged with the short git SHA (${COMPOSE_PROJECT_NAME}-<service>:<sha8>).
Rollback a single service
# 1. Find the last known-good image tag
docker images | grep passenger-api
# 2. Re-tag it as the current image
docker tag edr-passenger-main-passenger-api:<previous-sha> edr-passenger-main-passenger-api:latest
# 3. Restart the container from the previous image
docker compose --project-name edr-passenger-main up -d passenger-api --force-recreate
Rollback via re-run
Alternatively, trigger a workflow_dispatch on the last known-good commit SHA from the GitHub Actions UI — this rebuilds and redeploys that exact commit.
Production Security Checklist
Before deploying to production, verify:
JWT_SECRET,JWT_ACCESS_TOKEN_SECRET,JWT_REFRESH_TOKEN_SECRETare set to random 32+ char strings (openssl rand -hex 32)DATABASE_URLincludes?sslmode=require&connection_limit=10WAAFI_INSECURE_TLSisfalse(app will refuse to start iftruein production)NODE_ENV=productionis setGITHUB_PACKAGE_TOKENis a scoped read-only token, not a personal admin token- No
.envfiles are committed to the repository (git statusshould show none)
Data Retention Policy
The TasksService runs a daily purge cron at 02:00 EAT that automatically deletes:
| Table | Retention |
|---|---|
OtpCode |
1 hour after expiry or verification |
FaydaVerificationSession |
1 hour after expiry or completion |
AuditLog |
365 days |
PaymentWebhookEvent |
90 days |
GateValidationLog |
180 days |
No manual intervention is required. Monitor the TasksService log output for purge counts.
GitHub Actions Deployment Flow
Workflow file: .github/workflows/deploy.yml
1) prepare job
- Checks out repository once.
- Creates workspace artifact (
workspace.tgz) and uploads it.
2) deploy matrix job (parallel)
For each service:
- Downloads and extracts workspace artifact.
- Syncs that service env file from server path.
- Computes branch slug and sets:
COMPOSE_PROJECT_NAME=<project>-<branch-slug>
- Creates
.npmrc/.npmrc_tempfromNPM_TOKEN. - For
passenger-apiandpayment-api: builds and runs the migration image as a gated step before the app image. - Builds the service image and tags it with the short git SHA.
- Runs
docker compose up -d <service> --force-recreate. - For API services: polls
GET /health/readyevery 10s for up to 120s. Fails the job if the service does not become healthy. - Cleans
.npmrc/.npmrc_temp.
Branch/Environment Isolation
Compose project name is generated as:
<project>-<branch-slug>
Examples:
edr-freight-mainedr-freight-stagingedr-passenger-dev
This prevents container/network/volume name collisions between branches.
Local Manual Deployment (Optional)
From repo root:
DOCKER_BUILDKIT=1 docker compose build <service>
docker compose up -d <service>
If private packages are required locally, create .npmrc:
cat <<EOF > .npmrc
@tria-plc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<YOUR_TOKEN>
always-auth=true
EOF
Passenger API Startup Behavior
Passenger container entrypoint runs on startup:
npm run prisma:generatenpm run prisma:migrate(deploy mode)npm run prisma:seed- starts API process
Troubleshooting
Missing env file
Error:
Missing env file: ...
Fix:
- Create the required file in the server env directory for that project/branch slug.
Missing PORT in env file
Error:
Missing required PORT in env file: ...
Fix:
- Add
PORT=<number>to that service env file.
Private package install fails
Check:
NPM_TOKENexists in repo secrets.- Workflow created
.npmrcsuccessfully.
Prisma seed/migrate failures (passenger)
Check:
DATABASE_URLinpassenger-api.env- DB reachability from runner host/container network
- migration history consistency