feat(distribution): production install/update/backup scripts + public-repo sync (#106)
Builds the customer-facing distribution: a curated distribution/ directory containing only what an end customer needs (production docker-compose.yml with no build: sections, install.sh/update.sh/uninstall.sh/healthcheck.sh/ backup.sh/restore.sh, a customer-facing CHANGELOG.md, and README/LICENSE/ NOTICE/THIRD_PARTY_LICENSES) — pushed as the initial content of the new public alleyviper/argus repo. install.sh: detects OS, validates Docker/Compose, fetches all deployment files, generates a secure DB password, creates the required Docker network, pulls images, starts the stack, waits for health, and rotates the default admin/admin credentials via the auth API — printing the generated password once at the end. update.sh/healthcheck.sh/backup.sh/restore.sh/uninstall.sh formalize what was previously ad-hoc README snippets into real, safe-by-default scripts (uninstall.sh keeps data unless --remove-data is explicitly passed and confirmed; restore.sh requires typed confirmation and documents that it targets a fresh install, not a live merge). Added a sync-distribution job to release.yml: after a successful release, mirrors distribution/ into the public repo and creates a matching (customer-facing, commit-log-free) release marker there. Needs a one-time setup step — a fine-grained PAT scoped to alleyviper/argus added as the ARGUS_PUBLIC_REPO_TOKEN secret — the job cleanly no-ops until that's added. Not yet done: GHCR package visibility (argus-secure-api/-ui/-nginx) is still private, which blocks a genuine end-to-end curl-install test from a clean, unauthenticated environment — deferred at the user's request until they flip it manually (GitHub does not expose this via API).
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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}
|
||||
POSTGRES_USER: ${DB_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||||
POSTGRES_DB: ${DB_NAME:-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 ${DB_USER:-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
|
||||
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:
|
||||
image: ghcr.io/alleyviper/argus-secure-api: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: postgres://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-argus}?sslmode=disable
|
||||
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 — optional, off by default. Set these to
|
||||
# connect this instance to an already-running ANIS hub.
|
||||
ANIS_ENABLED: ${ANIS_ENABLED:-false}
|
||||
ANIS_URL: ${ANIS_URL:-}
|
||||
ANIS_LICENSE_KEY: ${ANIS_LICENSE_KEY:-}
|
||||
ANIS_ADMIN_KEY: ${ANIS_ADMIN_KEY:-}
|
||||
ANIS_SHARE_ATTACKERS: ${ANIS_SHARE_ATTACKERS:-false}
|
||||
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:
|
||||
image: ghcr.io/alleyviper/argus-secure-ui: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:
|
||||
image: ghcr.io/alleyviper/argus-secure-nginx: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
|
||||
Reference in New Issue
Block a user