networks: proxy_net: name: proxy_net driver: bridge gitea_backend: internal: true bookstack_backend: internal: true vikunja_backend: internal: true services: # --- BACKUP --- backup: image: offen/docker-volume-backup:v2 container_name: backup restart: unless-stopped env_file: ./backup.env volumes: - /var/run/docker.sock:/var/run/docker.sock:ro # Backup sources (read-only) - ./gitea/data:/backup/gitea-data:ro - ./gitea/db:/backup/gitea-db:ro - ./bookstack/app:/backup/bookstack-app:ro - ./bookstack/db:/backup/bookstack-db:ro - ./vikunja/files:/backup/vikunja-files:ro - ./vikunja/db:/backup/vikunja-db:ro - ./radicale/data:/backup/radicale-data:ro - ./vaultwarden/data:/backup/vaultwarden-data:ro - ./homepage:/backup/homepage:ro - ./actual-data:/backup/actual:ro # Local backup archive - ./backups:/archive deploy: resources: limits: cpus: '2' memory: 2G reservations: cpus: '1' memory: 1G logging: driver: "json-file" options: max-size: "100m" max-file: "5" labels: - "homepage.group=Infrastructure" - "homepage.name=Backup" - "homepage.icon=duplicati.png" - "homepage.description=Volume Backup Service" traefik: image: traefik:v3.6.6 container_name: traefik restart: unless-stopped command: - "--providers.docker=true" - "--providers.docker.exposedbydefault=false" ports: - "80:80" - "443:443" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./traefik.yml:/etc/traefik/traefik.yml - ./acme.json:/acme.json - ./logs/traefik:/var/log/traefik # Logs for Fail2Ban networks: - proxy_net deploy: resources: limits: cpus: '2' memory: 512M reservations: cpus: '0.5' memory: 256M healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 30s timeout: 10s retries: 3 start_period: 20s logging: driver: "json-file" options: max-size: "100m" max-file: "5" labels: - "traefik.enable=true" - "traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)" - "traefik.http.routers.dashboard.service=api@internal" - "traefik.http.routers.dashboard.middlewares=auth" # IMPORTANT: Replace with actual htpasswd hash or set via environment variable - "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_BASICAUTH_USERS}" # Homepage Monitoring - "homepage.group=Infrastructure" - "homepage.name=Traefik" - "homepage.icon=traefik.png" - "homepage.href=https://traefik.${DOMAIN}" homepage: image: ghcr.io/gethomepage/homepage:latest container_name: homepage restart: unless-stopped volumes: - ./homepage:/app/config - /var/run/docker.sock:/var/run/docker.sock:ro environment: HOMEPAGE_ALLOWED_HOSTS: "*" PUID: ${PUID} PGID: 988 networks: - proxy_net deploy: resources: limits: cpus: '1' memory: 256M reservations: cpus: '0.25' memory: 128M healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/"] interval: 30s timeout: 10s retries: 3 start_period: 20s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.homepage.rule=Host(`homepage.${DOMAIN}`)" - "traefik.http.routers.homepage.entrypoints=websecure" - "traefik.http.routers.homepage.tls.certresolver=letsencrypt" - "traefik.http.services.homepage.loadbalancer.server.port=3000" - "traefik.docker.network=proxy_net" # --- GITEA --- gitea-db: image: postgres:14-alpine container_name: gitea-db restart: always environment: POSTGRES_USER: ${POSTGRES_USER_GITEA} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD_GITEA} POSTGRES_DB: ${POSTGRES_NAME_GITEA} networks: - gitea_backend volumes: - ./gitea/db:/var/lib/postgresql/data deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '1' memory: 512M healthcheck: test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER_GITEA}"] interval: 10s timeout: 5s retries: 5 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "docker-volume-backup.stop-during-backup=true" gitea: image: gitea/gitea:latest container_name: gitea restart: unless-stopped environment: - USER_UID=${PUID} - USER_GID=${PGID} - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=${POSTGRES_HOST_GITEA} - GITEA__database__NAME=${POSTGRES_NAME_GITEA} - GITEA__database__USER=${POSTGRES_USER_GITEA} - GITEA__database__PASSWD=${POSTGRES_PASSWORD_GITEA} - GITEA__server__DOMAIN=gitea.${DOMAIN} - GITEA__server__SSH_PORT=2222 - GITEA__server__ROOT_URL=https://gitea.${DOMAIN}/ - GITEA__actions__ENABLED=true - GITEA__service__DISABLE_REGISTRATION=true ports: - "2222:22" volumes: - ./gitea/data:/data networks: - proxy_net - gitea_backend depends_on: gitea-db: condition: service_healthy deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '1' memory: 512M healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/"] interval: 30s timeout: 10s retries: 3 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.gitea.rule=Host(`gitea.${DOMAIN}`)" - "traefik.http.routers.gitea.entrypoints=websecure" - "traefik.http.routers.gitea.tls.certresolver=letsencrypt" - "traefik.http.services.gitea.loadbalancer.server.port=3000" - "homepage.group=Dev" - "homepage.name=Gitea" - "homepage.icon=gitea.png" - "homepage.href=https://gitea.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true" gitea-runner: image: gitea/act_runner:latest container_name: gitea-runner restart: unless-stopped environment: CONFIG_FILE: /config.yml GITEA_INSTANCE_URL: "${GITEA_INSTANCE_URL}" GITEA_RUNNER_REGISTRATION_TOKEN: "${GITEA_RUNNER_REGISTRATION_TOKEN}" GITEA_RUNNER_NAME: "${GITEA_RUNNER_NAME}" GITEA_RUNNER_LABELS: "${GITEA_RUNNER_LABELS}" volumes: - ./runner-data:/data - ./config.yml:/config.yml - /var/run/docker.sock:/var/run/docker.sock networks: - proxy_net depends_on: gitea: condition: service_healthy deploy: resources: limits: cpus: '4' memory: 2G reservations: cpus: '2' memory: 1G logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "homepage.group=Dev" - "homepage.icon=gitea.png" - "homepage.name=Gitea Runner" - "homepage.description=CI/CD Worker" # --- BOOKSTACK --- bookstack-db: image: lscr.io/linuxserver/mariadb:latest container_name: bookstack-db restart: always environment: - PUID=${PUID} - PGID=${PGID} - TZ=${MYSQL_TZ_BOOKSTACK} - MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD_BOOKSTACK} - MYSQL_DATABASE=${MYSQL_NAME_BOOKSTACK} - MYSQL_USER=${MYSQL_USER_BOOKSTACK} - MYSQL_PASSWORD=${MYSQL_PASSWORD_BOOKSTACK} networks: - bookstack_backend volumes: - ./bookstack/db:/config deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '1' memory: 512M healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_PASSWORD_BOOKSTACK}"] interval: 10s timeout: 5s retries: 5 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "docker-volume-backup.stop-during-backup=true" bookstack: image: lscr.io/linuxserver/bookstack:latest container_name: bookstack restart: unless-stopped environment: - PUID=${PUID} - PGID=${PGID} - DB_HOST=${MYSQL_HOST_BOOKSTACK} - DB_PORT=${MYSQL_PORT_BOOKSTACK} - DB_USERNAME=${MYSQL_USER_BOOKSTACK} - DB_PASSWORD=${MYSQL_PASSWORD_BOOKSTACK} - DB_DATABASE=${MYSQL_NAME_BOOKSTACK} - APP_URL=${BOOKSTACK_APP_URL} - APP_ASSET_URL=${BOOKSTACK_APP_URL} - APP_KEY=${BOOKSTACK_APP_KEY} volumes: - ./bookstack/app:/config networks: - proxy_net - bookstack_backend depends_on: bookstack-db: condition: service_healthy deploy: resources: limits: cpus: '2' memory: 512M reservations: cpus: '1' memory: 256M # Note: Bookstack uses LS.IO init system with process supervision # Health check disabled - container runs well under init supervision logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.bookstack.rule=Host(`bookstack.${DOMAIN}`)" - "traefik.http.routers.bookstack.entrypoints=websecure" - "traefik.http.routers.bookstack.tls.certresolver=letsencrypt" - "traefik.http.services.bookstack.loadbalancer.server.port=80" - "traefik.docker.network=proxy_net" - "homepage.group=Dev" - "homepage.name=Bookstack" - "homepage.icon=bookstack.png" - "homepage.href=https://bookstack.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true" # --- WEBSITE --- website: image: gitea.jmpgames.it/admin/website:latest container_name: website profiles: ["after"] restart: unless-stopped networks: - proxy_net deploy: resources: limits: cpus: '1' memory: 256M reservations: cpus: '0.5' memory: 128M healthcheck: test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"] interval: 30s timeout: 10s retries: 3 start_period: 20s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.website.rule=Host(`${DOMAIN}`) || Host(`www.${DOMAIN}`)" - "traefik.http.routers.website.entrypoints=websecure" - "traefik.http.routers.website.tls.certresolver=letsencrypt" - "traefik.http.services.website.loadbalancer.server.port=80" - "homepage.group=Public" - "homepage.name=Website" - "homepage.href=https://${DOMAIN}" # --- VIKUNJA --- vikunja-db: image: postgres:17-alpine container_name: vikunja-db restart: always environment: POSTGRES_USER: ${POSTGRES_USER_VIKUNJA} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD_VIKUNJA} POSTGRES_DB: ${POSTGRES_NAME_VIKUNJA} networks: - vikunja_backend volumes: - ./vikunja/db:/var/lib/postgresql/data deploy: resources: limits: cpus: '2' memory: 1G reservations: cpus: '1' memory: 512M healthcheck: test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER_VIKUNJA}"] interval: 10s timeout: 5s retries: 5 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "docker-volume-backup.stop-during-backup=true" vikunja: image: vikunja/vikunja:latest container_name: vikunja restart: unless-stopped user: "${PUID}:${PGID}" environment: VIKUNJA_SERVICE_PUBLICURL: https://vikunja.${DOMAIN}/ VIKUNJA_SERVICE_JWTSECRET: ${VIKUNJA_JWT_SECRET} VIKUNJA_DATABASE_TYPE: postgres VIKUNJA_DATABASE_HOST: vikunja-db VIKUNJA_DATABASE_DATABASE: ${POSTGRES_NAME_VIKUNJA} VIKUNJA_DATABASE_USER: ${POSTGRES_USER_VIKUNJA} VIKUNJA_DATABASE_PASSWORD: ${POSTGRES_PASSWORD_VIKUNJA} VIKUNJA_SERVICE_ENABLEREGISTRATION: false volumes: - ./vikunja/files:/app/vikunja/files networks: - proxy_net - vikunja_backend depends_on: vikunja-db: condition: service_healthy deploy: resources: limits: cpus: '2' memory: 512M reservations: cpus: '1' memory: 256M # Note: Vikunja image doesn't include shell/curl, relying on process supervision instead # healthcheck disabled - container runs well without it logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.vikunja.rule=Host(`vikunja.${DOMAIN}`)" - "traefik.http.routers.vikunja.entrypoints=websecure" - "traefik.http.routers.vikunja.tls.certresolver=letsencrypt" - "traefik.http.services.vikunja.loadbalancer.server.port=3456" - "traefik.docker.network=proxy_net" - "homepage.group=Dev" - "homepage.name=Vikunja" - "homepage.icon=vikunja.png" - "homepage.href=https://vikunja.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true" # --- VAULTWARDEN --- vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped environment: DOMAIN: https://vault.${DOMAIN} SIGNUPS_ALLOWED: ${VAULTWARDEN_SIGNUPS_ALLOWED:-false} ADMIN_TOKEN: ${VAULTWARDEN_ADMIN_TOKEN} volumes: - ./vaultwarden/data:/data networks: - proxy_net deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80/alive"] interval: 30s timeout: 10s retries: 3 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.vaultwarden.rule=Host(`vault.${DOMAIN}`)" - "traefik.http.routers.vaultwarden.entrypoints=websecure" - "traefik.http.routers.vaultwarden.tls.certresolver=letsencrypt" - "traefik.http.services.vaultwarden.loadbalancer.server.port=80" - "homepage.group=Tools" - "homepage.name=Vaultwarden" - "homepage.icon=vaultwarden.png" - "homepage.href=https://vault.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true" # --- RADICALE --- radicale: image: tomsquest/docker-radicale:latest container_name: radicale restart: unless-stopped volumes: - ./radicale/data:/data - ./radicale/config:/config:ro networks: - proxy_net deploy: resources: limits: cpus: '1' memory: 256M reservations: cpus: '0.5' memory: 128M healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5232/"] interval: 30s timeout: 10s retries: 3 start_period: 20s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.radicale.rule=Host(`radicale.${DOMAIN}`)" - "traefik.http.routers.radicale.entrypoints=websecure" - "traefik.http.routers.radicale.tls.certresolver=letsencrypt" - "traefik.http.services.radicale.loadbalancer.server.port=5232" - "homepage.group=Tools" - "homepage.name=Radicale" - "homepage.icon=radicale.png" - "homepage.href=https://radicale.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true" # --- Actual Budget --- actual_server: image: docker.io/actualbudget/actual-server:latest container_name: actual_server volumes: - ./actual-data:/data networks: - proxy_net restart: unless-stopped deploy: resources: limits: cpus: '1' memory: 512M reservations: cpus: '0.5' memory: 256M healthcheck: test: ['CMD-SHELL', 'node src/scripts/health-check.js'] interval: 30s timeout: 10s retries: 3 start_period: 30s logging: driver: "json-file" options: max-size: "50m" max-file: "3" labels: - "traefik.enable=true" - "traefik.http.routers.actual.rule=Host(`actual.${DOMAIN}`)" - "traefik.http.routers.actual.entrypoints=websecure" - "traefik.http.routers.actual.tls.certresolver=letsencrypt" - "traefik.http.services.actual.loadbalancer.server.port=5006" - "homepage.group=Tools" - "homepage.name=Actual" - "homepage.icon=actual.png" - "homepage.href=https://actual.${DOMAIN}" - "docker-volume-backup.stop-during-backup=true"