compose.yaml
Rama main · réplica de lectura del repositorio original.
name: rootguard
services:
# The one, narrow, auditable bridge letting cosign's own attestation
# checks (see core's/updater's ROOTGUARD_ATTESTATION_PROXY_URL below)
# reach GHCR/Sigstore from the fully internet-isolated `control`
# network - see compose.release.yaml's identical service for the full
# rationale. Wired in here too (not just the release compose file):
# found live that a local run overriding ROOTGUARD_*_IMAGE to real,
# signed ghcr.io digests (rather than ROOTGUARD_SKIP_ATTESTATION) hits
# the exact same isolated-network failure.
attestation-proxy:
build:
context: ./rootguard-attestation-proxy
image: "${ROOTGUARD_ATTESTATION_PROXY_IMAGE:-rootguard-attestation-proxy:dev}"
container_name: rootguard-attestation-proxy
restart: unless-stopped
read_only: true
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
networks:
- control
- egress
healthcheck:
test: ["CMD", "/rootguard-attestation-proxy", "healthcheck"]
interval: 15s
timeout: 3s
retries: 5
core:
build:
context: ./rootguard-core
image: "${ROOTGUARD_CORE_IMAGE:-rootguard-core:dev}"
container_name: rootguard-core
restart: unless-stopped
depends_on:
updater:
condition: service_healthy
attestation-proxy:
condition: service_healthy
environment:
PORT: "8081"
# See rootguard-core/internal/stack/attestation.go's
# runAttestationCommand doc comment - scoped to cosign's own
# subprocess only, never Core's ambient environment. Only actually
# exercised in the "real signed images" local-run mode (see
# ROOTGUARD_SKIP_ATTESTATION's own comment below) - harmless to
# leave set otherwise, since attestation is skipped entirely then.
ROOTGUARD_ATTESTATION_PROXY_URL: http://attestation-proxy:8888
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_CORE_CONTAINER: rootguard-core
# Found live-testing this exact file (2026-08-29): a locally-built
# dev image is never cosign-attested, so the first guided-setup
# deploy always hit the attestation gate (installer.Manager, see
# docs/security-audit-log.md) with no way to opt out - compose.release.yaml
# already forwards this, compose.yaml never did. Test-only escape
# hatch - see cmd/rootguard/main.go's own doc comment on
# ROOTGUARD_SKIP_ATTESTATION for what it disables and why; leave
# unset for a normal local run against real signed images.
ROOTGUARD_SKIP_ATTESTATION: "${ROOTGUARD_SKIP_ATTESTATION:-false}"
ROOTGUARD_INSTALLATION_DIR: /var/lib/rootguard/installation
ROOTGUARD_UNBOUND_IMAGE: "${ROOTGUARD_UNBOUND_IMAGE:-ghcr.io/foxly-it/rootguard-unbound:latest}"
ROOTGUARD_ADGUARD_IMAGE: "${ROOTGUARD_ADGUARD_IMAGE:-adguard/adguardhome:v0.107.80}"
ROOTGUARD_ADGUARD_BETA_IMAGE: "${ROOTGUARD_ADGUARD_BETA_IMAGE:-adguard/adguardhome:beta}"
ROOTGUARD_ADGUARD_UPDATE_IMAGE: "${ROOTGUARD_ADGUARD_UPDATE_IMAGE:-adguard/adguardhome:latest}"
ROOTGUARD_UNBOUND_UPDATE_IMAGE: "${ROOTGUARD_UNBOUND_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-unbound:latest}"
ROOTGUARD_BLOCKPAGE_IMAGE: "${ROOTGUARD_BLOCKPAGE_IMAGE:-ghcr.io/foxly-it/rootguard-blockpage:latest}"
ROOTGUARD_UPDATE_DIR: /var/lib/rootguard/updates
ROOTGUARD_CONTROL_PLANE_UPDATER_URL: http://updater:8082
ROOTGUARD_CONTROL_PLANE_UPDATER_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_UPDATER_UPDATE_IMAGE: "${ROOTGUARD_UPDATER_UPDATE_IMAGE:-ghcr.io/foxly-it/rootguard-updater:latest}"
ROOTGUARD_ATTESTATION_PROXY_UPDATE_IMAGE: "${ROOTGUARD_ATTESTATION_PROXY_UPDATE_IMAGE:-rootguard-attestation-proxy:dev}"
ROOTGUARD_SELF_UPDATE_DIR: /var/lib/rootguard/updater-self-update
ROOTGUARD_COMPOSE_FILE: /opt/rootguard/compose.yaml
ROOTGUARD_COMPOSE_PROJECT: rootguard
# Core never reads these four - they exist only so `docker compose -f
# compose.yaml ... up -d --no-deps updater` (used to replace the
# updater's own container, see ROOTGUARD_SELF_UPDATE_DIR above), and
# one hop further `... up -d --no-deps core webapp`, can interpolate
# the whole compose.yaml file, which also references them for the
# webapp service. Same reason the updater service carries them.
# Found in review: only the two `${VAR:?...}` ones were carried here -
# ROOTGUARD_ADMIN_USER/ROOTGUARD_WEB_PORT below were missed, so a
# self-update-triggered `up -d --no-deps webapp` silently re-created
# it with ROOTGUARD_ADMIN_USER=admin/ROOTGUARD_WEB_PORT=8080
# regardless of whatever an operator had actually set in .env.
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_WEB_PORT: "${ROOTGUARD_WEB_PORT:-8080}"
# Propagates the real host checkout directory into `docker compose -f
# /opt/rootguard/compose.yaml ...` re-invocations (self-update and the
# control-plane update both do this from inside a container). Without
# this, `${PWD}` below re-resolves against the wrong process's cwd and
# silently mounts an empty directory instead of the real file - see the
# matching comment on the volumes line.
PWD: "${PWD}"
UNBOUND_CONFIG_DIR: /var/lib/rootguard/unbound
UNBOUND_CONTAINER_CONFIG_DIR: /etc/unbound/unbound.d
UNBOUND_CONTAINER_NAME: rootguard-unbound
ADGUARD_INSTALLER_URL: http://rootguard-adguard:3000
ADGUARD_API_URL: http://rootguard-adguard:80
ADGUARD_DATA_DIR: /var/lib/rootguard/adguard
ADGUARD_UPSTREAM: 172.29.53.2:5335
ADGUARD_DNS_ADDRESS: rootguard-adguard:53
ROOTGUARD_BLOCKPAGE_AUTH_DIR: /var/lib/rootguard/adguard-auth
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- rootguard-data:/var/lib/rootguard
- unbound-config:/var/lib/rootguard/unbound
- adguard-auth:/var/lib/rootguard/adguard-auth
# ${PWD}, not a relative "./compose.yaml": docker compose resolves a
# relative bind source against the directory of whatever -f file is
# being read, not against this file's own location. That's fine for
# the top-level `docker compose up` (PWD = the real checkout dir), but
# breaks the moment Core or the Updater re-invoke `docker compose -f
# /opt/rootguard/compose.yaml ...` from inside their own container to
# recreate a sibling service: the relative path would then resolve
# against /opt/rootguard - a path that only exists inside containers,
# not on the real host - silently mounting an auto-vivified empty
# directory instead of the real file. ${PWD} sidesteps this since it's
# a literal environment-variable substitution, not filesystem-relative
# resolution; the PWD env var above keeps propagating the correct
# value through every such re-invocation.
- ${PWD}/compose.yaml:/opt/rootguard/compose.yaml:ro
networks:
- control
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8081/api/health"]
interval: 15s
timeout: 5s
retries: 5
webapp:
build:
context: ./rootguard-webapp
image: "${ROOTGUARD_WEBAPP_IMAGE:-rootguard-webapp:dev}"
container_name: rootguard-webapp
restart: unless-stopped
depends_on:
core:
condition: service_healthy
environment:
PORT: "8080"
ROOTGUARD_CORE_URL: http://core:8081
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_SESSION_FILE: /var/lib/rootguard-sessions/sessions.json
volumes:
- rootguard-sessions:/var/lib/rootguard-sessions
ports:
- "${ROOTGUARD_WEB_PORT:-8080}:8080/tcp"
networks:
- control
- edge
healthcheck:
# The distroless runtime image has neither wget nor a shell - unlike
# core/updater's docker:29-cli base, there's nothing here for a
# "CMD wget ..." style check to exec. rootguard-webapp itself
# implements -healthcheck for exactly this (see cmd/rootguard-webapp).
test: ["CMD", "/app/rootguard-webapp", "-healthcheck"]
interval: 15s
timeout: 5s
retries: 5
updater:
build:
context: ./rootguard-updater
image: "${ROOTGUARD_UPDATER_IMAGE:-rootguard-updater:dev}"
container_name: rootguard-updater
restart: unless-stopped
depends_on:
attestation-proxy:
condition: service_healthy
environment:
# See rootguard-updater/attestation.go's verifyAttestation doc
# comment - scoped to cosign's own subprocess only.
ROOTGUARD_ATTESTATION_PROXY_URL: http://attestation-proxy:8888
ROOTGUARD_UPDATER_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_UPDATER_DATA_DIR: /var/lib/rootguard/control-plane-updater
ROOTGUARD_COMPOSE_FILE: /opt/rootguard/compose.yaml
ROOTGUARD_COMPOSE_PROJECT: rootguard
ROOTGUARD_CORE_UPDATE_IMAGE: "${ROOTGUARD_CORE_UPDATE_IMAGE:-rootguard-core:dev}"
ROOTGUARD_WEBAPP_UPDATE_IMAGE: "${ROOTGUARD_WEBAPP_UPDATE_IMAGE:-rootguard-webapp:dev}"
ROOTGUARD_UPDATER_SKIP_PULL: "${ROOTGUARD_UPDATER_SKIP_PULL:-false}"
ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}"
ROOTGUARD_ADMIN_USER: "${ROOTGUARD_ADMIN_USER:-admin}"
ROOTGUARD_ADMIN_PASSWORD: "${ROOTGUARD_ADMIN_PASSWORD:?set ROOTGUARD_ADMIN_PASSWORD in .env}"
ROOTGUARD_RECOVERY_TOKEN: "${ROOTGUARD_RECOVERY_TOKEN:?set ROOTGUARD_RECOVERY_TOKEN in .env}"
ROOTGUARD_WEB_PORT: "${ROOTGUARD_WEB_PORT:-8080}"
ROOTGUARD_CORE_IMAGE: "${ROOTGUARD_CORE_IMAGE:-rootguard-core:dev}"
ROOTGUARD_WEBAPP_IMAGE: "${ROOTGUARD_WEBAPP_IMAGE:-rootguard-webapp:dev}"
ROOTGUARD_SESSION_DIR: /var/lib/rootguard-sessions
# See the matching PWD/${PWD} comments on the core service above - the
# same self-referential-mount hazard applies here now that Core can
# recreate this very service (self-update).
PWD: "${PWD}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- rootguard-data:/var/lib/rootguard
- rootguard-sessions:/var/lib/rootguard-sessions
- ${PWD}/compose.yaml:/opt/rootguard/compose.yaml:ro
networks:
- control
healthcheck:
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8082/health"]
interval: 15s
timeout: 5s
retries: 5
networks:
edge:
control:
internal: true
# Not internal - real internet access, but only
# rootguard-attestation-proxy sits here. Core/the Updater stay on
# `control` only; they never join `egress` directly.
egress:
volumes:
unbound-config:
name: rootguard-unbound-config
rootguard-data:
name: rootguard-data
rootguard-sessions:
name: rootguard-sessions
adguard-auth:
name: rootguard-adguard-auth