Feat/deploy container image #48
@@ -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
|
||||
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,131 @@
|
||||
# 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 boundaries, modules, and migrations, against exactly the
|
||||
# module set in this context. `pnpm boundaries` comes first and reads only sources: among its rules,
|
||||
# a module that imports something at run time without declaring it a production dependency fails
|
||||
# here, because the production-only install below would leave it unresolvable at API startup. Each
|
||||
# validate script builds first; the final build is a Turbo cache hit that keeps the intent explicit.
|
||||
RUN pnpm boundaries && 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,217 @@
|
||||
# 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 boundaries`, `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 module missing a production dependency fails the build, not the API.** The runtime receives
|
||||
production dependencies only, so a module that imports a package at run time but declares it only
|
||||
as a development or peer dependency would build and then fail at startup. `pnpm boundaries` runs
|
||||
in the build stage and rejects that module first
|
||||
(`runtime-imports-are-production-dependencies`, see
|
||||
[Repository boundaries](../docs/architecture/repository-boundaries.md)). The same applies to every
|
||||
other boundary rule: a module the platform's own CI would reject cannot be built into an image.
|
||||
|
||||
**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). A rebuild that reuses a warm pnpm store
|
||||
cache can come out about 21 MB larger. When esbuild's install script runs, it hard-links its
|
||||
`bin/esbuild` to the native binary it ships beside it; when pnpm restores esbuild from the store's
|
||||
cached post-install state instead, that file is a second full copy. Both are esbuild, one of the
|
||||
unused tools below.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
script, no CI deployment step, no container publish, no orchestration manifest. No `.env` has ever
|
||||
been committed; only `.env.example`.
|
||||
proxy, no public listener, and no deployment automation in this repository — no deploy script, no
|
||||
CI deployment step, no container publish, no orchestration manifest. The repository can **build** a
|
||||
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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user