# psyc — neuronetz.ai deployment stack. # # docker compose up -d --build # cockpit + mock-cert (no GPU) # docker compose --profile gpu up -d --build # + the live model (needs an NVIDIA GPU) # # 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"] env_file: .env # per-dev API keys (gitignored). cp .env.example .env first. environment: VIRTUAL_HOST: psyc.neuronetz.ai VIRTUAL_PORT: "8767" # Triggers nginxproxy/acme-companion (which must be running alongside # nginx-proxy on the host) to issue + auto-renew a Let's Encrypt cert # for psyc.neuronetz.ai. LETSENCRYPT_EMAIL comes from .env so per-env # configurable — falls back to the default if unset. LETSENCRYPT_HOST: psyc.neuronetz.ai LETSENCRYPT_EMAIL: ${LETSENCRYPT_EMAIL:-admin@neuronetz.ai} PSYC_MOCK_CERT_URL: http://mock-cert:8770 PSYC_SOAR_URL: http://mock-cert:8770 PSYC_INFERENCE_URL: http://inference:8771 PSYC_DOCKER_PROXY: http://docker-socket-proxy:2375 ports: - "8767:8767" # direct/debug access; the proxy serves psyc.neuronetz.ai on :80 volumes: - ./data:/data networks: [backend] restart: unless-stopped 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"] volumes: - ./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 # Read-only Docker daemon proxy. The cockpit's /admin/docker view queries this # over the backend network instead of touching /var/run/docker.sock directly, # so a compromise of the web app can't drive the daemon. Only GET on # containers/networks/ping is enabled — POST/DELETE/EXEC stay blocked. docker-socket-proxy: image: tecnativa/docker-socket-proxy:0.3 environment: CONTAINERS: "1" NETWORKS: "1" PING: "1" INFO: "1" POST: "0" DELETE: "0" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro networks: [backend] restart: unless-stopped # 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-v5/final", "--host", "0.0.0.0", "--port", "8771"] volumes: - ./data:/data - ./scripts:/scripts networks: [backend] restart: unless-stopped profiles: ["gpu"] healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8771/healthz')"] interval: 30s timeout: 5s retries: 3 start_period: 90s deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] networks: # The reverse-proxy + acme-companion need to share a docker network with the # cockpit so they can see each other. The actual network name differs by # environment (e.g. 'backend' in dev, 'neuronetz_default' in production), so # it's overridable via PSYC_PROXY_NETWORK in .env. Default keeps dev working. backend: name: ${PSYC_PROXY_NETWORK:-backend} external: true