Skip to content

Staged Deployment Contract

ADR-0010 sets the YourBOX deployment path. This procedure extends the same pattern to every application in the workspace. One container image flows through four environments in a fixed order, each environment runs the same smoke test before promotion advances, and the application’s environment configuration follows one env contract regardless of target.

flowchart LR
Build[Build image and sign] --> Compose[docker compose CI smoke]
Compose --> Vps[VPS via Ansible]
Vps --> Azure[Azure App Service]
Azure --> K8s[Homelab Kubernetes via Argo CD]

Each stage promotes the same image digest. No stage rebuilds the image. Rebuilds are reserved for the build stage.

Each stage signs a contract with the application code. The contract is the same for every application in the workspace.

Stage Image source Configuration source Smoke test runner
Build CI build step, signed with cosign n/a unit + integration suites
Compose GitLab registry, digest pinned prod.env (SOPS decrypted) CI smoke job
VPS GitLab registry, digest pinned prod.env (SOPS, tmpfs key) Ansible smoke role
Azure GitLab registry, digest pinned OpenBao dotenv artifact, one hour post deploy CI job
K8s GitLab registry, digest pinned External Secrets from OpenBao Kubernetes Job in cluster

The image digest is the link between stages. The CI pipeline carries it through as a job variable until the deploy stages consume it.

The env contract is described in each application’s “Secret Management” page. The contract is identical across stages:

  • A single set of environment variables consumed by the application at boot.
  • A boot check that fails fast when a required variable is missing.
  • DSN strings rather than per field variables (DB_DSN, STORAGE_DSN, SMTP_DSN).

Adding a stage later does not change the env contract. Adding a key to the contract goes through the secret onboarding checklist in the application’s documentation.

Each stage runs the same smoke test surface. The test exercises the golden path of the application end to end.

For YourBOX: GET /health returns 200, a video upload triggers a transcoding that completes within 60 seconds, the catalog renders the new video, the OIDC or form login completes.

For weather-app: GET /health returns 200, the frontend returns the homepage, the backend returns a weather payload for a known city, the WAF rejects a probe with a deliberately malformed User-Agent.

The smoke test definition lives in the application repository. The smoke runner is environment specific (CI job for compose and Azure, Ansible role for VPS, Kubernetes Job for k8s) and is shared across applications through the gitlab-ci-templates repository.

The container image is identical between stages. The differences are all consumed through environment variables and through the storage, identity and search adapters selected by their DSN.

  • Storage adapter: STORAGE_DSN selects s3://, azure-blob:// or file://. Application code is the same.
  • Identity: APP_ENV=prod enables the OIDC firewall; dev or vps enables the form firewall. Application code is the same.
  • Mailer: SMTP_DSN selects the SMTP provider. Application code is the same.
  • Cache: CACHE_DSN selects Redis when present, falls back to the in process filesystem cache otherwise.

A stage that needs a different code path is a design smell. Escalate to the application owner before forking the pipeline.

A commit lands on the protected main branch. The pipeline runs:

  1. Lint, test, build, scan, sign.
  2. Smoke compose: docker compose inside CI exercises the new image.
  3. Deploy VPS: Ansible play converges the VPS to the new image.
  4. Smoke VPS: end to end test against the public VPS URL.
  5. Deploy Azure: Terraform or App Service deploy step updates the running container image.
  6. Smoke Azure: end to end test against the Azure URL.
  7. Open MR against homelab-platform/kubernetes/apps/<app>/ to bump the image digest in the Argo CD Application.
  8. Operator reviews and merges. Argo CD reconciles within 5 minutes.
  9. Smoke k8s: Kubernetes Job exercises the new image inside the cluster.

Each deploy stage is gated by manual approval on the protected branch.

A rollback is a revert of the change that promoted the image. The revert affects exactly one resource per stage:

  • Compose: the previous tag stays cached on the docker compose host. Run the previous docker compose up and the application returns to the previous image.
  • VPS: re run the Ansible play with the previous image_digest.
  • Azure: re apply the previous App Service container reference, or the previous Terraform plan with the previous image variable.
  • Kubernetes: revert the MR that bumped the digest. Argo CD reverts within 5 minutes.

Argo CD’s selfHeal: true plus the explicit revert make the rollback deterministic. No manual kubectl rollout undo is needed.

Failure Stage that catches it Action
Build or scan failure Build Pipeline halts before any deploy
Compose smoke failure Compose MR cannot merge
VPS smoke failure VPS Rollback to previous tag, escalate
Azure smoke failure Azure Rollback Azure, freeze homelab promote
Argo CD sync failure Kubernetes Revert MR, escalate to platform on call
k8s smoke failure Kubernetes Revert MR, escalate to platform on call

The freeze pattern between Azure and Kubernetes is intentional. A failure on Azure means the homelab promotion does not happen. The two production-like environments cannot diverge through silent failures.

A new application that joins the workspace inherits the procedure by:

  1. Adding a compose.yml at the repository root that is identical for local development and the VPS smoke test.
  2. Adding an Ansible role under homelab-platform/ansible/roles/<app>/ with the layout described in the Ansible baseline (defaults, vars, tasks, handlers, Molecule, healthcheck).
  3. Adopting the env contract pattern with a typed boot check.
  4. Adding the Argo CD Application under homelab-platform/kubernetes/apps/<app>/.
  5. Wiring the smoke test in gitlab-ci-templates.

The new application appears in the deployment dashboards as soon as the first promotion lands.