From 80b2a020bdf3e0974b09a430fac1d85d57e8f64e Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <3223881+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:35:31 -0500 Subject: [PATCH] chore(compose): parameterize host port + rewrite backup drill to single-file pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docker-compose.yaml line 11 changes from "127.0.0.1:8420:8420" to "127.0.0.1:${AWARENESS_PORT:-8420}:8420" Default behavior unchanged — operators running `docker compose up -d` with no env continue to get 8420:8420 (verified via `docker compose config`). Setting AWARENESS_PORT=8421 remaps the host side to 8421:8420; the container-internal port stays 8420 regardless. Lets operators run alternate-port stacks (restore drill, port-conflict scenarios, dev-alongside-prod) without editing the production compose file. docs/backup.md §Practice the restore rewritten to use the production compose file with env overrides (AWARENESS_PORT + AWARENESS_PG_DATA). Drops the round-2 QA-compose-file substitution table. Drill now requires production to be briefly stopped (~10-15 min) because docker-compose.yaml still pins container_name values; for a single- maintainer deployment, planned downtime is simpler than parameterizing container names. The qa-compose path is preserved as the concurrent- with-prod alternative for CI / automated integration tests / future multi-user scenarios. docker-compose.qa.yaml unchanged — different use case. Closes #344. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 3 +++ docker-compose.yaml | 7 +++++- docs/backup.md | 53 ++++++++++++++++++++++++++++----------------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8688789..da92196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- **`docker-compose.yaml`: host-side port is now parameterizable** — line 11 changes from `"127.0.0.1:8420:8420"` to `"127.0.0.1:${AWARENESS_PORT:-8420}:8420"`. Default behavior unchanged (operators running `docker compose up -d` with no env continue to get `8420:8420`). Setting `AWARENESS_PORT=8421` remaps the host side to `8421:8420` — the container-internal port stays 8420 regardless. Lets operators run alternate-port stacks (restore drill, dev-alongside-prod, port-conflict scenarios) without editing the production compose file. Container-name values remain fixed, so alternate-port stacks currently require the production stack to be stopped first; `docker-compose.qa.yaml` remains the path for concurrent-with-prod drills. `docs/backup.md` §Practice the restore rewritten to use the production compose file with env overrides, per the new single-file pattern, dropping the round-2 QA-compose-file substitution table. Closes [#344](https://github.com/cmeans/mcp-awareness/issues/344). + ### Security - **Redact URL log output in the OAuth paths** — new `safe_url_for_log(url)` helper (in `src/mcp_awareness/helpers.py`) strips RFC 3986 `userinfo` (the `user:password@` netloc prefix), query string, and fragment before returning a URL for logging. OIDC discovery, JWKS URI discovery, userinfo-endpoint discovery, OAuth-proxy discovery failure, and OAuth-proxy discovered-endpoints logs (`oauth.py:_discover_oidc_config`, `oauth_proxy.py:discover_oidc_endpoints`) all now route through the helper. Defensive rather than currently-exploitable: the issuer URLs this server consumes today are plain `https://provider/...` strings with no credential-carrying parts, but the helper ensures future misconfiguration, provider change, or redirect can't leak an embedded credential into every log line. Split the IP-header-chain log in `oauth_proxy.py` by config path: the **default** branch logs a module-level literal display string (`"CF-Connecting-IP → X-Real-IP → ASGI client"` — plain text, no contributor-controlled value flowing into the sink), and the **override** branch logs only the *count* of custom entries and a pointer to the `AWARENESS_OAUTH_PROXY_IP_HEADERS` env var for operators who want to verify names. Closes CodeQL alerts #5, #6, #7, #8, and #9 (the split removes taint-flow for the header-chain log entirely; no UI dismissal required). 11 new unit tests in `tests/test_helpers.py::TestSafeUrlForLog` cover plain/userinfo-stripping/query-stripping/fragment-stripping/invalid-port-exception-path/edge cases. diff --git a/docker-compose.yaml b/docker-compose.yaml index 708cb88..de695ee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,12 @@ services: container_name: mcp-awareness restart: unless-stopped ports: - - "127.0.0.1:8420:8420" + # Host port is parameterized so operators can run an alternate-port + # stack (e.g., a restore-drill instance on :8421) without editing + # this file. Container-internal port stays 8420 regardless. Default + # matches the production pin so `docker compose up -d` with no env + # is unchanged. + - "127.0.0.1:${AWARENESS_PORT:-8420}:8420" environment: - AWARENESS_MOUNT_PATH=${AWARENESS_MOUNT_PATH:-} - AWARENESS_DATABASE_URL=${AWARENESS_DATABASE_URL:-} diff --git a/docs/backup.md b/docs/backup.md index 7697ddd..12f5e8d 100644 --- a/docs/backup.md +++ b/docs/backup.md @@ -211,41 +211,54 @@ Without these, a restored Postgres dump is not reachable by the application even ## Practice the restore -Backups that have never been restored are hopes, not backups. Schedule a restore drill at least once per quarter. The default `docker-compose.yaml` hardcodes the port mapping to `127.0.0.1:8420:8420`, so a naked second stack would collide with production. Use `docker-compose.qa.yaml` instead — it already maps the drill port to `127.0.0.1:8421:8420` and isolates drill data in a named volume (`awareness-qa-pg`) separate from production's bind mount. +Backups that have never been restored are hopes, not backups. Schedule a restore drill at least once per quarter. -**Important — the QA compose file uses different service and database names than production.** When you run §Restore against the drill stack, substitute the identifiers in every command: +The drill uses the **production compose file** (`docker-compose.yaml`) with two env-var overrides: the host-side port (so the drill doesn't collide with the running production instance) and the Postgres data directory (so the drill's Postgres data stays isolated from production's bind mount). -| §Restore command uses | Substitute for drill | -|-----------------------|----------------------| -| `docker compose` (implies `docker-compose.yaml`) | `docker compose -f docker-compose.qa.yaml` | -| Service: `mcp-awareness` | `mcp-awareness-qa` | -| Service: `postgres` | `postgres-qa` | -| `--dbname=awareness` (and `psql --dbname=postgres`) | `--dbname=awareness_qa` (and `psql --dbname=postgres` unchanged) | -| `DROP DATABASE … awareness` / `CREATE DATABASE awareness …` | `… awareness_qa` on both | +**The drill requires production to be briefly stopped.** Production and drill share the same `container_name` values in `docker-compose.yaml`, so they can't run concurrently against each other. For a single-maintainer deployment this is fine: stop production, spin up the drill, verify, tear down the drill, restart production. Plan for ~10-15 minutes of downtime per drill. Drill procedure: -1. Bring the drill stack up: +1. Stop production: ```bash - docker compose -f docker-compose.qa.yaml up -d + docker compose down ``` -2. Restore the most recent dump into the drill stack, applying the substitutions above. For example, §Restore step 3 becomes: +2. Bring the drill stack up on a different host port, with a separate Postgres data directory: ```bash - docker compose -f docker-compose.qa.yaml exec -T postgres-qa pg_restore \ - --username=awareness --dbname=awareness_qa \ + export AWARENESS_PORT=8421 + export AWARENESS_PG_DATA=~/awareness-pg-drill + docker compose up -d + ``` +3. Restore the most recent dump into the drill's Postgres. §Restore's commands work as-written — no substitutions, same service names, same database name: + ```bash + docker compose stop mcp-awareness + docker compose exec -T postgres psql --username=awareness --dbname=postgres <<'SQL' + DROP DATABASE IF EXISTS awareness; + CREATE DATABASE awareness OWNER awareness; + SQL + docker compose exec -T postgres pg_restore \ + --username=awareness --dbname=awareness \ --no-owner --no-acl \ < ~/backups/mcp-awareness/awareness-*.dump + docker compose start mcp-awareness ``` -3. Connect an MCP client to the drill instance on `http://127.0.0.1:8421/mcp` and call `get_stats()` and `get_briefing()`. Compare the counts to what the production instance reports at the same moment. -4. Tear the drill stack down and remove the drill's named volume so the next drill starts from a clean slate: +4. Connect an MCP client to the drill instance on `http://127.0.0.1:8421/mcp` and call `get_stats()` and `get_briefing()`. Compare the counts to what the most recent production dump reported — the drill should restore every row that made it into the backup. +5. Tear the drill stack down and remove the drill data directory so the next drill starts from a clean slate: ```bash - docker compose -f docker-compose.qa.yaml down -v + docker compose down + rm -rf ~/awareness-pg-drill + unset AWARENESS_PORT AWARENESS_PG_DATA ``` - The `-v` flag deletes the `awareness-qa-pg` and `awareness-qa-ollama` named volumes. Without it, the next `up -d` reuses the previous drill's data. +6. Bring production back up: + ```bash + docker compose up -d + ``` + +If step 4 surprises you (counts don't match, briefing is empty, restore hit unexpected errors), the instructions above drifted from reality — file an issue, fix the doc, try again. The point of a drill is to find the drift before you need the dump for real. -If step 3 surprises you (counts don't match, briefing is empty, restore hit unexpected errors), the instructions above drifted from reality — file an issue, fix the doc, try again. The point of a drill is to find the drift before you need the dump for real. +**Why the brief production downtime?** `docker-compose.yaml` sets fixed `container_name:` values (`mcp-awareness`, `awareness-postgres`, etc.) — Docker Compose uses those literal names regardless of project (`-p`) flag. Running a parallel drill stack against the same compose file would collide on those names. Aligning/parameterizing the container names to allow concurrent drills is a possible future enhancement; for a single-maintainer deployment, a 10-minute planned downtime per quarter is cheaper than the config complexity. -**Why not parameterize `docker-compose.yaml`'s port and keep production service names?** Today the production file hardcodes `127.0.0.1:8420:8420` and the QA file uses `-qa`-suffixed service/database names. Making the port a `${AWARENESS_PORT:-8420}` substitution and aligning the QA stack's names would let the drill use `docker-compose.yaml` directly with an env override and no identifier substitution — tracked in [#344](https://github.com/cmeans/mcp-awareness/issues/344). Until that lands, the substitution table above is the path. +**For concurrent-with-production drills** (CI, automated integration tests, or future multi-user deployments that can't easily stop production), use `docker-compose.qa.yaml` — it exposes `127.0.0.1:8421:8420`, uses `-qa`-suffixed service/database names, and isolates data in a named volume. The `-qa` name suffixes mean the §Restore commands need substitutions (`postgres` → `postgres-qa`, `mcp-awareness` → `mcp-awareness-qa`, `--dbname=awareness` → `--dbname=awareness_qa`) — so the production-compose-file pattern above is simpler and is preferred for the quarterly drill. ## Related