Migrate admin services to jefflix.lol and add Hetzner Storage Box integration

- Move sonarr, radarr, lidarr, prowlarr to *.jefflix.lol
- Move jellyseerr (requests) and qbittorrent (downloads) to jefflix.lol
- Add Hetzner Storage Box SSHFS mount scripts for u521871
- Add media-archive.sh for content migration between local and Hetzner
- Update all volume mounts to use /mnt/hetzner-media for media storage
- Update README with new URLs and tiered storage documentation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2025-12-21 00:49:01 -05:00
parent 3b85484d65
commit 4113ccf83d
6 changed files with 691 additions and 39 deletions

View File

@ -33,3 +33,9 @@ VPN_COUNTRY=Germany
# Jellyseerr
# Configure via web UI at https://requests.jeffemmett.com
# Hetzner Storage Box (Optional - for tiered storage/archival)
# Sign up at: https://www.hetzner.com/storage/storage-box/
# After signing up, run: sudo ./scripts/setup-hetzner-storage.sh
HETZNER_USER=
HETZNER_PASS=

128
README.md
View File

@ -41,12 +41,15 @@ A self-hosted media server stack with automated request management, running on N
┌─────────────────────────────────────────────────────────────┐
│ Local NVMe Storage (3TB) │
│ /media/movies /media/shows /media/music │
├─────────────────────────────────────────────────────────────┤
│ Hetzner Storage Box (Optional Tier) │
│ /media/archive/movies /media/archive/shows (Cold storage)│
└─────────────────────────────────────────────────────────────┘
```
## How It Works
1. **Users request media** via Jellyseerr (`requests.jeffemmett.com`)
1. **Users request media** via Jellyseerr (`requests.jefflix.lol`)
2. **Jellyseerr forwards** requests to Sonarr/Radarr/Lidarr
3. **Prowlarr searches** configured indexers for the content
4. **qBittorrent downloads** the files (optionally through VPN)
@ -83,19 +86,19 @@ Add these hostnames to your Cloudflare tunnel config (`/root/cloudflared/config.
```yaml
# Media Request System
- hostname: requests.jeffemmett.com
- hostname: requests.jefflix.lol
service: http://localhost:80
- hostname: invite.jeffemmett.com
- hostname: invite.jefflix.lol
service: http://localhost:80
- hostname: sonarr.jeffemmett.com
- hostname: sonarr.jefflix.lol
service: http://localhost:80
- hostname: radarr.jeffemmett.com
- hostname: radarr.jefflix.lol
service: http://localhost:80
- hostname: prowlarr.jeffemmett.com
- hostname: prowlarr.jefflix.lol
service: http://localhost:80
- hostname: lidarr.jeffemmett.com
- hostname: lidarr.jefflix.lol
service: http://localhost:80
- hostname: downloads.jeffemmett.com
- hostname: downloads.jefflix.lol
service: http://localhost:80
```
@ -104,34 +107,34 @@ Then restart cloudflared: `docker restart cloudflared`
### 4. Configure Services (First Time Setup)
#### Step 1: Configure Prowlarr (Indexers)
1. Go to `https://prowlarr.jeffemmett.com`
1. Go to `https://prowlarr.jefflix.lol`
2. Add indexers (torrent sites you have access to)
3. Go to Settings → Apps → Add Radarr, Sonarr, Lidarr
#### Step 2: Configure Download Client
1. Go to `https://downloads.jeffemmett.com`
1. Go to `https://downloads.jefflix.lol`
2. Default login: `admin` / `adminadmin` (change immediately!)
3. Settings → Downloads → Default Save Path: `/downloads`
#### Step 3: Configure Radarr (Movies)
1. Go to `https://radarr.jeffemmett.com`
1. Go to `https://radarr.jefflix.lol`
2. Settings → Media Management → Root Folder: `/movies`
3. Settings → Download Clients → Add qBittorrent:
- Host: `qbittorrent`
- Port: `8080`
#### Step 4: Configure Sonarr (TV Shows)
1. Go to `https://sonarr.jeffemmett.com`
1. Go to `https://sonarr.jefflix.lol`
2. Settings → Media Management → Root Folder: `/tv`
3. Settings → Download Clients → Add qBittorrent (same as above)
#### Step 5: Configure Lidarr (Music)
1. Go to `https://lidarr.jeffemmett.com`
1. Go to `https://lidarr.jefflix.lol`
2. Settings → Media Management → Root Folder: `/music`
3. Settings → Download Clients → Add qBittorrent (same as above)
#### Step 6: Configure Jellyseerr (Requests)
1. Go to `https://requests.jeffemmett.com`
1. Go to `https://requests.jefflix.lol`
2. Sign in with Jellyfin (use your Jellyfin server URL: `http://jellyfin:8096`)
3. Add Radarr/Sonarr servers:
- Hostname: `radarr` or `sonarr` (internal Docker hostname)
@ -159,13 +162,13 @@ Then restart cloudflared: `docker restart cloudflared`
|---------|-----|-------------|
| Jellyfin | https://movies.jeffemmett.com | Video streaming (movies & TV) |
| Navidrome | https://music.jeffemmett.com | Music streaming server |
| Jellyseerr | https://requests.jeffemmett.com | Media request interface |
| Jellyseerr | https://requests.jefflix.lol | Media request interface |
| Wizarr | https://invite.jeffemmett.com | User invitation system |
| Sonarr | https://sonarr.jeffemmett.com | TV show management |
| Radarr | https://radarr.jeffemmett.com | Movie management |
| Lidarr | https://lidarr.jeffemmett.com | Music management |
| Prowlarr | https://prowlarr.jeffemmett.com | Indexer management |
| qBittorrent | https://downloads.jeffemmett.com | Download client |
| Sonarr | https://sonarr.jefflix.lol | TV show management |
| Radarr | https://radarr.jefflix.lol | Movie management |
| Lidarr | https://lidarr.jefflix.lol | Music management |
| Prowlarr | https://prowlarr.jefflix.lol | Indexer management |
| qBittorrent | https://downloads.jefflix.lol | Download client |
## Security Recommendations
@ -221,8 +224,91 @@ network_mode: "service:gluetun"
│ └── complete/ # Completed downloads
└── cache/
└── jellyfin/ # Transcoding cache
/mnt/hetzner-media/ # Hetzner Storage Box (optional)
└── archive/
├── movies/ # Archived movies
├── shows/ # Archived TV shows
└── music/ # Archived music
```
## Tiered Storage with Hetzner Storage Box
For additional storage capacity, you can add a Hetzner Storage Box as an archive tier. This is cost-effective (~€3/TB/month) and keeps older content accessible without filling up local NVMe.
### Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Jellyfin Libraries │
├─────────────────────────────────────────────────────────────┤
│ │
│ /media (Local NVMe - Fast) /media/archive (Hetzner) │
│ ├── movies/ ← Active ├── movies/ ← Archived │
│ ├── shows/ ← Recent ├── shows/ ← Old seasons │
│ └── music/ ← Favorites └── music/ ← Full lib │
│ │
│ Hot storage: New downloads Cold storage: Older media │
│ Fast transcoding & streaming Slower but cost-effective │
└─────────────────────────────────────────────────────────────┘
```
### Setup
1. **Sign up for Hetzner Storage Box**: https://www.hetzner.com/storage/storage-box/
- Recommended: BX21 (5-10TB) for ~€11/month
- Enable SMB/CIFS in Hetzner Console
2. **Run setup script on Netcup**:
```bash
ssh netcup
cd /opt/media-server
sudo ./scripts/setup-hetzner-storage.sh
```
3. **Restart media stack**:
```bash
docker compose down && docker compose up -d
```
4. **Add archive library in Jellyfin**:
- Dashboard → Libraries → Add Media Library
- Content type: Movies (or Shows)
- Folder: `/media/archive/movies`
### Managing Archived Content
Use the `media-archive.sh` script to move content between local and Hetzner:
```bash
# Check storage status
./scripts/media-archive.sh status
# List content older than 90 days
./scripts/media-archive.sh list-old
# Archive a specific movie
./scripts/media-archive.sh archive movies "The Matrix (1999)"
# Archive all shows older than 180 days
./scripts/media-archive.sh archive shows --older-than 180
# Restore content from archive
./scripts/media-archive.sh restore movies "The Matrix (1999)"
# Backup all local media to Hetzner
./scripts/media-archive.sh sync
```
### Pricing Reference
| Plan | Storage | Monthly |
|------|---------|---------|
| BX11 | 1 TB | ~€3.50 |
| BX21 | 5-10 TB | ~€11 |
| BX31 | 20 TB | ~€25 |
| BX41 | 40 TB | ~€47 |
## Upload Script Usage
```bash
@ -288,7 +374,7 @@ Lyrics are automatically fetched from LRCLIB and displayed in Symfonium during p
### Spotify Playlist Import
Lidarr syncs with your Spotify playlists:
1. Go to `https://lidarr.jeffemmett.com`
1. Go to `https://lidarr.jefflix.lol`
2. Settings → Import Lists → Add → Spotify Playlists
3. Authenticate with Spotify
4. Select playlists to monitor

View File

@ -10,13 +10,14 @@ services:
volumes:
- ./config/jellyfin:/config
- ./cache/jellyfin:/cache
- ./media:/media
# Hetzner Storage Box - all media served from here
- /mnt/hetzner-media/media:/media:ro
networks:
- media-network
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`movies.jeffemmett.com`) || Host(`movies.jefflix.lol`) || Host(`jefflix.lol`) || Host(`www.jefflix.lol`)"
- "traefik.http.routers.jellyfin.rule=Host(`movies.jeffemmett.com`) || Host(`movies.jefflix.lol`)"
- "traefik.http.routers.jellyfin.entrypoints=web"
- "traefik.http.routers.jellyfin.middlewares=jellyfin-headers"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
@ -38,7 +39,7 @@ services:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyseerr.rule=Host(`requests.jeffemmett.com`) || Host(`requests.jefflix.lol`)"
- "traefik.http.routers.jellyseerr.rule=Host(`requests.jefflix.lol`)"
- "traefik.http.routers.jellyseerr.entrypoints=web"
- "traefik.http.routers.jellyseerr.middlewares=jellyseerr-headers"
- "traefik.http.services.jellyseerr.loadbalancer.server.port=5055"
@ -60,7 +61,8 @@ services:
- ND_IMAGECACHESIZE=500MB
volumes:
- ./config/navidrome:/data
- ./media/music:/music:ro
# Hetzner Storage Box - music library
- /mnt/hetzner-media/media/music:/music:ro
networks:
- media-network
- traefik-public
@ -83,14 +85,15 @@ services:
- TZ=Europe/Berlin
volumes:
- ./config/sonarr:/config
- ./media/shows:/tv
- ./downloads:/downloads
# Hetzner Storage Box - TV shows and downloads
- /mnt/hetzner-media/media/shows:/tv
- /mnt/hetzner-media/downloads:/downloads
networks:
- media-network
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.sonarr.rule=Host(`sonarr.jeffemmett.com`)"
- "traefik.http.routers.sonarr.rule=Host(`sonarr.jefflix.lol`)"
- "traefik.http.routers.sonarr.entrypoints=web"
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
- "traefik.docker.network=traefik-public"
@ -105,14 +108,15 @@ services:
- TZ=Europe/Berlin
volumes:
- ./config/radarr:/config
- ./media/movies:/movies
- ./downloads:/downloads
# Hetzner Storage Box - movies and downloads
- /mnt/hetzner-media/media/movies:/movies
- /mnt/hetzner-media/downloads:/downloads
networks:
- media-network
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.radarr.rule=Host(`radarr.jeffemmett.com`)"
- "traefik.http.routers.radarr.rule=Host(`radarr.jefflix.lol`)"
- "traefik.http.routers.radarr.entrypoints=web"
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
- "traefik.docker.network=traefik-public"
@ -132,7 +136,7 @@ services:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.jeffemmett.com`)"
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.jefflix.lol`)"
- "traefik.http.routers.prowlarr.entrypoints=web"
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
- "traefik.docker.network=traefik-public"
@ -147,14 +151,15 @@ services:
- TZ=Europe/Berlin
volumes:
- ./config/lidarr:/config
- ./media/music:/music
- ./downloads:/downloads
# Hetzner Storage Box - music and downloads
- /mnt/hetzner-media/media/music:/music
- /mnt/hetzner-media/downloads:/downloads
networks:
- media-network
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.lidarr.rule=Host(`lidarr.jeffemmett.com`)"
- "traefik.http.routers.lidarr.rule=Host(`lidarr.jefflix.lol`)"
- "traefik.http.routers.lidarr.entrypoints=web"
- "traefik.http.services.lidarr.loadbalancer.server.port=8686"
- "traefik.docker.network=traefik-public"
@ -188,7 +193,7 @@ services:
profiles:
- vpn
# Download client (routes through VPN when vpn profile enabled)
# Download client - downloads directly to Hetzner Storage Box
qbittorrent:
image: linuxserver/qbittorrent:latest
container_name: qbittorrent
@ -200,7 +205,8 @@ services:
- WEBUI_PORT=8080
volumes:
- ./config/qbittorrent:/config
- ./downloads:/downloads
# Hetzner Storage Box - all downloads go here
- /mnt/hetzner-media/downloads:/downloads
# When VPN enabled, use gluetun network
# network_mode: "service:gluetun"
networks:
@ -208,7 +214,7 @@ services:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.qbittorrent.rule=Host(`downloads.jeffemmett.com`)"
- "traefik.http.routers.qbittorrent.rule=Host(`downloads.jefflix.lol`)"
- "traefik.http.routers.qbittorrent.entrypoints=web"
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8080"
- "traefik.docker.network=traefik-public"
@ -247,7 +253,7 @@ services:
- TRANSMISSION_WEB_HOME=/web
volumes:
- ./config/transmission:/config
- ./downloads:/downloads
- /mnt/hetzner-media/downloads:/downloads
- ./watch:/watch
ports:
- 9091:9091

240
scripts/media-archive.sh Executable file
View File

@ -0,0 +1,240 @@
#!/bin/bash
# Media Archive Manager - Move content between local and Hetzner storage
# Usage: media-archive.sh [archive|restore|status|list-old]
set -e
# Configuration
LOCAL_MEDIA="/opt/media-server/media"
HETZNER_MEDIA="/mnt/hetzner-media/archive"
LOG_FILE="/opt/media-server/logs/archive.log"
DAYS_OLD=90 # Content older than this is considered for archiving
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Ensure log directory exists
mkdir -p "$(dirname "$LOG_FILE")"
log() {
echo "[$(date -Iseconds)] $1" | tee -a "$LOG_FILE"
}
check_mount() {
if ! mountpoint -q "$HETZNER_MEDIA" 2>/dev/null; then
# Try to trigger automount
ls "$HETZNER_MEDIA" &>/dev/null 2>&1
sleep 2
if ! mountpoint -q "$HETZNER_MEDIA" 2>/dev/null; then
echo -e "${RED}Error: Hetzner storage not mounted at $HETZNER_MEDIA${NC}"
echo "Run: sudo systemctl start mnt-hetzner\\x2dmedia.mount"
exit 1
fi
fi
echo -e "${GREEN}Hetzner storage mounted${NC}"
}
show_status() {
echo -e "${BLUE}Storage Status${NC}"
echo "=============="
echo ""
echo -e "${YELLOW}Local Storage (/opt/media-server/media):${NC}"
if [ -d "$LOCAL_MEDIA" ]; then
du -sh "$LOCAL_MEDIA"/* 2>/dev/null || echo " (empty or not accessible)"
else
echo " Not found"
fi
echo ""
echo -e "${YELLOW}Hetzner Storage (/mnt/hetzner-media):${NC}"
if mountpoint -q "/mnt/hetzner-media" 2>/dev/null || ls "/mnt/hetzner-media" &>/dev/null 2>&1; then
df -h "/mnt/hetzner-media"
echo ""
echo "Archive contents:"
du -sh "$HETZNER_MEDIA"/* 2>/dev/null || echo " (empty)"
else
echo " Not mounted"
fi
}
list_old_content() {
echo -e "${BLUE}Content older than $DAYS_OLD days${NC}"
echo "================================"
echo ""
for media_type in movies shows music; do
echo -e "${YELLOW}$media_type:${NC}"
if [ -d "$LOCAL_MEDIA/$media_type" ]; then
find "$LOCAL_MEDIA/$media_type" -maxdepth 1 -mindepth 1 -type d -mtime +$DAYS_OLD 2>/dev/null | while read dir; do
size=$(du -sh "$dir" 2>/dev/null | cut -f1)
name=$(basename "$dir")
echo " [$size] $name"
done
fi
echo ""
done
}
archive_content() {
local media_type=$1
local name=$2
if [ -z "$media_type" ] || [ -z "$name" ]; then
echo "Usage: media-archive.sh archive <movies|shows|music> <name>"
echo " media-archive.sh archive <movies|shows|music> --older-than <days>"
echo ""
echo "Examples:"
echo " media-archive.sh archive movies 'The Matrix (1999)'"
echo " media-archive.sh archive shows --older-than 180"
exit 1
fi
check_mount
# Handle batch archival
if [ "$name" == "--older-than" ]; then
local days=${3:-$DAYS_OLD}
echo -e "${YELLOW}Archiving $media_type older than $days days...${NC}"
find "$LOCAL_MEDIA/$media_type" -maxdepth 1 -mindepth 1 -type d -mtime +$days 2>/dev/null | while read dir; do
local item_name=$(basename "$dir")
archive_single "$media_type" "$item_name"
done
return
fi
archive_single "$media_type" "$name"
}
archive_single() {
local media_type=$1
local name=$2
local source="$LOCAL_MEDIA/$media_type/$name"
local dest="$HETZNER_MEDIA/$media_type/$name"
if [ ! -d "$source" ]; then
echo -e "${RED}Error: $source not found${NC}"
return 1
fi
local size=$(du -sh "$source" 2>/dev/null | cut -f1)
echo -e "${BLUE}Archiving: $name ($size)${NC}"
# Create destination directory
mkdir -p "$HETZNER_MEDIA/$media_type"
# Use rsync for reliable transfer with progress
log "Archiving $media_type/$name ($size) to Hetzner"
rsync -avh --progress --remove-source-files "$source/" "$dest/"
# Remove empty source directory
rmdir "$source" 2>/dev/null || true
echo -e "${GREEN}Archived: $name${NC}"
log "Completed archiving $media_type/$name"
}
restore_content() {
local media_type=$1
local name=$2
if [ -z "$media_type" ] || [ -z "$name" ]; then
echo "Usage: media-archive.sh restore <movies|shows|music> <name>"
echo ""
echo "List available archives:"
echo " ls /mnt/hetzner-media/archive/<type>/"
exit 1
fi
check_mount
local source="$HETZNER_MEDIA/$media_type/$name"
local dest="$LOCAL_MEDIA/$media_type/$name"
if [ ! -d "$source" ]; then
echo -e "${RED}Error: $source not found in archive${NC}"
echo ""
echo "Available in $media_type archive:"
ls -1 "$HETZNER_MEDIA/$media_type/" 2>/dev/null || echo " (empty)"
return 1
fi
local size=$(du -sh "$source" 2>/dev/null | cut -f1)
echo -e "${BLUE}Restoring: $name ($size)${NC}"
log "Restoring $media_type/$name ($size) from Hetzner"
rsync -avh --progress "$source/" "$dest/"
echo -e "${GREEN}Restored: $name${NC}"
echo "Content available at: $dest"
log "Completed restoring $media_type/$name"
}
sync_to_hetzner() {
# One-way sync from local to Hetzner (backup mode)
check_mount
echo -e "${BLUE}Syncing local media to Hetzner (backup)...${NC}"
for media_type in movies shows music; do
if [ -d "$LOCAL_MEDIA/$media_type" ]; then
echo -e "${YELLOW}Syncing $media_type...${NC}"
mkdir -p "$HETZNER_MEDIA/$media_type"
rsync -avh --progress "$LOCAL_MEDIA/$media_type/" "$HETZNER_MEDIA/$media_type/"
fi
done
echo -e "${GREEN}Sync complete${NC}"
}
# Main command handler
case "${1:-status}" in
status)
show_status
;;
list-old|list)
list_old_content
;;
archive)
archive_content "$2" "$3" "$4"
;;
restore)
restore_content "$2" "$3"
;;
sync|backup)
sync_to_hetzner
;;
help|--help|-h)
echo "Media Archive Manager"
echo "====================="
echo ""
echo "Usage: media-archive.sh <command> [options]"
echo ""
echo "Commands:"
echo " status Show storage status and usage"
echo " list-old List content older than $DAYS_OLD days"
echo " archive <type> <name> Archive specific content to Hetzner"
echo " archive <type> --older-than <days> Archive all content older than N days"
echo " restore <type> <name> Restore content from Hetzner to local"
echo " sync Sync all local media to Hetzner (backup)"
echo ""
echo "Types: movies, shows, music"
echo ""
echo "Examples:"
echo " media-archive.sh status"
echo " media-archive.sh list-old"
echo " media-archive.sh archive movies 'The Matrix (1999)'"
echo " media-archive.sh archive shows --older-than 180"
echo " media-archive.sh restore movies 'The Matrix (1999)'"
;;
*)
echo "Unknown command: $1"
echo "Run 'media-archive.sh help' for usage"
exit 1
;;
esac

158
scripts/setup-hetzner-sshfs.sh Executable file
View File

@ -0,0 +1,158 @@
#!/bin/bash
# Setup Hetzner Storage Box via SSHFS on Netcup RS 8000
# Storage Box: u521871 (BX21 - 5-10TB)
set -e
# Configuration
HETZNER_USER="u521871"
HETZNER_HOST="${HETZNER_USER}.your-storagebox.de"
HETZNER_PORT="23"
SSH_KEY="/root/.ssh/hetzner_storagebox"
MOUNT_POINT="/mnt/hetzner-media"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${GREEN}Hetzner Storage Box SSHFS Setup${NC}"
echo "=================================="
echo "User: $HETZNER_USER"
echo "Host: $HETZNER_HOST"
echo "Mount: $MOUNT_POINT"
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root (sudo)${NC}"
exit 1
fi
# Check SSH key exists
if [ ! -f "$SSH_KEY" ]; then
echo -e "${RED}SSH key not found at $SSH_KEY${NC}"
echo "Generate one with: ssh-keygen -t ed25519 -f $SSH_KEY -C 'storagebox@netcup'"
echo "Then upload with: cat ${SSH_KEY}.pub | ssh -p23 ${HETZNER_USER}@${HETZNER_HOST} install-ssh-key"
exit 1
fi
# Install sshfs
echo -e "${GREEN}Installing SSHFS...${NC}"
apt-get update -qq
apt-get install -y sshfs
# Create mount point
echo -e "${GREEN}Creating mount point...${NC}"
mkdir -p "$MOUNT_POINT"
# Test SSH connection (use 'ls' as Hetzner has restricted shell)
echo -e "${GREEN}Testing SSH connection...${NC}"
if ! ssh -p "$HETZNER_PORT" -i "$SSH_KEY" -o BatchMode=yes -o ConnectTimeout=10 \
"${HETZNER_USER}@${HETZNER_HOST}" "ls" &>/dev/null; then
echo -e "${RED}SSH connection failed!${NC}"
echo "Make sure:"
echo " 1. SSH is enabled in Hetzner Console"
echo " 2. Your key is uploaded: cat ${SSH_KEY}.pub | ssh -p23 ${HETZNER_USER}@${HETZNER_HOST} install-ssh-key"
exit 1
fi
echo -e "${GREEN}SSH connection successful${NC}"
# Create directory structure on Hetzner
echo -e "${GREEN}Creating directory structure on Hetzner...${NC}"
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/movies" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/shows" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p media/music" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/movies" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/tv" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/complete/music" 2>/dev/null || true
ssh -p "$HETZNER_PORT" -i "$SSH_KEY" "${HETZNER_USER}@${HETZNER_HOST}" "mkdir -p downloads/incomplete" 2>/dev/null || true
echo -e "${GREEN}Directories created${NC}"
# Create systemd mount unit
echo -e "${GREEN}Creating systemd mount unit...${NC}"
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.mount << EOF
[Unit]
Description=Hetzner Storage Box (u521871 - SSHFS)
After=network-online.target
Wants=network-online.target
[Mount]
What=${HETZNER_USER}@${HETZNER_HOST}:/
Where=${MOUNT_POINT}
Type=fuse.sshfs
Options=port=${HETZNER_PORT},IdentityFile=${SSH_KEY},allow_other,default_permissions,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,_netdev,uid=1000,gid=1000
[Install]
WantedBy=multi-user.target
EOF
# Create automount unit for on-demand mounting
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.automount << EOF
[Unit]
Description=Automount Hetzner Storage Box
After=network-online.target
Wants=network-online.target
[Automount]
Where=${MOUNT_POINT}
TimeoutIdleSec=0
[Install]
WantedBy=multi-user.target
EOF
# Enable user_allow_other in fuse.conf
if ! grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null; then
echo "user_allow_other" >> /etc/fuse.conf
fi
# Reload systemd and start mount
echo -e "${GREEN}Enabling and starting mount...${NC}"
systemctl daemon-reload
systemctl enable mnt-hetzner\\x2dmedia.mount
systemctl start mnt-hetzner\\x2dmedia.mount
# Verify mount
sleep 2
if mountpoint -q "$MOUNT_POINT"; then
echo -e "${GREEN}Mount successful!${NC}"
echo ""
df -h "$MOUNT_POINT"
echo ""
echo "Directory structure:"
ls -la "$MOUNT_POINT"
else
echo -e "${RED}Mount may have failed. Checking status...${NC}"
systemctl status mnt-hetzner\\x2dmedia.mount
exit 1
fi
# Save config reference
cat > /opt/media-server/.hetzner-config << EOF
HETZNER_USER=$HETZNER_USER
HETZNER_HOST=$HETZNER_HOST
MOUNT_POINT=$MOUNT_POINT
SSH_KEY=$SSH_KEY
MOUNT_TYPE=sshfs
SETUP_DATE=$(date -Iseconds)
EOF
chmod 600 /opt/media-server/.hetzner-config
echo ""
echo -e "${GREEN}Setup complete!${NC}"
echo ""
echo "Storage mounted at: $MOUNT_POINT"
echo ""
echo "Directory structure:"
echo " $MOUNT_POINT/media/movies - Movie library"
echo " $MOUNT_POINT/media/shows - TV show library"
echo " $MOUNT_POINT/media/music - Music library"
echo " $MOUNT_POINT/downloads/ - qBittorrent downloads"
echo ""
echo "Next steps:"
echo " 1. Deploy updated docker-compose: docker compose up -d"
echo " 2. Configure Jellyfin library paths"
echo " 3. Verify qBittorrent download path"
echo ""

156
scripts/setup-hetzner-storage.sh Executable file
View File

@ -0,0 +1,156 @@
#!/bin/bash
# Setup Hetzner Storage Box mount on Netcup RS 8000
# Run this script ON the Netcup server after signing up for Hetzner Storage Box
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
echo -e "${GREEN}Hetzner Storage Box Setup${NC}"
echo "=========================="
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${RED}Please run as root (sudo)${NC}"
exit 1
fi
# Prompt for Hetzner credentials if not provided via env
if [ -z "$HETZNER_USER" ]; then
read -p "Enter Hetzner Storage Box username (e.g., u123456): " HETZNER_USER
fi
if [ -z "$HETZNER_PASS" ]; then
read -sp "Enter Hetzner Storage Box password: " HETZNER_PASS
echo ""
fi
# Validate input
if [ -z "$HETZNER_USER" ] || [ -z "$HETZNER_PASS" ]; then
echo -e "${RED}Error: Username and password are required${NC}"
exit 1
fi
HETZNER_HOST="${HETZNER_USER}.your-storagebox.de"
MOUNT_POINT="/mnt/hetzner-media"
CREDENTIALS_FILE="/etc/smbcredentials/hetzner"
echo -e "${YELLOW}Configuration:${NC}"
echo " Host: $HETZNER_HOST"
echo " User: $HETZNER_USER"
echo " Mount: $MOUNT_POINT"
echo ""
# Install required packages
echo -e "${GREEN}Installing CIFS utilities...${NC}"
apt-get update -qq
apt-get install -y cifs-utils
# Create mount point
echo -e "${GREEN}Creating mount point...${NC}"
mkdir -p "$MOUNT_POINT"
mkdir -p "$MOUNT_POINT/archive"
mkdir -p "$MOUNT_POINT/movies"
mkdir -p "$MOUNT_POINT/shows"
mkdir -p "$MOUNT_POINT/music"
# Create credentials file
echo -e "${GREEN}Creating credentials file...${NC}"
mkdir -p /etc/smbcredentials
cat > "$CREDENTIALS_FILE" << EOF
username=$HETZNER_USER
password=$HETZNER_PASS
EOF
chmod 600 "$CREDENTIALS_FILE"
# Create systemd mount unit
echo -e "${GREEN}Creating systemd mount unit...${NC}"
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.mount << EOF
[Unit]
Description=Hetzner Storage Box (Media Archive)
After=network-online.target
Wants=network-online.target
[Mount]
What=//${HETZNER_HOST}/${HETZNER_USER}
Where=${MOUNT_POINT}
Type=cifs
Options=credentials=${CREDENTIALS_FILE},iocharset=utf8,rw,vers=3.0,uid=1000,gid=1000,file_mode=0664,dir_mode=0775,nofail,_netdev
[Install]
WantedBy=multi-user.target
EOF
# Create automount unit for on-demand mounting
cat > /etc/systemd/system/mnt-hetzner\\x2dmedia.automount << EOF
[Unit]
Description=Automount Hetzner Storage Box
After=network-online.target
Wants=network-online.target
[Automount]
Where=${MOUNT_POINT}
TimeoutIdleSec=300
[Install]
WantedBy=multi-user.target
EOF
# Reload systemd and enable mount
echo -e "${GREEN}Enabling mount...${NC}"
systemctl daemon-reload
systemctl enable mnt-hetzner\\x2dmedia.automount
systemctl start mnt-hetzner\\x2dmedia.automount
# Test the mount
echo -e "${GREEN}Testing mount...${NC}"
if mountpoint -q "$MOUNT_POINT" || ls "$MOUNT_POINT" &>/dev/null; then
echo -e "${GREEN}Mount successful!${NC}"
df -h "$MOUNT_POINT"
else
echo -e "${YELLOW}Mount will activate on first access (automount enabled)${NC}"
# Trigger automount by accessing
ls "$MOUNT_POINT" &>/dev/null
sleep 2
if mountpoint -q "$MOUNT_POINT"; then
echo -e "${GREEN}Mount successful!${NC}"
df -h "$MOUNT_POINT"
else
echo -e "${RED}Mount may have failed. Check with: systemctl status mnt-hetzner\\x2dmedia.mount${NC}"
fi
fi
# Create directory structure on Hetzner
echo -e "${GREEN}Creating directory structure on Hetzner...${NC}"
mkdir -p "$MOUNT_POINT/archive/movies"
mkdir -p "$MOUNT_POINT/archive/shows"
mkdir -p "$MOUNT_POINT/archive/music"
# Save config for reference
cat > /opt/media-server/.hetzner-config << EOF
HETZNER_USER=$HETZNER_USER
HETZNER_HOST=$HETZNER_HOST
MOUNT_POINT=$MOUNT_POINT
SETUP_DATE=$(date -Iseconds)
EOF
chmod 600 /opt/media-server/.hetzner-config
echo ""
echo -e "${GREEN}Setup complete!${NC}"
echo ""
echo "Mount point: $MOUNT_POINT"
echo ""
echo "Directory structure:"
echo " $MOUNT_POINT/archive/movies - Archived movies"
echo " $MOUNT_POINT/archive/shows - Archived TV shows"
echo " $MOUNT_POINT/archive/music - Archived music"
echo ""
echo "Next steps:"
echo " 1. Run: docker compose down && docker compose up -d"
echo " 2. Use media-archive.sh to move content to Hetzner"
echo ""