The production image now contains zero feature modules and is the same for every
installation. Installed modules live in module storage that one Core resolver
selects from EKKLESIA_MODULES_DIR: the checkout's modules/ when unset, an
absolute path that must exist when set (/var/lib/ekklesia/modules in the image).
A relative, blank, missing, or unreadable configured directory fails closed with
a sanitized error that never contains the value.
- Host configuration reads EKKLESIA_MODULES_DIR. The API, migration plan, apply
and validate, module validation, module enablement, and Doctor all resolve it
the same way; the test-only API override still takes precedence.
- Doctor receives Core's migrations directory explicitly instead of deriving it
from the modules directory's parent.
- The migration runner retains and reports the applied history of a module that
is not installed, and plans and applies Core's and present modules' migrations
around it. Core's history and every installed module's history stay strict: a
missing or changed applied migration, or a downgrade, still fails. No down
migrations, no data deletion.
- deploy/Dockerfile copies no module and installs nothing for the module
template; .dockerignore excludes modules/ and the build refuses a context that
contains it; the runtime has an empty root-owned /var/lib/ekklesia/modules and
sets EKKLESIA_MODULES_DIR.
- deploy/compose.yaml bind-mounts EKKLESIA_MODULES_HOST_DIR read-only into api
and migrate, with create_host_path disabled.
- scripts/check-image.sh proves the image holds no modules and that a populated
modules/ directory leaves its runtime file system identical; CI runs it.
- The split deploy smoke now uses the same generic image: it boots with no
modules, installs a self-contained test module packaged from the template
(test-only), migrates and restarts, removes it (Core runs, planning reports the
retained history, data survives), and restores it with its data.
Docs: deploy README (module storage, install/update/remove/restore, the runtime
modules target), .env.example, API configuration, module development guide,
testing, workflow, security, and the architecture overview.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A module that imports a package at run time without declaring it a
production dependency now fails the image build instead of the API's
startup, since the runtime receives production dependencies only.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Add deploy/Dockerfile and .dockerignore (ADR 0044): one image for the
api service and every operator-run CLI, built from an already-assembled
workspace (the platform plus whatever modules/<id> the context holds).
- node:24.18.0-trixie-slim pinned by index digest; pnpm via Corepack at
the root packageManager version.
- Build stage: frozen install, modules:validate, migrations:validate,
build. Runtime: production-only node_modules plus every dist/, core
migrations, and each module directory, in the repository layout the
API and CLIs resolve paths from; root-owned, run as node.
- NODE_ENV=production and EKKLESIA_API_HOST=0.0.0.0 inside the image
only; the platform default stays 127.0.0.1. Configuration comes only
from the environment; no .env enters the context or any layer.
- HEALTHCHECK on /api/v1/health/ready with Node's fetch; OCI labels for
source, revision, and version.
- CI builds the image without modules: build only, no credentials, no
push.
- deploy/README.md documents the image, the read-only production
posture (verified), init: true, and the exact CLI commands.
Co-Authored-By: Claude Opus 5.5 <[email protected]>