docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# 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 <domain>`
# 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: