Phase 7: Cosmos deployment

- Dockerfile with a check target (typecheck and tests) and one image per
  service on a pinned Node 22.20 Alpine base, each with only its own
  production dependencies and a health check.
- deploy/compose.yaml: gk-content, gk-admin and gk-site on homelab_network
  with no host ports, read-only root filesystems, no capabilities, the vault's
  Site folder mounted into the content service, and backup.dump=sqlite on it.
- deploy/deploy.sh builds and checks the commit, restarts the stack, and goes
  back to the last good tag if it doesn't come up healthy; rollback.sh swaps
  tags. .gitea/workflows/deploy.yml runs it on a host runner on Cosmos.
- docs/deploy.md runbook (settings, runner, mirror, NPM hosts, Turnstile,
  Healthchecks, backups, cutover) and the stack's inventory note in vault-kit.
- Site: trusts X-Forwarded-Host/Proto for gregkilmer.com hosts so form posts
  pass Astro's origin check behind the proxy; rate limits read
  CF-Connecting-IP; site URL set for absolute links.
- Content service: the held-change webhook carries a Discord-ready message;
  .env.example lists the core settings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-12 00:43:03 +00:00
parent f9a8fb1acd
commit 754c6482be
17 changed files with 472 additions and 5 deletions
+15
View File
@@ -1,5 +1,20 @@
# Content service settings. Copy to .env (never commit it) or set them in the
# container's environment. See docs/content-service.md, "Running it".
# On Cosmos this file is /home/greg/homelab/gregkilmer/content.env (docs/deploy.md).
# --- Core -----------------------------------------------------------------------
# role:token pairs, 24+ random characters each (openssl rand -hex 24).
# reader: the site; admin: the admin page; intake: the site's forms;
# agent: AI agents over MCP; editor: future editing tools.
CONTENT_TOKENS=
# Healthchecks ping after every full scan of the Site folder.
HEALTHCHECK_URL=
# Discord (or any) webhook, told whenever a change is held for review.
NOTIFY_WEBHOOK_URL=
# Email addresses an agent may add to a note without a hold.
ALLOWED_EMAILS=greg@gregkilmer.com,contact@gregkilmer.com,orders@gregkilmer.com
# SCAN_MINUTES=5
# SUBMISSION_MAX_MB=10
# --- Sync jobs (docs/content-service.md, "Sync jobs") ------------------------
# Public sources to run: archidekt (hourly), bgg (daily), covers (every 15 min).
+6 -1
View File
@@ -23,7 +23,12 @@ const service = new ContentService({
fetch(config.notifyWebhookUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ event: "held", held: heldView(held) }),
// `content` is what a Discord webhook shows; other receivers read `held`.
body: JSON.stringify({
content: `GregKilmer.com held ${held.path} (${held.reasons.map((r) => r.code).join(", ") || "waiting for review"}). Review it on the admin page.`,
event: "held",
held: heldView(held),
}),
}).catch((err) => console.error("Notify failed:", err));
}
},