Add postgres service (todo §3); pin postgres:18.4-alpine3.23, one DB per Kratos/Keto/Hydra via init.sql

This commit is contained in:
2026-06-16 17:13:40 +02:00
parent a602f794d1
commit bc15f00c44
5 changed files with 53 additions and 1 deletions

View File

@@ -443,6 +443,7 @@ src/menu-config.ts loadMenuConfig()/defineMenu(): read config/menu.ts (central
views/ Core EJS templates (index = the app-shell People dashboard, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, pagination, form field, auth card, menu/popover, theme switch, icon sprite) views/ Core EJS templates (index = the app-shell People dashboard, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, pagination, form field, auth card, menu/popover, theme switch, icon sprite)
public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt) public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt)
config/menu.ts Central menu override + branding (optional; defaults apply if absent) config/menu.ts Central menu override + branding (optional; defaults apply if absent)
ory/ Ory service config + storage init (postgres/init/init.sql: one DB per Kratos/Keto/Hydra)
plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in) (planned) plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in) (planned)
docs/ Reference docs (plugin-contract.md — the authoritative plugin API) docs/ Reference docs (plugin-contract.md — the authoritative plugin API)
e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it) e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it)

View File

@@ -11,3 +11,25 @@ services:
CACHE_TEMPLATES: "true" CACHE_TEMPLATES: "true"
REQUIRE_SECURE_SECRETS: "true" REQUIRE_SECURE_SECRETS: "true"
restart: unless-stopped restart: unless-stopped
# Ory's storage only (Kratos/Keto/Hydra) — the web app never connects here.
# init/init.sql creates one database per service. Dev defaults below; supply
# POSTGRES_USER/PASSWORD via env in production.
postgres:
image: postgres:18.4-alpine3.23
environment:
POSTGRES_USER: ${POSTGRES_USER:-ory}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ory}
POSTGRES_DB: ory
volumes:
- ./ory/postgres/init:/docker-entrypoint-initdb.d:ro
- pgdata:/var/lib/postgresql # PG18+: mount the parent, not /data (version-subdir layout)
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-ory} -d ory"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
volumes:
pgdata:

View File

@@ -0,0 +1,6 @@
-- Runs once on first boot (docker-entrypoint-initdb.d), as the POSTGRES_USER.
-- One database per Ory service: each owns its schema and runs its own migrations,
-- so they never collide. The web app never connects here (stateless — see README).
CREATE DATABASE kratos;
CREATE DATABASE keto;
CREATE DATABASE hydra;

23
src/postgres.test.ts Normal file
View File

@@ -0,0 +1,23 @@
// Guards the Ory Postgres config (§3): image stays pinned to an exact version
// (AGENTS.md rule) and each Ory service keeps its own database. Real container
// behaviour is verified by booting postgres in CI/e2e; this catches edits.
import { test } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
const ORY_DATABASES = ["hydra", "keto", "kratos"]; // one DB per Ory service
test("compose pins the postgres image to an exact version", () => {
const tag = read("compose.yml").match(/image:\s*postgres:(\S+)/)?.[1];
assert.ok(tag, "compose.yml pins a postgres image");
assert.match(tag, /^\d+\.\d+/, `${tag} pins major.minor`);
assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`);
});
test("init SQL gives each Ory service its own database", () => {
const sql = read("ory/postgres/init/init.sql");
for (const db of ORY_DATABASES) {
assert.match(sql, new RegExp(`CREATE DATABASE ${db}\\b`, "i"), `creates ${db}`);
}
});

View File

@@ -57,7 +57,7 @@ everything via Docker.
- [x] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. → Reviewed all 24 test files. The suite already follows the deliberate per-module "matrix + edge" pattern from the §0/§1 merge (line 22), so most files carry no fat and force-merging distinct concerns would only hurt readability. Removed the genuine §2-era overlaps, all in `app.test.ts`: merged the two HTTP static tests into one (GET/HEAD + traversal/NUL→403), and dropped the standalone "renders the 403 error page" `ejs.renderFile` stopgap (its comment even said "403 has no first-party route yet") — the gated plugin route now exercises 403 over HTTP, so the template assertions (status + 403.ejs body + stylesheet link) moved there; also dropped the now-unused `ejs` import. Unified `view-resolver.test.ts`'s two `resolveViewPath` cases (resolve + reject) into one. 113 → 110 tests, zero coverage lost; typecheck + tests green. - [x] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us. → Reviewed all 24 test files. The suite already follows the deliberate per-module "matrix + edge" pattern from the §0/§1 merge (line 22), so most files carry no fat and force-merging distinct concerns would only hurt readability. Removed the genuine §2-era overlaps, all in `app.test.ts`: merged the two HTTP static tests into one (GET/HEAD + traversal/NUL→403), and dropped the standalone "renders the 403 error page" `ejs.renderFile` stopgap (its comment even said "403 has no first-party route yet") — the gated plugin route now exercises 403 over HTTP, so the template assertions (status + 403.ejs body + stylesheet link) moved there; also dropped the now-unused `ejs` import. Unified `view-resolver.test.ts`'s two `resolveViewPath` cases (resolve + reject) into one. 113 → 110 tests, zero coverage lost; typecheck + tests green.
## 3. Ory stack — compose + config ## 3. Ory stack — compose + config
- [ ] `postgres` service (pinned tag); separate DB/schema per Kratos/Keto/Hydra. - [x] `postgres` service (pinned tag); separate DB/schema per Kratos/Keto/Hydra.`compose.yml` `postgres` service pinned to `postgres:18.4-alpine3.23` (verified latest stable PG + newest Alpine the official image ships); `ory/postgres/init/init.sql` (mounted at `docker-entrypoint-initdb.d`) creates one DB per service (`kratos`/`keto`/`hydra`) so each owns its schema + migrations. Dev defaults (`ory`/`ory`, env-overridable for prod), named `pgdata` volume mounted at `/var/lib/postgresql` (PG18+ version-subdir layout — not `/data`), `pg_isready` healthcheck. Web app never connects. Verified live: boots healthy, three DBs present, then torn down. `postgres.test.ts` guards the pin + DB-per-service. typecheck + 112 units green.
- [ ] `kratos` service (pinned) + `migrate`; identity schema (traits: email, name). - [ ] `kratos` service (pinned) + `migrate`; identity schema (traits: email, name).
- [ ] Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages. - [ ] Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages.
- [ ] Kratos OIDC/SSO providers (Google/Microsoft/SAML) config (secrets via env). **None enabled by default** — a clean clone runs password-only; a provider activates purely by supplying its env creds. - [ ] Kratos OIDC/SSO providers (Google/Microsoft/SAML) config (secrets via env). **None enabled by default** — a clean clone runs password-only; a provider activates purely by supplying its env creds.