Files
hops/services
T
Stephen Klein cd30d45fbf Add Caddy reverse proxy support to HOPS
### New Features
- Added Caddy reverse proxy as a service option
- Proper Docker container configuration with ports 80, 443, 2019
- Health check monitoring via Caddy admin API
- Volume mounts for Caddyfile, site content, and data persistence
- Integration with existing service selection and categorization

### Configuration Scope
- HOPS provides: Container setup, volume mounts, networking, health checks
- User provides: Caddyfile configuration, routing rules, SSL settings
- Clear documentation about configuration responsibilities
- Example Caddyfile provided in README

### Documentation Updates
- Updated README.md with Caddy service listing and configuration guide
- Updated CLAUDE.md with Caddy in supported services
- Added comprehensive configuration scope documentation
- Updated version references to 3.2.0

### Technical Implementation
- Added generate_caddy() function to services file
- Integrated Caddy into service selection switch
- Added port mapping for conflict detection (80, 443, 2019)
- Categorized under proxy & security services
- Added to available services listing

This addition provides users with another reverse proxy option while
maintaining HOPS' philosophy of providing infrastructure while allowing
users to maintain control over their specific configuration needs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 05:29:25 -04:00

1371 lines
37 KiB
Bash
Executable File

#!/bin/bash
# HOPS Service Definitions
# Contains all Docker Compose service configurations
# Version: 3.2.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
# macOS doesn't need timezone mount, use TZ environment variable
echo ""
else
# Linux timezone mount
echo "$(get_timezone_mount)"
fi
}
# Get GPU device access for current platform
get_gpu_devices() {
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS doesn't support GPU passthrough to Docker containers
echo ""
else
# Linux GPU device access
cat <<EOF
$(get_gpu_devices)
EOF
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 v3.2.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
}