initial scaffold: emdash catalog, helm chart, woodpecker pipeline, ddev

- app/: Emdash scaffold (Astro 6, node target) with cmses/plugins/pages collections
- app/seed/seed.json: WordPress→Emdash parity for kotkanagrilli.fi (~30 entries)
- Dockerfile + docker/entrypoint.sh: multi-stage build, single PVC at /app/state
- deploy/helm/: chart mirroring emdash-kotkanagrilli (single-replica, sqlite, kotkan)
- deploy/fleet-overlay/: HelmRelease/source/image-automation templates for
  anton-helm-workloads (staging + production)
- .woodpecker/container.yaml: arm64 build, three OCI tags per push
  (immutable 0.1.<pipeline> + floating <branch> + <branch>-latest)
- .ddev/: local dev with nginx proxy to emdash on :4321
- README/DEPLOYMENT/ARCHITECTURE/CLAUDE: docs covering the three-repo
  pipeline (cms-plugins + anton-helm-workloads + Gitea OCI registry)
This commit is contained in:
Oleks
2026-05-20 11:19:00 +03:00
commit 67b07634ae
52 changed files with 2856 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# deploy/
Two sibling directories with very different lifecycles:
- **`helm/`** — the Helm chart that runs the pod. FluxCD pulls it
directly from this repo on the branch matching each environment
(no `helm push` step). Edit this in lockstep with the app code that
depends on it.
- **`fleet-overlay/`** — templates for the FluxCD manifests that live in
the `anton-helm-workloads` repo. Not consumed from here — they're
versioned alongside the chart so the chart's contract with Flux stays
legible.
See `../DEPLOYMENT.md` for the end-to-end pipeline.
+50
View File
@@ -0,0 +1,50 @@
# Fleet overlay templates
The YAMLs under `cms-plugins-staging/` and `cms-plugins-production/` are the
FluxCD manifests that drive each environment. They are **not** consumed from
this repo — they live here as a versioned blueprint, intended to be copied
into the workloads repo that Flux watches:
```
git.oleks.space/anton/helm-workloads
├─ cms-plugins-staging/ ← copy from deploy/fleet-overlay/cms-plugins-staging/
├─ cms-plugins-production/ ← copy from deploy/fleet-overlay/cms-plugins-production/
└─ kustomization.yaml ← add both directories to `resources:`
```
See `../../DEPLOYMENT.md` for the full pipeline and the first-time setup
checklist (deploy keys, sops secrets, Woodpecker secrets, DNS).
## Shape
Each env directory contains five files, mirroring the emdash-kotkanagrilli
layout in `~/projects/servers/fleet/apps/base/`:
- `source.yaml``GitRepository` pointing at this repo on the matching
branch (`staging` / `production`), restricted to `/deploy/helm` via the
`ignore` rule so Flux only pulls the chart.
- `helmrelease.yaml``HelmRelease` consuming the chart from `./deploy/helm`
in that `GitRepository`. Pinned by digest (see image-automation.yaml).
- `image-automation.yaml``ImageRepository` + `ImagePolicy` +
`ImageUpdateAutomation`. Watches the floating `staging` / `production`
tag in the Gitea OCI registry, resolves the current digest, and rewrites
the digest setter in `helmrelease.yaml` (which is what actually makes
`helm upgrade` see a change when CI retags the image).
- `secrets.yaml` — two Secrets per env: the SSH deploy key Flux uses to
clone this repo (`cms-plugins-deploy-key`, in `flux-system`), and the
pod's env-var secret (`cms-plugins-{staging,production}-secrets`, in
`kotkan`). **Templates here are NOT encrypted** — sops-encrypt them
before pushing to anton-helm-workloads.
- `kustomization.yaml` — bundles the above.
## Why this lives in two repos
The chart (`deploy/helm/`) ships with the app — that way a chart change
is reviewed and tagged alongside the code that depends on it. The
HelmRelease references the chart as a path inside a `GitRepository`,
not as an OCI artifact, so there's no "publish chart" step in CI.
The HelmRelease itself lives in the workloads repo because that repo is
the source of truth for what runs on the kotkanagrilli.fi subdomain
pool. Same convention as the existing `kotkanagrilli/` (legacy WP) and
`hello-kotkan/` entries there.
@@ -0,0 +1,48 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cms-plugins-production
namespace: flux-system
spec:
interval: 5m
chart:
spec:
chart: ./deploy/helm
sourceRef:
kind: GitRepository
name: cms-plugins-production
namespace: flux-system
reconcileStrategy: Revision
releaseName: cms-plugins-production
targetNamespace: kotkan
install:
disableWait: true
remediation:
retries: 3
upgrade:
disableWait: true
remediation:
retries: 3
values:
existingSecret: cms-plugins-production-secrets
image:
# `tag` stays human-readable. The chart prefers `digest` when set
# and renders `repository@<digest>` — that's what actually pins
# the pod. Without digest pinning, helm upgrade would see no spec
# change when CI retags the floating `production` tag.
tag: production
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-production:digest"}
pullPolicy: Always
ingress:
host: cms-plugins-production.kotkanagrilli.fi
nodeSelector:
kubernetes.io/hostname: kotkan
persistence:
size: 10Gi
resources:
requests:
cpu: 100m
memory: 384Mi
limits:
cpu: "1"
memory: 1Gi
@@ -0,0 +1,78 @@
---
# Watch the Gitea OCI registry for the floating `production` tag. Every
# push to the production branch retags the new image as `production`,
# overwriting the previous binding (OCI tag→manifest is single-valued).
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: cms-plugins-production
namespace: kotkan
spec:
image: git.oleks.space/oleks/cms-plugins
interval: 1m
secretRef:
name: gitea-registry-creds
---
# Only the `production` floating tag is in scope. There's at most one
# match at a time, so alphabetical ordering is a no-op — the policy
# just resolves to that single tag's current digest.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: cms-plugins-production
namespace: kotkan
spec:
interval: 1m
imageRepositoryRef:
name: cms-plugins-production
filterTags:
pattern: '^production$'
# Extract and reflect the resolved digest into helmrelease.yaml.
# This enables IUA to pin by digest, which makes helm upgrade detect
# changes when the floating tag is reassigned.
digestReflectionPolicy: Always
policy:
alphabetical:
order: asc
---
# IUA writes the resolved digest into helmrelease.yaml — pinning by
# digest is what makes `helm upgrade` see a change when the floating
# tag is reassigned (without digest, tag stays `production` literal and
# helm upgrade is a no-op).
#
# NOTE: `sourceRef` must reference a GitRepository that points at
# THIS workloads repo (anton-helm-workloads) with write access. If it
# doesn't exist yet, create one alongside this manifest. The
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
# because its HelmReleases live in the fleet repo.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: cms-plugins-production
namespace: kotkan
spec:
interval: 1m
sourceRef:
kind: GitRepository
name: anton-workloads-image-automation
namespace: flux-system
git:
checkout:
ref:
branch: main
commit:
author:
email: flux-bot@oleks.space
name: flux-bot
messageTemplate: |
chore(cms-plugins-production): pin new digest
Files:
{{ range $filename, $_ := .Changed.FileChanges -}}
- {{ $filename }}
{{ end -}}
push:
branch: main
update:
path: ./cms-plugins-production
strategy: Setters
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- source.yaml
- helmrelease.yaml
- secrets.yaml
- image-automation.yaml
@@ -0,0 +1,46 @@
# Two secrets per environment:
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the production branch
# of cms-plugins (only `read` on this Gitea repo).
# One pair is shared between production + production;
# commit it under whichever env directory is
# applied first.
# 2. cms-plugins-production-secrets — env vars consumed by the pod via the
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
# is required; everything else is optional.
#
# These are TEMPLATES — encrypt them with sops before committing to the
# anton-helm-workloads repo:
#
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
# mv secrets.enc.yaml secrets.yaml
#
# Generation:
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
# Keys → "cms-plugins Flux deploy", read-only.
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
---
apiVersion: v1
kind: Secret
metadata:
name: cms-plugins-deploy-key
namespace: flux-system
type: Opaque
stringData:
identity: |
-----BEGIN OPENSSH PRIVATE KEY-----
REPLACE_WITH_PRIVATE_KEY
-----END OPENSSH PRIVATE KEY-----
identity.pub: |
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
known_hosts: |
git.oleks.space REPLACE_WITH_HOST_KEY
---
apiVersion: v1
kind: Secret
metadata:
name: cms-plugins-production-secrets
namespace: kotkan
type: Opaque
stringData:
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
@@ -0,0 +1,19 @@
---
# Flux pulls the chart from the `production` branch of cms-plugins on Gitea.
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
# pushes don't trigger chart re-reconcile.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: cms-plugins-production
namespace: flux-system
spec:
interval: 1m0s
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
ref:
branch: production
secretRef:
name: cms-plugins-deploy-key
ignore: |
/*
!/deploy/helm
@@ -0,0 +1,46 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cms-plugins-staging
namespace: flux-system
spec:
interval: 5m
chart:
spec:
chart: ./deploy/helm
sourceRef:
kind: GitRepository
name: cms-plugins-staging
namespace: flux-system
reconcileStrategy: Revision
releaseName: cms-plugins-staging
targetNamespace: kotkan
install:
disableWait: true
remediation:
retries: 3
upgrade:
disableWait: true
remediation:
retries: 3
values:
existingSecret: cms-plugins-staging-secrets
image:
# `tag` stays human-readable. The chart prefers `digest` when set
# and renders `repository@<digest>` — that's what actually pins
# the pod. Without digest pinning, helm upgrade would see no spec
# change when CI retags the floating `staging` tag.
tag: staging
digest: "" # {"$imagepolicy": "kotkan:cms-plugins-staging:digest"}
pullPolicy: Always
ingress:
host: cms-plugins-staging.kotkanagrilli.fi
nodeSelector:
kubernetes.io/hostname: kotkan
resources:
requests:
cpu: 50m
memory: 192Mi
limits:
cpu: 500m
memory: 768Mi
@@ -0,0 +1,78 @@
---
# Watch the Gitea OCI registry for the floating `staging` tag. Every
# push to the staging branch retags the new image as `staging`,
# overwriting the previous binding (OCI tag→manifest is single-valued).
# The image's immutable `0.1.<N>` tag stays in the registry as audit.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: cms-plugins-staging
namespace: kotkan
spec:
image: git.oleks.space/oleks/cms-plugins
interval: 1m
secretRef:
name: gitea-registry-creds
---
# Only the `staging` floating tag is in scope. There's at most one
# match at a time, so alphabetical ordering is a no-op — the policy
# just resolves to that single tag's current digest.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: cms-plugins-staging
namespace: kotkan
spec:
interval: 1m
imageRepositoryRef:
name: cms-plugins-staging
filterTags:
pattern: '^staging$'
# Extract and reflect the resolved digest into helmrelease.yaml.
# This enables IUA to pin by digest, which makes helm upgrade detect
# changes when the floating tag is reassigned.
digestReflectionPolicy: Always
policy:
alphabetical:
order: asc
---
# IUA writes the resolved digest into helmrelease.yaml — pinning by
# digest is what makes `helm upgrade` see a change when the floating
# tag is reassigned (without digest, tag stays `staging` literal and
# helm upgrade is a no-op).
#
# NOTE: `sourceRef` must reference a GitRepository that points at
# THIS workloads repo (anton-helm-workloads) with write access. If it
# doesn't exist yet, create one alongside this manifest. The
# emdash-kotkanagrilli equivalent uses `oleks-fleet-image-automation`
# because its HelmReleases live in the fleet repo.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageUpdateAutomation
metadata:
name: cms-plugins-staging
namespace: kotkan
spec:
interval: 1m
sourceRef:
kind: GitRepository
name: anton-workloads-image-automation
namespace: flux-system
git:
checkout:
ref:
branch: main
commit:
author:
email: flux-bot@oleks.space
name: flux-bot
messageTemplate: |
chore(cms-plugins-staging): pin new digest
Files:
{{ range $filename, $_ := .Changed.FileChanges -}}
- {{ $filename }}
{{ end -}}
push:
branch: main
update:
path: ./cms-plugins-staging
strategy: Setters
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- source.yaml
- helmrelease.yaml
- secrets.yaml
- image-automation.yaml
@@ -0,0 +1,46 @@
# Two secrets per environment:
# 1. cms-plugins-deploy-key — Flux's SSH key for cloning the staging branch
# of cms-plugins (only `read` on this Gitea repo).
# One pair is shared between staging + production;
# commit it under whichever env directory is
# applied first.
# 2. cms-plugins-staging-secrets — env vars consumed by the pod via the
# chart's `existingSecret`. EMDASH_ENCRYPTION_KEY
# is required; everything else is optional.
#
# These are TEMPLATES — encrypt them with sops before committing to the
# anton-helm-workloads repo:
#
# sops --encrypt --age <recipient-key> secrets.yaml > secrets.enc.yaml
# mv secrets.enc.yaml secrets.yaml
#
# Generation:
# ssh-keygen -t ed25519 -f /tmp/cms-plugins-deploy -N ""
# → upload /tmp/cms-plugins-deploy.pub to Gitea: Repo Settings → Deploy
# Keys → "cms-plugins Flux deploy", read-only.
# openssl rand -hex 32 → EMDASH_ENCRYPTION_KEY (one per env, do not reuse).
---
apiVersion: v1
kind: Secret
metadata:
name: cms-plugins-deploy-key
namespace: flux-system
type: Opaque
stringData:
identity: |
-----BEGIN OPENSSH PRIVATE KEY-----
REPLACE_WITH_PRIVATE_KEY
-----END OPENSSH PRIVATE KEY-----
identity.pub: |
ssh-ed25519 REPLACE_WITH_PUBLIC_KEY flux@cms-plugins
known_hosts: |
git.oleks.space REPLACE_WITH_HOST_KEY
---
apiVersion: v1
kind: Secret
metadata:
name: cms-plugins-staging-secrets
namespace: kotkan
type: Opaque
stringData:
EMDASH_ENCRYPTION_KEY: REPLACE_WITH_RANDOM_HEX_32
@@ -0,0 +1,19 @@
---
# Flux pulls the chart from the `staging` branch of cms-plugins on Gitea.
# The `ignore` rule restricts reconciliation to /deploy/helm so app-code
# pushes don't trigger chart re-reconcile.
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: cms-plugins-staging
namespace: flux-system
spec:
interval: 1m0s
url: ssh://git@git.oleks.space/oleks/cms-plugins.git
ref:
branch: staging
secretRef:
name: cms-plugins-deploy-key
ignore: |
/*
!/deploy/helm
+8
View File
@@ -0,0 +1,8 @@
.DS_Store
.git/
.gitignore
*.swp
*.tmp
*.bak
*.orig
README.md
+14
View File
@@ -0,0 +1,14 @@
apiVersion: v2
name: cms-plugins
description: CMS plugins catalog — Emdash-based catalog of WordPress→Emdash plugin parity
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- emdash
- cms
- astro
- catalog
home: https://git.oleks.space/oleks/cms-plugins
maintainers:
- name: oleks
+34
View File
@@ -0,0 +1,34 @@
{{/* Expand the name of the chart. */}}
{{- define "cms-plugins.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/* Create a default fully qualified app name. */}}
{{- define "cms-plugins.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/* Common labels. Flux appends `+<sha>` to chart versions for GitRepository
sources; `+` is illegal in k8s labels, so we replace it with `_`. */}}
{{- define "cms-plugins.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/* Selector labels */}}
{{- define "cms-plugins.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cms-plugins.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
+100
View File
@@ -0,0 +1,100 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cms-plugins.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "cms-plugins.labels" . | nindent 4 }}
spec:
# SQLite is single-writer; do not scale beyond 1.
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
{{- include "cms-plugins.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "cms-plugins.selectorLabels" . | nindent 8 }}
app.kubernetes.io/version: {{ .Values.image.tag | quote }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: cms-plugins
# When `image.digest` is provided, pin by digest so a floating
# tag (staging, production) doesn't confuse Helm into a no-op
# upgrade when the underlying image changes. Tag stays as a
# human-readable hint via the imagePullPolicy fallback path.
image: "{{ .Values.image.repository }}{{- if .Values.image.digest -}}@{{ .Values.image.digest }}{{- else -}}:{{ .Values.image.tag }}{{- end }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.containerSecurityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
env:
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
# EMDASH_SITE_URL gates the CSRF check on plugin POST routes.
# Astro inside the pod sees http://localhost:4321/, so without
# this any browser request from https://<ingress.host>/ trips
# the same-origin check. Derived from the ingress host so we
# don't need to set it per-environment.
{{- if and .Values.ingress.enabled .Values.ingress.host }}
- name: EMDASH_SITE_URL
value: "https://{{ .Values.ingress.host }}"
{{- end }}
envFrom:
- secretRef:
name: {{ .Values.existingSecret | default (printf "%s-secrets" (include "cms-plugins.fullname" .)) }}
volumeMounts:
- name: state
mountPath: {{ .Values.persistence.mountPath }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.liveness.timeoutSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
timeoutSeconds: {{ .Values.probes.readiness.timeoutSeconds }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumes:
- name: state
{{- if .Values.persistence.enabled }}
persistentVolumeClaim:
claimName: {{ include "cms-plugins.fullname" . }}-state
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
+27
View File
@@ -0,0 +1,27 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "cms-plugins.fullname" . -}}
# Plain Ingress object (no TLS) — TLS terminates at the Caddy reverse-proxy
# at the cluster edge (matches the woodpecker / emdash-kotkanagrilli pattern).
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ $fullName }}
namespace: {{ .Release.Namespace }}
annotations:
caddy.oleks.space/ingress: "true"
labels:
{{- include "cms-plugins.labels" . | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className | default "kube-system-traefik" }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ $fullName }}
port:
number: {{ .Values.service.port }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- if .Values.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "cms-plugins.fullname" . }}-state
namespace: {{ .Release.Namespace }}
labels:
{{- include "cms-plugins.labels" . | nindent 4 }}
annotations:
# Holds the single-writer SQLite DB and uploaded media. Keep it on
# `helm uninstall` / chart-name changes — losing it is unrecoverable
# data loss, not a redeployable artifact.
helm.sh/resource-policy: keep
spec:
accessModes:
- ReadWriteOnce
storageClassName: {{ .Values.persistence.storageClass | quote }}
resources:
requests:
storage: {{ .Values.persistence.size | quote }}
{{- end }}
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "cms-plugins.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "cms-plugins.labels" . | nindent 4 }}
spec:
type: ClusterIP
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
selector:
{{- include "cms-plugins.selectorLabels" . | nindent 4 }}
+19
View File
@@ -0,0 +1,19 @@
# Production overrides — applied via the FluxCD HelmRelease (or directly with
# `helm upgrade -f values-production.yaml`).
image:
tag: production-latest
ingress:
host: cms-plugins-production.kotkanagrilli.fi
persistence:
size: 10Gi
resources:
requests:
cpu: 100m
memory: 384Mi
limits:
cpu: "1"
memory: 1Gi
+17
View File
@@ -0,0 +1,17 @@
# Staging overrides — applied via the FluxCD HelmRelease (or directly with
# `helm upgrade -f values-staging.yaml`).
image:
tag: staging-latest
ingress:
host: cms-plugins-staging.kotkanagrilli.fi
# Slimmer staging — non-critical workload, can run lean.
resources:
requests:
cpu: 50m
memory: 192Mi
limits:
cpu: 500m
memory: 768Mi
+88
View File
@@ -0,0 +1,88 @@
# Defaults for the cms-plugins chart.
# Per-env overrides come from values-staging.yaml / values-production.yaml
# and from the FluxCD HelmRelease's `values:` block.
image:
repository: git.oleks.space/oleks/cms-plugins
tag: develop-latest
# The tag is a mutable floating pointer (CI retags <branch>-latest onto
# each new build), so kubelet must always re-pull — IfNotPresent would
# pin the node to whatever digest it cached first and never roll.
pullPolicy: Always
service:
port: 4321
ingress:
enabled: true
host: cms-plugins.kotkanagrilli.fi
# TLS terminates at the Caddy reverse-proxy at the cluster edge
# (matches the woodpecker / emdash-kotkanagrilli pattern). The
# Ingress object is plain — no inline TLS, no cert-manager Certificate.
className: kube-system-traefik
# SQLite is single-writer — pin to one node so the local-path PV is sticky.
# kotkan hosts the kotkanagrilli subdomain pool, matching the
# anton-helm-workloads convention (hello-kotkan, kotkanagrilli, etc.).
nodeSelector:
kubernetes.io/hostname: kotkan
tolerations: []
affinity: {}
persistence:
enabled: true
storageClass: local-path
size: 5Gi
# Mounted at /app/state. The image symlinks /app/data.db and /app/uploads
# into this volume, so a single PVC covers SQLite + uploaded media.
mountPath: /app/state
# Plain env values (non-secret).
env:
HOST: "0.0.0.0"
PORT: "4321"
NODE_ENV: production
DEPLOY_TARGET: node
STATE_DIR: /app/state
EMDASH_ALLOWED_ORIGINS: ""
# All secrets project from one Secret. Keys expected:
# - EMDASH_ENCRYPTION_KEY (required)
existingSecret: cms-plugins-secrets
imagePullSecrets:
- name: gitea-registry-creds
probes:
liveness:
# /_emdash/api/health requires auth (401 to unauthenticated requests),
# so kubelet probes fail and the pod gets killed. The site root is
# public and a 200 from it is a reasonable proxy for "the server is up".
path: /
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
readiness:
path: /
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
podSecurityContext:
fsGroup: 1001
containerSecurityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
allowPrivilegeEscalation: false
capabilities:
drop: [ALL]