Files
Stephen Klein a7c38cd58d Fix critical and high bugs B1-B6
- B1: Replace recursive get_timezone_mount/get_gpu_devices with literal YAML strings
- B3: Expand /home/*/hops glob via compgen -G instead of storing as array literal;
  fix eval echo ~$SUDO_USER -> getent passwd in uninstall
- B4: Correct services source path in setup_firewall (hops_service_definitions.sh -> services)
- B5: Replace all ((x++)) with x=$((x + 1)) to avoid set -e abort on zero pre-increment
- B6: Add Linux-only guard at top of hops entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-10 21:54:46 -04:00

1365 lines
37 KiB
Bash
Executable File

#!/bin/bash
# HOPS Service Definitions
# Contains all Docker Compose service configurations
# Version: 1.0.0
# This script provides functions to generate Docker Compose service definitions
# Usage: Source this script and call generate_service_definition <service_name>
# --------------------------------------------
# COMMON CONFIGURATIONS
# --------------------------------------------
# Common environment variables for LinuxServer containers
get_linuxserver_env() {
cat <<EOF
- PUID=\${PUID}
- PGID=\${PGID}
- TZ=\${TZ}
- UMASK=002
EOF
}
# Get timezone mount path for current platform
get_timezone_mount() {
if [[ "$(uname -s)" == "Darwin" ]]; then
echo ""
else
echo " - /etc/localtime:/etc/localtime:ro"
fi
}
# Get GPU device access for current platform
get_gpu_devices() {
if [[ "$(uname -s)" == "Darwin" ]]; then
echo ""
else
printf " devices:\n - /dev/dri:/dev/dri\n"
fi
}
# Common restart policy
get_restart_policy() {
echo " restart: unless-stopped"
}
# Common network configuration
get_homelab_network() {
cat <<EOF
networks:
- homelab
EOF
}
# Common healthcheck for web services
get_web_healthcheck() {
local port=$1
local path=${2:-"/"}
cat <<EOF
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:$port$path || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
EOF
}
# --------------------------------------------
# MEDIA MANAGEMENT SERVICES (*ARR STACK)
# --------------------------------------------
generate_sonarr() {
cat <<EOF
sonarr:
image: lscr.io/linuxserver/sonarr:latest
container_name: sonarr
$(get_restart_policy)
ports:
- "8989:8989"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/sonarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 8989)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(\`sonarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.sonarr.entrypoints=websecure"
- "traefik.http.routers.sonarr.tls.certresolver=letsencrypt"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
EOF
}
generate_radarr() {
cat <<EOF
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
$(get_restart_policy)
ports:
- "7878:7878"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/radarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 7878)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(\`radarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.radarr.entrypoints=websecure"
- "traefik.http.routers.radarr.tls.certresolver=letsencrypt"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
EOF
}
generate_lidarr() {
cat <<EOF
lidarr:
image: lscr.io/linuxserver/lidarr:latest
container_name: lidarr
$(get_restart_policy)
ports:
- "8686:8686"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/lidarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 8686)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.lidarr.rule=Host(\`lidarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.lidarr.entrypoints=websecure"
- "traefik.http.routers.lidarr.tls.certresolver=letsencrypt"
- "traefik.http.services.lidarr.loadbalancer.server.port=8686"
EOF
}
generate_readarr() {
cat <<EOF
readarr:
image: lscr.io/linuxserver/readarr:develop
container_name: readarr
$(get_restart_policy)
ports:
- "8787:8787"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/readarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 8787)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.readarr.rule=Host(\`readarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.readarr.entrypoints=websecure"
- "traefik.http.routers.readarr.tls.certresolver=letsencrypt"
- "traefik.http.services.readarr.loadbalancer.server.port=8787"
EOF
}
generate_bazarr() {
cat <<EOF
bazarr:
image: lscr.io/linuxserver/bazarr:latest
container_name: bazarr
$(get_restart_policy)
ports:
- "6767:6767"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/bazarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 6767)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.bazarr.rule=Host(\`bazarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.bazarr.entrypoints=websecure"
- "traefik.http.routers.bazarr.tls.certresolver=letsencrypt"
- "traefik.http.services.bazarr.loadbalancer.server.port=6767"
EOF
}
generate_prowlarr() {
cat <<EOF
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
container_name: prowlarr
$(get_restart_policy)
ports:
- "9696:9696"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/prowlarr:/config
$(get_timezone_mount)
$(get_web_healthcheck 9696)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(\`prowlarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.prowlarr.entrypoints=websecure"
- "traefik.http.routers.prowlarr.tls.certresolver=letsencrypt"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
EOF
}
generate_tdarr() {
cat <<EOF
tdarr:
image: ghcr.io/haveagitgat/tdarr:latest
container_name: tdarr
$(get_restart_policy)
ports:
- "8265:8265"
- "8266:8266"
environment:
$(get_linuxserver_env)
- serverIP=0.0.0.0
- serverPort=8266
- webUIPort=8265
- internalNode=true
- nodeName=MainNode
volumes:
- \${CONFIG_ROOT}/tdarr/server:/app/server
- \${CONFIG_ROOT}/tdarr/configs:/app/configs
- \${CONFIG_ROOT}/tdarr/logs:/app/logs
- \${DATA_ROOT}/media:/media
- \${DATA_ROOT}/downloads/tdarr:/temp
$(get_gpu_devices)
$(get_web_healthcheck 8265)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.tdarr.rule=Host(\`tdarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.tdarr.entrypoints=websecure"
- "traefik.http.routers.tdarr.tls.certresolver=letsencrypt"
- "traefik.http.services.tdarr.loadbalancer.server.port=8265"
EOF
}
generate_huntarr() {
cat <<EOF
huntarr:
image: ghcr.io/plexguide/huntarr:latest
container_name: huntarr
$(get_restart_policy)
ports:
- "9705:9705"
environment:
$(get_linuxserver_env)
- BASE_URL=\${BASE_URL:-}
volumes:
- \${CONFIG_ROOT}/huntarr:/config
- \${DATA_ROOT}:/data
$(get_timezone_mount)
$(get_web_healthcheck 9705 "/health")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.huntarr.rule=Host(\`huntarr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.huntarr.entrypoints=websecure"
- "traefik.http.routers.huntarr.tls.certresolver=letsencrypt"
- "traefik.http.services.huntarr.loadbalancer.server.port=9705"
EOF
}
# --------------------------------------------
# DOWNLOAD CLIENTS
# --------------------------------------------
generate_qbittorrent() {
cat <<EOF
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
$(get_restart_policy)
ports:
- "8082:8082"
- "6881:6881"
- "6881:6881/udp"
environment:
$(get_linuxserver_env)
- WEBUI_PORT=8082
volumes:
- \${CONFIG_ROOT}/qbittorrent:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents
$(get_timezone_mount)
$(get_web_healthcheck 8082)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.qbittorrent.rule=Host(\`qbittorrent.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.qbittorrent.entrypoints=websecure"
- "traefik.http.routers.qbittorrent.tls.certresolver=letsencrypt"
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8082"
EOF
}
generate_transmission() {
cat <<EOF
transmission:
image: lscr.io/linuxserver/transmission:latest
container_name: transmission
$(get_restart_policy)
ports:
- "9091:9091"
- "51413:51413"
- "51413:51413/udp"
environment:
$(get_linuxserver_env)
- USER=admin
- PASS=\${DEFAULT_ADMIN_PASSWORD}
volumes:
- \${CONFIG_ROOT}/transmission:/config
- \${DATA_ROOT}/downloads/torrents:/data/torrents
- \${DATA_ROOT}/downloads/torrents/watch:/watch
$(get_timezone_mount)
$(get_web_healthcheck 9091 "/transmission/web/")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.transmission.rule=Host(\`transmission.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.transmission.entrypoints=websecure"
- "traefik.http.routers.transmission.tls.certresolver=letsencrypt"
- "traefik.http.services.transmission.loadbalancer.server.port=9091"
EOF
}
generate_nzbget() {
cat <<EOF
nzbget:
image: lscr.io/linuxserver/nzbget:latest
container_name: nzbget
$(get_restart_policy)
ports:
- "6789:6789"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/nzbget:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet
$(get_timezone_mount)
$(get_web_healthcheck 6789)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.nzbget.rule=Host(\`nzbget.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.nzbget.entrypoints=websecure"
- "traefik.http.routers.nzbget.tls.certresolver=letsencrypt"
- "traefik.http.services.nzbget.loadbalancer.server.port=6789"
EOF
}
generate_sabnzbd() {
cat <<EOF
sabnzbd:
image: lscr.io/linuxserver/sabnzbd:latest
container_name: sabnzbd
$(get_restart_policy)
ports:
- "8080:8080"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/sabnzbd:/config
- \${DATA_ROOT}/downloads/usenet:/data/usenet
$(get_timezone_mount)
$(get_web_healthcheck 8080)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.sabnzbd.rule=Host(\`sabnzbd.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.sabnzbd.entrypoints=websecure"
- "traefik.http.routers.sabnzbd.tls.certresolver=letsencrypt"
- "traefik.http.services.sabnzbd.loadbalancer.server.port=8080"
EOF
}
# --------------------------------------------
# MEDIA SERVERS
# --------------------------------------------
generate_jellyfin() {
cat <<EOF
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
$(get_restart_policy)
ports:
- "8096:8096"
- "8920:8920" # HTTPS
- "7359:7359/udp" # Auto-discovery
- "1900:1900/udp" # DLNA
environment:
- JELLYFIN_PublishedServerUrl=http://\${DOMAIN:-localhost}:8096
volumes:
- \${CONFIG_ROOT}/jellyfin:/config
- \${CONFIG_ROOT}/jellyfin/cache:/cache
- \${DATA_ROOT}/media:/media:ro
$(get_timezone_mount)
$(get_gpu_devices)
group_add:
- "109" # render group for GPU access
$(get_web_healthcheck 8096 "/health")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(\`jellyfin.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
EOF
}
generate_plex() {
cat <<EOF
plex:
image: plexinc/pms-docker:latest
container_name: plex
$(get_restart_policy)
ports:
- "32400:32400"
- "1900:1900/udp" # DLNA
- "3005:3005" # Plex Companion
- "5353:5353/udp" # Bonjour/Avahi
- "8324:8324" # Roku via Plex Companion
- "32410:32410/udp" # GDM Network Discovery
- "32412:32412/udp" # GDM Network Discovery
- "32413:32413/udp" # GDM Network Discovery
- "32414:32414/udp" # GDM Network Discovery
- "32469:32469" # Plex DLNA Server
environment:
- PLEX_CLAIM=\${PLEX_CLAIM_TOKEN:-}
- PLEX_UID=\${PUID}
- PLEX_GID=\${PGID}
- TZ=\${TZ}
- HOSTNAME=PlexServer
- ADVERTISE_IP=http://\${DOMAIN:-localhost}:32400/
volumes:
- \${CONFIG_ROOT}/plex:/config
- \${CONFIG_ROOT}/plex/transcode:/transcode
- \${DATA_ROOT}/media:/data:ro
$(get_timezone_mount)
$(get_gpu_devices)
$(get_web_healthcheck 32400 "/identity")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.plex.rule=Host(\`plex.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.plex.entrypoints=websecure"
- "traefik.http.routers.plex.tls.certresolver=letsencrypt"
- "traefik.http.services.plex.loadbalancer.server.port=32400"
EOF
}
generate_emby() {
cat <<EOF
emby:
image: emby/embyserver:latest
container_name: emby
$(get_restart_policy)
ports:
- "8097:8096"
- "8920:8920" # HTTPS
environment:
- UID=\${PUID}
- GID=\${PGID}
- GIDLIST=\${PGID}
volumes:
- \${CONFIG_ROOT}/emby:/config
- \${DATA_ROOT}/media:/data:ro
$(get_timezone_mount)
$(get_gpu_devices)
$(get_web_healthcheck 8096)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.emby.rule=Host(\`emby.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.emby.entrypoints=websecure"
- "traefik.http.routers.emby.tls.certresolver=letsencrypt"
- "traefik.http.services.emby.loadbalancer.server.port=8096"
EOF
}
generate_jellystat() {
cat <<EOF
jellystat-db:
image: postgres:15
container_name: jellystat-db
$(get_restart_policy)
environment:
- POSTGRES_DB=jfstat
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=\${DEFAULT_DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- database
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
jellystat:
image: cyfershepard/jellystat:latest
container_name: jellystat
$(get_restart_policy)
ports:
- "3000:3000"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=\${DEFAULT_DB_PASSWORD}
- POSTGRES_IP=jellystat-db
- POSTGRES_PORT=5432
- JWT_SECRET=\${DEFAULT_ADMIN_PASSWORD}
volumes:
- \${CONFIG_ROOT}/jellystat/backup-data:/app/backend/backup-data
depends_on:
- jellystat-db
$(get_web_healthcheck 3000)
networks:
- homelab
- database
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellystat.rule=Host(\`jellystat.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.jellystat.entrypoints=websecure"
- "traefik.http.routers.jellystat.tls.certresolver=letsencrypt"
- "traefik.http.services.jellystat.loadbalancer.server.port=3000"
EOF
}
# --------------------------------------------
# REQUEST MANAGEMENT
# --------------------------------------------
generate_overseerr() {
cat <<EOF
overseerr:
image: sctx/overseerr:latest
container_name: overseerr
$(get_restart_policy)
ports:
- "5055:5055"
environment:
- LOG_LEVEL=debug
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/overseerr:/app/config
$(get_timezone_mount)
$(get_web_healthcheck 5055)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.overseerr.rule=Host(\`overseerr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.overseerr.entrypoints=websecure"
- "traefik.http.routers.overseerr.tls.certresolver=letsencrypt"
- "traefik.http.services.overseerr.loadbalancer.server.port=5055"
EOF
}
generate_jellyseerr() {
cat <<EOF
jellyseerr:
image: fallenbagel/jellyseerr:latest
container_name: jellyseerr
$(get_restart_policy)
ports:
- "5056:5055"
environment:
- LOG_LEVEL=debug
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/jellyseerr:/app/config
$(get_timezone_mount)
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:5055/ || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 120s
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(\`jellyseerr.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.jellyseerr.entrypoints=websecure"
- "traefik.http.routers.jellyseerr.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
EOF
}
generate_ombi() {
cat <<EOF
ombi:
image: lscr.io/linuxserver/ombi:latest
container_name: ombi
$(get_restart_policy)
ports:
- "3579:3579"
environment:
$(get_linuxserver_env)
volumes:
- \${CONFIG_ROOT}/ombi:/config
$(get_timezone_mount)
$(get_web_healthcheck 3579)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.ombi.rule=Host(\`ombi.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.ombi.entrypoints=websecure"
- "traefik.http.routers.ombi.tls.certresolver=letsencrypt"
- "traefik.http.services.ombi.loadbalancer.server.port=3579"
EOF
}
# --------------------------------------------
# REVERSE PROXY & SECURITY
# --------------------------------------------
generate_traefik() {
cat <<EOF
traefik:
image: traefik:v3.0
container_name: traefik
$(get_restart_policy)
ports:
- "80:80"
- "443:443"
- "8080:8080" # Dashboard
environment:
- TRAEFIK_API_DASHBOARD=true
- TRAEFIK_API_INSECURE=true
- TRAEFIK_ENTRYPOINTS_WEB_ADDRESS=:80
- TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS=:443
- TRAEFIK_PROVIDERS_DOCKER=true
- TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT=false
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL=\${ACME_EMAIL:-admin@localhost}
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE=/letsencrypt/acme.json
- TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT=web
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- \${CONFIG_ROOT}/traefik/letsencrypt:/letsencrypt
- \${CONFIG_ROOT}/traefik:/etc/traefik
networks:
- traefik
- homelab
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(\`traefik.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.traefik.entrypoints=websecure"
- "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
EOF
}
generate_nginx-proxy-manager() {
cat <<EOF
nginx-proxy-manager:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
$(get_restart_policy)
ports:
- "80:80"
- "443:443"
- "81:81" # Admin interface
environment:
- DB_SQLITE_FILE=/data/database.sqlite
volumes:
- \${CONFIG_ROOT}/nginx-proxy-manager/data:/data
- \${CONFIG_ROOT}/nginx-proxy-manager/letsencrypt:/etc/letsencrypt
$(get_web_healthcheck 81)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.npm.rule=Host(\`npm.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.npm.entrypoints=websecure"
- "traefik.http.routers.npm.tls.certresolver=letsencrypt"
- "traefik.http.services.npm.loadbalancer.server.port=81"
EOF
}
generate_caddy() {
cat <<EOF
# Caddy Reverse Proxy
# NOTE: HOPS provides the container only - Caddyfile configuration is user responsibility
# Place your Caddyfile in \${CONFIG_ROOT}/caddy/Caddyfile
# Documentation: https://caddyserver.com/docs/
caddy:
image: caddy:latest
container_name: caddy
$(get_restart_policy)
ports:
- "80:80"
- "443:443"
- "2019:2019" # Admin API
environment:
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/caddy/Caddyfile:/etc/caddy/Caddyfile
- \${CONFIG_ROOT}/caddy/site:/srv
- \${CONFIG_ROOT}/caddy/data:/data
- \${CONFIG_ROOT}/caddy/config:/config
$(get_web_healthcheck 2019 "/config/")
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.caddy.rule=Host(\`caddy.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.caddy.entrypoints=websecure"
- "traefik.http.routers.caddy.tls.certresolver=letsencrypt"
- "traefik.http.services.caddy.loadbalancer.server.port=2019"
EOF
}
generate_authelia() {
cat <<EOF
authelia:
image: authelia/authelia:latest
container_name: authelia
$(get_restart_policy)
ports:
- "9091:9091"
environment:
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/authelia:/config
command: --config /config/configuration.yml
$(get_web_healthcheck 9091)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(\`auth.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.authelia.entrypoints=websecure"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
EOF
}
# --------------------------------------------
# MONITORING & MANAGEMENT
# --------------------------------------------
generate_portainer() {
cat <<EOF
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
$(get_restart_policy)
ports:
- "9000:9000"
- "9443:9443" # HTTPS
environment:
- TZ=\${TZ}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- \${CONFIG_ROOT}/portainer:/data
command: --admin-password-file /data/admin_password
$(get_web_healthcheck 9000)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.portainer.rule=Host(\`portainer.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.portainer.entrypoints=websecure"
- "traefik.http.routers.portainer.tls.certresolver=letsencrypt"
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
EOF
}
generate_watchtower() {
cat <<EOF
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
$(get_restart_policy)
environment:
- TZ=\${TZ}
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM daily
- WATCHTOWER_NOTIFICATIONS=email
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=\${WATCHTOWER_EMAIL_FROM:-watchtower@localhost}
- WATCHTOWER_NOTIFICATION_EMAIL_TO=\${WATCHTOWER_EMAIL_TO:-admin@localhost}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER=\${WATCHTOWER_EMAIL_SERVER:-}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=\${WATCHTOWER_EMAIL_PORT:-587}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=\${WATCHTOWER_EMAIL_USER:-}
- WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=\${WATCHTOWER_EMAIL_PASSWORD:-}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
networks:
- homelab
EOF
}
generate_uptime-kuma() {
cat <<EOF
uptime-kuma:
image: louislam/uptime-kuma:1
container_name: uptime-kuma
$(get_restart_policy)
ports:
- "3001:3001"
environment:
- TZ=\${TZ}
volumes:
- \${CONFIG_ROOT}/uptime-kuma:/app/data
$(get_web_healthcheck 3001)
$(get_homelab_network)
labels:
- "traefik.enable=true"
- "traefik.http.routers.uptime-kuma.rule=Host(\`uptime.\${DOMAIN:-localhost}\`)"
- "traefik.http.routers.uptime-kuma.entrypoints=websecure"
- "traefik.http.routers.uptime-kuma.tls.certresolver=letsencrypt"
- "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001"
EOF
}
# --------------------------------------------
# DATABASE SERVICES
# --------------------------------------------
generate_postgres() {
cat <<EOF
postgres:
image: postgres:15-alpine
container_name: postgres
$(get_restart_policy)
ports:
- "5432:5432"
environment:
- POSTGRES_DB=homelab
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=\${DEFAULT_DB_PASSWORD}
- PGDATA=/var/lib/postgresql/data/pgdata
volumes:
- postgres_data:/var/lib/postgresql/data
- \${CONFIG_ROOT}/postgres/init:/docker-entrypoint-initdb.d
networks:
- database
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
EOF
}
generate_redis() {
cat <<EOF
redis:
image: redis:7-alpine
container_name: redis
$(get_restart_policy)
ports:
- "6379:6379"
environment:
- TZ=\${TZ}
volumes:
- redis_data:/data
- \${CONFIG_ROOT}/redis/redis.conf:/usr/local/etc/redis/redis.conf
command: redis-server /usr/local/etc/redis/redis.conf --requirepass \${DEFAULT_DB_PASSWORD}
networks:
- database
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
EOF
}
# --------------------------------------------
# UTILITY FUNCTIONS
# --------------------------------------------
# Generate service definition based on service name
generate_service_definition() {
local service_name="$1"
case "$service_name" in
# Media Management (*arr stack)
"sonarr") generate_sonarr ;;
"radarr") generate_radarr ;;
"lidarr") generate_lidarr ;;
"readarr") generate_readarr ;;
"bazarr") generate_bazarr ;;
"prowlarr") generate_prowlarr ;;
"tdarr") generate_tdarr ;;
"huntarr") generate_huntarr ;;
# Download Clients
"qbittorrent") generate_qbittorrent ;;
"transmission") generate_transmission ;;
"nzbget") generate_nzbget ;;
"sabnzbd") generate_sabnzbd ;;
# Media Servers
"jellyfin") generate_jellyfin ;;
"plex") generate_plex ;;
"emby") generate_emby ;;
"jellystat") generate_jellystat ;;
# Request Management
"overseerr") generate_overseerr ;;
"jellyseerr") generate_jellyseerr ;;
"ombi") generate_ombi ;;
# Reverse Proxy & Security
"traefik") generate_traefik ;;
"nginx-proxy-manager") generate_nginx-proxy-manager ;;
"caddy") generate_caddy ;;
"authelia") generate_authelia ;;
# Monitoring & Management
"portainer") generate_portainer ;;
"watchtower") generate_watchtower ;;
"uptime-kuma") generate_uptime-kuma ;;
# Database Services
"postgres") generate_postgres ;;
"redis") generate_redis ;;
*)
echo "# Service '$service_name' not found"
return 1
;;
esac
}
# Generate complete docker-compose.yml file
generate_complete_compose() {
local services=("$@")
local compose_file="docker-compose.yml"
# Start with the base compose structure
cat > "$compose_file" <<EOF
networks:
homelab:
driver: bridge
ipam:
config:
- subnet: \${DOCKER_SUBNET:-172.20.0.0/16}
traefik:
external: true
name: traefik
database:
driver: bridge
volumes:
postgres_data:
redis_data:
services:
EOF
# Add each selected service
for service in "${services[@]}"; do
echo " # --- $service ---" >> "$compose_file"
if generate_service_definition "$service" >> "$compose_file"; then
echo "✅ Added service: $service"
else
echo "⚠️ Failed to add service: $service"
fi
done
echo "📝 Docker Compose file generated with ${#services[@]} services"
}
# Create service-specific configuration directories and files
create_service_configs() {
local services=("$@")
local config_root="${CONFIG_ROOT:-/opt/appdata}"
for service in "${services[@]}"; do
local service_dir="$config_root/$service"
mkdir -p "$service_dir"
case "$service" in
"portainer")
# Create admin password file for Portainer
if [[ -n "${DEFAULT_ADMIN_PASSWORD}" ]] && command -v htpasswd &>/dev/null; then
echo -n "${DEFAULT_ADMIN_PASSWORD}" | htpasswd -niB admin | cut -d: -f2 > "$service_dir/admin_password"
fi
;;
"traefik")
mkdir -p "$service_dir/letsencrypt"
mkdir -p "$service_dir/dynamic"
# Create basic traefik config
cat > "$service_dir/traefik.yml" <<EOF
api:
dashboard: true
insecure: true
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ":443"
providers:
docker:
endpoint: "unix://$(get_docker_socket_path)"
exposedByDefault: false
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
letsencrypt:
acme:
email: \${ACME_EMAIL:-admin@localhost}
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web
EOF
;;
"redis")
# Create basic Redis config
cat > "$service_dir/redis.conf" <<EOF
# Redis configuration for HOPS
bind 0.0.0.0
port 6379
timeout 300
keepalive 60
maxmemory 256mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000
EOF
;;
"postgres")
mkdir -p "$service_dir/init"
# Create initialization script for additional databases
cat > "$service_dir/init/create_databases.sql" <<EOF
-- Create additional databases for services that need them
CREATE DATABASE IF NOT EXISTS jellyfin_db;
CREATE DATABASE IF NOT EXISTS authelia_db;
EOF
;;
"authelia")
# Create basic Authelia configuration
cat > "$service_dir/configuration.yml" <<EOF
# Authelia configuration
host: 0.0.0.0
port: 9091
log_level: info
theme: dark
jwt_secret: \${DEFAULT_ADMIN_PASSWORD}
default_redirection_url: https://\${DOMAIN:-localhost}
server:
host: 0.0.0.0
port: 9091
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2id
iterations: 1
salt_length: 16
parallelism: 8
memory: 64
access_control:
default_policy: deny
rules:
- domain: "*.localhost"
policy: one_factor
session:
name: authelia_session
secret: \${DEFAULT_ADMIN_PASSWORD}
expiration: 3600
inactivity: 300
domain: localhost
regulation:
max_retries: 3
find_time: 120
ban_time: 300
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
EOF
# Create users database
cat > "$service_dir/users_database.yml" <<EOF
users:
admin:
displayname: "Administrator"
password: "\$argon2id\$v=19\$m=65536,t=3,p=4\$c29tZXNhbHQ\$MNzk5BtR2vUhrp6qQEjRNw" # password
email: admin@localhost
groups:
- admins
- dev
EOF
;;
esac
# Set proper ownership if running as root
if [[ $EUID -eq 0 && -n "${PUID}" && -n "${PGID}" ]]; then
chown -R "${PUID}:${PGID}" "$service_dir" 2>/dev/null || true
fi
done
}
# Get service dependencies
get_service_dependencies() {
local service="$1"
case "$service" in
"jellystat")
echo "postgres"
;;
"authelia")
echo "redis"
;;
*)
# No dependencies
;;
esac
}
# Get all dependencies for a list of services
resolve_dependencies() {
local services=("$@")
local all_services=()
local processed=()
# Add services and their dependencies
for service in "${services[@]}"; do
if [[ ! " ${processed[*]} " =~ " ${service} " ]]; then
all_services+=("$service")
processed+=("$service")
# Add dependencies
local deps=$(get_service_dependencies "$service")
for dep in $deps; do
if [[ ! " ${processed[*]} " =~ " ${dep} " ]]; then
all_services+=("$dep")
processed+=("$dep")
fi
done
fi
done
echo "${all_services[@]}"
}
# Get service ports for conflict checking
get_service_ports() {
local service="$1"
case "$service" in
"sonarr") echo "8989" ;;
"radarr") echo "7878" ;;
"lidarr") echo "8686" ;;
"readarr") echo "8787" ;;
"bazarr") echo "6767" ;;
"prowlarr") echo "9696" ;;
"tdarr") echo "8265 8266" ;;
"qbittorrent") echo "8082 6881 6881/udp" ;;
"transmission") echo "9091 51413 51413/udp" ;;
"nzbget") echo "6789" ;;
"sabnzbd") echo "8080" ;;
"jellyfin") echo "8096 8920 7359/udp 1900/udp" ;;
"plex") echo "32400 1900/udp 3005 5353/udp 8324 32410/udp 32412/udp 32413/udp 32414/udp 32469" ;;
"emby") echo "8097 8920" ;;
"jellystat") echo "3000" ;;
"overseerr") echo "5055" ;;
"jellyseerr") echo "5056" ;;
"ombi") echo "3579" ;;
"traefik") echo "80 443 8080" ;;
"nginx-proxy-manager") echo "80 443 81" ;;
"caddy") echo "80 443 2019" ;;
"authelia") echo "9091" ;;
"portainer") echo "9000 9443" ;;
"uptime-kuma") echo "3001" ;;
"postgres") echo "5432" ;;
"redis") echo "6379" ;;
*) echo "" ;;
esac
}
# Print service summary
print_service_summary() {
local services=("$@")
echo "==========================="
echo "HOPS SERVICE SUMMARY"
echo "==========================="
echo "Selected services: ${#services[@]}"
echo
# Categorize services
local media_mgmt=()
local download_clients=()
local media_servers=()
local request_mgmt=()
local proxy_security=()
local monitoring=()
local databases=()
for service in "${services[@]}"; do
case "$service" in
sonarr|radarr|lidarr|readarr|bazarr|prowlarr|tdarr)
media_mgmt+=("$service") ;;
qbittorrent|transmission|nzbget|sabnzbd)
download_clients+=("$service") ;;
jellyfin|plex|emby|jellystat)
media_servers+=("$service") ;;
overseerr|jellyseerr|ombi)
request_mgmt+=("$service") ;;
traefik|nginx-proxy-manager|caddy|authelia)
proxy_security+=("$service") ;;
portainer|watchtower|uptime-kuma)
monitoring+=("$service") ;;
postgres|redis)
databases+=("$service") ;;
esac
done
[[ ${#media_mgmt[@]} -gt 0 ]] && echo "📺 Media Management: ${media_mgmt[*]}"
[[ ${#download_clients[@]} -gt 0 ]] && echo "⬇️ Download Clients: ${download_clients[*]}"
[[ ${#media_servers[@]} -gt 0 ]] && echo "🎞️ Media Servers: ${media_servers[*]}"
[[ ${#request_mgmt[@]} -gt 0 ]] && echo "🎛️ Request Management: ${request_mgmt[*]}"
[[ ${#proxy_security[@]} -gt 0 ]] && echo "🔒 Proxy & Security: ${proxy_security[*]}"
[[ ${#monitoring[@]} -gt 0 ]] && echo "📈 Monitoring: ${monitoring[*]}"
[[ ${#databases[@]} -gt 0 ]] && echo "🗄️ Databases: ${databases[*]}"
echo
}
# Main function to generate everything
generate_hops_stack() {
local services=("$@")
if [[ ${#services[@]} -eq 0 ]]; then
echo "Error: No services specified"
return 1
fi
echo "Generating HOPS stack for: ${services[*]}"
# Resolve dependencies
local all_services=($(resolve_dependencies "${services[@]}"))
echo "Services with dependencies: ${all_services[*]}"
# Print summary
print_service_summary "${all_services[@]}"
# Generate docker-compose.yml
generate_complete_compose "${all_services[@]}"
# Create service configurations
create_service_configs "${all_services[@]}"
echo "HOPS stack generation complete!"
}
# Helper function to list all available services
list_available_services() {
echo "Available HOPS services:"
echo
echo "📺 MEDIA MANAGEMENT:"
echo " sonarr radarr lidarr readarr bazarr prowlarr tdarr"
echo
echo "⬇️ DOWNLOAD CLIENTS:"
echo " qbittorrent transmission nzbget sabnzbd"
echo
echo "🎞️ MEDIA SERVERS:"
echo " jellyfin plex emby jellystat"
echo
echo "🎛️ REQUEST MANAGEMENT:"
echo " overseerr jellyseerr ombi"
echo
echo "🔒 PROXY & SECURITY:"
echo " traefik nginx-proxy-manager caddy authelia"
echo
echo "📈 MONITORING:"
echo " portainer watchtower uptime-kuma"
echo
echo "🗄️ DATABASES:"
echo " postgres redis"
}
# Usage information
show_usage() {
cat <<EOF
HOPS Service Definitions Script v1.0.0
Usage:
source services
generate_hops_stack service1 service2 service3...
Examples:
generate_hops_stack sonarr radarr jellyfin qbittorrent
generate_hops_stack plex overseerr traefik portainer
Functions:
generate_service_definition <service> - Generate single service
generate_complete_compose <services> - Generate docker-compose.yml
create_service_configs <services> - Create config directories
list_available_services - Show all available services
resolve_dependencies <services> - Add required dependencies
get_service_ports <service> - Get service port mappings
EOF
}