Manifests for deploying this CMS, served at
https://campfire.wsj27.scouterna.net/_services/cms in the wsj27 namespace
on Scouterna's shared AKS cluster.
Nothing applies these automatically — CI only builds and pushes the image. Applying is a deliberate manual act:
export KUBECONFIG=~/.kube/wsj27.yaml
IMG=ghcr.io/scouterna/wsj27-cms
(cd k8s && kustomize edit set image "$IMG=$IMG:<git-sha>") && kubectl apply -k k8s
# then revert the kustomization edit — never commit a real tagThe committed newTag is a deliberate placeholder so that applying unedited
fails fast (ImagePullBackOff) instead of silently deploying latest. Always
deploy a git-SHA tag; CI pushes one per commit.
-
Make the GHCR package public after the first image build (Scouterna org → Packages → wsj27-cms). The manifests use no
imagePullSecrets— same as wsj27-auth-api — so a private package givesImagePullBackOff. -
Create the database on the in-cluster Postgres (
wsj27-postgres-temp), with its own role so the CMS cannot touch the other apps' data:kubectl exec -it statefulset/wsj27-postgres-temp -n wsj27 -- \ psql -U wsj27 -d wsj27 \ -c "CREATE ROLE wsj27cms LOGIN PASSWORD '<generate one>';" \ -c "CREATE DATABASE wsj27cms OWNER wsj27cms;"
The server is called temp for a reason — when it is replaced, the CMS only needs
DATABASE_URLin the secret updated and a pod restart. Payload's migrations run at boot, so a fresh database initializes itself. -
Create the secret (the deployment's
envFromexpects both keys):kubectl create secret generic wsj27-cms-secrets -n wsj27 \ --from-literal=PAYLOAD_SECRET="$(openssl rand -hex 32)" \ --from-literal=DATABASE_URL="postgresql://wsj27cms:<password>@wsj27-postgres-temp:5432/wsj27cms"
PAYLOAD_SECRETsigns Payload's own tokens/cookies — rotating it logs everyone out but destroys nothing. -
Create the font configmap. The brand fonts (Bravely Script, and TeeFranklin which is the face behind the profile's "Lieberath Grotesque") are commercial and deliberately not in this public repository. They mount into
public/fonts/from a configmap; the volume isoptional, so without it the pages fall back to system fonts instead of failing:# from the contingent's graphic package; woff2_compress is in the # `woff2` package on Debian woff2_compress BravelyScript-Regular.otf woff2_compress TeeFranklin-Book.otf # a.k.a. TeeFraBoo.otf woff2_compress TeeFranklin-Bold.otf # a.k.a. TeeFraBol.otf kubectl create configmap wsj27-cms-fonts -n wsj27 \ --from-file=BravelyScript-Regular.woff2 \ --from-file=TeeFranklin-Book.woff2 \ --from-file=TeeFranklin-Bold.woff2
The file names are load-bearing — the @font-face declarations in
src/app/(frontend)/handbok/page.tsxreference them verbatim. -
Apply (command at the top), then watch:
kubectl rollout status deploy/wsj27-cms -n wsj27 kubectl logs -l app=wsj27-cms -n wsj27 --tail=50 curl -s -o /dev/null -w '%{http_code}\n' https://campfire.wsj27.scouterna.net/_services/cms -
Give someone access. Login is SSO-only via wsj27-auth-api. CMT members (any
wsj27:cmt…project role) get editor access out of the box; everyone else needs awsj27-cms:editorgrant in wsj27-project-api's role map, and admin (user management) always requires an explicitwsj27-cms:admin. A user with none of these is treated as logged out — there is no local fallback account, by design.
replicas: 1+strategy: Recreate— the uploads PVC is ReadWriteOnce (an Azure disk), so a rolling replacement pod can generally not mount it. A deploy therefore has a few seconds of downtime. If the CMS ever needs more replicas, media storage has to move to blob storage (@payloadcms/storage-azure) first.- The TLS secret belongs to
testapp. ThetestappIngress carries the cert-manager annotations for campfire.wsj27.scouterna.net; this ingress (like wsj27-auth-api's) only referencestestapp-tls. If testapp is ever removed, the annotations — and the ownership of the certificate — must move to one of the surviving ingresses on the host. - The base path is baked into the image. Changing the serving path means
changing the
NEXT_BASE_PATHbuild-arg in the app repo's CI and rebuilding, not just editing the ingress. The ingress deliberately has no strip-prefix. - Traefik does not cap request body size by default, so media uploads work
without a middleware. j26 capped it at 50 MiB as protection; if that is
wanted here it is a
MiddlewareCRD, which the wsj27 kubeconfig currently has no rights to create.