Staged Deployment Contract
Purpose
Section titled “Purpose”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.
Stages
Section titled “Stages”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.
Stage Contract
Section titled “Stage Contract”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.
Env Contract
Section titled “Env Contract”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.
Smoke Test Per Stage
Section titled “Smoke Test Per Stage”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.
Configuration Boundaries
Section titled “Configuration Boundaries”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_DSNselectss3://,azure-blob://orfile://. Application code is the same. - Identity:
APP_ENV=prodenables the OIDC firewall;devorvpsenables the form firewall. Application code is the same. - Mailer:
SMTP_DSNselects the SMTP provider. Application code is the same. - Cache:
CACHE_DSNselects 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.
Promotion Path
Section titled “Promotion Path”A commit lands on the protected main branch. The pipeline runs:
- Lint, test, build, scan, sign.
- Smoke compose: docker compose inside CI exercises the new image.
- Deploy VPS: Ansible play converges the VPS to the new image.
- Smoke VPS: end to end test against the public VPS URL.
- Deploy Azure: Terraform or App Service deploy step updates the running container image.
- Smoke Azure: end to end test against the Azure URL.
- Open MR against
homelab-platform/kubernetes/apps/<app>/to bump the image digest in the Argo CD Application. - Operator reviews and merges. Argo CD reconciles within 5 minutes.
- Smoke k8s: Kubernetes Job exercises the new image inside the cluster.
Each deploy stage is gated by manual approval on the protected branch.
Rollback Path
Section titled “Rollback Path”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 upand 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 Surface And Escalation
Section titled “Failure Surface And Escalation”| 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.
Onboarding A New Application
Section titled “Onboarding A New Application”A new application that joins the workspace inherits the procedure by:
- Adding a
compose.ymlat the repository root that is identical for local development and the VPS smoke test. - Adding an Ansible role under
homelab-platform/ansible/roles/<app>/with the layout described in the Ansible baseline (defaults, vars, tasks, handlers, Molecule, healthcheck). - Adopting the env contract pattern with a typed boot check.
- Adding the Argo CD Application under
homelab-platform/kubernetes/apps/<app>/. - Wiring the smoke test in
gitlab-ci-templates.
The new application appears in the deployment dashboards as soon as the first promotion lands.