Skip to main content
Production deployments are fully manual and gated. The Release Pipeline workflow must be triggered explicitly via workflow_dispatch from the main branch. A human approval step on the production GitHub Actions environment is required before any deployment runs.
The production deployment permanently replaces the running container on the remote host. Ensure the commit has already passed staging validation before triggering the pipeline.

Release pipeline overview

push to main


┌─────────────────────┐
│   validate-staging  │  Runs on every push to main
│   (staging env)     │  Tests · coverage · desktop build
└─────────────────────┘
          │  workflow_dispatch only

┌──────────────────────────┐
│  build-production-image  │  Builds and pushes image to ghcr.io
│  (packages write)        │
└──────────────────────────┘


┌──────────────────────┐
│   deploy-production  │  ← Manual approval required
│   (production env)   │    SSH → docker pull → docker run
└──────────────────────┘


   production-release-<sha> artifact uploaded
The build-production-image and deploy-production jobs only run when the workflow is triggered via workflow_dispatch on main. Pushes to main only run validate-staging.

Triggering the pipeline

1

Confirm main is ready

Verify main is clean and the latest commit has a green validate-staging run. Check that no open regressions exist.
2

Dispatch the workflow

Go to Actions → Release Pipeline → Run workflow in the GitHub repository and select the main branch. Alternatively, use the GitHub CLI:
gh workflow run cd.yml --ref main
3

Wait for image build

The build-production-image job runs validate-staging again, then builds and pushes the backend Docker image to:
ghcr.io/<owner>/fleet-management-backend:<git-sha>
ghcr.io/<owner>/fleet-management-backend:production-latest
4

Approve the deployment

Once build-production-image is green, GitHub will pause at deploy-production and wait for a reviewer with access to the production environment to approve. Navigate to the workflow run and click Review deployments → Approve.
5

Verify the deployment

After the job completes:
  1. Confirm the container fleet-management-backend is in state Up on the remote host.
  2. Send a request to GET /api/health against the production URL.
  3. Test admin login and a basic dashboard read.
  4. Confirm the uploads directory is still present and intact.

Running the deployment script locally

The deploy:production script can be run outside of CI if all required environment variables are set:
yarn deploy:production
This runs scripts/deploy-production.sh, which:
  1. Validates that all required variables are present.
  2. Writes a backend.production.env file locally from the PRODUCTION_* variables.
  3. Runs ssh-keyscan to collect the remote host’s key.
  4. Copies the env file to the remote host over SCP.
  5. Over SSH: moves the env file into place, logs in to ghcr.io, pulls the image, removes the existing container, and starts the new one.
docker run -d \
  --name fleet-management-backend \
  --restart unless-stopped \
  -p <PRODUCTION_APP_PORT>:3001 \
  --env-file <deploy_path>/.env \
  -v <deploy_path>/uploads:/app/packages/backend/uploads \
  <IMAGE_REF>
The uploads directory is bind-mounted from the host so user-uploaded files survive container replacements.

Docker image

The backend image is built from packages/backend/Dockerfile using the full repository as the build context. The image is tagged with both the exact git SHA and production-latest:
ghcr.io/<owner>/fleet-management-backend:<git-sha>
ghcr.io/<owner>/fleet-management-backend:production-latest
The IMAGE_REF passed to deploy-production.sh always uses the SHA tag for deterministic deploys.

Production secrets

These must be configured in the production GitHub Actions environment.
SecretDescription
PRODUCTION_DATABASE_URLFull Prisma-compatible connection string
PRODUCTION_DB_HOSTDatabase host (default: 127.0.0.1)
PRODUCTION_DB_PORTDatabase port (default: 5432)
PRODUCTION_DB_NAMEDatabase name (default: fleet_management)
PRODUCTION_DB_USERDatabase user (default: postgres)
PRODUCTION_DB_PASSWORDDatabase password
PRODUCTION_JWT_SECRETSecret used to sign JWT tokens
PRODUCTION_DEFAULT_LOGIN_PASSWORDInitial admin password
PRODUCTION_SMTP_HOSTSMTP server host (default: 127.0.0.1)
PRODUCTION_SMTP_PORTSMTP server port (default: 25)
PRODUCTION_SMTP_SECUREtrue for TLS, false otherwise (default: false)
PRODUCTION_SMTP_USERSMTP authentication user
PRODUCTION_SMTP_PASSSMTP authentication password
PRODUCTION_EMAIL_FROMSender address for outgoing emails

Remote host requirements

Before the first deployment, ensure the remote host meets these requirements:
  • Docker is installed and the SSH user can run docker commands.
  • The port defined by PRODUCTION_APP_PORT (default 3001) is open to incoming traffic.
  • The host has outbound access to ghcr.io to pull images.
  • The production database and SMTP server are reachable from the host.

Rollback

To roll back to a previous image:
1

Identify the target commit

Find the git SHA of the last known-good deployment from the production-release-<sha> artifact in GitHub Actions.
2

Re-trigger the workflow

Run the Release Pipeline workflow again on the target commit or re-tag main to that SHA. You can also run deploy:production locally with the specific IMAGE_REF:
IMAGE_REF=ghcr.io/<owner>/fleet-management-backend:<previous-sha> \
  yarn deploy:production
3

Approve and verify

Approve the deploy-production gate again and verify GET /api/health returns a healthy response.

Build docs developers (and LLMs) love