# Management plane: portainer, vault, github-runner, watchtower. # These manage / underpin the application stacks rather than being part of them, # so they are deployed by raw `docker compose` (NOT a Portainer-managed stack) — # Portainer cannot manage the stack that contains Portainer itself. # # This file owns the shared infrastructure networks: # - edge : the single reverse-proxy network. Caddy + every service it # proxies join this. Replaces caddy's old per-stack network # membership. App stacks reference it as external. # - portainer_proxy : legacy net for caddy -> portainer. Kept until the monolith # is fully dismantled, then dropped (portainer is on edge too). # # Vault lives here (moved out of the dev stack): it is estate-wide secrets infra, # must boot before the app stacks, and should not reseal when a dev tool changes. # Vault data is the /config/vault bind mount — moving the service definition here # does not touch any data. Re-unseal after (apply-compose.ps1 -VaultUnseal). # # Fresh-install order: # docker compose -f portainer-compose.yml up -d # networks + mgmt plane first # (then create the Portainer git stacks for proxy + each app domain) services: portainer: container_name: portainer image: portainer/portainer-ce:latest restart: unless-stopped networks: - portainer_proxy - edge volumes: - /var/run/docker.sock:/var/run/docker.sock - /config/portainer:/data ports: - "9100:9000" labels: - "com.centurylabs.watchtower.enable=false" # ============================================================ # VAULT (estate-wide secret management) # One-time setup after first start: # docker exec -it vault vault operator init # vault operator unseal (3x with unseal keys) # Unseal keys + root token live in .env (they are the one secret Vault # itself cannot hold). Re-unseal after a restart with: # powershell -File apply-compose.ps1 -VaultUnseal # ============================================================ vault: container_name: vault image: hashicorp/vault@sha256:3fd53308acccd9e4e83fde3e6c6cf5b15e0f7fac1ff7510780b8b23f5e4c3de7 restart: unless-stopped networks: - edge cap_add: - IPC_LOCK # Newer vault:latest mlocks more than docker's default 64KB memlock ulimit; # without this it crash-loops with "Failed to lock memory: cannot allocate # memory" despite IPC_LOCK being granted. ulimits: memlock: soft: -1 hard: -1 environment: - VAULT_ADDR=http://0.0.0.0:8200 volumes: - /config/vault/config:/vault/config:ro - /config/vault/data:/vault/data - /config/vault/logs:/vault/logs command: vault server -config=/vault/config/vault.hcl ports: - "8200:8200" labels: - "com.centurylabs.watchtower.enable=false" postgres_gitea: container_name: postgres_gitea image: postgres:16-alpine restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: - gitea_internal environment: POSTGRES_USER: gitea POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} POSTGRES_DB: gitea volumes: - /config/gitea/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"] interval: 10s timeout: 5s retries: 5 gitea: container_name: gitea image: gitea/gitea:latest restart: unless-stopped networks: gitea_internal: {} edge: aliases: - gitea.ginnoir.com environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=postgres_gitea:5432 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} - GITEA__server__DOMAIN=gitea.ginnoir.com - GITEA__server__ROOT_URL=https://gitea.ginnoir.com - GITEA__server__SSH_DOMAIN=gitea.ginnoir.com - GITEA__server__SSH_PORT=2222 - GITEA__repository__ROOT=/repos - GITEA__lfs__PATH=/repos-lfs volumes: - /config/gitea:/data - /storage1/labdata/gitea/repositories:/repos - /storage1/labdata/gitea/lfs:/repos-lfs - /etc/localtime:/etc/localtime:ro ports: - "3030:3000" - "2222:22" depends_on: postgres_gitea: condition: service_healthy labels: - "com.centurylabs.watchtower.enable=false" gitea-runner: container_name: gitea_runner image: gitea/act_runner:latest restart: unless-stopped networks: - gitea_internal environment: - GITEA_INSTANCE_URL=https://gitea.ginnoir.com - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} - GITEA_RUNNER_NAME=valhalla-management-runner - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:24-bookworm,node-24:docker://node:24-bookworm - CONFIG_FILE=/data/config.yaml volumes: - /config/gitea/act_runner:/data - /var/run/docker.sock:/var/run/docker.sock - /home/ginnoir/valhalla-lab:/valhalla-lab - /config/caddy:/config/caddy depends_on: gitea: condition: service_started labels: - "com.centurylabs.watchtower.enable=false" registry: container_name: registry image: registry:2 restart: unless-stopped networks: - registry_internal - edge environment: - REGISTRY_AUTH=htpasswd - REGISTRY_AUTH_HTPASSWD_REALM=Valhalla Registry - REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd - REGISTRY_STORAGE_DELETE_ENABLED=true volumes: - /storage1/labdata/registry:/var/lib/registry - /config/registry/auth:/auth:ro ports: - "5000:5000" labels: - "com.centurylabs.watchtower.enable=false" registry-ui: container_name: registry_ui image: joxit/docker-registry-ui:latest restart: unless-stopped networks: - registry_internal - edge environment: - SINGLE_REGISTRY=true - REGISTRY_TITLE=Valhalla Registry - NGINX_PROXY_PASS_URL=http://registry:5000 - SHOW_CONTENT_DIGEST=true - DELETE_IMAGES=true depends_on: - registry labels: - "com.centurylabs.watchtower.enable=false" github-runner: container_name: github_runner image: myoung34/github-runner:latest restart: unless-stopped environment: - ACCESS_TOKEN=${GITHUB_RUNNER_ACCESS_TOKEN} - REPO_URL=https://github.com/ginnoir/homelabstack - RUNNER_NAME=valhalla - RUNNER_LABELS=self-hosted,Linux,x64 - RUNNER_SCOPE=repo - DOCKER_GROUP_GID=1001 volumes: - /var/run/docker.sock:/var/run/docker.sock - /home/ginnoir/valhalla-lab:/valhalla-lab - /config/caddy:/config/caddy labels: - "com.centurylabs.watchtower.enable=false" watchtower: container_name: watchtower image: containrrr/watchtower:latest restart: unless-stopped environment: - DOCKER_API_VERSION=1.40 - WATCHTOWER_NOTIFICATION_SKIP_TITLE=true - WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.ginnoir.com/watchtower?title=WatchtowerUpdates volumes: - /var/run/docker.sock:/var/run/docker.sock labels: - "com.centurylabs.watchtower.enable=false" networks: edge: name: edge driver: bridge portainer_proxy: name: portainer_proxy driver: bridge gitea_internal: name: gitea_internal driver: bridge registry_internal: name: registry_internal driver: bridge