services: # ============================================================ # FOUNDRY VTT # ============================================================ foundry: image: felddy/foundryvtt:latest hostname: valhalla-primary init: true restart: unless-stopped networks: - foundry volumes: - /storage1/foundry:/data - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash - /storage1/Books/Tabletop:/data/Data/storage environment: - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} - CONTAINER_VERBOSE=true - FOUNDRY_HOSTNAME=foundry.ginnoir.com - FOUNDRY_PROXY_PORT=443 - FOUNDRY_PROXY_SSL=true - CONTAINER_PRESERVE_OWNER=/data/Data/storage - FOUNDRY_UID=1000 - FOUNDRY_GID=1000 - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} ports: - "30000:30000" foundry2: image: felddy/foundryvtt:latest hostname: valhalla-secondary init: true restart: unless-stopped networks: - foundry volumes: - /storage1/foundry2:/data - /storage1/foundry/updatemain.bash:/home/foundry/updatemain.bash - /storage1/Books/Tabletop:/data/Data/storage environment: - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN_KEY} - CONTAINER_VERBOSE=true - FOUNDRY_HOSTNAME=foundry2.ginnoir.com - FOUNDRY_PROXY_PORT=443 - FOUNDRY_PROXY_SSL=true - CONTAINER_PRESERVE_OWNER=/data/Data/storage - FOUNDRY_UID=1000 - FOUNDRY_GID=1000 - CONTAINER_PATCH_URLS=${FOUNDRY_PATCH_URLS} ports: - "30001:30000" # ============================================================ # CADDY (reverse proxy) # serfriz/caddy-cloudflare-ddns bundles: # - caddy-dns/cloudflare (DNS-01 ACME challenge) # - caddy-cloudflare-ip (trust Cloudflare proxy IPs for real client IPs) # - caddy-dynamicdns (keeps DNS records current if WAN IP changes) # ============================================================ caddy: image: serfriz/caddy-cloudflare-ddns:latest restart: unless-stopped networks: - foundry - owncloud - resume - media_external - media - famapp - authentik ports: - "80:80" - "443:443" environment: - CF_API_TOKEN=${CF_API_TOKEN} volumes: - /config/caddy/Caddyfile:/etc/caddy/Caddyfile - /config/caddy/site:/srv - /config/caddy/data:/data - /config/caddy/config:/config - /storage1/Books:/srv/Books # ============================================================ # MEDIA STACK # ============================================================ deluge: container_name: deluge image: lscr.io/linuxserver/deluge:latest restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - ${ROOT}/storage1/torrents:/downloads - ${ROOT}/config/deluge:/config ports: - "8112:8112" nzbget: container_name: nzbget image: lscr.io/linuxserver/nzbget:latest restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - ${ROOT}/storage1/complete:/downloads - ${ROOT}/config/nzbget:/config ports: - "6789:6789" sonarr: container_name: sonarr image: lscr.io/linuxserver/sonarr:latest restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - /etc/localtime:/etc/localtime:ro - ${ROOT}/config/sonarr:/config - ${ROOT}/storage1/Media/TV:/tv - ${ROOT}/storage1/Media/Anime:/anime - ${ROOT}/storage1/complete:/downloads ports: - "8989:8989" radarr: container_name: radarr image: lscr.io/linuxserver/radarr:latest restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - /etc/localtime:/etc/localtime:ro - ${ROOT}/config/radarr:/config - ${ROOT}/storage1/Media/Movies:/movies - ${ROOT}/storage1/complete:/downloads ports: - "7878:7878" bazarr: container_name: bazarr image: lscr.io/linuxserver/bazarr:latest restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} - UMASK_SET=022 volumes: - ${ROOT}/config/bazarr:/config - ${ROOT}/storage1/Media/Movies:/movies - ${ROOT}/storage1/Media/TV:/tv - ${ROOT}/storage1/Media/Anime:/anime ports: - "6767:6767" jackett: container_name: jackett image: ghcr.io/hotio/jackett:latest restart: unless-stopped networks: - media environment: - PUID=1000 - PGID=1000 - UMASK=002 - TZ=${TZ} volumes: - /config/jackett:/config ports: - "9117:9117" prowlarr: container_name: prowlarr image: ghcr.io/hotio/prowlarr:latest restart: unless-stopped networks: - media environment: - PUID=1000 - PGID=1000 - UMASK=002 - TZ=${TZ} volumes: - /config/prowlarr:/config ports: - "9696:9696" overseerr: container_name: overseerr image: sctx/overseerr:latest networks: - media_external restart: unless-stopped environment: - PUID=1000 - PGID=1000 - UMASK=002 - TZ=${TZ} volumes: - /config/overseerr:/app/config ports: - "5055:5055" # ============================================================ # MEDIA UTILITIES # ============================================================ tautulli: image: ghcr.io/linuxserver/tautulli:latest container_name: tautulli restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - ${ROOT}/config/tautulli:/config - ${ROOT}/storage1/PMS:/logs ports: - "8181:8181" nzbhydra2: image: ghcr.io/linuxserver/nzbhydra2:latest container_name: nzbhydra2 restart: "no" networks: - media environment: - PUID=${PUID} - PGID=${PGID} - TZ=${TZ} volumes: - ${ROOT}/config/nzbhydra:/config - ${ROOT}/storage1/complete:/downloads ports: - "5076:5076" qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent restart: unless-stopped networks: - media environment: - PUID=1000 - PGID=1000 - TZ=${TZ} - WEBUI_PORT=3232 - TORRENTING_PORT=6881 volumes: - /config/qbittorrent:/config - /storage1/complete:/downloads ports: - "3232:3232" - "6881:6881" - "6881:6881/udp" whisparr: container_name: whisparr image: ghcr.io/hotio/whisparr:latest restart: unless-stopped networks: - media environment: - PUID=1000 - PGID=1000 - UMASK=002 - TZ=${TZ} - WEBUI_PORTS=6969/tcp volumes: - /config/whisparr:/config - /storage1:/data ports: - "6969:6969" stash: image: stashapp/stash:latest container_name: stash restart: unless-stopped networks: - media logging: driver: json-file options: max-file: "10" max-size: "2m" environment: - PUID=1000 - PGID=1000 - STASH_STASH=/data/ - STASH_GENERATED=/generated/ - STASH_METADATA=/metadata/ - STASH_CACHE=/cache/ - STASH_PORT=6970 volumes: - /etc/localtime:/etc/localtime:ro - /config/stash:/root/.stash - /storage1/LinuxISOs:/data - /storage1/stash/metadata:/metadata - /storage1/stash/cache:/cache - /storage1/stash/blobs:/blobs - /storage1/stash/generated:/generated ports: - "6971:6970" # ============================================================ # OWNCLOUD STACK # ============================================================ owncloud: image: owncloud/server:latest container_name: owncloud_server restart: always networks: - owncloud ports: - "8080:8080" depends_on: mariadb: condition: service_healthy redis: condition: service_healthy environment: - OWNCLOUD_DOMAIN=${OWNCLOUD_DOMAIN} - OWNCLOUD_DB_TYPE=mysql - OWNCLOUD_DB_NAME=owncloud - OWNCLOUD_DB_USERNAME=${OWNCLOUD_DB_USER} - OWNCLOUD_DB_PASSWORD=${OWNCLOUD_DB_PASSWORD} - OWNCLOUD_DB_HOST=mariadb - OWNCLOUD_ADMIN_USERNAME=${OWNCLOUD_ADMIN_USER} - OWNCLOUD_ADMIN_PASSWORD=${OWNCLOUD_ADMIN_PASSWORD} - OWNCLOUD_MYSQL_UTF8MB4=true - OWNCLOUD_REDIS_ENABLED=true - OWNCLOUD_REDIS_HOST=redis healthcheck: test: ["CMD", "/usr/bin/healthcheck"] interval: 30s timeout: 10s retries: 5 volumes: - /config/owncloud:/mnt/data - /storage1/Books:/mnt/books - /mnt/dropbox:/mnt/data/files/dropbox - /storage1/ROMs:/mnt/roms mariadb: image: mariadb:latest container_name: owncloud_mariadb restart: always networks: - owncloud environment: - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} - MYSQL_USER=${OWNCLOUD_DB_USER} - MYSQL_PASSWORD=${OWNCLOUD_DB_PASSWORD} - MYSQL_DATABASE=owncloud command: ["--max-allowed-packet=128M", "--innodb-log-file-size=64M"] healthcheck: test: ["CMD", "mariadb-admin", "ping", "-u", "root", "--password=${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 5 volumes: - /config/mysql:/var/lib/mysql redis: image: redis:latest container_name: owncloud_redis restart: always networks: - owncloud command: ["--databases", "1"] healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 volumes: - /config/redis:/data # ============================================================ # REACTIVE RESUME STACK # ============================================================ postgres: image: postgres:16-alpine restart: unless-stopped networks: - resume volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_DB=postgres - POSTGRES_USER=postgres - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 10s timeout: 5s retries: 5 resume-minio: image: minio/minio:latest restart: unless-stopped networks: - resume command: server /data --console-address :9001 ports: - "9000:9000" - "9004:9001" volumes: - minio_data:/data environment: - MINIO_ROOT_USER=${MINIO_ROOT_USER} - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] interval: 30s timeout: 20s retries: 3 start_period: 30s chrome: image: ghcr.io/browserless/chromium:latest restart: unless-stopped networks: - resume environment: - TIMEOUT=10000 - CONCURRENT=10 - TOKEN=${CHROME_TOKEN} - EXIT_ON_HEALTH_FAILURE=true - PRE_REQUEST_HEALTH_CHECK=true app: image: amruthpillai/reactive-resume:latest restart: unless-stopped networks: - resume ports: - "3000:3000" depends_on: postgres: condition: service_healthy resume-minio: condition: service_healthy chrome: condition: service_started environment: - PORT=3000 - NODE_ENV=production - APP_URL=https://resume.ginnoir.com - STORAGE_URL=https://storage.j-costa.com/default - PRINTER_ENDPOINT=http://chrome:3000 - PRINTER_TOKEN=${CHROME_TOKEN} - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres - AUTH_SECRET=${RESUME_AUTH_SECRET} - MAIL_FROM=noreply@localhost - STORAGE_ENDPOINT=resume-minio - STORAGE_PORT=9000 - STORAGE_REGION=us-east-1 - STORAGE_BUCKET=default - STORAGE_ACCESS_KEY=${MINIO_ROOT_USER} - STORAGE_SECRET_KEY=${MINIO_ROOT_PASSWORD} - STORAGE_USE_SSL=false - STORAGE_SKIP_BUCKET_CHECK=false # ============================================================ # NOTIFICATIONS & RSS # ============================================================ ntfy: image: binwiederhier/ntfy:latest container_name: ntfy command: - serve restart: unless-stopped environment: - TZ=${TZ} - NTFY_DEFAULT_HOST=https://ntfy.ginnoir.com - NTFY_BASE_URL=https://ntfy.ginnoir.com - NTFY_CACHE_FILE=/var/lib/ntfy/cache.db - NTFY_AUTH_FILE=/var/lib/ntfy/auth.db - NTFY_AUTH_DEFAULT_ACCESS=read-write - NTFY_BEHIND_PROXY=true - NTFY_ATTACHMENT_CACHE_DIR=/var/lib/ntfy/attachments - NTFY_ENABLE_LOGIN=true user: 1000:1000 networks: - famapp volumes: - /config/ntfy/var/cache/ntfy:/var/cache/ntfy - /config/ntfy/etc/ntfy:/etc/ntfy - /config/ntfy/var/lib/ntfy:/var/lib/ntfy ports: - "8000:80" healthcheck: test: [ "CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1", ] interval: 60s timeout: 10s retries: 3 start_period: 40s freshrss: image: freshrss/freshrss:latest container_name: freshrss hostname: freshrss restart: unless-stopped networks: - famapp logging: options: max-size: 10m volumes: - /config/freshrss/data:/var/www/FreshRSS/data - /config/freshrss/extensions:/var/www/FreshRSS/extensions ports: - "9999:80" environment: - CRON_MIN=3,33 - TRUSTED_PROXY=172.16.0.1/12 192.168.0.1/16 - FRESHRSS_INSTALL=--default-user ginnoir --api-enabled --base-url https://freshrss.ginnoir.com - FRESHRSS_USER=--api-password ${FRESHRSS_API_PASSWORD} --email ${FRESHRSS_EMAIL} --password ${FRESHRSS_PASSWORD} --user ginnoir vigilant: image: ghcr.io/verifiedjoseph/vigilant:latest restart: unless-stopped environment: - VIGILANT_NOTIFICATION_SERVICE=ntfy - VIGILANT_NOTIFICATION_NTFY_URL=https://ntfy.ginnoir.com/ - VIGILANT_NOTIFICATION_NTFY_TOPIC=RSS volumes: - /config/vigilant/feeds.yaml:/app/feeds.yaml - cache:/app/cache security_opt: - no-new-privileges:true # ============================================================ # REMOTE ACCESS # ============================================================ hbbr: container_name: hbbr image: rustdesk/rustdesk-server:latest command: hbbr restart: unless-stopped ports: - "21117:21117" volumes: - /config/rustdesk/hbbr/data:/root hbbs: container_name: hbbs image: rustdesk/rustdesk-server:latest command: hbbs restart: unless-stopped ports: - "21115:21115" - "21116:21116" - "21116:21116/udp" volumes: - /config/rustdesk/hbbs/data:/root depends_on: - hbbr # ============================================================ # UTILITIES # ============================================================ 5etools: image: ghcr.io/5etools-mirror-3/5etools-src:latest restart: unless-stopped networks: - foundry ports: - "9009:80" watchtower: image: containrrr/watchtower:latest restart: unless-stopped environment: - WATCHTOWER_NOTIFICATION_SKIP_TITLE=true - WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.ginnoir.com/watchtower?title=WatchtowerUpdates volumes: - /var/run/docker.sock:/var/run/docker.sock # ============================================================ # FAMAPP # ============================================================ famapp: image: ${FAMAPP_IMAGE:-ghcr.io/ginnoir/famapp:latest} pull_policy: ${FAMAPP_PULL_POLICY:-always} restart: unless-stopped networks: - famapp environment: NODE_ENV: production AUTH_URL: ${AUTH_URL} DATABASE_URL: postgres://${FAMAPP_DB_USER}:${FAMAPP_DB_PASSWORD}@famapp-db:5432/${FAMAPP_DB_NAME} AUTH_SECRET: ${AUTH_SECRET} AUTH_OIDC_ISSUER: ${AUTH_OIDC_ISSUER} AUTH_OIDC_CLIENT_ID: ${AUTH_OIDC_CLIENT_ID} AUTH_OIDC_CLIENT_SECRET: ${AUTH_OIDC_CLIENT_SECRET} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY} VAPID_SUBJECT: ${VAPID_SUBJECT} NTFY_URL: ${NTFY_URL:-} NTFY_TOPIC: ${NTFY_TOPIC:-} LOG_LEVEL: ${LOG_LEVEL:-info} RUN_MIGRATIONS: ${RUN_MIGRATIONS:-true} MINIO_ENDPOINT: http://famapp-minio:9000 MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} MINIO_BUCKET: ${famapp_MINIO_BUCKET} OPENPLANTBOOK_CLIENT_ID: ${famapp_OPENPLANTBOOK_CLIENT_ID} OPENPLANTBOOK_CLIENT_SECRET: ${famapp_OPENPLANTBOOK_CLIENT_SECRET} ports: - "3010:3000" depends_on: famapp-db: condition: service_healthy famapp-minio: condition: service_healthy famapp-db: image: postgres:16-alpine restart: unless-stopped networks: - famapp environment: POSTGRES_USER: ${FAMAPP_DB_USER} POSTGRES_PASSWORD: ${FAMAPP_DB_PASSWORD} POSTGRES_DB: ${FAMAPP_DB_NAME} volumes: - famapp_db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${FAMAPP_DB_USER} -d ${FAMAPP_DB_NAME}"] interval: 10s timeout: 5s retries: 5 famapp-minio: image: minio/minio:latest command: server /data --console-address ":9003" restart: unless-stopped environment: MINIO_ROOT_USER: ${famapp_MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${famapp_MINIO_ROOT_PASSWORD} volumes: - garden_uploads:/data ports: - "9002:9000" - "9003:9003" healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] interval: 30s timeout: 20s retries: 3 start_period: 30s networks: - famapp # ============================================================ # AUTHENTIK # ============================================================ authentik-server: image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} restart: unless-stopped command: server networks: - authentik environment: AUTHENTIK_REDIS__HOST: authentik-redis AUTHENTIK_POSTGRESQL__HOST: authentik-db AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} AUTHENTIK_ERROR_REPORTING__ENABLED: "false" ports: - "9200:9000" depends_on: authentik-db: condition: service_healthy authentik-redis: condition: service_healthy authentik-worker: image: ghcr.io/goauthentik/server:${AUTHENTIK_IMAGE_TAG:-2024.12.3} restart: unless-stopped command: worker networks: - authentik environment: AUTHENTIK_REDIS__HOST: authentik-redis AUTHENTIK_POSTGRESQL__HOST: authentik-db AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_DB_USER:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD} AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_DB_NAME:-authentik} AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY} AUTHENTIK_ERROR_REPORTING__ENABLED: "false" depends_on: authentik-db: condition: service_healthy authentik-redis: condition: service_healthy authentik-db: image: postgres:16-alpine restart: unless-stopped networks: - authentik environment: POSTGRES_USER: ${AUTHENTIK_DB_USER:-authentik} POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASSWORD} POSTGRES_DB: ${AUTHENTIK_DB_NAME:-authentik} volumes: - authentik_db_data:/var/lib/postgresql/data healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${AUTHENTIK_DB_USER:-authentik} -d ${AUTHENTIK_DB_NAME:-authentik}", ] interval: 10s timeout: 5s retries: 5 authentik-redis: image: redis:7-alpine restart: unless-stopped networks: - authentik command: --save 60 1 --loglevel warning volumes: - authentik_redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: foundry: owncloud: resume: media_external: media: famapp: authentik: volumes: minio_data: postgres_data: cache: famapp_db_data: authentik_db_data: authentik_redis_data: garden_uploads: