Skip to content
BoringStack
Star

Production releases (k3s)

3 min read

The k3s target deploys one coordinated release. It replaces per-image auto-updating. .github/workflows/production-release.yml is the only publisher for this target. It builds four candidate images for one commit (api, migrations, ui and the public site), then pins all four digests in infra/k3s/overlays/prod/kustomization.yaml in a single commit. ArgoCD syncs that commit.

The per-image approach shipped the UI before the API image it needed existed. It also produced a stream of automatic update commits. Full detail lives in infra/k3s/README.md.

  1. Your push to main waits, up to 45 minutes, for every applicable CI workflow at the same commit. Any missing, cancelled, failed or timed-out check blocks the release.
  2. The workflow builds boringstack-api, boringstack-migrations, boringstack-ui and boringstack-site as candidate-<sha> images. Nothing moves latest or sha-* for this target. The site image takes its build-time values from infra/k3s/overlays/prod/site-build.env, so a change there rebuilds it.
  3. Only after all four builds succeed does it pin the digests in one commit. A newer main commit supersedes an older release.
  4. Argo syncs in wave order: infrastructure, then the migration Job (wave -1), then the API (wave 0), then the UI and the public site (wave 1) once the API is healthy.

A successful workflow means the digests were promoted. It does not mean Argo has finished deploying. Check Synced and Healthy, then smoke-test sign-in and the main screens.

  1. Fresh fork: nothing to remove.
  2. If an older Argo Application still has argocd-image-updater annotations, remove them first. Otherwise a per-image updater can overwrite the coordinated digests.
  3. Set the repository variable PRODUCTION_RELEASE_MODE=coordinated. Until it is set, the release job is skipped and no image is built.
  4. Allow the release bot to push the promotion commit to main. Do not disable branch protection to make a release pass.

The API and migrations images run with a read-only root filesystem. Only /tmp is writable. The API’s CMD runs bun --no-install, so it never installs packages at startup.

Postgres TLS follows one policy for the runtime and the migration Job. The CA comes from the CloudNativePG-generated boringstack-db-ca secret and is injected as DATABASE_SSL_CA. The policy lives in apps/api/src/lib/postgres/connection.ts. Keep it in DATABASE_SSL_*, not in sslmode on the URL.

The migration Job runs db:migrate only. It runs on every sync, so it must never seed. db:seed upserts reference data and would revert admin edits. Run the first admin bootstrap as a deliberate one-off. The steps are in infra/k3s/README.md. Its db:seed is idempotent: it creates the admin only when that user does not exist.

Restore all four digests from one earlier promotion in a reviewed commit, then sync the whole Application. The site image follows the same rule. It has no separate rollback path. Do not selectively sync Deployments. Migrations do not reverse automatically, so confirm the older API works with the current schema first.