name: rootguard services: unbound: build: context: ./rootguard-unbound image: rootguard-unbound:dev container_name: rootguard-unbound restart: unless-stopped read_only: true cap_drop: - ALL security_opt: - no-new-privileges:true volumes: - unbound-config:/etc/unbound/unbound.d - unbound-state:/var/lib/unbound networks: dns: ipv4_address: 172.29.53.2 forwarding_test: ipv4_address: 172.30.54.3 forwarder: image: rootguard-unbound:dev container_name: rootguard-forwarder restart: unless-stopped entrypoint: ["unbound", "-d", "-c", "/etc/unbound/forwarder.conf"] user: "0:0" read_only: true cap_drop: - ALL cap_add: - NET_BIND_SERVICE security_opt: - no-new-privileges:true volumes: - ./testdata/forwarder/unbound.conf:/etc/unbound/forwarder.conf:ro networks: forwarding_test: ipv4_address: 172.30.54.2 healthcheck: test: ["CMD", "dig", "+short", "+time=1", "+tries=1", "@127.0.0.1", "-p", "53", "example.", "A"] interval: 2s timeout: 2s retries: 15 adguard: image: "${ROOTGUARD_ADGUARD_IMAGE:-adguard/adguardhome:v0.107.80}" container_name: rootguard-adguard restart: unless-stopped depends_on: updater: condition: service_healthy unbound: condition: service_healthy ports: - "${ROOTGUARD_DNS_BIND:-0.0.0.0}:${ROOTGUARD_DNS_PORT:-53}:53/tcp" - "${ROOTGUARD_DNS_BIND:-0.0.0.0}:${ROOTGUARD_DNS_PORT:-53}:53/udp" volumes: - adguard-work:/opt/adguardhome/work - adguard-config:/opt/adguardhome/conf networks: - dns core: build: context: ./rootguard-core image: rootguard-core:dev container_name: rootguard-core restart: unless-stopped depends_on: unbound: condition: service_healthy forwarder: condition: service_healthy adguard: condition: service_started environment: PORT: "8081" ROOTGUARD_API_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}" # This file's own unbound/adguard are pre-provisioned compose # services (bootstrapped directly, never through # installer.Manager.deploy()), so the attestation gate's code path # isn't actually reached here today - added for consistency with # compose.yaml/compose.release.yaml anyway, so a future test that # does exercise POST /api/installation/deploy against this file # doesn't hit the same live-testing surprise compose.yaml did # (2026-08-29, see docs/security-audit-log.md). ROOTGUARD_SKIP_ATTESTATION: "${ROOTGUARD_SKIP_ATTESTATION:-false}" ROOTGUARD_INSTALLATION_DIR: /var/lib/rootguard/installation ROOTGUARD_CONTROL_PLANE_UPDATER_URL: http://updater:8082 ROOTGUARD_CONTROL_PLANE_UPDATER_TOKEN: "${ROOTGUARD_API_TOKEN:?set ROOTGUARD_API_TOKEN in .env}" UNBOUND_CONFIG_DIR: /var/lib/rootguard/unbound UNBOUND_CONTAINER_CONFIG_DIR: /etc/unbound/unbound.d UNBOUND_CONTAINER_NAME: rootguard-unbound # Empty by default - Diagnose keeps querying the real internet # (example.com/dnssec-failed.org), correct for an actual # deployment. ci.yml sets both to a local, throwaway DNSSEC test # zone (scripts/ci/dnssec-test-zone) so its own integration test of # this exact endpoint doesn't depend on the runner's own network - # found in review, confirmed live: main's own scheduled run of that # check failed on a transient DNS hiccup unrelated to any code # change. ROOTGUARD_UNBOUND_DIAGNOSTIC_RESOLUTION_DOMAIN: "${ROOTGUARD_UNBOUND_DIAGNOSTIC_RESOLUTION_DOMAIN:-}" ROOTGUARD_UNBOUND_DIAGNOSTIC_DNSSEC_DOMAIN: "${ROOTGUARD_UNBOUND_DIAGNOSTIC_DNSSEC_DOMAIN:-}" 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 # Propagates the real host checkout directory into a # `docker compose -f /opt/rootguard/compose.yaml ...` # re-invocation from inside this container - see the matching # comment on the volumes line below and on compose.yaml's own core # service (the identical fix, applied there first). PWD: "${PWD}" volumes: - /var/run/docker.sock:/var/run/docker.sock - rootguard-data:/var/lib/rootguard - unbound-config:/var/lib/rootguard/unbound # Aliased to the fixed "compose.yaml" name (not this file's own # "compose.integration.yaml") because # internal/updater/manager.go's composeUp hardcodes that filename - # same reason compose.yaml/compose.release.yaml's own core service # alias-mounts theirs. ${PWD}, not a relative "./...", for the same # reason those two files use it: a relative source re-resolves # against the wrong directory the moment this container re-invokes # `docker compose -f ...` against itself. # Nothing in this file currently drives Core's own # updater-of-updater self-update path (no ROOTGUARD_SELF_UPDATE_DIR/ # ROOTGUARD_COMPOSE_FILE wired here, no attestation-proxy service), # so this mount is currently unused - added so the same silent # "missing file" surprise found live in compose.yaml (2026-08-29, # see docs/security-audit-log.md) can't recur here the moment that # gets wired up. - ${PWD}/compose.integration.yaml:/opt/rootguard/compose.yaml:ro networks: - control - dns 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: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 # Found in review: missing here despite compose.yaml (dev) having it. # 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). healthcheck: test: ["CMD", "/app/rootguard-webapp", "-healthcheck"] interval: 15s timeout: 5s retries: 5 updater: build: context: ./rootguard-updater image: rootguard-updater:dev container_name: rootguard-updater restart: unless-stopped environment: 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.integration.yaml ROOTGUARD_COMPOSE_PROJECT: rootguard ROOTGUARD_CORE_UPDATE_IMAGE: rootguard-core:dev ROOTGUARD_WEBAPP_UPDATE_IMAGE: rootguard-webapp:dev ROOTGUARD_UPDATER_SKIP_PULL: "true" 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_SESSION_DIR: /var/lib/rootguard-sessions # See the matching PWD/${PWD} comments on the core service above # and on compose.yaml/compose.release.yaml's identical updater # service - required for the volumes line below to resolve # correctly when this container re-invokes `docker compose -f # /opt/rootguard/compose.integration.yaml ...` against itself. PWD: "${PWD}" volumes: - /var/run/docker.sock:/var/run/docker.sock - rootguard-data:/var/lib/rootguard - rootguard-sessions:/var/lib/rootguard-sessions # ${PWD}, not a relative "./compose.integration.yaml" - found in # review: this file was missed when compose.yaml/compose.release.yaml # got the identical fix (2026-08-22, docs/security-audit-log.md). A # relative bind source resolves against the directory of whatever # -f file is being read, not this file's own location - breaks the # moment this container re-invokes `docker compose -f # /opt/rootguard/compose.integration.yaml ...` against itself, # silently mounting an auto-vivified empty directory instead of the # real file. - ${PWD}/compose.integration.yaml:/opt/rootguard/compose.integration.yaml:ro networks: - control healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8082/health"] interval: 5s timeout: 3s retries: 10 networks: edge: control: internal: true dns: ipam: config: - subnet: 172.29.53.0/24 forwarding_test: internal: true ipam: config: - subnet: 172.30.54.0/24 volumes: adguard-work: adguard-config: unbound-config: unbound-state: rootguard-data: rootguard-sessions: