feat(deploy): add the production container image
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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ef1383d080
commit
756e9b821c
@@ -0,0 +1,25 @@
|
|||||||
|
# Build context for deploy/Dockerfile (ADR 0044). The image installs its own dependencies with
|
||||||
|
# `pnpm install --frozen-lockfile` and builds its own outputs, so nothing installed or built on the
|
||||||
|
# host may enter the context, and no secret or environment file may enter any layer.
|
||||||
|
|
||||||
|
# Host-installed dependencies and build outputs: the image produces its own.
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/.turbo
|
||||||
|
**/coverage
|
||||||
|
**/*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment and secret material. Configuration reaches the container only through its runtime
|
||||||
|
# environment; `.env.example` is excluded too, since nothing in the image reads it.
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
|
||||||
|
# Repository and tooling metadata the image never reads.
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitea
|
||||||
|
.forgejo
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
**/*.log
|
||||||
|
**/.DS_Store
|
||||||
@@ -103,3 +103,35 @@ jobs:
|
|||||||
|
|
||||||
- name: Exercise transactional migrations against PostgreSQL
|
- name: Exercise transactional migrations against PostgreSQL
|
||||||
run: pnpm test:migrations:integration
|
run: pnpm test:migrations:integration
|
||||||
|
|
||||||
|
image-build:
|
||||||
|
needs: verify
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
env:
|
||||||
|
# Cache mounts in deploy/Dockerfile need BuildKit; this selects it even where the Docker CLI
|
||||||
|
# has no buildx plugin.
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
with:
|
||||||
|
# `actions/checkout` defaults to leaving the job token in `.git/config`, where any later
|
||||||
|
# step — including one a pull request added to this file — could read or reuse it. Nothing
|
||||||
|
# in this workflow pushes, so the credential is never needed after checkout.
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
# The platform-only image (ADR 0044): a checkout has no `modules/`, so this proves the image
|
||||||
|
# builds, validates, and assembles with zero modules. Build only — no registry login, no
|
||||||
|
# credentials, no push, no tag anywhere but this runner's local image store.
|
||||||
|
- name: Build the production image without modules
|
||||||
|
run: >-
|
||||||
|
docker build
|
||||||
|
--file deploy/Dockerfile
|
||||||
|
--build-arg REVISION="$GITHUB_SHA"
|
||||||
|
--tag ekklesia-ci:"$GITHUB_SHA"
|
||||||
|
.
|
||||||
|
|
||||||
|
- name: Remove the image from the runner
|
||||||
|
if: always()
|
||||||
|
run: docker image rm --force ekklesia-ci:"$GITHUB_SHA" || true
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# Ekklesia's one production image (ADR 0044): the `api` service and every operator-run CLI, from one
|
||||||
|
# build. See deploy/README.md for how to build and run it.
|
||||||
|
#
|
||||||
|
# The build context is an already-assembled workspace: the platform, zero or more module packages in
|
||||||
|
# `modules/<id>`, and the `pnpm-lock.yaml` that describes exactly that set. This file clones nothing
|
||||||
|
# and chooses no modules. With an empty or absent `modules/`, it builds the platform alone.
|
||||||
|
#
|
||||||
|
# No `# syntax=` directive, deliberately: it would make BuildKit pull an unpinned frontend image.
|
||||||
|
# The built-in frontend supports everything below, including cache mounts.
|
||||||
|
|
||||||
|
# Node.js 24.18.0 (matching `.node-version`) on Debian 13 slim, pinned by its multi-architecture
|
||||||
|
# index digest. glibc rather than musl, because module dependencies such as sharp ship glibc
|
||||||
|
# prebuilt binaries. Every stage starts from this exact image.
|
||||||
|
FROM node:24.18.0-trixie-slim@sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573 AS base
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
# toolchain: pnpm through Corepack, at exactly the root `packageManager` version.
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
FROM base AS toolchain
|
||||||
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
|
PNPM_HOME=/pnpm \
|
||||||
|
TURBO_TELEMETRY_DISABLED=1 \
|
||||||
|
DO_NOT_TRACK=1
|
||||||
|
WORKDIR /opt/ekklesia
|
||||||
|
COPY package.json ./
|
||||||
|
# Corepack reads `packageManager` (pnpm@<exact version>) from the root manifest, so the version is
|
||||||
|
# declared once, in the repository, and never restated here.
|
||||||
|
RUN corepack enable && corepack install
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
# manifests: the lockfile, workspace configuration, and every workspace package.json, at their
|
||||||
|
# repository paths. Dependency installation keys on these alone, so a source-only change reuses the
|
||||||
|
# installed layers.
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
FROM base AS manifests
|
||||||
|
WORKDIR /context
|
||||||
|
COPY . .
|
||||||
|
RUN set -eu; \
|
||||||
|
mkdir /manifests; \
|
||||||
|
cp package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc /manifests/; \
|
||||||
|
for manifest in apps/*/package.json builders/*/package.json core/package.json \
|
||||||
|
modules/*/package.json packages/*/package.json surfaces/*/package.json \
|
||||||
|
templates/*/package.json; do \
|
||||||
|
if [ -f "$manifest" ]; then install -D -m 0644 "$manifest" "/manifests/$manifest"; fi; \
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
# build: full install, validation, build, then the exact runtime file set.
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
FROM toolchain AS build
|
||||||
|
# Sources first, then one install: workspace packages publish `bin` commands from source files (the
|
||||||
|
# module UI builder's, which every module build invokes), and pnpm links a bin only when its target
|
||||||
|
# already exists. The store cache mount keeps a rebuild off the network all the same.
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,id=ekklesia-pnpm-store,target=/pnpm/store \
|
||||||
|
pnpm install --frozen-lockfile --store-dir /pnpm/store
|
||||||
|
# The same gates `pnpm verify` runs for modules and migrations, against exactly the module set in this
|
||||||
|
# context. Each script builds first; the final build is a Turbo cache hit that keeps the intent
|
||||||
|
# explicit.
|
||||||
|
RUN pnpm modules:validate && pnpm migrations:validate && pnpm build
|
||||||
|
# What the runtime resolves at startup, at the paths it resolves them from:
|
||||||
|
# - every workspace package's `dist/` (every package exports only from `dist/`), which includes
|
||||||
|
# the Dashboard and display host assets under `surfaces/shell/dist/browser/`;
|
||||||
|
# - Core's migrations, `core/drizzle/` with its `meta/` journal;
|
||||||
|
# - each module directory whole, minus `node_modules`, because each manifest names its own
|
||||||
|
# entrypoints, UI artifacts, and migrations directory.
|
||||||
|
# `node_modules` comes from the production-only install below, never from this stage.
|
||||||
|
RUN set -eu; \
|
||||||
|
mkdir -p /runtime/core /runtime/modules; \
|
||||||
|
for package in apps/* core packages/* surfaces/*; do \
|
||||||
|
if [ -d "$package/dist" ]; then \
|
||||||
|
mkdir -p "/runtime/$package"; \
|
||||||
|
cp -a "$package/dist" "/runtime/$package/dist"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
cp -a core/drizzle /runtime/core/drizzle; \
|
||||||
|
if [ -d modules ]; then \
|
||||||
|
tar -C modules --exclude='node_modules' -cf - . | tar -C /runtime/modules -xf -; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
# prod-deps: the same lockfile, production dependencies only. No compiler, bundler, or test runner
|
||||||
|
# reaches the runtime image.
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
FROM toolchain AS prod-deps
|
||||||
|
COPY --from=manifests /manifests/ ./
|
||||||
|
RUN --mount=type=cache,id=ekklesia-pnpm-store,target=/pnpm/store \
|
||||||
|
pnpm install --frozen-lockfile --prod --store-dir /pnpm/store
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
# runtime
|
||||||
|
# ---------------------------------------------------------------------------------------------------
|
||||||
|
FROM base AS runtime
|
||||||
|
|
||||||
|
ARG VERSION=""
|
||||||
|
ARG REVISION=""
|
||||||
|
ARG SOURCE=""
|
||||||
|
LABEL org.opencontainers.image.title="Ekklesia" \
|
||||||
|
org.opencontainers.image.description="Ekklesia API, Dashboard, display host, and operator CLIs" \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.source="${SOURCE}" \
|
||||||
|
org.opencontainers.image.base.name="docker.io/library/node:24.18.0-trixie-slim" \
|
||||||
|
org.opencontainers.image.base.digest="sha256:ae91dcc111a68c9d2d81ff2a17bda61be126426176fde6fe7d08ab13b7f50573"
|
||||||
|
|
||||||
|
# Only the container's own listen address differs from the platform default: outside a container
|
||||||
|
# the API still binds 127.0.0.1. `production` is stated rather than relied on as the default.
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
EKKLESIA_API_HOST=0.0.0.0 \
|
||||||
|
EKKLESIA_API_PORT=3000
|
||||||
|
|
||||||
|
# The repository layout, preserved: the API finds the repository root three directories above
|
||||||
|
# `apps/api/dist/`, and every CLI uses its working directory as the root.
|
||||||
|
WORKDIR /opt/ekklesia
|
||||||
|
COPY --from=prod-deps /opt/ekklesia/ ./
|
||||||
|
COPY --from=build /runtime/ ./
|
||||||
|
|
||||||
|
# Files stay root-owned: the unprivileged runtime user can read the installation, never modify it.
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Readiness, not liveness: an instance whose database is unreachable or that is shutting down is
|
||||||
|
# unhealthy. Node's own fetch, so the image needs no curl.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.EKKLESIA_API_PORT || '3000') + '/api/v1/health/ready', { signal: AbortSignal.timeout(4000) }).then((response) => process.exit(response.status === 200 ? 0 : 1), () => process.exit(1))"]
|
||||||
|
|
||||||
|
# `pnpm api:start` without its build step and without `--env-file`: configuration comes only from
|
||||||
|
# the container environment. The same image runs every CLI: `node core/dist/cli/<command>.js`.
|
||||||
|
CMD ["node", "apps/api/dist/server.js"]
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
This directory holds Ekklesia's reference self-hosted deployment
|
||||||
|
([ADR 0044](../docs/architecture/decisions/0044-self-hosted-reference-deployment.md)). Today it
|
||||||
|
contains the production container image. The Compose project, proxy configuration, and backup job
|
||||||
|
the ADR describes are separate, later work.
|
||||||
|
|
||||||
|
## The production image
|
||||||
|
|
||||||
|
[`Dockerfile`](Dockerfile) builds **one image** for two jobs: the long-running `api` service, and
|
||||||
|
every operator-run command (migrations, Doctor, initial-owner creation, module enablement). There
|
||||||
|
is no second image and no second build.
|
||||||
|
|
||||||
|
### What it builds
|
||||||
|
|
||||||
|
The build context is an **already-assembled workspace**: this repository, plus zero or more module
|
||||||
|
packages already present in `modules/<id>`, plus a `pnpm-lock.yaml` describing exactly that set.
|
||||||
|
The Dockerfile clones nothing and chooses no modules. It builds what it is given. With no
|
||||||
|
`modules/` directory, it builds the platform alone; CI builds that case on every change.
|
||||||
|
|
||||||
|
Assembling the context (checking out the platform at a release tag, each module at a pinned tag or
|
||||||
|
commit, and the instance's own lockfile) belongs to the installation's instance composition, not to
|
||||||
|
this file.
|
||||||
|
|
||||||
|
| Stage | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `base` | `node:24.18.0-trixie-slim`, pinned by its multi-architecture index digest. Debian, so glibc, because module dependencies such as sharp ship glibc prebuilt binaries. Every stage starts here. |
|
||||||
|
| `toolchain` | Enables pnpm through Corepack, at exactly the root `packageManager` version. |
|
||||||
|
| `manifests` | Collects the lockfile, workspace configuration, `.npmrc`, and every workspace `package.json` at its repository path. |
|
||||||
|
| `build` | `pnpm install --frozen-lockfile`, then `pnpm modules:validate`, `pnpm migrations:validate`, and `pnpm build`, against exactly the modules in the context. Never uses host `node_modules`: `.dockerignore` excludes them. |
|
||||||
|
| `prod-deps` | The same lockfile with `--prod`: production dependencies only. |
|
||||||
|
| `runtime` | Production dependencies plus built outputs, owned by root and run as the unprivileged `node` user (uid 1000). No pnpm, no compiler, no curl. |
|
||||||
|
|
||||||
|
The runtime keeps the repository layout at `/opt/ekklesia`, because that layout is how the platform
|
||||||
|
finds its files:
|
||||||
|
|
||||||
|
- the API takes the repository root as three directories above `apps/api/dist/`, and reads
|
||||||
|
`package.json`, `packages/module-sdk/package.json`, and `modules/` beneath it;
|
||||||
|
- every CLI takes its **working directory** as the root and also reads Core's migrations in
|
||||||
|
`core/drizzle/`, so commands must run from `/opt/ekklesia`, the image's `WORKDIR`;
|
||||||
|
- the Dashboard and display host files are read from `surfaces/shell/dist/browser/`;
|
||||||
|
- each module directory is copied whole, minus `node_modules`, because each manifest names its
|
||||||
|
own entrypoints, UI artifacts, and migrations directory.
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build \
|
||||||
|
--file deploy/Dockerfile \
|
||||||
|
--build-arg VERSION=0.1.0 \
|
||||||
|
--build-arg REVISION="$(git rev-parse HEAD)" \
|
||||||
|
--build-arg SOURCE=https://git.example.org/your-org/ekklesia \
|
||||||
|
--tag ekklesia:0.1.0 \
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
The three build arguments become the OCI labels `org.opencontainers.image.version`, `.revision`,
|
||||||
|
and `.source`. They have no defaults: the repository cannot know an installation's Git host. The
|
||||||
|
image also records its base image name and digest. BuildKit is required (Docker's default builder);
|
||||||
|
set `DOCKER_BUILDKIT=1` if your Docker CLI has no buildx plugin.
|
||||||
|
|
||||||
|
No secret is a build argument, and none enters any layer. `.env` files are excluded from the build
|
||||||
|
context entirely, and no registry credentials are needed.
|
||||||
|
|
||||||
|
**A platform-only image does not exercise native module dependencies.** With no modules in the
|
||||||
|
context, nothing like sharp is installed, so the CI image build proves nothing about them. An
|
||||||
|
instance build that includes such a module must smoke-test the built image itself: start it,
|
||||||
|
confirm `/api/v1/health/ready` answers `200`, and exercise the module's native code path (for
|
||||||
|
example, process one image).
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Configuration comes **only from the container environment**, never from a file inside the image.
|
||||||
|
Supply it from a root-owned environment file on the host, outside any repository (ADR 0044
|
||||||
|
Secrets). The image sets exactly three variables itself:
|
||||||
|
|
||||||
|
| Variable | Image value | Why |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `NODE_ENV` | `production` | Stated rather than relied on. Anything other than `development` already validates as production. |
|
||||||
|
| `EKKLESIA_API_HOST` | `0.0.0.0` | Reachable from other containers. Outside a container the platform default stays `127.0.0.1`. |
|
||||||
|
| `EKKLESIA_API_PORT` | `3000` | The platform default, made explicit; `EXPOSE 3000`. |
|
||||||
|
|
||||||
|
Everything else is the ordinary host configuration in
|
||||||
|
[API § Required configuration](../docs/development/api.md#required-configuration). In production
|
||||||
|
that means `DATABASE_URL` with certificate-verified TLS: `EKKLESIA_DATABASE_CA` is the CA's PEM text
|
||||||
|
itself, passed in the environment, so no certificate file is mounted.
|
||||||
|
|
||||||
|
Never publish the API's port to the host or the Internet. Only the proxy publishes ports.
|
||||||
|
|
||||||
|
### The `api` service
|
||||||
|
|
||||||
|
The image's default command is `node apps/api/dist/server.js`: `pnpm api:start` without its build
|
||||||
|
step and without `--env-file`. It never applies migrations.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
image: ekklesia:0.1.0
|
||||||
|
init: true
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
env_file:
|
||||||
|
- /etc/ekklesia/ekklesia.env
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use `init: true`.** It runs a minimal init as PID 1 that forwards signals and reaps children, so
|
||||||
|
`docker stop` reaches the API's own SIGTERM handler promptly. The API then drains in-flight
|
||||||
|
requests, stops modules in reverse order, and closes its pool.
|
||||||
|
|
||||||
|
**The read-only flags are the supported production posture.** The image was verified under exactly
|
||||||
|
`--read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges`, with `--init`:
|
||||||
|
|
||||||
|
- the API reached `healthy` and served `/api/v1/health/ready`, `GET /`, and the Dashboard script;
|
||||||
|
- `plan-migrations.js`, `apply-migrations.js`, and `doctor.js` ran under the same flags;
|
||||||
|
- nothing wrote to `/tmp` in that run. It stays mounted as headroom for module code that needs
|
||||||
|
scratch space.
|
||||||
|
|
||||||
|
The installation, `/home/node` included, is not writable, so anything that needs a writable path
|
||||||
|
other than `/tmp` should be reported as a defect rather than worked around.
|
||||||
|
|
||||||
|
### Health
|
||||||
|
|
||||||
|
The image's `HEALTHCHECK` calls `GET /api/v1/health/ready` on `127.0.0.1` with Node's built-in
|
||||||
|
`fetch`, every 30 seconds after a 30-second start period, with three retries. Readiness means the
|
||||||
|
database answered within 2 seconds and the instance is not shutting down; see
|
||||||
|
[API § Health](../docs/development/api.md#health). Because it probes `127.0.0.1`, keep
|
||||||
|
`EKKLESIA_API_HOST` at `0.0.0.0` (or any address that includes loopback) inside the container.
|
||||||
|
|
||||||
|
A Compose health check or external monitor can use the same endpoint through the proxy. Use
|
||||||
|
`/api/v1/health/live` to ask only whether the process is serving.
|
||||||
|
|
||||||
|
### Operator commands
|
||||||
|
|
||||||
|
The same image runs every CLI, directly with `node core/dist/cli/<command>.js`. The `pnpm` scripts
|
||||||
|
of the same names rebuild first and read `.env`, so they are for a development checkout, not for the
|
||||||
|
image.
|
||||||
|
|
||||||
|
**Nothing applies migrations automatically.** Neither the API nor the image's default command ever
|
||||||
|
runs `apply-migrations.js`; only an operator does, by running the command below.
|
||||||
|
|
||||||
|
A `migrate` service makes those commands one line each. It shares the API's image and environment,
|
||||||
|
never restarts, and does not run the API's health check:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
migrate:
|
||||||
|
image: ekklesia:0.1.0
|
||||||
|
profiles: ["operator"]
|
||||||
|
restart: "no"
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /tmp
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
env_file:
|
||||||
|
- /etc/ekklesia/ekklesia.env
|
||||||
|
healthcheck:
|
||||||
|
disable: true
|
||||||
|
command: ["node", "core/dist/cli/plan-migrations.js"]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Task | Compose | Plain `docker run` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Plan migrations (read-only) | `docker compose run --rm migrate` | `docker run --rm --env-file /etc/ekklesia/ekklesia.env ekklesia:0.1.0 node core/dist/cli/plan-migrations.js` |
|
||||||
|
| Apply migrations | `docker compose run --rm migrate node core/dist/cli/apply-migrations.js` | `docker run --rm --env-file /etc/ekklesia/ekklesia.env ekklesia:0.1.0 node core/dist/cli/apply-migrations.js` |
|
||||||
|
| Doctor (read-only report) | `docker compose run --rm migrate node core/dist/cli/doctor.js` | `docker run --rm --env-file /etc/ekklesia/ekklesia.env ekklesia:0.1.0 node core/dist/cli/doctor.js` |
|
||||||
|
| Create the initial owner (interactive) | `docker compose run --rm migrate node core/dist/cli/create-initial-owner.js --email [email protected]` | `docker run --rm -it --env-file /etc/ekklesia/ekklesia.env ekklesia:0.1.0 node core/dist/cli/create-initial-owner.js --email [email protected]` |
|
||||||
|
| Enable or disable a module | `docker compose run --rm migrate node core/dist/cli/set-module-enabled.js --enable --module <id>` | `docker run --rm --env-file /etc/ekklesia/ekklesia.env ekklesia:0.1.0 node core/dist/cli/set-module-enabled.js --disable --module <id>` |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `docker compose run` allocates a terminal by default. With plain `docker run`, the initial-owner
|
||||||
|
command needs `-it`: it prompts for the password with hidden input and never accepts it on the
|
||||||
|
command line ([Identity](../docs/development/identity.md)). It also accepts the password on
|
||||||
|
standard input with `-i` and no `-t`.
|
||||||
|
- Run `--rm` commands on the same Docker network as `postgres`, so `DATABASE_URL` resolves; with
|
||||||
|
Compose that is automatic.
|
||||||
|
- Add the same read-only flags to plain `docker run` commands if you want them; every command above
|
||||||
|
was verified under them except the interactive and module-enablement ones, which were not run.
|
||||||
|
|
||||||
|
### Upgrading
|
||||||
|
|
||||||
|
The ADR's procedure, with this image: build the new image → back up → run
|
||||||
|
`docker compose run --rm migrate` (plan), review it, then
|
||||||
|
`docker compose run --rm migrate node core/dist/cli/apply-migrations.js` → restart `api` on the
|
||||||
|
new image. Migrations always run before the new API starts, and only when an operator runs them.
|
||||||
|
|
||||||
|
### Size
|
||||||
|
|
||||||
|
The platform-only image is about 378 MB: the Debian slim Node base (about 230 MB), production
|
||||||
|
dependencies (about 141 MB), and built outputs (about 6 MB).
|
||||||
|
|
||||||
|
About 57 MB of the production dependencies are development tools that no runtime code imports:
|
||||||
|
`vitest`, `vite`, `drizzle-kit`, `tsx`, `esbuild`, and their native bindings. They arrive as
|
||||||
|
optional peer dependencies of Better Auth (and `esbuild` through the module UI builder), which pnpm
|
||||||
|
resolves because the workspace already has them. Excluding them means changing dependency
|
||||||
|
resolution and the lockfile, which is separate work.
|
||||||
@@ -464,9 +464,11 @@ Until that exists, the trusted-code model above is the security model.
|
|||||||
## Current deployment state
|
## Current deployment state
|
||||||
|
|
||||||
As of this branch, **Ekklesia is not deployed**. There is no production API instance, no reverse
|
As of this branch, **Ekklesia is not deployed**. There is no production API instance, no reverse
|
||||||
proxy, no public listener, and no deployment automation of any kind in this repository — no deploy
|
proxy, no public listener, and no deployment automation in this repository — no deploy script, no
|
||||||
script, no CI deployment step, no container publish, no orchestration manifest. No `.env` has ever
|
CI deployment step, no container publish, no orchestration manifest. The repository can **build** a
|
||||||
been committed; only `.env.example`.
|
production image ([`deploy/`](../../deploy/README.md)), and CI builds it without modules on every
|
||||||
|
change, but nothing pushes, publishes, or runs it anywhere. No `.env` has ever been committed; only
|
||||||
|
`.env.example`, which the image build context excludes.
|
||||||
|
|
||||||
Two production defaults are relevant but are **not** by themselves a deployment guarantee:
|
Two production defaults are relevant but are **not** by themselves a deployment guarantee:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user