The deploy/fleet-overlay templates had drifted from what actually runs in anton-helm-workloads (verified live + against the emdash-kotkanagrilli reference). Canonical design co-locates everything in the `kotkan` namespace: - source.yaml: GitRepository flux-system -> kotkan, so the HelmRelease chart sourceRef resolves same-namespace (no cross-namespace ref). - secrets.yaml: deploy-key Secret -> kotkan, defined once in the staging overlay; dropped the duplicate definition from the production overlay (production references the shared key by name). - image-automation.yaml: IUA write-back sourceRef anton-workloads-image-automation/flux-system -> anton-helm-workloads/kotkan (the existing read source already has push access). - README.md / DEPLOYMENT.md: namespace + ownership docs corrected.
9.7 KiB
Deployment
How a code change in this repo becomes a running pod at
cms-plugins-{staging,production}.kotkanagrilli.fi.
Repos involved
| Repo | Lives at | Owns |
|---|---|---|
| cms-plugins (this one) | git.oleks.space/oleks/cms-plugins |
App source, Dockerfile, Helm chart, Woodpecker pipeline |
| anton-helm-workloads | git.oleks.space/anton/helm-workloads |
The FluxCD HelmReleases for each subdomain on kotkanagrilli.fi. The legacy WP kotkanagrilli/ and the dev hello-kotkan/ releases also live here. |
| Gitea OCI registry | git.oleks.space/oleks/cms-plugins (image) |
Container images pushed by CI |
The cms-plugins repo and anton-helm-workloads repo are linked by:
- The Helm chart path. The HelmRelease in
anton-helm-workloadsreferenceschart: ./deploy/helmfrom aGitRepositorysource that points back at thestagingorproductionbranch ofcms-plugins. Flux pulls the chart directly from this repo — there is no "publish chart" step in CI. - The image policy. A FluxCD
ImagePolicyinanton-helm-workloadswatches thestaging/productionfloating tag in the Gitea OCI registry and rewrites the resolved digest intohelmrelease.yaml, which is what makeshelm upgradesee a change when CI retags.
Branch → environment
Three branches, three environments. Promotion is fast-forward only — no PR ceremony.
| Branch | Environment | URL | Where it runs |
|---|---|---|---|
develop |
dev | https://cms-plugins.ddev.site/ |
Local DDEV |
staging |
staging | https://cms-plugins-staging.kotkanagrilli.fi/ |
k3s on kotkan |
production |
production | https://cms-plugins-production.kotkanagrilli.fi/ |
k3s on kotkan |
What happens on a push
A push to staging or production triggers .woodpecker/container.yaml:
- Spins up a remote buildkit at
buildkit-rootless-arm64.infra.svc.cluster.local:1234. - Builds the root
Dockerfile. - Publishes three refs for each build:
git.oleks.space/oleks/cms-plugins:0.1.<pipeline>— immutable audit tag.git.oleks.space/oleks/cms-plugins:<branch>— floating channel pointer; this is what FluxCD'sImagePolicywatches.git.oleks.space/oleks/cms-plugins:<branch>-latest— cosmetic; falls back to a real ref if the chart'simage.tagis consulted (which the digest-pinning path normally bypasses).
FluxCD then takes over (it polls every minute):
- The
ImagePolicyinanton-helm-workloads/cms-plugins-<env>/image-automation.yamlresolves the new digest behind the<branch>tag. ImageUpdateAutomationwrites that digest back intohelmrelease.yamland commits/pushes toanton-helm-workloads:main.- The
GitRepositoryinanton-helm-workloads/cms-plugins-<env>/source.yamlsyncs thecms-pluginsbranch and exposesdeploy/helm/. - The
HelmReleasereconciles:helm upgradesees a new digest invalues.image.digestand rolls the Deployment.
End-to-end deploy time: ≈4 min (build ≈3 min + Flux interval ≈1 min).
Local development
cd ~/projects/cms-plugins
ddev start
# → https://cms-plugins.ddev.site/ (admin at /_emdash/admin)
This is not a prod-parity smoke test — DDEV runs npx emdash dev with
hot reload; production runs the built node ./dist/server/entry.mjs. The
shared artifact is the Astro source under app/. To smoke-test the
production image standalone:
docker build -t cms-plugins:dev .
docker run --rm -p 4321:4321 \
-e EMDASH_ENCRYPTION_KEY=$(openssl rand -hex 32) \
-v cms-plugins-state:/app/state \
cms-plugins:dev
Promoting develop → staging
git switch staging
git merge --ff-only develop
git push origin staging
The Woodpecker pipeline triggers automatically. Watch it at the Woodpecker UI; once green, Flux reconciles within ≤1 min.
Promoting staging → production
After staging looks good:
git switch production
git merge --ff-only staging
git push origin production
If the fast-forward fails, something is out of order — investigate before forcing.
Rolling back
The chart pins by digest (set by FluxCD's
ImageUpdateAutomation), with <branch>-latest as a fallback tag. To
roll back:
- Identify the previous good build's immutable tag from the Gitea
registry — it is
0.1.<N>where<N>is the pipeline number. - In
anton-helm-workloads/cms-plugins-<env>/helmrelease.yaml, hard-code the previous digest undervalues.image.digestand remove the{"$imagepolicy": ...}marker (so IUA stops overwriting it). Commit and push. - Flux reconciles in ≤1 min.
To resume tracking the floating tag, restore the marker and the auto-update resumes.
Secrets
All secrets are sops-encrypted in anton-helm-workloads:
anton-helm-workloads/cms-plugins-staging/secrets.yamlanton-helm-workloads/cms-plugins-production/secrets.yaml
Each env has two distinct Secrets:
| Secret | Namespace | Purpose |
|---|---|---|
cms-plugins-deploy-key |
kotkan |
SSH deploy key for Flux to clone cms-plugins (one pair shared between staging + production — same key reads both branches). Defined once in the staging overlay; co-located with the GitRepositories in kotkan so the secretRef is same-namespace. |
cms-plugins-<env>-secrets |
kotkan |
Env vars the pod consumes via existingSecret. Required key: EMDASH_ENCRYPTION_KEY. |
To rotate a credential:
cd ~/projects/servers/anton/anton-helm-workloads/cms-plugins-staging
sops secrets.yaml # opens decrypted in $EDITOR
git add . && git commit -m "rotate cms-plugins staging credential"
git push # Flux re-applies in ≤1 min
The Deployment does NOT restart on Secret change (k8s envFrom doesn't
watch). Force a roll after rotating:
kubectl -n kotkan rollout restart deployment/cms-plugins-staging
First-time setup checklist
This is the once-per-environment dance to bring staging or production online.
A. In this (cms-plugins) repo
- Push to
git.oleks.space/oleks/cms-pluginswith three branches:develop,staging,production. The initial commit lives ondevelop; the other two are fast-forward copies. - Add the Woodpecker secrets at Repo → Settings → Secrets in the
Woodpecker UI (
ci.oleks.space), scoped to eventpush:Secret Purpose gitea_clone_tokenGitea PAT for cloning during the build step. registry_tokenGitea PAT with write access to git.oleks.space/oleks/cms-plugins(container packages).
B. In the anton-helm-workloads repo
-
Generate an SSH deploy key (one pair, shared across envs):
ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N "" -
Upload
/tmp/cms-plugins-deploy.pubto thecms-pluginsrepo on Gitea: Settings → Deploy Keys → "cms-plugins Flux deploy", read-only. -
Generate one
EMDASH_ENCRYPTION_KEYper env (do NOT reuse):openssl rand -hex 32 # staging openssl rand -hex 32 # production -
Copy this repo's
deploy/fleet-overlay/cms-plugins-staging/andcms-plugins-production/directories into the root ofanton-helm-workloads, then edit eachsecrets.yamlto put the real key material in, and sops-encrypt:cd anton-helm-workloads/cms-plugins-staging sops --encrypt --age <recipient> secrets.yaml > secrets.enc.yaml mv secrets.enc.yaml secrets.yaml -
Add both directories to
anton-helm-workloads/kustomization.yaml:resources: - hello-kotkan - kotkanagrilli - max-openclaw - terminal-agent - cms-plugins-staging # ← add - cms-plugins-production # ← add -
Ensure a writable
GitRepositorynamedanton-workloads-image-automationexists influx-systemfor theImageUpdateAutomationto push digest commits toanton-helm-workloads:main. If it doesn't exist yet, create it (this is a one-time setup for the whole workloads repo). Theemdash-kotkanagrillianalogue isoleks-fleet-image-automation. -
Commit and push
anton-helm-workloads. Flux picks it up in ≤1 min.
C. DNS
dig +short cms-plugins-staging.kotkanagrilli.fi
dig +short cms-plugins-production.kotkanagrilli.fi
Both should resolve to the kotkan ingress IP (the same one
emdash-staging.kotkanagrilli.fi already uses).
D. First deploy
-
Push to
stagingto trigger the first build. Watch Woodpecker. -
Once the image lands, Flux reconciles. Watch the rollout:
kubectl -n kotkan get pods -w | grep cms-plugins flux get all -n flux-system | grep cms-plugins -
Once
cms-plugins-staging.kotkanagrilli.fireturns a 200, fast-forwardproductiontostagingand push.
Why this shape
- Single replica, pinned to
kotkan. Emdash uses local SQLite (single-writer); horizontal scaling isn't a thing here. The PVC islocal-path, so the pod can only run where the volume lives. - Mutable
<branch>tag. The sha-tagged ref (0.1.<pipeline>) gives audit trail; the mutable<branch>tag is what Flux watches. Together they give visibility without forcing ananton-helm-workloadscommit per deploy (theImageUpdateAutomationdoes that automatically). - Flux pulls the chart from this repo (no OCI step). Keeps the chart in the same git history as the app code; a chart change rolls with the same release.
- HelmRelease lives in
anton-helm-workloads, not in fleet. Matches the convention that the kotkanagrilli.fi subdomain pool (legacykotkanagrilli/,hello-kotkan/, future subdomains) is reconciled out of the workloads repo, not the personal fleet repo. - kotkan, not armer. The legacy
kotkanagrilli.fisite and the emdash replacement both live onkotkan; colocating reduces DNS / ingress drift while the WP → Emdash story plays out.