- #3 Liveness probe targets full SSR DB-querying / route, coupling pod liveness to SQLite - #4 Chart values-staging/production.yaml are dead config under Flux; drift trap - #6 tsconfig includes gitignored emdash-env.d.ts that only the dev server generates - #7 Dockerfile package-lock glob + npm install fallback can silently build an unlocked image - #8 Dockerfile creates runtime user without pinning its GID - #9 entrypoint.sh gates `emdash init` on data.db absence, skipping migrations on PVC reuse - #10 pullPolicy: Always vs digest pinning - #11 Dockerfile state symlinks contradict the STATE_DIR contract; Dockerfile does not set ENV STATE_DIR - #12 astro is a production dependency, so npm prune --omit=dev keeps build-only tooling - #14 Two ImageUpdateAutomations write back to the same anton-helm-workloads main branch - #16 memoryCache provider is per-process; correctness depends implicitly on replicas:1 - #17 Root catch-all [slug].astro couples nav links to pages-collection rows + DB hit per unmatched path - #18 Detail pages render a 200-style body under a 404 status and have no try/catch around getEmDash* calls - #19 vite allowedHosts hardcodes ddev hostnames (dev-only; no prod impact)
6.3 KiB
Architecture
This file is the contract between the build (Dockerfile, Woodpecker, Helm chart)
and the FluxCD reconciliation in anton-helm-workloads. See DEPLOYMENT.md for
the operational walkthrough.
Repo shape
cms-plugins/
├── app/ ← Emdash/Astro source
│ ├── astro.config.mjs ← node target, sqlite() + local()
│ ├── package.json
│ ├── src/ ← pages, layouts, components
│ └── seed/seed.json ← collections + kotkanagrilli plugin entries
├── Dockerfile ← multi-stage build, single PVC at /app/state
├── docker/entrypoint.sh ← runs `emdash init` on first boot
├── deploy/
│ ├── helm/ ← chart consumed by Flux directly from this repo
│ └── fleet-overlay/ ← HelmRelease/source/image-automation templates
│ ├── cms-plugins-staging/ ← copy → anton-helm-workloads/cms-plugins-staging/
│ └── cms-plugins-production/ ← copy → anton-helm-workloads/cms-plugins-production/
├── .woodpecker/container.yaml ← build pipeline
└── .ddev/ ← local dev (DDEV nginx → emdash service on :4321)
Build pipeline
A push to develop / staging / production runs
.woodpecker/container.yaml. The pipeline labels itself arch: arm64
because the deploy target (kotkan) is an arm64 node, so building
natively avoids a cross-compile step.
For each build the pipeline pushes three OCI tags to
git.oleks.space/oleks/cms-plugins:
| Tag | Mutability | Purpose |
|---|---|---|
0.1.<pipeline> |
Immutable | Audit trail. Use as a rollback target. |
<branch> |
Floating | Watched by FluxCD's ImagePolicy. Retagged on every build. |
<branch>-latest |
Floating | Cosmetic. Resolves the chart's image.tag fallback to a real ref when the digest path is unset. |
Only the staging and production branches have an ImagePolicy, so
only those move pods.
Container shape
Multi-stage Dockerfile:
deps—node:22-bookworm-slim+python3 make g++(better-sqlite3 build deps). Falls back tonpm installif there's no committed lockfile.build—npm run build.runtime—node:22-bookworm-slim+tini. Runs as uid 1001. Persistent state symlinked from/app/data.dband/app/uploads→/app/state/...so the one PVC mounted at/app/statecovers both SQLite and uploaded media.
Entrypoint runs emdash init on first boot (idempotent), then execs
the Astro node server on port 4321.
Helm chart
deploy/helm/ ships with the app. The chart is not packaged or pushed
to an OCI registry — FluxCD's GitRepository source pulls it directly from
the matching branch of this repo. The ignore rule in source.yaml
restricts reconciliation to /deploy/helm, so app-source pushes don't
trigger chart re-reconcile.
Per-env values come from two independent sources that must be kept in sync by hand:
- The inline
values:block in each FluxCDHelmRelease(inanton-helm-workloads, templated underdeploy/fleet-overlay/). This is the only source Flux applies to the cluster. values-staging.yaml/values-production.yamlindeploy/helm/, used only for directhelm upgrade -f ...invocations. Flux never reads these files, so editing them has no effect on the live deploy and they can silently drift from the HelmRelease.
Key shape decisions:
replicas: 1,strategy: Recreate. SQLite is single-writer. This also keeps the in-process Astro response cache (memoryCache()inapp/astro.config.mjs, fed byAstro.cache.set(cacheHint)on the content pages) coherent: it is per-process, so a second replica would hold a divergent, independently-expiring cache. Scaling out would require a shared cache provider, not just relaxing the SQLite writer constraint.nodeSelector: kotkan.local-pathPV is sticky to one node; emdash is colocated with the legacy kotkanagrilli WP install.- Pinned by digest, not tag. The HelmRelease sets
values.image.digest: "<sha256:...>" # {"$imagepolicy": ...}so thathelm upgradedetects a change when the floating tag is reassigned. Without digest pinning,stagingstays the literal string"staging"through every build and Helm sees no spec change. - Single PVC,
helm.sh/resource-policy: keep. Losing the PVC means losing all CMS data — uninstalls do not delete it.
Flux contract
Two repos cooperate:
- cms-plugins (this one) exposes the chart at
deploy/helm/on each branch. - anton-helm-workloads runs the show:
GitRepositorypoints at the matching branch of cms-plugins,HelmReleasereferenceschart: ./deploy/helm,ImagePolicy+ImageUpdateAutomationwatch the floating tag and rewrite the digest setter back intohelmrelease.yaml.
For the workloads repo to write digest commits, a GitRepository named
anton-workloads-image-automation in flux-system namespace must exist
with write access to anton-helm-workloads:main. The emdash-kotkanagrilli
analogue is oleks-fleet-image-automation in the personal fleet repo.
Data model
Three Emdash collections (app/seed/seed.json):
- cmses — CMS platforms. Seeded with WordPress + Emdash.
- plugins — one entry per plugin. Fields: title, purpose, source_cms,
target_cms, category, status (
port/built-in/saas/drop/gated/done/proposed), source_repo_url, target_repo_url, notes (portableText). - pages — static content (About, Submit, etc.).
The seeded plugin entries come from
~/projects/emdash.kotkanagrilli.fi/docs/parity.md and the
mu-plugins/ / plugins-custom/ directories in
~/projects/kotkanagrilli.fi/. New entries are added through the Emdash
admin (/_emdash/admin).
What this does NOT solve
- Cloudflare Workers target. The kotkanagrilli sister project has
Cloudflare boundary files (
src/worker.ts,wrangler.jsonc) for a later flip; this repo skips them. They can be added back from the emdash-kotkanagrilli pattern when needed. - Authentication on the admin. Emdash's default passkey auth runs;
the first visit to
/_emdash/adminredirects to/setupand creates the first admin. - Multi-language. No i18n — the catalog is English-only by default.