ADR 0021: Continuous delivery — GitHub Actions and GitHub Container Registry
Ratification
Adopted under ADR 0018. Link the discussion Issue and merge PR when they exist. If not, use git history as the record.
- Discussion Issue: none linked for this merge
- Merge PR: see git history for this file
- Accepted: 2026-04-12
Context
This ADR explains why we automate publishing a container image from Git, what problem that solves for newcomers and operators, and how far automation goes in this repository (registry delivery, not full production rollout).
What problem are we solving?
Before this decision, the service could be built and tested locally and in CI (continuous integration), but there was no single, repeatable path that said: “every accepted change on the integration branch produces the same container image artifact in a registry that others can pull.” That gap matters because:
- Reproducibility — A deployment (even a manual one) should start from a known image digest or tag, not from “whatever was built on someone’s laptop yesterday.”
- Traceability — You want a straight line from a Git commit (or release tag) to the image that runs in an environment, for debugging and compliance.
-
Onboarding — New contributors read “we have a
Dockerfile” (ADR 0015) but may not know how that image gets to a place where a VM or a PaaS can consume it. This ADR documents the first automated hop: build → push to GHCR.
CI vs CD (for beginners)
Continuous integration (CI) answers: “Is this code in a good state?” — tests, lint, types, generated docs, and related checks run on a clean machine (for example GitHub Actions) when you open a pull request or push to the integration branch. CI does not, by itself, put your app on the internet.
Continuous delivery (CD) usually means: “After CI passes, deliver a deployable artifact.” In practice that often means building a Docker image and pushing it to a container registry so any environment authorized to pull can run that exact image. Some teams also automate the next step (deploy to staging/production) in the same pipeline; this repository stops at registry delivery unless you add external steps yourself.
Why stop at the registry? Hosting (VM, managed service, or your deployment platform) needs secrets, database URLs, networking, and policies that are specific to your infrastructure. Those belong in the deployment platform or runbooks, not hard-coded in a generic open-source workflow. This ADR still delivers a concrete, automatable outcome: a published image per successful pipeline.
Why GitHub Container Registry (GHCR)?
GHCR (ghcr.io) is GitHub’s container registry. It fits this project because:
-
The source code already lives on GitHub; the workflow can authenticate with the default
GITHUB_TOKEN(withpackages: writeon the publish job) — no extra secrets for the push path. -
Image names align with the repository (for example
ghcr.io/<owner>/<repo>, lowercased), which is easy to document and discover under the repo’s Packages view. -
Teams that later prefer AWS ECR, Google Artifact Registry, or Docker Hub can mirror the same
Dockerfilebuild; the decision here is the default path, not an exclusive lock-in.
How this relates to ADR 0015
ADR 0015 defines what goes into
the
image (Dockerfile layout, entrypoint). This ADR defines automation
that
builds that image in CI and pushes it to GHCR after the same quality gates that protect merges. It does not
replace
local development with make run; it complements packaging for downstream environments.
Decision
-
Add a GitHub Actions job
publish-imagein.github/workflows/ci.ymlthat builds the repository rootDockerfileand pushes to GHCR atghcr.io/<lowercase owner/repo>, using Docker Buildx anddocker/metadata-actionfor tags. -
When it runs — only on
push(not on pull requests), after successful jobsqualityandchangelog, and only when the ref ismain,master, or a semantic version tag matchingv*(for examplev1.2.0). Thechangelogjob is included in the graph for tag pushes so dependent jobs are not skipped by GitHub Actions’needsrules. -
Tags applied to the image — short Git SHA;
latestwhen the push is tomainormaster; semver-style tags when the Git ref is a version tag (per metadata action configuration). -
Authentication —
docker/login-actiontoghcr.iowithsecrets.GITHUB_TOKEN; job permissionpackages: write. No custom repository secrets are required for the push path. -
Build cache — use GitHub Actions cache backends for layer cache (
type=gha) to keep builds reasonably fast. -
Runtime configuration — environment variables for the database,
APP_ENV, API keys, and similar settings are not injected by this workflow; they are the responsibility of whoever runs the container (host, PaaS, or another runtime), consistent with ADR 0010.
Scope
-
In scope: GitHub Actions workflow changes, GHCR as the default registry, tagging policy tied to
branches and
v*release tags (ADR 0017), documentation and ADR index updates. - Out of scope: automated deployment to a specific cloud, VM, or cluster; GitOps; image vulnerability scanning gates; multi-architecture image matrices; promoting images across staging/production environments with manual approvals. Those may be added later as separate decisions.
Alternatives considered
-
Manual
docker buildanddocker pushonly- Pros: no CI minutes; full manual control.
- Cons: easy to forget; inconsistent tags; no guarantee the image matches a commit that passed CI. Rejected as the default for the integration branch.
-
A separate workflow file for CD only
- Pros: clearer separation of “CI” vs “CD” in the GitHub Actions UI.
- Cons: duplicated trigger conditions; a single workflow with
needskeeps ordering obvious. We keep CD inci.ymlfor simplicity.
-
Deploy to a host (SSH or remote API) in the same job
- Pros: end-to-end automation from push to running service.
- Cons: requires secrets and target infrastructure; not generic. Deferred until a concrete environment exists.
Consequences
Positive
- Every successful merge to the integration branch (and version tags) yields a pullable image that matches the Dockerfile from ADR 0015.
- New contributors get one path: CI green → image on GHCR → pull and run anywhere Docker can reach the registry.
- The standard GitHub-hosted publish path does not require an extra personal access token for the push step.
Trade-offs
- GitHub Actions minutes accrue (public repos have generous limits; private repos need billing awareness).
- Package visibility and pull authentication for consumers of the image remain an operator concern (configure package visibility or tokens as needed).
- Running the container in production still requires a real database and secrets outside the workflow.
Compatibility and migration
- Backward-compatibility impact: none for API contracts; automation and docs are additive only.
- Migration plan: not required for API consumers.
- Rollback: remove or disable the
publish-imagejob; local development workflow is unchanged.
Validation
- Technical: on push to
main, the workflow completespublish-image; the image appears under the repository’s Packages and is pullable with correct authentication. - Documentation: this ADR, the ADR index, and contributor notes link to the workflow as needed.
References
-
Related ADRs:
0015 (container image),
0013 (changelog gates),
0017 (branches and
v*tags), 0010 (runtime configuration), 0008 (Make entrypoints and CI alignment). - Workflow:
.github/workflows/ci.yml(jobpublish-image). - Developer guide: Docker image and container (local build; links to registry automation).
Page history
| Date | Change | Author |
|---|---|---|
| Added Page history section (repository baseline). | Ivan Boyarkin |