238 lines
10 KiB
YAML
238 lines
10 KiB
YAML
# commons/compositions/apex/docker-compose.yml
|
|
# The shared apex CORE composition. Available to node compositions through include:.
|
|
# FULLY ${env}-parameterized; NO './' host paths (relative paths in an included file
|
|
# resolve against THIS dir — always use absolute ${APEX_*} vars). The engine exports
|
|
# APEX_TIER*/APEX_TIER*_SHARED/APEX_TIER_ROOT*/APEX_SUBNET/APEX_NODE_FQDN at invoke.
|
|
x-environment: &shared-environment
|
|
TZ: "${TZ:-Etc/UTC}" # override per node via TZ in the env; default UTC
|
|
|
|
services:
|
|
adguard:
|
|
image: adguard/adguardhome:v0.107.79
|
|
container_name: apex-adguard
|
|
volumes:
|
|
- ${APEX_TIER1}/adguard/data:/opt/adguardhome/work
|
|
- ${APEX_TIER1}/adguard/conf:/opt/adguardhome/conf
|
|
environment:
|
|
<<: *shared-environment
|
|
network_mode: "service:traefik"
|
|
restart: unless-stopped
|
|
labels:
|
|
# backup (resticontainer): adguard config only (work data regenerable)
|
|
- "restic.enable=true"
|
|
- "restic.backup.paths=/opt/adguardhome/conf"
|
|
cap_add:
|
|
- NET_BIND_SERVICE
|
|
- SYS_NICE
|
|
- CAP_CHOWN
|
|
profiles: [ adguard ]
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
|
|
# --- optional: observability agent (profile-gated) ---
|
|
# Collect host/container metrics and logs for configured remote endpoints.
|
|
# Alloy requires outbound access to those endpoints through the direct network.
|
|
alloy:
|
|
image: grafana/alloy:v1.20.0
|
|
container_name: apex-alloy
|
|
volumes:
|
|
- ${APEX_COMMONS}/etc/alloy/config.alloy:/etc/alloy/config.alloy:ro
|
|
- ${APEX_TIER2}/alloy/data:/var/lib/alloy
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
- /run/containerd/containerd.sock:/run/containerd/containerd.sock:ro # cadvisor per-container stats on nodes using the containerd snapshotter (overlayfs)
|
|
- /proc:/host/proc:ro
|
|
- /sys:/host/sys:ro
|
|
- /:/rootfs:ro
|
|
- /var/log/journal:/var/log/journal:ro
|
|
- /run/log/journal:/run/log/journal:ro
|
|
- /etc/machine-id:/etc/machine-id:ro
|
|
environment:
|
|
<<: *shared-environment
|
|
APEX_NODE_HOST: "${APEX_NODE_HOST?error}" # node + instance metric/log labels
|
|
# push targets + machine auth (per-node scalars in apex.env / secret .env)
|
|
APEX_LOKI_PUSH_URL: "${APEX_LOKI_PUSH_URL?error}"
|
|
APEX_PROM_PUSH_URL: "${APEX_PROM_PUSH_URL?error}"
|
|
APEX_OBS_BASIC_AUTH_USER: "${APEX_OBS_BASIC_AUTH_USER?error}"
|
|
APEX_OBS_BASIC_AUTH_PASS: "${APEX_OBS_BASIC_AUTH_PASS?error}"
|
|
GOMEMLIMIT: "${ALLOY_GOMEMLIMIT:-160MiB}"
|
|
GOGC: "${ALLOY_GOGC:-75}"
|
|
networks:
|
|
direct: {}
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
command:
|
|
- run
|
|
- --server.http.listen-addr=0.0.0.0:12345
|
|
- --storage.path=/var/lib/alloy
|
|
- /etc/alloy/config.alloy
|
|
restart: unless-stopped
|
|
user: "0:0" # root: required for host metrics, journal, and docker.sock
|
|
cgroup: host # host cgroup namespace: the cadvisor exporter needs it to read per-container cgroups (else machine-root only)
|
|
hostname: "${APEX_NODE_HOST}" # UTS hostname => node_exporter's node_uname_info `nodename` (else the container id)
|
|
profiles: [ alloy ]
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
|
|
# --- backups (host network, manual profile, one-shot) ---
|
|
# resticontainer (github.com/deytenit/resticontainer): a restic wrapper that
|
|
# discovers per-service backup intent from `restic.*` compose labels
|
|
# (restic.enable / restic.backup.paths / restic.backup.stop / restic.hooks.*).
|
|
# On `backup` it queries the docker daemon, runs each service's pre-hook (e.g.
|
|
# pg_dump), stops any container flagged restic.backup.stop, resolves the labeled
|
|
# container mounts to host paths (via the /srv mount), snapshots the union
|
|
# in a single restic run, then restarts + runs post-hooks. Every other subcommand
|
|
# (init/snapshots/forget/…) passes straight through to restic. Needs the docker
|
|
# socket (list/inspect/exec/stop/start) and read-only access to /srv.
|
|
resticontainer:
|
|
image: ghcr.io/deytenit/resticontainer:0.19.1@sha256:85457ee665b1c6c8438ac3494c286d267169ee023857da3a605a6fe16dab3d82
|
|
container_name: apex-resticontainer
|
|
volumes:
|
|
- ${APEX_TIER3}/restic/cache:/var/restic-cache
|
|
- /var/run/docker.sock:/var/run/docker.sock # query + exec/stop/start
|
|
# apex tier data + checkouts live under /srv, and the @tierN bind mounts are
|
|
# ABSOLUTE symlinks into /srv (compositions/<p>/@tierN -> /srv/com-…-tierN/<p>).
|
|
# Mount /srv at the same path with RESTIC_HOSTFS=/ so restic follows those
|
|
# symlinks and records the real host paths — a bare /:/hostfs cannot, because
|
|
# the absolute symlink target /srv/… is dangling inside the container.
|
|
- /srv:/srv:ro # read the data (resolves @tierN symlinks)
|
|
environment:
|
|
<<: *shared-environment
|
|
RESTIC_HOSTFS: "/" # mount sources are already absolute (/srv/…)
|
|
RESTIC_REPOSITORY: "${APEX_RESTIC_REPOSITORY:-}"
|
|
RESTIC_PASSWORD: "${APEX_RESTIC_PASSWORD:-}"
|
|
AWS_ACCESS_KEY_ID: "${APEX_RESTIC_AWS_ACCESS_KEY_ID:-}"
|
|
AWS_SECRET_ACCESS_KEY: "${APEX_RESTIC_AWS_SECRET_ACCESS_KEY:-}"
|
|
# S3 backend region + endpoint. Defaults suit AWS S3 (us-east-1, no custom
|
|
# endpoint); set APEX_RESTIC_AWS_REGION / APEX_RESTIC_AWS_ENDPOINT (node apex.env)
|
|
# for any other S3-compatible store (Yandex, Backblaze B2, MinIO, …).
|
|
AWS_REGION: "${APEX_RESTIC_AWS_REGION:-us-east-1}"
|
|
AWS_ENDPOINT_URL: "${APEX_RESTIC_AWS_ENDPOINT:-}"
|
|
# Compression requires repository format v2. The default is max;
|
|
# override it with APEX_RESTIC_COMPRESSION for direct container use.
|
|
RESTIC_COMPRESSION: "${APEX_RESTIC_COMPRESSION:-max}"
|
|
RESTIC_CACHE_DIR: /var/restic-cache
|
|
network_mode: "host"
|
|
profiles: [ manual ]
|
|
|
|
# --- docker socket proxy ---
|
|
socket-proxy:
|
|
image: tecnativa/docker-socket-proxy:0
|
|
container_name: apex-socket-proxy
|
|
volumes:
|
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
environment:
|
|
CONTAINERS: 1
|
|
SERVICES: 1
|
|
TASKS: 1
|
|
networks:
|
|
socket: {}
|
|
restart: unless-stopped
|
|
user: "0:0"
|
|
|
|
# --- edge reverse proxy (the core anchor on direct) ---
|
|
traefik:
|
|
image: traefik:3.7
|
|
container_name: apex-traefik
|
|
depends_on:
|
|
- socket-proxy
|
|
volumes:
|
|
- ${APEX_TIER1_SHARED}/apex/traefik/letsencrypt:/letsencrypt
|
|
- ${APEX_TIER3}/traefik/logs:/logs
|
|
- ${APEX_TIER1}/traefik/dynamic:/etc/traefik/dynamic:ro
|
|
environment:
|
|
<<: *shared-environment
|
|
CF_DNS_API_TOKEN: "${APEX_TRAEFIK_CF_DNS_API_TOKEN:-}"
|
|
UMASK: "0002"
|
|
ports:
|
|
- '0.0.0.0:80:80'
|
|
- '0.0.0.0:443:443/tcp'
|
|
- '0.0.0.0:443:443/udp'
|
|
networks:
|
|
direct:
|
|
ipv4_address: ${APEX_TRAEFIK_DIRECT_IP?error}
|
|
enclave: {}
|
|
socket: {}
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
command:
|
|
- "--api.dashboard=true"
|
|
- "--log.level=INFO"
|
|
- "--accesslog=true"
|
|
- "--accesslog.filepath=/logs/access.log"
|
|
- "--providers.file.directory=/etc/traefik/dynamic"
|
|
- "--providers.file.watch=true"
|
|
- "--providers.docker=true"
|
|
- "--providers.docker.network=enclave"
|
|
- "--providers.docker.exposedByDefault=false"
|
|
- "--providers.docker.endpoint=tcp://apex-socket-proxy:2375"
|
|
- "--entrypoints.web.address=:80"
|
|
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
|
|
- "--entryPoints.web.http.redirections.entrypoint.scheme=https"
|
|
- "--entrypoints.websecure.address=:443"
|
|
- "--entrypoints.websecure.asDefault=true"
|
|
- "--entrypoints.websecure.http.tls.certresolver=cloudflare"
|
|
- "--entrypoints.websecure.http3"
|
|
- "--certificatesresolvers.cloudflare.acme.dnschallenge=true"
|
|
- "--certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare"
|
|
- "--certificatesresolvers.cloudflare.acme.email=${APEX_TRAEFIK_ACME_EMAIL:-admin@localhost}"
|
|
- "--certificatesresolvers.cloudflare.acme.storage=/letsencrypt/acme.json"
|
|
restart: unless-stopped
|
|
labels:
|
|
- "traefik.enable=true"
|
|
- "traefik.http.routers.apex-traefik.rule=Host(`${APEX_NODE_FQDN}`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
|
|
- "traefik.http.middlewares.xray-enclave.ipallowlist.sourcerange=${APEX_ENCLAVE_SUBNET:-198.18.0.0/16}"
|
|
- "traefik.http.routers.apex-traefik.middlewares=xray-enclave@docker"
|
|
- "traefik.http.routers.apex-traefik.service=api@internal"
|
|
# adguard route (backend only present when the adguard profile is active)
|
|
- "traefik.http.routers.apex-adguard.rule=Host(`${APEX_NODE_FQDN}`) && PathPrefix(`/adguard`)"
|
|
- "traefik.http.routers.apex-adguard.middlewares=apex-adguard-strip-prefix"
|
|
- "traefik.http.middlewares.apex-adguard-strip-prefix.stripprefix.prefixes=/adguard"
|
|
- "traefik.http.routers.apex-adguard.service=apex-adguard"
|
|
- "traefik.http.services.apex-adguard.loadbalancer.server.port=3000"
|
|
# backup (resticontainer): dynamic routing rules only
|
|
# (letsencrypt certs are regenerable via ACME/Cloudflare DNS-01)
|
|
- "restic.enable=true"
|
|
- "restic.backup.paths=/etc/traefik/dynamic"
|
|
user: "${APEX_UID?error}:${APEX_GID?error}"
|
|
group_add:
|
|
- "${APEX_SHARED_GID?error}"
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
xray:
|
|
image: ghcr.io/xtls/xray-core:26.9.9
|
|
container_name: apex-xray
|
|
volumes:
|
|
- ${APEX_TIER1}/xray/data/config.jsonc:/etc/xray/config.jsonc:ro
|
|
- ${APEX_TIER1_SHARED}/apex/letsencrypt:/etc/letsencrypt/live:ro
|
|
- ${APEX_TIER3_SHARED}/apex/xray/log:/var/log/xray/
|
|
- ${APEX_TIER3_SHARED}/apex/xray/shm:/dev/shm/xray/
|
|
environment:
|
|
<<: *shared-environment
|
|
network_mode: "host"
|
|
command: [ "-c", "/etc/xray/config.jsonc" ]
|
|
restart: unless-stopped
|
|
labels:
|
|
# backup (resticontainer): xray config only (logs + shm are ephemeral)
|
|
- "restic.enable=true"
|
|
- "restic.backup.paths=/etc/xray/config.jsonc"
|
|
user: "0:0"
|
|
profiles: [ xray ]
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
|
|
networks:
|
|
direct:
|
|
name: direct
|
|
driver: bridge
|
|
ipam:
|
|
config:
|
|
- subnet: ${APEX_SUBNET?error}
|
|
enclave:
|
|
name: enclave
|
|
driver: bridge
|
|
internal: true
|
|
socket:
|
|
name: socket
|
|
driver: bridge
|
|
internal: true
|