services: mediamtx: image: bluenviron/mediamtx:latest container_name: mediamtx user: "1000:1000" # recordings owned by the debian user, so the archive pull can delete them ports: - "1935:1935" # RTMP ingest - "8890:8890/udp" # SRT ingest volumes: - ./mediamtx.yml:/mediamtx.yml:ro - /srv/media/streams:/recordings restart: unless-stopped copyparty: image: copyparty/ac:latest container_name: copyparty ports: - "127.0.0.1:3923:3923" volumes: - /srv/media:/w - ./copyparty.conf:/cfg/copyparty.conf:ro - copyparty_hist:/cfg/hist restart: unless-stopped syncthing: image: syncthing/syncthing:latest container_name: syncthing environment: - PUID=1000 - PGID=1000 ports: - "22000:22000/tcp" - "22000:22000/udp" - "127.0.0.1:8384:8384" # admin UI, reach via ssh tunnel volumes: - /srv/media/.syncthing:/var/syncthing/config # created by install.sh, owned by uid 1000 - /srv/media/clips:/var/syncthing/clips restart: unless-stopped caddy: image: caddy:latest container_name: caddy depends_on: [copyparty] network_mode: host # talks to copyparty (127.0.0.1:3923) and dumpd (127.0.0.1:8080) on loopback volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./dock:/srv/dock:ro - /srv/live:/srv/live:ro # live portal static build (deployLive.sh) - caddy_data:/data - caddy_config:/config restart: unless-stopped # ---------------------------------------------------------------- identity (Authentik) authentik-db: image: docker.io/library/postgres:16-alpine container_name: authentik-db environment: POSTGRES_DB: authentik POSTGRES_USER: authentik POSTGRES_PASSWORD: ${AUTHENTIK_PG_PASS} volumes: - /srv/appdata/authentik/db:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -d authentik -U authentik"] interval: 30s timeout: 5s retries: 5 start_period: 20s restart: unless-stopped authentik-redis: image: docker.io/library/redis:alpine container_name: authentik-redis command: --save 60 1 --loglevel warning volumes: - /srv/appdata/authentik/redis:/data healthcheck: test: ["CMD-SHELL", "redis-cli ping | grep PONG"] interval: 30s timeout: 3s retries: 5 restart: unless-stopped authentik-server: image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.8} container_name: authentik-server command: server environment: &authentik_env AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} AUTHENTIK_REDIS__HOST: authentik-redis AUTHENTIK_POSTGRESQL__HOST: authentik-db AUTHENTIK_POSTGRESQL__USER: authentik AUTHENTIK_POSTGRESQL__NAME: authentik AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_PG_PASS} AUTHENTIK_ERROR_REPORTING__ENABLED: "false" ports: - "127.0.0.1:9000:9000" volumes: - /srv/appdata/authentik/media:/media - /srv/appdata/authentik/templates:/templates depends_on: authentik-db: { condition: service_healthy } authentik-redis: { condition: service_healthy } restart: unless-stopped authentik-worker: image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.8} container_name: authentik-worker command: worker environment: *authentik_env volumes: - /srv/appdata/authentik/media:/media - /srv/appdata/authentik/templates:/templates - /srv/appdata/authentik/certs:/certs depends_on: authentik-db: { condition: service_healthy } authentik-redis: { condition: service_healthy } restart: unless-stopped # ---------------------------------------------------------------- watching (Jellyfin, direct play only) jellyfin: image: jellyfin/jellyfin:latest container_name: jellyfin user: "1000:1000" environment: JELLYFIN_PublishedServerUrl: https://${WATCH_HOST:-watch.example.com} ports: - "127.0.0.1:8096:8096" volumes: - /srv/appdata/jellyfin/config:/config - /srv/appdata/jellyfin/cache:/cache - /srv/media/streams:/media/streams:ro - /srv/media/clips:/media/clips:ro - /srv/media/finals:/media/finals:ro restart: unless-stopped # ---------------------------------------------------------------- database (for the Discord bot and friends) mariadb: image: mariadb:11 container_name: mariadb network_mode: host # PUBLIC on 3306: host networking so the nftables input rules (db_allow / rate # limit) actually apply — Docker-published ports bypass the host firewall. environment: MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} MARIADB_ROOT_HOST: localhost # root only from the box itself; the bot user may connect from anywhere MARIADB_DATABASE: ${BOT_DB_NAME:-vanguardbot} MARIADB_USER: ${BOT_DB_USER:-bot} MARIADB_PASSWORD: ${BOT_DB_PASSWORD} GITEA_DB_PASSWORD: ${GITEA_DB_PASSWORD} # used by mariadb/init/10-gitea.sh on first init volumes: - /srv/appdata/mariadb:/var/lib/mysql - ./mariadb/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 20s timeout: 5s retries: 6 start_period: 30s restart: unless-stopped # ---------------------------------------------------------------- Discord bot (python; code lives in ./bot/app) discord-bot: build: ./bot container_name: discord-bot user: "1000:1000" environment: DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-} DISCORD_GUILD_ID: ${DISCORD_GUILD_ID:-} DB_HOST: host.docker.internal # mariadb runs on the host network DB_PORT: "3306" DB_NAME: ${BOT_DB_NAME:-vanguardbot} DB_USER: ${BOT_DB_USER:-bot} DB_PASSWORD: ${BOT_DB_PASSWORD} MEDIA_CLIPS: /media/clips WEB_HOST: 0.0.0.0 WEB_PORT: "8000" WEB_PUBLIC_URL: https://${BOT_HOST:-bot.example.com} AUTH_HEADER_USER: X-Authentik-Username AUTH_HEADER_GROUPS: X-Authentik-Groups AUTH_HEADER_EMAIL: X-Authentik-Email TZ: America/New_York PYTHONUNBUFFERED: "1" ports: - "127.0.0.1:8000:8000" # web UI; public only via Caddy + Authentik forward-auth at https://BOT_HOST/ volumes: - ./bot/app:/app # the bot's source; edit and `docker compose restart discord-bot` - /srv/media/clips:/media/clips # rw: for a clip-submissions feature (writes into clips//) - /srv/appdata/discord-bot:/data # persistent scratch (sqlite, caches) if the bot wants files extra_hosts: - "host.docker.internal:host-gateway" depends_on: mariadb: { condition: service_healthy } restart: unless-stopped # ---------------------------------------------------------------- Gitea + Actions runner gitea: image: docker.gitea.com/gitea:1.24 container_name: gitea environment: USER_UID: "1000" USER_GID: "1000" GITEA__database__DB_TYPE: mysql GITEA__database__HOST: host.docker.internal:3306 # mariadb runs on the host network GITEA__database__NAME: gitea GITEA__database__USER: gitea GITEA__database__PASSWD: ${GITEA_DB_PASSWORD} GITEA__server__DOMAIN: ${GIT_HOST:-git.example.com} GITEA__server__ROOT_URL: https://${GIT_HOST:-git.example.com}/ GITEA__server__HTTP_PORT: "3000" GITEA__server__SSH_DOMAIN: ${GIT_HOST:-git.example.com} GITEA__server__SSH_PORT: "2222" # what users put in their remotes GITEA__server__SSH_LISTEN_PORT: "22" # inside the container GITEA__service__DISABLE_REGISTRATION: "true" # accounts are created by an admin (or via Authentik OIDC later) GITEA__actions__ENABLED: "true" GITEA__actions__DEFAULT_ACTIONS_URL: github GITEA__webhook__ALLOWED_HOST_LIST: private,loopback,external GITEA__time__DEFAULT_UI_LOCATION: America/New_York ports: - "127.0.0.1:3000:3000" - "2222:22" volumes: - /srv/appdata/gitea:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro extra_hosts: - "host.docker.internal:host-gateway" depends_on: mariadb: { condition: service_healthy } restart: unless-stopped act_runner: image: docker.gitea.com/act_runner:latest container_name: act_runner environment: GITEA_INSTANCE_URL: https://${GIT_HOST:-git.example.com}/ GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_TOKEN:-} # set by ./registerGiteaRunner.sh GITEA_RUNNER_NAME: vanguard-runner GITEA_RUNNER_LABELS: "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest,ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04,dotnet:docker://mcr.microsoft.com/dotnet/sdk:10.0,python:docker://python:3.12" volumes: - /srv/appdata/act_runner:/data - /var/run/docker.sock:/var/run/docker.sock # jobs run as sibling containers; root-equivalent on the host depends_on: - gitea restart: unless-stopped volumes: copyparty_hist: caddy_data: caddy_config: