# muninn-gate — WG-internal HTTP front door for the Muninn ingestion queue (#377). # Deployed on corviduo-dev (10.250.50.152), co-located with the worldtree-personal # muninn watcher (worldtree-personal-worldtree-muninn-1). infra-ops-managed stack, # separate from the worldtree CI/CD compose. # # Image is built out-of-band (the Gitea read token rides as a BuildKit secret, so # it never lands in a layer) — see README.md: # DOCKER_BUILDKIT=1 docker build --secret id=gitea_pw,src= -t muninn-gate:0.0.14 . # # Real config (with bearer-key secrets) lives on the server at # /opt/docker/conf/muninn-gate/muninn-gate.yaml (gitignored); repo carries the # redacted conf/muninn-gate.example.yaml. services: muninn-gate: image: muninn-gate:0.0.14 container_name: muninn-gate restart: unless-stopped # Run as the ingestion-owner uid (vh, 1000:1000) so the gate can WRITE the # queue (ingestion dir is 1000:1000 0755). Overrides the image's build user; # non-root, least-privilege (no root needed — binds :8090, reads config + # staging, writes only ingestion). The mounted config is chowned 1000:1000 0600. user: "1000:1000" # WG-internal bind straight to the host WG address; the launcher owns the bind # (the app's `bind` field is observability-only). Host networking so --host/--port # land directly on corviduo-dev. network_mode: host command: - uvicorn - muninn_gate.entrypoint:app_factory - --factory - --host - "10.250.50.152" - --port - "8090" environment: MUNINN_GATE_CONFIG: /app/config/muninn-gate.yaml volumes: # ingestion_root = /data/state/ingestion — the SAME volume the personal muninn # watcher uses, byte-identical path in both containers (absolute-path agreement). # NOTE (hardening candidate): whole state volume per muninn-dev's spec; a subpath # mount of just `ingestion` -> /data/state/ingestion would be tighter (gate only # needs RW on ingestion). Flag to muninn-dev before adopting. - worldtree-personal_worldtree-state:/data/state # single-writer mounted config (bearer-key secrets), read-only: - /opt/docker/conf/muninn-gate/muninn-gate.yaml:/app/config/muninn-gate.yaml:ro # shared staging root — SAME absolute path as the watcher's bind, read-only # (gate only resolves + reads staging; never writes there): - /mnt/muninn-staging/mimir-inbox:/mnt/muninn-staging/mimir-inbox:ro healthcheck: # /ping is the one anonymous route; returns {"service":"ok"}. NB /health must # NOT be the liveness probe — it is always-200 by design even when the watcher # is down, so it would never restart the gate (it would wrongly signal the # watcher's container). test: ["CMD", "python", "-c", "import urllib.request,sys; sys.exit(0 if b'ok' in urllib.request.urlopen('http://10.250.50.152:8090/ping', timeout=3).read() else 1)"] interval: 30s timeout: 5s retries: 3 start_period: 10s labels: - homepage.group=Worldtree - homepage.name=Muninn Gate - homepage.icon=mdi-gate-arrow-right - homepage.description=Muninn ingestion queue front door (#377) - homepage.href=http://10.250.50.152:8090/ping volumes: # The worldtree-personal muninn watcher's state volume — external, managed by the # worldtree-personal compose. We attach to it read/write for the ingestion queue. worldtree-personal_worldtree-state: external: true