608 lines
17 KiB
YAML
608 lines
17 KiB
YAML
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"
|