# soc-ai compose stack. # # Usage: # docker compose up -d # build from source + start # SOC_AI_IMAGE_TAG=1.2.9 docker compose pull soc-ai && docker compose up -d # # run a pinned published GHCR image — never pull bare :latest # # soc-ai talks to a LiteLLM gateway you run separately (most setups put it on a # GPU host). The RAG/reranker engine and its vector store arrive with the v1.1 # "heavy" deployment; they are not part of this stack. # # Before starting: # 1. Copy .env.example → .env and populate all required values. # 2. Generate a self-signed (or CA-signed) cert pair: # mkdir -p ./certs # openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ # -subj "/CN=soc-ai" -keyout ./certs/key.pem -out ./certs/cert.pem # 3. (Optional) Run `soc-ai blocklists refresh` once to seed enrichment data. services: soc-ai: # ── Image: published release (pull) or local build ─────────────────────── # With both `image:` and `build:` declared, plain `docker compose up` still # BUILDS from source when the image isn't in the local store (Compose # prefers `build:` over pulling) — so the existing setup.sh / `up --build` # flow is unchanged. To run the prebuilt release image instead, pull it # explicitly first, PINNED to a release (never bare :latest — an # unaudited moving target): # SOC_AI_IMAGE_TAG=1.2.9 docker compose pull soc-ai && docker compose up -d # (or `./setup.sh --prebuilt`, which pins it for you). image: ghcr.io/nuk3s/soc-ai:${SOC_AI_IMAGE_TAG:-latest} build: context: . dockerfile: Dockerfile args: # Stamps the built image with the commit it came from, so a quality # regression can be attributed to a code change. Empty unless the # builder exports it — `export SOC_AI_COMMIT=$(git rev-parse HEAD)` # before `docker compose build` — and empty is recorded as "unknown", # never guessed. SOC_AI_COMMIT: ${SOC_AI_COMMIT:-} container_name: soc-ai # ── Port ───────────────────────────────────────────────────────────────── # uvicorn terminates TLS on 8443 on the direct path. The caddy service # below puts Caddy in front for the proxy path. # Published on all interfaces by default. Set SOC_AI_BIND=127.0.0.1 to bind # loopback only (e.g. behind a reverse proxy on the same host). NOTE: Docker's # publish rules bypass firewalld — `firewall-cmd --list-ports` won't show this # port; restrict it here (SOC_AI_BIND) or via the DOCKER-USER chain, not the # host firewall zones. ports: - "${SOC_AI_BIND:-0.0.0.0}:${SOC_AI_PORT:-8443}:8443" # ── Environment & secrets ───────────────────────────────────────────────── # .env is bind-mounted read-only so secrets never bake into an image layer. # pydantic-settings reads it from the working directory (/opt/soc-ai). env_file: - path: .env required: true # fail fast if the operator forgot to create it # ── Mounts ─────────────────────────────────────────────────────────────── # NOTE: mounted files must be READABLE BY UID 1000 (the in-container soc-ai # user). `openssl` writes the key 0600 owned by you — `chmod 644 ./certs/*`. # The `,Z` suffix relabels each bind mount for the container on SELinux hosts # (Fedora/RHEL/Rocky/podman); without it the container gets EACCES even at # mode 644. On non-SELinux hosts (Ubuntu/Debian) `,Z` is a harmless no-op, so # it ships on by default. volumes: # TLS certificate — read-only; generate once, mount permanently. # Generate with: openssl req -x509 -newkey rsa:2048 -nodes -days 365 \ # -subj "/CN=soc-ai" -keyout ./certs/key.pem -out ./certs/cert.pem - ./certs/cert.pem:/etc/soc-ai/cert.pem:ro,Z # TLS private key — read-only; readable by uid 1000 (see note above). - ./certs/key.pem:/etc/soc-ai/key.pem:ro,Z # PCAP fetch SSH key (OPTIONAL — only for t_get_pcap). Uncomment + provide # the de-privileged sensor key, then set SO_SSH_KEY=/etc/soc-ai/so_pcap in # .env. Read-only; readable by uid 1000. # - ./certs/so_pcap:/etc/soc-ai/so_pcap:ro,Z # SQLite database + session store. Named volume so data survives # container replacement (image updates, `docker compose down`). # Maps to SOC_AI_DATA_DIR=/var/lib/soc-ai/data (set in .env or default). - soc_ai_data:/var/lib/soc-ai/data # Blocklist cache (URLhaus, Feodo, Tor, etc.) — optional but recommended. # Seed with: docker compose run --rm soc-ai soc-ai blocklists refresh - soc_ai_blocklists:/var/lib/soc-ai/blocklists # MaxMind GeoLite2 .mmdb files. Optional. No command downloads them. Copy # GeoLite2-City.mmdb and GeoLite2-ASN.mmdb into this volume by hand, then # restart. See docs/BLOCKLISTS.md, "MaxMind GeoIP". - soc_ai_maxmind:/var/lib/soc-ai/maxmind # Cloud-provider prefix JSON (AWS/GCP/Azure/Cloudflare) — optional. # Seed with: docker compose run --rm soc-ai soc-ai blocklists refresh - soc_ai_cloud_prefixes:/var/lib/soc-ai/cloud_prefixes # Nightly quality-eval artifacts: the per-alert bundles and oracle # critiques behind every point on the dashboard's Quality card. NOT # optional if you want the regression alarm to be actionable — the # critiques are the only evidence for or against an alarm, and without # this volume they live in the container filesystem, so every recreate # (image update, `docker compose up -d`) deletes them. - soc_ai_evals:/var/lib/soc-ai/evals # ── Liveness check ─────────────────────────────────────────────────────── # Mirrors the HEALTHCHECK in the Dockerfile. It tries HTTPS, then HTTP, so # it passes on the direct path and behind the caddy service. # # LIVENESS, not health: a green container here means this process answered, # nothing more. It probes no dependency and must not, or an outage upstream # would become a restart loop down here. For the verdict, run # `docker compose exec soc-ai python -m soc_ai doctor`. healthcheck: test: ["CMD-SHELL", "curl -fsk https://127.0.0.1:8443/healthz || curl -fs http://127.0.0.1:8443/healthz"] interval: 30s timeout: 10s start_period: 60s retries: 3 restart: unless-stopped # ── Log rotation ────────────────────────────────────────────────────────── # The always-on triage loop + the 30s healthcheck curl write to the default # json-file driver, which grows UNBOUNDED without a cap — the failure mode # most likely to fill the disk over months. Cap at 5×50m (250m ceiling). logging: driver: json-file options: max-size: "50m" max-file: "5" # ── Hardening ───────────────────────────────────────────────────────────── # The app runs as non-root (UID 1000) and binds 8443 (>1024), so it needs no # Linux capabilities: drop them all and block privilege escalation (setuid). security_opt: - no-new-privileges:true cap_drop: - ALL # ── Resource hints (adjust to your hardware) ────────────────────────────── # The app is primarily I/O-bound (ES queries + LLM calls); these are soft # limits that prevent a runaway investigation loop from starving the host. deploy: resources: limits: memory: 1G reservations: memory: 256M caddy: # ── The proxy path: Caddy in front of soc-ai ───────────────────────────── # Caddy terminates TLS and renews its certificate. soc-ai serves plain HTTP # inside the compose network. The service sits under the "proxy" profile, # so a plain `docker compose up -d` does not start it. COMPOSE_PROFILES=proxy # in .env turns the profile on for every compose command, so a later # `docker compose up -d` keeps Caddy. `scripts/tls-proxy.sh enable ` # writes that line and the other proxy settings. See docs/DOCKER.md, TLS paths. profiles: ["proxy"] # Digest-pinned, the same rule as the Dockerfile base images. Tag: 2.11.4. image: caddy:2.11.4@sha256:0c994536bddb66445885237f1a5dcc1916bccea922661c76b4e9fc24061f9b52 container_name: soc-ai-caddy restart: unless-stopped depends_on: - soc-ai # Two variables from the project .env, and none of soc-ai's secrets. Both # use the default form: compose interpolates the whole file before it # filters profiles, so a required form would break the direct path. # SOC_AI_DOMAIN names the site. SOC_AI_CADDY_TLS holds the tls directive: # empty for automatic HTTPS from Let's Encrypt, "tls internal" for Caddy's # own CA, or "tls /certs/proxy-cert.pem /certs/proxy-key.pem" for your files. environment: SOC_AI_DOMAIN: "${SOC_AI_DOMAIN:-}" SOC_AI_CADDY_TLS: "${SOC_AI_CADDY_TLS:-}" ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro,Z # Your certificate and key, for SOC_AI_CADDY_TLS="tls /certs/proxy-cert.pem # /certs/proxy-key.pem". The script copies them in. ./certs exists before # the first start, or Docker creates it as root. - ./certs:/certs:ro,Z - caddy_data:/data - caddy_config:/config security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: "20m" max-file: "3" postgres: # ── The PostgreSQL store (optional) ────────────────────────────────────── # soc-ai keeps its store in SQLite by default. SQLite has one writer, and # at corporate scale the scheduler, the sweeps and the console contend for # it. This service sits under the "postgres" profile, so a plain # `docker compose up -d` does not start it. COMPOSE_PROFILES=postgres in # .env turns the profile on for every compose command. setup.sh asks, then # writes that line, SOC_AI_POSTGRES_PASSWORD and SOC_AI_DATABASE_URL. # soc-ai waits up to 60 s at start for the server to accept connections. # See docs/DOCKER.md, PostgreSQL. profiles: ["postgres"] # Digest-pinned, the same rule as the Dockerfile base images. Tag: 17.6. image: postgres:17.6@sha256:00bc86618629af00d2937fdc5a5d63db3ff8450acf52f0636ec813c7f4902929 container_name: soc-ai-postgres restart: unless-stopped # The default form, as for caddy: compose interpolates the whole file # before it filters profiles. An empty password stops the server at its # first start, so the profile cannot run without one. environment: POSTGRES_USER: soc_ai POSTGRES_DB: soc_ai POSTGRES_PASSWORD: "${SOC_AI_POSTGRES_PASSWORD:-}" # No published port. soc-ai reaches the server on the compose network as # postgres:5432, and nothing else needs to. volumes: - soc_ai_postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U soc_ai -d soc_ai"] interval: 10s timeout: 5s start_period: 30s retries: 6 security_opt: - no-new-privileges:true logging: driver: json-file options: max-size: "20m" max-file: "3" # ── Named volumes ───────────────────────────────────────────────────────────── volumes: # Persistent application data (SQLite DB + session store). soc_ai_data: # Enrichment caches — safe to recreate; re-seed with `soc-ai blocklists refresh`. soc_ai_blocklists: soc_ai_cloud_prefixes: # GeoLite2 .mmdb files. Safe to recreate. Copy them back in by hand. # See docs/BLOCKLISTS.md, "MaxMind GeoIP". soc_ai_maxmind: # Nightly quality-eval artifacts — evidence, not cache: nothing regenerates # a deleted bundle, and the trend row that points at it outlives it by # ~3 months. soc_ai_evals: # The PostgreSQL store, when the "postgres" profile is on. This is the data # itself: back it up with pg_dump, and never recreate it to fix a start. soc_ai_postgres: # Caddy keeps its certificates and its CA here. Keep it across restarts. caddy_data: # Caddy's own runtime config. Safe to recreate. caddy_config: