From f283069ab19d6b17d37eeb18d28daa0fe3e55f87 Mon Sep 17 00:00:00 2001 From: "m2 (AI Agent)" Date: Thu, 2 Jul 2026 03:14:18 +0200 Subject: [PATCH] T014: ledger Dockerfile + deploy notes --- ledger/Dockerfile | 22 ++++++++++++++++ ledger/README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 ledger/Dockerfile create mode 100644 ledger/README.md diff --git a/ledger/Dockerfile b/ledger/Dockerfile new file mode 100644 index 0000000..d7e7a9c --- /dev/null +++ b/ledger/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install the ledger package itself (deps come from pyproject.toml [project.dependencies]). +COPY pyproject.toml ./ +COPY src ./src +RUN pip install --no-cache-dir . + +RUN groupadd --system ledger && useradd --system --gid ledger --home-dir /app ledger \ + && mkdir -p /data && chown -R ledger:ledger /data /app +USER ledger + +ENV LEDGER_DB_PATH=/data/ledger.db + +VOLUME ["/data"] +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)" || exit 1 + +CMD ["uvicorn", "m2_ledger.api:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/ledger/README.md b/ledger/README.md new file mode 100644 index 0000000..59e78f4 --- /dev/null +++ b/ledger/README.md @@ -0,0 +1,65 @@ +# m2-ledger + +Append-only credits ledger — transactions `{ts, from, to, amount, reason, ref}`, +balances always derived by folding the tx log (never stored authoritatively). See +`specs/001-market-first-wedge/contracts/ledger-api.md` (frozen v1 contract) and +`docs/config.md` (env var / key reference) for the full spec. + +## Run locally + +```bash +cd ledger +uv sync +LEDGER_DB_PATH=./ledger.db \ +LEDGER_SERVICE_KEYS=svc_devkey \ +LEDGER_ADMIN_KEYS=admin_devkey \ + uv run uvicorn m2_ledger.api:app --reload --port 8000 +``` + +`GET http://localhost:8000/health` should return `{"status":"healthy"}`. All other +endpoints require an `X-API-Key` header (service or admin class — see +`docs/config.md` §4). + +## Deploy as a Coolify app + +`m2-ledger` is the one sanctioned new service in the first wedge (constitution I); +it deploys as a plain Coolify app on the `coolify` Docker network, not baked into +any image. + +1. **Build**: point the Coolify app at this repo/subpath (`ledger/`) with + `ledger/Dockerfile` — no build args, no secrets at build time (constitution VI: + no secrets in repos or images). +2. **Network + alias**: attach the app to the `coolify` network with alias + `m2-ledger`, so callers use `http://m2-ledger:8000` per the frozen contract's + base URL. +3. **Env vars** (inject at runtime via Coolify's environment panel — never commit + real values, see `docs/config.md` §5): + - `LEDGER_DB_PATH=/data/ledger.db` (already set as the image default; override + only if the persistent volume is mounted elsewhere) + - `LEDGER_SERVICE_KEYS` — comma-separated `service`-class keys (CLI/Store/indexer) + - `LEDGER_ADMIN_KEYS` — comma-separated `admin`-class keys (grants, snapshots); + keep this set small + - `LEDGER_PLATFORM_PCT` (default `0.10`, UNCONFIRMED) and `LEDGER_STARTER_GRANT` + (default `100`, UNCONFIRMED) — optional, override the code defaults + - `REGISTRY_REPO_URL` + `FORGEJO_TOKEN` — required once `/snapshot` actually + commits to `m2/market-registry` (see TODO below) +4. **Persistent volume**: mount a Coolify persistent volume at `/data` (the image + declares `VOLUME /data`) so `ledger.db` survives redeploys. This is the single + source of truth for the tx log — never point it at ephemeral container storage. +5. **Health check**: the image's `HEALTHCHECK` hits `GET /health` (no auth). Coolify + should also be configured to poll the same endpoint for its own readiness gate. +6. **Canary-first** (constitution VI): roll out to one environment before any + fleet-wide dependency on `m2-ledger` — there is no fleet-wide blast for this + service since it's a single shared instance, but verify `/health` and one + `/balance/{operator_id}` call with a real service key before pointing the CLI's + `ledger_url` at it in `~/.m2-market/config.toml`. + +## Snapshot cron (TODO T034) + +`POST /snapshot` (admin) currently computes `audit/YYYY-MM-DD.json` balances and +returns the JSON body but does **not** commit it anywhere — see the +`TODO(T034)` in `src/m2_ledger/api.py`. Once T034 lands (Forgejo commit via +`REGISTRY_REPO_URL` + `FORGEJO_TOKEN`), add a daily Coolify scheduled task (or +cron container) that calls `POST /snapshot` with an admin key once every 24h, +per constitution V ("Daily balance snapshots MUST be committed to Forgejo for +audit") and the contract's "admin, also daily cron" note. Not wired up yet.