Files
argus-installer/docker-compose.yml
T
root 0e677d5257 Pin update.sh to a specific version, never :latest (ADR-0051)
:latest is three independently-mutable registry tags with no cross-image
transaction -- update.sh pulling it directly could observe a torn set
mid-promotion (api already retagged, ui/proxy not yet). latest-version.json
is a single, atomically-committed pointer that argus-appliance's release
pipeline only ever writes AFTER every image it names has finished
promoting, so resolving it first cannot observe a partial release: by the
time it names a version, that version is already guaranteed complete.

docker-compose.yml: image tags now ${ARGUS_VERSION:-latest} -- default
unchanged for manual/non-update.sh use (plain `docker compose pull` with
no env var set behaves exactly as before).

update.sh: resolves latest-version.json first (fails loud and untouched if
unreachable/malformed, never falls back to :latest), exports ARGUS_VERSION
for its own docker compose calls only (not written to .env), and -- new --
verifies every service's actual running image tag matches the target
version before declaring success, not just that the API container itself
reports healthy. A torn pull (network hiccup, disk full on one image) is
now a loud failure instead of a silently-reported success.
2026-08-07 12:56:59 +00:00

263 lines
9.1 KiB
YAML

services:
# TimescaleDB (PostgreSQL with time-series optimization)
db:
image: timescale/timescaledb:latest-pg17
container_name: argus-db
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
environment:
TZ: ${TZ:-UTC}
# Internal database credentials, not customer-configurable — nothing
# outside this compose stack ever connects to Postgres directly, so
# there's no real reason to let these vary. Only the password (below)
# needs to be a real secret; the username/db name are just labels.
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: argus
command: postgres -c timezone=${TZ:-UTC} -c log_timezone=${TZ:-UTC} -c shared_preload_libraries=timescaledb -c timescaledb.telemetry_level=off -c max_locks_per_transaction=512
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
# Valkey (Redis-compatible) for caching
valkey:
image: valkey/valkey:9-alpine
container_name: argus-valkey
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
environment:
TZ: ${TZ:-UTC}
command: >
valkey-server
--maxmemory 256mb
--maxmemory-policy allkeys-lru
--appendonly yes
--appendfsync everysec
volumes:
- valkey_data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# Restricts the api service's Docker Engine API access to exactly what it
# needs (list/inspect/exec/restart existing containers for log collection
# and nginx reload) — it cannot create new containers or touch the host
# beyond that. See docker-socket-proxy/README.md for the exact allowed set.
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:latest
container_name: argus-docker-socket-proxy
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
environment:
CONTAINERS: 1
IMAGES: 1
VOLUMES: 1
SYSTEM: 1
EXEC: 1
VERSION: 1
POST: 1
# Restart (stop/restart/kill on an *existing* container) only --
# create/prune stay denied by the haproxy template's own explicit
# deny-list regardless of POST, and start/stop stay off (ALLOW_START/
# ALLOW_STOP unset) since nothing needs them yet. This comment block's
# own claim of "restart" capability (above) was aspirational until
# this line existed -- appliance#43's Diagnostics recovery actions are
# the first real caller.
ALLOW_RESTARTS: 1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./docker-socket-proxy/haproxy.cfg.template:/usr/local/etc/haproxy/haproxy.cfg.template:ro
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Go API Server
api:
# ARGUS_VERSION is set by update.sh before `docker compose pull`, pinned
# to a specific already-fully-promoted version tag rather than the
# floating :latest (ADR-0051 -- argus-appliance repo). Manual/non-
# update.sh use (plain `docker compose pull`, no env var set) is
# unaffected: the default is :latest, identical to before this change.
image: git-cloud.weboria.eu/weboria/argus-api:${ARGUS_VERSION:-latest}
container_name: argus-api
restart: unless-stopped
# Container hardening: no-new-privileges blocks setuid privilege
# escalation; cap_drop ALL removes every Linux capability, re-adding only
# what's needed (DAC_OVERRIDE for the shared /etc/nginx volume owned by
# the nginx user, NET_BIND_SERVICE for the DNS Security Engine's :53).
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
cap_add:
- DAC_OVERRIDE
- NET_BIND_SERVICE
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
TZ: ${TZ:-UTC}
PORT: "8080"
# DATABASE_URL is not set here — the API builds it internally from
# DB_PASSWORD using net/url (proper percent-encoding), instead of this
# file naively interpolating the password into a raw connection
# string. A raw '${DB_PASSWORD}@...' interpolation breaks outright if
# the password ever contains '@', ':', '/', or similar (confirmed
# live: an openssl-rand-base64-generated password containing '/'
# produced an unparseable URL and the API never became healthy).
DB_PASSWORD: ${DB_PASSWORD:-postgres}
REDIS_URL: redis://valkey:6379/0
ENVIRONMENT: ${ENVIRONMENT:-production}
NGINX_CONTAINER: argus-proxy
NGINX_SKIP_TEST: "false"
NGINX_STATUS_URL: "http://host.docker.internal:${NGINX_HTTP_PORT:-80}/nginx_status"
NGINX_ACCESS_LOG: "/etc/nginx/logs/access_raw.log"
BACKUP_PATH: ${BACKUP_PATH:-/app/data/backups}
DOCKER_API_VERSION: ${DOCKER_API_VERSION:-}
DOCKER_HOST: tcp://docker-socket-proxy:2375
NGINX_HTTP_PORT: ${NGINX_HTTP_PORT:-}
NGINX_HTTPS_PORT: ${NGINX_HTTPS_PORT:-}
API_HOST_PORT: ${API_HOST_PORT:-9080}
API_HOST: ${API_HOST:-}
# ANIS community intelligence — on by default against the public
# Community Edition hub (community tier, no signup required). There is
# no self-hosted-ANIS deployment model: the URL is fixed, not
# customer-configurable. A license key upgrades the tier, but is only
# ever set from Threat Intel -> ANIS Connection in the dashboard
# (write-only there — never round-trips back to the client), not from
# this file, so it's seeded empty (community tier) here.
# (No ANIS_ADMIN_KEY or ANIS_SHARE_ATTACKERS here: both confirmed dead
# via grep, zero references anywhere in internal/config.go or the ANIS
# client. A prior version of this file carried ANIS_ADMIN_KEY and
# falsely claimed ARGUS "fails closed at startup" without it.)
ANIS_ENABLED: ${ANIS_ENABLED:-true}
ANIS_URL: https://anis.weboria.eu
ANIS_LICENSE_KEY: ""
DNS_SECURITY_LISTEN_ADDR: ":53"
ports:
- "127.0.0.1:${API_HOST_PORT:-9080}:8080"
# DNS Security Engine. Host-only by default — a bare forwarding
# resolver reachable from the network is exactly the profile abused
# for DNS amplification attacks against third parties. Set
# DNS_LISTEN_HOST to an internal interface IP to route real client
# DNS traffic through it (never 0.0.0.0 on an untrusted network).
- "${DNS_LISTEN_HOST:-127.0.0.1}:53:53/udp"
- "${DNS_LISTEN_HOST:-127.0.0.1}:53:53/tcp"
volumes:
- nginx_data:/etc/nginx:rw
- api_data:/app/data:rw
depends_on:
db:
condition: service_healthy
valkey:
condition: service_started
docker-socket-proxy:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 360s
# React UI (Admin Panel), served over HTTPS
ui:
# See the api service's own comment above -- same ADR-0051 pinning.
image: git-cloud.weboria.eu/weboria/argus-ui:${ARGUS_VERSION:-latest}
container_name: argus-ui
restart: unless-stopped
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
environment:
TZ: ${TZ:-UTC}
ports:
- "${UI_PORT:-81}:443"
volumes:
- ui_data:/app/ssl:rw
depends_on:
- api
# Nginx reverse proxy — host network mode, so real client IPs are visible
# directly without needing PROXY protocol.
nginx:
# See the api service's own comment above -- same ADR-0051 pinning.
image: git-cloud.weboria.eu/weboria/argus-proxy:${ARGUS_VERSION:-latest}
container_name: argus-proxy
restart: always
network_mode: host
# Not cap_drop'd: runs nginx in host-network mode with a root master that
# drops workers to the nginx user, needing CHOWN/SETUID/SETGID plus binds
# on privileged 80/443.
security_opt:
- "no-new-privileges:true"
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
environment:
TZ: ${TZ:-UTC}
ulimits:
nofile:
soft: 65535
hard: 65535
volumes:
- nginx_data:/etc/nginx:rw
depends_on:
- api
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:${NGINX_HTTP_PORT:-80}/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s
volumes:
postgres_data:
name: argus_postgres_data
valkey_data:
name: argus_valkey_data
nginx_data:
name: argus_nginx_data
api_data:
name: argus_api_data
ui_data:
name: argus_ui_data
networks:
default:
name: argus-network
external: true