Ctrl K
ComunidadGuías…

Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# syntax=docker/dockerfile:1
# ─────────────────────────────────────────────────────────────────────────────
# soc-ai — multi-stage build
#
# Stage 1 (builder):  installs uv, creates a venv, and syncs production deps.
# Stage 2 (frontend): builds the React SPA bundle (frontend/dist).
# Stage 3 (runtime):  copies the venv + source + SPA into a slim Python 3.12
#                     image and runs as a non-root user.
#
# Nothing secret (no .env, no certs, no data) is baked into the image.
# All mutable state is injected at runtime via bind-mounts and volumes:
#   - /opt/soc-ai/.env          (read-only bind from the host)
#   - /etc/soc-ai/cert.pem      (read-only bind)
#   - /etc/soc-ai/key.pem       (read-only bind)
#   - /var/lib/soc-ai/data      (named volume — SQLite DB lives here)
#   - /var/lib/soc-ai/blocklists  (named volume — optional, survives restarts)
#   - /var/lib/soc-ai/maxmind     (named volume — optional GeoIP .mmdb files)
#   - /var/lib/soc-ai/cloud_prefixes (named volume — optional cloud prefix JSON)
#   - /var/lib/soc-ai/evals     (named volume — nightly quality-eval artifacts)
# The /var/lib/soc-ai/* directories are created HERE, owned by uid 1000, so a
# named volume mounted over one inherits that ownership; a volume mounted at a
# path the image never created would be root-owned and unwritable.
# ─────────────────────────────────────────────────────────────────────────────

# ── Stage 1: dependency build ─────────────────────────────────────────────────
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 AS builder
# ^ pinned by digest (supply-chain hardening, mirrors the SHA-pinned GitHub
#   Actions in release.yml); tag above is for humans only — bump both
#   together when refreshing. Resolve a new digest with:
#     docker buildx imagetools inspect ghcr.io/astral-sh/uv:python3.12-bookworm-slim

WORKDIR /build

# Copy only the files uv needs to resolve and lock deps.
# uv sync --no-install-project means it installs deps but not the project
# package itself; we copy the full source in the runtime stage.
COPY pyproject.toml uv.lock ./

# Sync production deps (no dev extras) into /build/.venv.
# --frozen: honour the lock file exactly (no re-resolution).
# --no-install-project: don't attempt to install the not-yet-copied source.
# --no-dev: skip [dependency-groups.dev].
# --extra postgres: asyncpg, for a store in PostgreSQL (SOC_AI_DATABASE_URL and
# the compose "postgres" profile). SQLite stays the default and needs nothing.
# --extra ml: scikit-learn and numpy, for the tier 3 estate model
# (estate_model_enabled, off by default). About 220 MB of site-packages on
# 2026-10-04. The app imports them only when the estate model runs.
RUN uv sync --frozen --no-install-project --no-dev --extra postgres --extra ml


# ── Stage 2: frontend build (React SPA → /fe/dist) ────────────────────────────
# Built here and copied into the runtime image at /opt/soc-ai/frontend/dist —
# where main.py's FRONTEND_DIST resolves, so FastAPI serves the SPA at /app.
FROM node:22-bookworm-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3 AS frontend
# ^ digest-pinned (see the builder stage's FROM comment above); tag: 22-bookworm-slim.

WORKDIR /fe

# Install against the lockfile first (cached unless package*.json changes).
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci

# Build the production bundle (tsc -b && vite build → /fe/dist).
COPY frontend/ ./
RUN npm run build


# ── Stage 3: runtime ─────────────────────────────────────────────────────────
FROM python:3.12-slim@sha256:a0f57064cbdcd6285fa45f51237ab0b35ea8ebaed06a1f5973e89e19dfb86704 AS runtime
# ^ digest-pinned (see the builder stage's FROM comment above); tag: 3.12-slim.

# ── OS-level deps ─────────────────────────────────────────────────────────────
# curl: HEALTHCHECK; ca-certificates: trust chain for outbound HTTPS;
# openssh-client: the PCAP tool (soc_ai/tools/get_pcap.py) shells out to `ssh` to
# run tcpdump on the Security Onion sensor — without it, t_get_pcap fails with
# "ssh: not found" (the host having ssh masked this on non-container installs).
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates openssh-client \
    && rm -rf /var/lib/apt/lists/*

# ── Non-root user ─────────────────────────────────────────────────────────────
RUN groupadd --gid 1000 soc-ai \
    && useradd --uid 1000 --gid soc-ai --no-create-home --shell /sbin/nologin soc-ai

# ── Directory layout ──────────────────────────────────────────────────────────
# /opt/soc-ai          — application source
# /var/lib/soc-ai/...  — runtime-writable data (mounted as volumes)
# /etc/soc-ai          — TLS cert + key (mounted read-only by the host)
RUN mkdir -p \
        /opt/soc-ai \
        /var/lib/soc-ai/data \
        /var/lib/soc-ai/blocklists \
        /var/lib/soc-ai/maxmind \
        /var/lib/soc-ai/cloud_prefixes \
        /var/lib/soc-ai/evals \
        /etc/soc-ai \
    && chown -R soc-ai:soc-ai \
        /opt/soc-ai \
        /var/lib/soc-ai \
        /etc/soc-ai

# ── Copy venv from builder ─────────────────────────────────────────────────────
COPY --from=builder --chown=soc-ai:soc-ai /build/.venv /opt/soc-ai/.venv

# ── Copy application source ────────────────────────────────────────────────────
# soc_ai/ package (contains migrations in soc_ai/store/migrations/ and the
# CLI-only soc_ai/store/alembic.ini — the app runs migrations programmatically
# from Path(__file__), so the ini is only for the `alembic` CLI:
#   alembic -c soc_ai/store/alembic.ini upgrade head
# pyproject.toml: read by some tooling; harmless to include.
COPY --chown=soc-ai:soc-ai soc_ai/       /opt/soc-ai/soc_ai/
COPY --chown=soc-ai:soc-ai pyproject.toml /opt/soc-ai/pyproject.toml

# runbooks/: the shipped starter-pack markdown (runbooks/starter-pack/*.md).
# POST /runbooks/starter-pack resolves it as parent-of-package (soc_ai/store/
# runbook_pack.py:STARTER_PACK_DIR → /opt/soc-ai/runbooks) — same layout trick
# as the frontend bundle below. Without this COPY the endpoint 404s honestly.
COPY --chown=soc-ai:soc-ai runbooks/     /opt/soc-ai/runbooks/

# docs/: not documentation for the operator, prompt input. The agent's system
# prompts are assembled from docs/OQL_PRIMER.md and docs/OQL_HUNT_EXAMPLES.md,
# resolved as parent-of-package (soc_ai/agent/prompts.py:PROMPT_ASSETS →
# /opt/soc-ai/docs), the same layout trick as runbooks/ above. Without this COPY
# the container starts and answers, but every investigator, hunt and chat prompt
# carries a stub saying the query language is unavailable, which is what shipped
# until 2026-09-07. Copied WHOLESALE, like soc_ai/ and runbooks/: naming the two
# files individually is exactly the mistake that produced the defect, because the
# next asset added under docs/ would silently not ship. The heavy published-only
# subtrees (docs/img, docs/superpowers) are excluded in .dockerignore, and
# tests/test_image_layout.py fails if any declared asset stops resolving here.
COPY --chown=soc-ai:soc-ai docs/         /opt/soc-ai/docs/

# Demo packaging (inert in normal deployments — the default CMD below never
# touches it). mock_es.py + its demo_dataset import are the bundled loopback
# Elasticsearch/LLM stand-in that docker/demo-entrypoint.sh starts on
# 127.0.0.1:9200; ONLY these two files are un-ignored in .dockerignore — the
# rest of scripts/ never ships in the image. The entrypoint itself is selected
# explicitly by docker-compose.demo.yml / render.yaml.
COPY --chown=soc-ai:soc-ai scripts/demo/mock_es.py scripts/demo/demo_dataset.py \
     /opt/soc-ai/scripts/demo/
COPY --chmod=0755 --chown=soc-ai:soc-ai docker/demo-entrypoint.sh \
     /opt/soc-ai/docker/demo-entrypoint.sh

# ── Copy the built React SPA ───────────────────────────────────────────────────
# FastAPI serves this at /app (mounted only when the dir exists). Without it the
# app still boots and the JSON API works, but /app 404s.
COPY --from=frontend --chown=soc-ai:soc-ai /fe/dist /opt/soc-ai/frontend/dist

WORKDIR /opt/soc-ai

# ── Build stamp ───────────────────────────────────────────────────────────────
# The commit this image was built from, baked in so the running process can name
# it. It cannot work this out for itself: .dockerignore excludes .git, and every
# build of one release carries the same version string while the deployments run
# the image as :latest — so without this, "quality dropped after Tuesday" has
# nothing to attribute itself to. Empty when the builder did not pass one, which
# soc_ai/__init__.py reads back as None rather than as a guess.
ARG SOC_AI_COMMIT=""

# ── Environment ───────────────────────────────────────────────────────────────
# PATH: venv bin dir takes precedence over system Python.
# PYTHONPATH: makes soc_ai importable without an editable install — hatchling
#   (the build backend) is not a runtime dep, so we skip pip install entirely.
# pydantic-settings reads .env relative to cwd (/opt/soc-ai) — WORKDIR above.
# TLS + data-dir defaults match the compose volume mount paths; all can be
# overridden in .env without rebuilding the image.
ENV SOC_AI_COMMIT="${SOC_AI_COMMIT}" \
    PATH="/opt/soc-ai/.venv/bin:$PATH" \
    PYTHONPATH="/opt/soc-ai" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    SOC_AI_HOST="0.0.0.0" \
    SOC_AI_PORT="8443" \
    SOC_AI_TLS_CERT="/etc/soc-ai/cert.pem" \
    SOC_AI_TLS_KEY="/etc/soc-ai/key.pem" \
    SOC_AI_DATA_DIR="/var/lib/soc-ai/data" \
    BLOCKLIST_DATA_DIR="/var/lib/soc-ai/blocklists" \
    MAXMIND_DATA_DIR="/var/lib/soc-ai/maxmind" \
    CLOUD_PREFIX_DATA_DIR="/var/lib/soc-ai/cloud_prefixes" \
    HOME="/var/lib/soc-ai"

# Console entry point. Stage 1's `uv sync --no-install-project` installs deps but
# NOT the project, so pyproject's [project.scripts] `soc-ai` is never generated in
# the venv — yet the docs and operator muscle-memory expect `soc-ai <cmd>` (backup,
# restore, blocklists refresh, audit verify, doctor) to work in-container (e.g.
# `docker exec soc-ai soc-ai backup`). Ship a tiny wrapper on PATH that runs the
# CLI through the venv python; PYTHONPATH (set above) makes soc_ai importable.
RUN printf '#!/opt/soc-ai/.venv/bin/python\nimport sys\nfrom soc_ai.cli import main\nsys.exit(main())\n' \
      > /usr/local/bin/soc-ai \
    && chmod 0755 /usr/local/bin/soc-ai

USER soc-ai

EXPOSE 8443

# ── Liveness check ────────────────────────────────────────────────────────────
# This is a LIVENESS probe, and the word `healthy` that `docker ps` prints from
# it means only that this process answered. /healthz probes no dependency: it
# stays green with an unreachable grid, a dead model gateway and no analyst
# model at all, and it has to, because a probe that failed on a dependency
# outage would have Docker restart a container whose dependencies are merely
# down. The health verdict is `GET /api/v1/health` (live per-dependency probes)
# or `docker compose exec soc-ai python -m soc_ai doctor`; the response body
# says so too, so it reads correctly in `docker inspect`'s health log.
#
# /healthz answers over HTTPS on the direct path and over HTTP on the proxy
# path. The check tries HTTPS first, with -k to skip verify, then HTTP.
# Interval is generous (30s) so a slow cold-start (DB migration + bootstrap)
# doesn't flip the container unhealthy before the server is ready.
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    CMD curl -fsk https://127.0.0.1:8443/healthz || curl -fs http://127.0.0.1:8443/healthz || exit 1

# ── Default command ───────────────────────────────────────────────────────────
# Invoked as `python -m uvicorn`, NOT the `uvicorn` console script: the venv is
# built at /build/.venv (Stage 1) and copied to /opt/soc-ai/.venv, so the
# script's baked-in shebang (#!/build/.venv/bin/python) is dead — but the venv
# python + the uvicorn module are both fine. Cert/key paths come from ENV above.
# soc-ai applies its own forwarded-header trust rule (PROXY_TRUSTED_IPS).
CMD ["sh", "-c", \
     "exec python -m uvicorn soc_ai.main:app \
        --host 0.0.0.0 \
        --port 8443 \
        --no-proxy-headers \
        --ssl-certfile \"${SOC_AI_TLS_CERT}\" \
        --ssl-keyfile \"${SOC_AI_TLS_KEY}\" \
        --ssl-ciphers 'ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM'"]