From 372ee7235329c8546de852256983f6abc8aab846 Mon Sep 17 00:00:00 2001 From: m17hr1l Date: Mon, 18 May 2026 22:57:33 +0200 Subject: [PATCH] stage-9: consolidate into one compose stack behind nginx-proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit psyc now runs as a single docker compose stack — cockpit + mock-cert + (gpu-profile) inference — on the shared external `backend` network, fronted by nginx-proxy as psyc.neuronetz.ai. Replaces the venv processes + one-off docker run. MOCK_CERT_BASE and INFERENCE_URL are now env-configurable (PSYC_MOCK_CERT_URL / PSYC_INFERENCE_URL) so the cockpit reaches the other services by compose service name. Restart policies + healthchecks. deploy.md rewritten to match. Verified: cockpit serves directly and via the proxy; the full scout→…→courier→ledger chain runs over the compose network. Co-Authored-By: Claude Opus 4.7 --- docker-compose.yml | 72 ++++++++++++++++++++++++--------- docs/deploy.md | 75 +++++++++++++++++------------------ src/psyc/cockpit/inference.py | 3 +- src/psyc/lines/route.py | 3 +- 4 files changed, 94 insertions(+), 59 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3a1d6ae..ae320f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,71 @@ -# psyc — company-network deployment (cockpit + mock destination receiver). +# psyc — neuronetz.ai deployment stack. # -# docker compose up -d --build +# docker compose up -d --build # cockpit + mock-cert (no GPU) +# docker compose --profile gpu up -d --build # + the live model (needs an NVIDIA GPU) # -# WARNING: psyc has no built-in authentication. The cockpit exposes cases, the -# ledger, and sealed-package metadata to anyone who can reach port 8767. Deploy -# behind the company reverse proxy / SSO / VPN, or firewall the ports to the -# SOC subnet. See docs/deploy.md. +# The cockpit is fronted by the external `backend` network's nginx-proxy as +# psyc.neuronetz.ai (point DNS for that name at the proxy host). mock-cert and +# the inference server stay internal — no VIRTUAL_HOST, reachable only inside +# `backend` by service name. +# +# WARNING: psyc has no built-in authentication. The reverse proxy / network +# perimeter is the security boundary. See docs/deploy.md. services: cockpit: build: . image: psyc:latest command: ["psyc", "serve", "--host", "0.0.0.0", "--port", "8767"] + environment: + VIRTUAL_HOST: psyc.neuronetz.ai + VIRTUAL_PORT: "8767" + PSYC_MOCK_CERT_URL: http://mock-cert:8770 + PSYC_INFERENCE_URL: http://inference:8771 ports: - - "8767:8767" + - "8767:8767" # direct/debug access; the proxy serves psyc.neuronetz.ai on :80 volumes: - - psyc-data:/data + - ./data:/data + networks: [backend] restart: unless-stopped - # Behind a company egress proxy, uncomment and set: - # environment: - # HTTPS_PROXY: http://proxy.corp:3128 - # HTTP_PROXY: http://proxy.corp:3128 + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8767/healthz')"] + interval: 30s + timeout: 5s + retries: 3 mock-cert: image: psyc:latest command: ["psyc", "mock-cert", "--host", "0.0.0.0", "--port", "8770"] - ports: - - "8770:8770" volumes: - - psyc-data:/data - depends_on: - - cockpit + - ./data:/data + networks: [backend] restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8770/healthz')"] + interval: 30s + timeout: 5s + retries: 3 -volumes: - psyc-data: + # The live fine-tuned model behind the Classifier bot. GPU-only — opt in with + # `--profile gpu`. Uses the psyc-trainer image (built from Dockerfile.train). + inference: + image: psyc-trainer + command: ["/scripts/serve_model.py", "--adapter", "/data/adapters/psyc-v4/final", "--host", "0.0.0.0", "--port", "8771"] + volumes: + - ./data:/data + - ./scripts:/scripts + networks: [backend] + restart: unless-stopped + profiles: ["gpu"] + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + +networks: + backend: + name: backend + external: true diff --git a/docs/deploy.md b/docs/deploy.md index 193ec9a..4d19f07 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,57 +1,59 @@ # psyc — deployment -Deploying the psyc platform (cockpit + workers) as Docker containers — e.g. on a -Proxmox-hosted VM in the company network. +psyc deploys as a three-service Docker Compose stack, fronted by an existing +`jwilder/nginx-proxy` on the shared external `backend` network and served as +`psyc.neuronetz.ai`. ## Read this before deploying - **No built-in authentication.** The cockpit exposes cases, the ledger, and - sealed-package metadata to anyone who can reach port 8767. Deploy it **behind - the company reverse proxy / SSO / VPN**, or firewall the ports to the SOC - subnet. Do not expose 8767 to the open network. (If you want in-app auth - instead of relying on the perimeter, that's a feature to add — not present today.) -- **The live model is separate.** This image has no GPU and no torch. The - fine-tuned-model bot needs `serve_model.py` running in the CUDA container on a - GPU host (Proxmox GPU passthrough to a VM). Without it the Classifier bot - falls back to rules — the platform works fine, just rules-only. + sealed-package metadata to anyone who can reach it. The reverse proxy / network + perimeter **is** the security boundary — keep `psyc.neuronetz.ai` on the + internal network or behind SSO. nginx-proxy can add per-vhost basic auth via a + mounted `htpasswd` file if you need a quick gate. (In-app auth is not built.) +- **The live model is GPU-only.** The `inference` service needs an NVIDIA GPU + and the `psyc-trainer` image (`docker build -f Dockerfile.train`). It is gated + behind the `gpu` compose profile. Without it the Classifier bot falls back to + rules — the platform runs fine. - **Outbound network.** Scoutline (URLhaus / CISA KEV / Feodo) and Mapline (ip-api.com) make outbound HTTPS. Behind a company egress proxy, set - `HTTPS_PROXY` / `HTTP_PROXY` in the container environment (see the commented - block in `docker-compose.yml`). -- **mock-cert is a stand-in.** It accepts submissions for testing — it is not a - real destination. Wire real CERT / MISP / abuse endpoints (and their - credentials, per `docs/dossier.md` §18) before relying on routing in production. + `HTTPS_PROXY` / `HTTP_PROXY` on the `cockpit` service. +- **mock-cert is a stand-in** for real destinations — wire real CERT / MISP / + abuse endpoints (and credentials, per `docs/dossier.md` §18) before relying on + routing in production. -## Proxmox +## Prerequisites -Docker is not native to Proxmox. Run it inside a Proxmox **VM** (recommended — -clean isolation, simplest Docker support) or a privileged LXC. Install Docker + -the Compose plugin in that guest, give it outbound network for the feeds, then -deploy as below. The GPU inference server, if used, needs a separate VM with -GPU passthrough. +- A Docker host (on Proxmox: a VM running Docker — cleanest; or a privileged LXC). +- The external `backend` network and an `nginx-proxy` on it (the shared + reverse-proxy stack). psyc joins that network; the proxy auto-discovers the + cockpit by its `VIRTUAL_HOST`. +- DNS: point `psyc.neuronetz.ai` at the proxy host. +- For the live model: an NVIDIA GPU + the NVIDIA container runtime, and the + `psyc-trainer` image built. ## Deploy ```bash git clone ssh://git@gitea.neuronetz.ai:222/m17hr1l/psyc.git cd psyc -docker compose up -d --build + +docker compose up -d --build # cockpit + mock-cert +docker compose --profile gpu up -d --build # + the live model (GPU host) ``` -Starts two containers from one `psyc:latest` image: - -| Service | Port | Role | +| Service | Exposure | Role | |---|---|---| -| `cockpit` | 8767 | operator UI | -| `mock-cert` | 8770 | stand-in destination receiver (testing) | +| `cockpit` | `psyc.neuronetz.ai` via the proxy (+ `:8767` direct, debug) | operator UI | +| `mock-cert` | internal to `backend` only | stand-in destination receiver | +| `inference` | internal to `backend` only · `gpu` profile | live fine-tuned model | -The sqlite db, sealed packages, and recipient keys persist in the `psyc-data` -named volume — they survive container restarts and rebuilds. +Data (sqlite db, sealed packages, recipient keys, model adapters) is bind-mounted +from `./data` and persists across restarts and rebuilds. ## First run -The schema is created on cockpit startup, but there are no cases until you -ingest. Run inside the container: +The schema is created on cockpit startup; ingest to populate it: ```bash docker compose exec cockpit psyc fetch-all @@ -59,21 +61,18 @@ docker compose exec cockpit psyc classify-all docker compose exec cockpit psyc map-all ``` -Keep it ingesting by scheduling `fetch-all` — a host cron entry calling -`docker compose exec cockpit psyc fetch-all`, e.g. hourly. +Schedule `fetch-all` (host cron → `docker compose exec`) to keep ingesting. ## Updating ```bash git pull -docker compose up -d --build +docker compose --profile gpu up -d --build ``` -The `psyc-data` volume is preserved across updates. - ## Health ```bash -curl http://:8767/healthz # cockpit -curl http://:8770/healthz # mock-cert +curl -H 'Host: psyc.neuronetz.ai' http:///healthz +docker compose ps ``` diff --git a/src/psyc/cockpit/inference.py b/src/psyc/cockpit/inference.py index 0e35eef..e0d993f 100644 --- a/src/psyc/cockpit/inference.py +++ b/src/psyc/cockpit/inference.py @@ -8,6 +8,7 @@ gracefully — if the server is down, callers get None and fall back to rules. from __future__ import annotations import json +import os from typing import Optional import httpx @@ -17,7 +18,7 @@ from psyc.lines.train import SEVERITY_INSTRUCTION, severity_features from psyc.models import Case -INFERENCE_URL = "http://127.0.0.1:8771" +INFERENCE_URL = os.environ.get("PSYC_INFERENCE_URL", "http://127.0.0.1:8771") _log = log.get(__name__) diff --git a/src/psyc/lines/route.py b/src/psyc/lines/route.py index 7ca613c..8e40244 100644 --- a/src/psyc/lines/route.py +++ b/src/psyc/lines/route.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os from typing import Dict, List, Optional, Tuple from pydantic import BaseModel, Field @@ -15,7 +16,7 @@ _log = log.get(__name__) _TLP_RANK: Dict[TLP, int] = {TLP.CLEAR: 0, TLP.GREEN: 1, TLP.AMBER: 2, TLP.RED: 3} -MOCK_CERT_BASE = "http://127.0.0.1:8770" +MOCK_CERT_BASE = os.environ.get("PSYC_MOCK_CERT_URL", "http://127.0.0.1:8770") ENDPOINTS: Dict[str, str] = { "CERT-Bund": f"{MOCK_CERT_BASE}/cert-bund/submit",