CI / verify (pull_request) Successful in 54s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m48s
CI / image-build (pull_request) Successful in 27s
CI / deploy-smoke (pull_request) Successful in 42s
Implement ADR 0044's reference deployment as a base Compose file plus overlays, so an installation that already runs a proxy can use the base alone. - compose.yaml: api, postgres, object-storage, and the operator-only migrate (profile ops, plan by default). No published port. api and migrate run init, read-only, tmpfs /tmp, cap_drop ALL, no-new-privileges. postgres is only on the internal data network. - TLS everywhere from one installation-private CA: PostgreSQL TLS 1.3 only, with pg_hba rejecting every non-TLS connection, and the API on verify-full via EKKLESIA_DATABASE_CA (PEM text). SeaweedFS serves S3 over HTTPS (ADR 0030 requires https in production), trusted by the API through NODE_EXTRA_CA_CERTS. No Core change. - compose.proxy.yaml + Caddyfile.example: Caddy pinned by digest, ACME HTTP-01 (DNS-01 documented), routes /, /api/*, /display/*, /assets/* to api and refuses everything else; the storage hostname preserves Host. The API trusts exactly the proxy's fixed /32 on an edge network whose dynamic range excludes the fixed addresses; Caddy overwrites X-Forwarded-For and strips caller-supplied forwarding headers. - compose.tunnel.yaml: cloudflared pinned by digest, token from its own env file; Caddy honors CF-Connecting-IP only from its fixed address. - Four per-service root-owned env files, examples with no values; scripts/init-tls.sh (idempotent, never overwrites a key) and scripts/gen-secrets.sh (prints only). - CI: pnpm deploy:config in ci:verify for all three combinations, and a deploy-smoke job (scripts/smoke-deploy.sh) proving readiness, the Dashboard, signed storage delivery, the client-address chain, initial owner sign-in/out, verify-full database TLS, and Doctor through the proxy, against disposable services. tests/deploy.test.ts pins the structural guarantees. - ADR 0044 (still Proposed) amended to match: API-to-storage HTTPS, DNS-01 needs a custom Caddy build and tunnel-only needs DNS-01, the tunnel's origin TLS settings live in the Cloudflare dashboard, port 80 serves HTTP-01, and secrets are one env file per service. Co-Authored-By: Claude Opus 5.5 <[email protected]>
154 lines
5.6 KiB
YAML
154 lines
5.6 KiB
YAML
# Ekklesia's reference self-hosted deployment: the base project (ADR 0044).
|
|
#
|
|
# `api`, `postgres`, and `object-storage`, plus the operator-only `migrate` service. Nothing here
|
|
# publishes a port. Add `compose.proxy.yaml` for the reference TLS proxy, and `compose.tunnel.yaml`
|
|
# on top of that for a Cloudflare Tunnel connector; or attach `api` and `object-storage` to an
|
|
# existing proxy's network yourself (deploy/README.md).
|
|
#
|
|
# Non-secret settings come from `deploy/.env` (copy `env/compose.env.example`). Secrets live in
|
|
# root-owned, mode-600 files under EKKLESIA_ENV_DIR, one per service, so no container receives
|
|
# another service's credentials. TLS material comes from `scripts/init-tls.sh`.
|
|
|
|
name: ekklesia
|
|
|
|
x-ekklesia-hardening: &ekklesia-hardening
|
|
init: true
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp
|
|
cap_drop:
|
|
- ALL
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
|
|
x-ekklesia-app: &ekklesia-app
|
|
<<: *ekklesia-hardening
|
|
image: ${EKKLESIA_IMAGE:?EKKLESIA_IMAGE is required, for example registry.example.org/ekklesia:0.1.0}
|
|
env_file:
|
|
- ${EKKLESIA_ENV_DIR:-/etc/ekklesia}/ekklesia.env
|
|
environment:
|
|
# Core's object-storage client trusts Node's default CA set. Adding the installation-private CA
|
|
# to it, from the file below, is what lets the API verify `object-storage`'s certificate
|
|
# without any storage TLS exception. The database CA travels separately, as PEM text in
|
|
# EKKLESIA_DATABASE_CA, and is the only CA the database connection trusts.
|
|
NODE_EXTRA_CA_CERTS: /run/ekklesia/tls/ca.crt
|
|
volumes:
|
|
- ${EKKLESIA_TLS_DIR:-/etc/ekklesia/tls}/ca.crt:/run/ekklesia/tls/ca.crt:ro
|
|
networks:
|
|
data: {}
|
|
storage: {}
|
|
egress: {}
|
|
|
|
services:
|
|
api:
|
|
<<: *ekklesia-app
|
|
restart: unless-stopped
|
|
# The image's own HEALTHCHECK (GET /api/v1/health/ready) applies unchanged.
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
object-storage:
|
|
condition: service_healthy
|
|
|
|
# Operator commands only. `profiles` keeps `docker compose up` from ever starting it; it runs only
|
|
# through `docker compose run --rm migrate …`. The default command is the read-only plan.
|
|
migrate:
|
|
<<: *ekklesia-app
|
|
profiles:
|
|
- ops
|
|
restart: "no"
|
|
healthcheck:
|
|
disable: true
|
|
command: ["node", "core/dist/cli/plan-migrations.js"]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
object-storage:
|
|
condition: service_healthy
|
|
|
|
postgres:
|
|
image: postgres:18.4-alpine3.24@sha256:9a8afca54e7861fd90fab5fdf4c42477a6b1cb7d293595148e674e0a3181de15
|
|
restart: unless-stopped
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
env_file:
|
|
- ${EKKLESIA_ENV_DIR:-/etc/ekklesia}/postgres.env
|
|
environment:
|
|
POSTGRES_INITDB_ARGS: --data-checksums --auth-host=scram-sha-256 --auth-local=trust
|
|
TZ: UTC
|
|
# TLS with the installation-private CA's server certificate for the name `postgres`, TLS 1.3
|
|
# only, and a host-based authentication file that rejects every non-TLS network connection.
|
|
# The API connects with verify-full; there is no plaintext path to fall back to.
|
|
command:
|
|
- postgres
|
|
- -c
|
|
- ssl=on
|
|
- -c
|
|
- ssl_cert_file=/run/ekklesia/tls/postgres.crt
|
|
- -c
|
|
- ssl_key_file=/run/ekklesia/tls/postgres.key
|
|
- -c
|
|
- ssl_min_protocol_version=TLSv1.3
|
|
- -c
|
|
- hba_file=/etc/ekklesia/pg_hba.conf
|
|
volumes:
|
|
- postgres-data:/var/lib/postgresql
|
|
- ${EKKLESIA_TLS_DIR:-/etc/ekklesia/tls}/postgres.crt:/run/ekklesia/tls/postgres.crt:ro
|
|
- ${EKKLESIA_TLS_DIR:-/etc/ekklesia/tls}/postgres.key:/run/ekklesia/tls/postgres.key:ro
|
|
- ./postgres/pg_hba.conf:/etc/ekklesia/pg_hba.conf:ro
|
|
# Only `data`: never a proxy-facing network, and no route out of the host.
|
|
networks:
|
|
data: {}
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 12
|
|
start_period: 20s
|
|
|
|
object-storage:
|
|
image: chrislusf/seaweedfs:4.28@sha256:d071979843142451d41566bc09f6adaa36902c112683d900a417247c82e9c3c1
|
|
restart: unless-stopped
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
env_file:
|
|
- ${EKKLESIA_ENV_DIR:-/etc/ekklesia}/object-storage.env
|
|
environment:
|
|
TZ: UTC
|
|
# The S3 API over HTTPS on 8443, with the installation-private CA's certificate for the name
|
|
# `object-storage`: production storage must be https (ADR 0030), for the API and for the proxy.
|
|
command:
|
|
- mini
|
|
- -dir=/data
|
|
- -s3.port.https=8443
|
|
- -s3.cert.file=/run/ekklesia/tls/object-storage.crt
|
|
- -s3.key.file=/run/ekklesia/tls/object-storage.key
|
|
volumes:
|
|
- object-storage-data:/data
|
|
- ${EKKLESIA_TLS_DIR:-/etc/ekklesia/tls}/object-storage.crt:/run/ekklesia/tls/object-storage.crt:ro
|
|
- ${EKKLESIA_TLS_DIR:-/etc/ekklesia/tls}/object-storage.key:/run/ekklesia/tls/object-storage.key:ro
|
|
networks:
|
|
storage: {}
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9333/cluster/status"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 18
|
|
start_period: 15s
|
|
|
|
networks:
|
|
# The database network: postgres, api, and migrate. Internal, so nothing on it reaches beyond the
|
|
# host and no proxy is ever attached to it.
|
|
data:
|
|
internal: true
|
|
# The storage network: object-storage, api, and migrate. Internal for the same reason; browsers
|
|
# reach storage only through a proxy that joins it on `edge`.
|
|
storage:
|
|
internal: true
|
|
# Outbound access for api and migrate (an SMTP relay, for example). Nothing listens on it.
|
|
egress: {}
|
|
|
|
volumes:
|
|
postgres-data:
|
|
object-storage-data:
|