Full reasoning behind the Docker setup summarized in ARCHITECTURE.md §7. Not
needed every session — read this when touching docker-compose.yml,
backend/Dockerfile, or frontend/Dockerfile.
Six services: PostgreSQL, Redis, OWASP ZAP sidecar, FastAPI backend, Celery worker, Next.js frontend (standalone Node server, not Nginx). Ollama runs natively on the host, not in Docker.
services:
postgres:
image: postgres:16-alpine
redis:
image: redis:7-alpine
zap:
image: zaproxy/zap-stable:latest # sidecar, not bundled into backend
backend:
build:
context: . # repo ROOT, not ./backend
dockerfile: backend/Dockerfile
worker:
build:
context: .
dockerfile: backend/Dockerfile
command: celery -A tasks.celery_app worker --loglevel=info -c 5
frontend:
build:
context: ./frontend
args:
NEXT_INTERNAL_API_URL: http://backend:8000 # build ARG, not env — see belowThe reference machine already has Ollama installed as a systemd service
with qwen2.5:7b pulled and GPU passthrough working bare-metal — simpler
than configuring nvidia-container-toolkit for an equivalent containerized
setup. Containers reach it via host.docker.internal (mapped through
extra_hosts: ["host.docker.internal:host-gateway"] on backend/worker)
at OLLAMA_URL=http://host.docker.internal:11434.
Required host-level change (security-relevant — widens Ollama's network
exposure, confirm with the user before applying): Ollama's default bind is
127.0.0.1:11434 — loopback only, unreachable from Docker's bridge network
even via host.docker.internal (verified: connect ECONNREFUSED against
the bridge gateway IP). Fixed via systemd override:
sudo systemctl edit ollama
# add under [Service]:
# Environment="OLLAMA_HOST=0.0.0.0:11434"
sudo systemctl daemon-reload && sudo systemctl restart ollamaThis makes Ollama reachable from the local network, not just Docker —
acceptable on a personal/dev workstation, worth a firewall rule (block
11434 from non-Docker sources) on a shared machine. The ollama_data
volume in docker-compose.yml is unused while Ollama stays native —
reserved for a future fully-containerized migration.
backend/config.py has ZAP_URL (empty string = native dev, falls back to
the per-scan-port local-daemon spawn/kill behavior in _run_zap()). When
ZAP_URL is set (Docker: http://zap:8090), _run_zap() skips the local
process entirely and calls zap.core.new_session(name=scan_id) for
per-scan isolation instead — the port-hash isolation scheme doesn't apply
to a daemon shared across concurrent scans. The spider/active-scan/alert
logic in the middle of _run_zap() is unchanged in both branches.
- Base image pinned to
python:3.11-slim-bookworm, not barepython:3.11-slim. The unpinned tag resolves to Debian "trixie" (testing), whereniktoisn't in apt's repos at all (neither trixie nor bookworm has it — Ubuntu-universe-only). Bookworm pin needed regardless for everything elseapt-get installs. niktoinstalled from GitHub source (git clone https://github.com/sullo/nikto.git, symlinked to/usr/local/bin/nikto), same pattern astestssl.sh. Requireslibjson-perlandlibxml-writer-perlfrom apt —webscan.pycalls nikto with-Format json, solibjson-perlis load-bearing.libpango-1.0-0+libpangoft2-1.0-0required from apt for WeasyPrint's PDF rendering. Without them,import weasyprintfails withOSError: cannot load library 'libgobject-2.0-0'.psutilinrequirements.txt— used inwebscan.pyfor ZAP process lifecycle management; was a real omission from the pinned dependency list.- Build context is the repo root, not
./backend.alembic.iniandmigrations/live at the repo root as siblings ofbackend/(permigrations/env.py'ssys.path.insert(..., '..', 'backend')logic) — a context scoped to./backendalone can't see them. Root.dockerignoreexcludesfrontend/,.git/, etc. migrations/env.pyoverridesalembic.ini's staticsqlalchemy.urlwithsettings.DATABASE_URLright afterconfig = context.config— the ini's hardcodedlocalhostURL never resolves to thepostgresDocker service.unzip -o, not plainunzip, for httpx/Naabu/Katana. Those ProjectDiscovery zips bundleLICENSE.md/README.mdalongside the binary; a plainunzipprompts interactively to overwrite once an earlier tool's zip already dropped a same-named file in/tmp, and a non-interactive Docker build has no stdin to answer with (confirmed via an actual failed build).- httpx binary installed AFTER
pip install -r requirements.txt, not alongside amass/Naabu/Katana. The Pythonhttpxpackage (afastapidependency) ships aconsole_scriptsshim also namedhttpxin/usr/local/bin. Installing the binary beforepip installlet pip's shim silently overwrite the ProjectDiscovery CLI (confirmed:httpx -versionreturned the Python client's usage text, not the version banner). Installing afterpip installmakes the binary win. - Initial Alembic migration was hand-written, not autogenerated — no
native PostgreSQL was available earlier in the build to run
alembic revision --autogenerateagainst. One bug caught while writing it: callingscan_status.create(op.get_bind(), checkfirst=True)explicitly and using the same enum object as a column type increate_table()double-creates the type (create_table'sbefore_createDDL event auto-creates it too) → raisesDuplicateObject. Fix: don't call.create()explicitly, let the column handle it.
whois is required — backend/tasks/recon.py shells out to the whois
binary (bounded subprocess); without it WHOIS recon is skipped (logged,
non-fatal).
next.config.mjs's rewrites() is evaluated once at next build time
and baked into .next/standalone's routes-manifest.json — it is not
re-evaluated per-request at container runtime, even with output: 'standalone'. A runtime-only environment: NEXT_INTERNAL_API_URL: http://backend:8000 was silently ignored (confirmed
via connect ECONNREFUSED ::1:8000 in frontend logs despite the runtime
var being set correctly). Fix: pass it as a Docker build ARG (default
matching the compose service name, http://backend:8000) so it's present
while npm run build executes rewrites().
Not a direct host-path bind mount — ~/.config/subfinder/provider-config.yaml
may not exist on whoever's machine runs docker compose up, and Docker
would silently mount an empty directory there instead of a file,
breaking subfinder ungracefully. Instead:
backend/subfinder-config/provider-config.yaml.example is committed
(template); backend/subfinder-config/provider-config.yaml (real keys) is
gitignored and excluded from the Docker build context, reaching the
container only via the runtime bind mount
./backend/subfinder-config/provider-config.yaml:/root/.config/subfinder/provider-config.yaml:ro.
First-time setup copies .example to the real filename.
Same privilege class as nmap's -O OS fingerprint. Naabu's helper detects
permission-denied at runtime and falls back to CONNECT scan (-sT). The
worker container runs unprivileged; add cap_add: [NET_RAW] to worker:
if Naabu SYN performance becomes a bottleneck. Not enabled by default —
NET_RAW broadens the container's kernel-level attack surface.