From 4d04de25b39fd21b06c34d3c79b115661fd3191b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 21:09:09 +0000 Subject: [PATCH] Add additional Docker apps and Kopia container backup New Docker applications: - Jellyfin (free media server with hardware acceleration) - Frigate NVR (AI-powered object detection) - Caddy (reverse proxy with automatic HTTPS) - ddclient (dynamic DNS updater) - ntfy (self-hosted push notifications) - Uptime Kuma (service uptime monitoring) - wg-easy (WireGuard VPN with web UI) - Traccar (GPS tracking server) - Portainer (Docker management UI) Container backup system: - Kopia backup for all Docker container data - Backup script for configs, databases, app state - Restore script for disaster recovery - Backs up Immich memories, Emby metadata, Minecraft worlds, etc. All apps use docker-compose in ~/docker/{appname}/ with storage on primary drive where appropriate. --- README.md | 56 +++ ubuntu-post-install.sh | 827 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 883 insertions(+) diff --git a/README.md b/README.md index 65df06c..cd8f7f6 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,18 @@ Install containerized applications to `~/docker/{appname}/`: - **Mealie** - Recipe manager & meal planner - **Minecraft Server** - Fabric server with configurable RAM limit - **linux-to-sync** - Private repository setup +- **Jellyfin** - Free media server (alternative to Emby) +- **Frigate** - NVR with AI object detection +- **Caddy** - Reverse proxy with automatic HTTPS +- **ddclient** - Dynamic DNS updater +- **ntfy** - Self-hosted push notifications +- **Uptime Kuma** - Service uptime monitoring +- **wg-easy** - WireGuard VPN with web UI +- **Traccar** - GPS tracking server +- **Portainer** - Docker management UI + +### Container Backup & Restore (Kopia) +Backup all Docker container data (configs, databases, app data) to your backup drives for disaster recovery. Includes restore functionality to recover containers after OS drive failure. ### Backup System (Optional) @@ -549,12 +561,44 @@ docker compose up -d | Immich | 2283 | http://localhost:2283 | | Audiobookshelf | 13378 | http://localhost:13378 | | Emby | 8096 | http://localhost:8096 | +| Jellyfin | 8097 | http://localhost:8097 | | A.R.M. | 8080 | http://localhost:8080 | | Filebrowser | 8085 | http://localhost:8085 | | Magic Mirror | 8081-8083 | http://localhost:808X | | Lyrion (LMS) | 9000 | http://localhost:9000 | | Mealie | 9925 | http://localhost:9925 | | Minecraft | 25565 | localhost:25565 | +| Frigate | 5000 | http://localhost:5000 | +| Caddy | 80, 443 | http://localhost | +| ntfy | 8090 | http://localhost:8090 | +| Uptime Kuma | 3001 | http://localhost:3001 | +| wg-easy | 51821 | http://localhost:51821 | +| Traccar | 8082 | http://localhost:8082 | +| Portainer | 9443 | https://localhost:9443 | + +### Container Backup & Restore (Kopia) + +Backup all Docker container data to your backup drives for disaster recovery. + +**Run Container Backup:** +```bash +~/docker/kopia/backup-containers.sh +``` + +**Restore Containers (after OS drive failure):** +```bash +~/docker/kopia/restore-containers.sh +``` + +**What Gets Backed Up:** +- All container configs and databases +- Immich facial recognition data and memories +- Emby/Jellyfin metadata and watch history +- Minecraft worlds, mods, and permissions +- Mealie recipes, Audiobookshelf progress +- All application state and settings + +**Kopia Repository Location:** Your backup drive(s) in `~/drives/backupX/container-backups/` ### Private Repository (linux-to-sync) @@ -900,6 +944,18 @@ This script is provided as-is for Ubuntu 24.04 Desktop installations. ## Changelog +- **v2.5**: Additional Docker apps and container backup + - Added Jellyfin (free media server with hardware acceleration) + - Added Frigate NVR (AI-powered object detection) + - Added Caddy (reverse proxy with automatic HTTPS) + - Added ddclient (dynamic DNS updater) + - Added ntfy (self-hosted push notifications) + - Added Uptime Kuma (service monitoring) + - Added wg-easy (WireGuard with web UI) + - Added Traccar (GPS tracking server) + - Added Portainer (Docker management UI) + - Added Kopia backup for all Docker containers + - Added container import/restore for disaster recovery - **v2.4**: Self-hosted Docker applications - Added Immich (photo/video backup) - Added Audiobookshelf (audiobook server) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index a3cd1bd..f7bd914 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -1795,6 +1795,833 @@ MC_ENV fi fi + # ---- JELLYFIN (Alternative to Emby) ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ JELLYFIN - Free media server (alternative to Emby) │" + echo "│ Stream movies, TV, music. No premium features locked. │" + echo "│ Port: 8096 │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Jellyfin? (y/n):" "n" INSTALL_JELLYFIN + + if [ "$INSTALL_JELLYFIN" = "y" ] || [ "$INSTALL_JELLYFIN" = "Y" ]; then + echo "Installing Jellyfin..." + JELLYFIN_DIR="$DOCKER_DIR/jellyfin" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $JELLYFIN_DIR" + else + mkdir -p "$JELLYFIN_DIR" + cd "$JELLYFIN_DIR" + + prompt_text "Path to media folder [default: $ACTUAL_HOME/drives/primary/media]:" "$ACTUAL_HOME/drives/primary/media" MEDIA_PATH + + # Get render group ID for hardware acceleration + RENDER_GID=$(getent group render | cut -d: -f3 2>/dev/null || echo "989") + + cat > docker-compose.yml << JELLYFIN_COMPOSE +name: jellyfin + +services: + jellyfin: + image: jellyfin/jellyfin:latest + container_name: jellyfin + hostname: jellyfin + restart: unless-stopped + environment: + - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 + group_add: + - "$RENDER_GID" + volumes: + - ./config:/config + - ./cache:/cache + - \${MEDIA_PATH}:/media:ro + ports: + - "8096:8096" + - "1900:1900/udp" + - "7359:7359/udp" +JELLYFIN_COMPOSE + + cat > .env << JELLYFIN_ENV +MEDIA_PATH=$MEDIA_PATH +JELLYFIN_ENV + + mkdir -p config cache + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR" + + echo "" + echo "✓ Jellyfin configured at $JELLYFIN_DIR" + echo " Start with: cd $JELLYFIN_DIR && docker compose up -d" + echo " Access at: http://localhost:8096" + echo " Note: Hardware acceleration enabled (Intel GPU)" + echo "" + fi + fi + + # ---- FRIGATE NVR ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ FRIGATE - AI-powered NVR for security cameras │" + echo "│ Object detection, recordings, 24/7 monitoring. │" + echo "│ Port: 5000 (web), 8554 (RTSP), 8555 (WebRTC) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Frigate? (y/n):" "n" INSTALL_FRIGATE + + if [ "$INSTALL_FRIGATE" = "y" ] || [ "$INSTALL_FRIGATE" = "Y" ]; then + echo "Installing Frigate..." + FRIGATE_DIR="$DOCKER_DIR/frigate" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $FRIGATE_DIR" + else + mkdir -p "$FRIGATE_DIR" + cd "$FRIGATE_DIR" + + prompt_text "Path for recordings [default: $ACTUAL_HOME/drives/primary/frigate]:" "$ACTUAL_HOME/drives/primary/frigate" FRIGATE_PATH + + cat > docker-compose.yml << FRIGATE_COMPOSE +name: frigate + +services: + frigate: + image: ghcr.io/blakeblackshear/frigate:stable + container_name: frigate + hostname: frigate + restart: unless-stopped + privileged: true + shm_size: "256mb" + environment: + - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + devices: + - /dev/dri/renderD128:/dev/dri/renderD128 + volumes: + - ./config:/config + - \${FRIGATE_MEDIA}:/media/frigate + - type: tmpfs + target: /tmp/cache + tmpfs: + size: 1000000000 + ports: + - "5000:5000" + - "8554:8554" + - "8555:8555/tcp" + - "8555:8555/udp" +FRIGATE_COMPOSE + + cat > .env << FRIGATE_ENV +FRIGATE_MEDIA=$FRIGATE_PATH +FRIGATE_ENV + + mkdir -p config + mkdir -p "$FRIGATE_PATH" + + # Create basic config.yml + cat > config/config.yml << 'FRIGATE_CONFIG' +mqtt: + enabled: false + +cameras: + # Example camera - edit with your camera details + # front_door: + # ffmpeg: + # inputs: + # - path: rtsp://user:pass@camera-ip:554/stream + # roles: + # - detect + # - record + # detect: + # width: 1280 + # height: 720 + # fps: 5 + +detectors: + default: + type: cpu + +record: + enabled: true + retain: + days: 7 + mode: motion + +snapshots: + enabled: true + retain: + default: 7 +FRIGATE_CONFIG + + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR" + + echo "" + echo "✓ Frigate configured at $FRIGATE_DIR" + echo " Start with: cd $FRIGATE_DIR && docker compose up -d" + echo " Access at: http://localhost:5000" + echo " IMPORTANT: Edit config/config.yml to add your cameras!" + echo "" + fi + fi + + # ---- CADDY REVERSE PROXY ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ CADDY - Automatic HTTPS reverse proxy │" + echo "│ Route domains to containers with automatic SSL certificates. │" + echo "│ Ports: 80, 443 │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY + + if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then + echo "Installing Caddy..." + CADDY_DIR="$DOCKER_DIR/caddy" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $CADDY_DIR" + else + mkdir -p "$CADDY_DIR" + cd "$CADDY_DIR" + + cat > docker-compose.yml << 'CADDY_COMPOSE' +name: caddy + +services: + caddy: + image: caddy:latest + container_name: caddy + hostname: caddy + restart: unless-stopped + ports: + - "80:80" + - "443:443" + - "443:443/udp" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./config:/config + - ./data:/data + - ./site:/srv + +networks: + default: + name: caddy_net + external: true +CADDY_COMPOSE + + # Create Docker network for Caddy + docker network create caddy_net 2>/dev/null || true + + # Create example Caddyfile + cat > Caddyfile << 'CADDY_FILE' +# Caddy reverse proxy configuration +# Replace example.com with your domain + +# Example: Reverse proxy to Immich +# immich.example.com { +# reverse_proxy immich_server:2283 +# } + +# Example: Reverse proxy to Jellyfin +# jellyfin.example.com { +# reverse_proxy jellyfin:8096 +# } + +# Example: Simple file server +# files.example.com { +# root * /srv +# file_server browse +# } + +# Localhost catch-all (for testing) +:80 { + respond "Caddy is running! Edit Caddyfile to add your sites." +} +CADDY_FILE + + mkdir -p config data site + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$CADDY_DIR" + + echo "" + echo "✓ Caddy configured at $CADDY_DIR" + echo " Start with: cd $CADDY_DIR && docker compose up -d" + echo " Edit Caddyfile to add your reverse proxy rules" + echo " Created 'caddy_net' Docker network for container routing" + echo "" + fi + fi + + # ---- DDCLIENT DYNAMIC DNS ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ DDCLIENT - Dynamic DNS updater │" + echo "│ Keep your domain pointing to your home IP. │" + echo "│ Supports: Cloudflare, DuckDNS, No-IP, and more. │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install ddclient? (y/n):" "n" INSTALL_DDCLIENT + + if [ "$INSTALL_DDCLIENT" = "y" ] || [ "$INSTALL_DDCLIENT" = "Y" ]; then + echo "Installing ddclient..." + DDCLIENT_DIR="$DOCKER_DIR/ddclient" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $DDCLIENT_DIR" + else + mkdir -p "$DDCLIENT_DIR" + cd "$DDCLIENT_DIR" + + cat > docker-compose.yml << 'DDCLIENT_COMPOSE' +name: ddclient + +services: + ddclient: + image: lscr.io/linuxserver/ddclient:latest + container_name: ddclient + hostname: ddclient + restart: unless-stopped + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + volumes: + - ./config:/config +DDCLIENT_COMPOSE + + cat > .env << DDCLIENT_ENV +TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") +DDCLIENT_ENV + + mkdir -p config + + # Create example ddclient.conf + cat > config/ddclient.conf << 'DDCLIENT_CONF' +# ddclient configuration +# Uncomment and edit the section for your DNS provider + +daemon=300 +syslog=yes +pid=/var/run/ddclient/ddclient.pid +ssl=yes + +# Example: Cloudflare +# use=web, web=cloudflare +# protocol=cloudflare +# zone=example.com +# login=token +# password=your-api-token +# example.com + +# Example: DuckDNS +# use=web +# protocol=duckdns +# password=your-duckdns-token +# yourdomain.duckdns.org +DDCLIENT_CONF + + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DDCLIENT_DIR" + + echo "" + echo "✓ ddclient configured at $DDCLIENT_DIR" + echo " IMPORTANT: Edit config/ddclient.conf with your DNS provider settings!" + echo " Start with: cd $DDCLIENT_DIR && docker compose up -d" + echo "" + fi + fi + + # ---- NTFY NOTIFICATIONS ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ NTFY - Push notifications server │" + echo "│ Send notifications from scripts to your phone. │" + echo "│ Port: 8090 │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install ntfy? (y/n):" "n" INSTALL_NTFY + + if [ "$INSTALL_NTFY" = "y" ] || [ "$INSTALL_NTFY" = "Y" ]; then + echo "Installing ntfy..." + NTFY_DIR="$DOCKER_DIR/ntfy" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $NTFY_DIR" + else + mkdir -p "$NTFY_DIR" + cd "$NTFY_DIR" + + cat > docker-compose.yml << 'NTFY_COMPOSE' +name: ntfy + +services: + ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + hostname: ntfy + restart: unless-stopped + command: serve + environment: + - TZ=${TZ} + volumes: + - ./cache:/var/cache/ntfy + - ./config:/etc/ntfy + ports: + - "8090:80" +NTFY_COMPOSE + + cat > .env << NTFY_ENV +TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") +NTFY_ENV + + mkdir -p cache config + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR" + + echo "" + echo "✓ ntfy configured at $NTFY_DIR" + echo " Start with: cd $NTFY_DIR && docker compose up -d" + echo " Access at: http://localhost:8090" + echo "" + echo " Send notification: curl -d \"Hello!\" localhost:8090/mytopic" + echo " Subscribe on phone: ntfy app → Add subscription → localhost:8090/mytopic" + echo "" + fi + fi + + # ---- UPTIME KUMA ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ UPTIME KUMA - Service monitoring dashboard │" + echo "│ Monitor websites, servers, Docker containers. │" + echo "│ Port: 3001 │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Uptime Kuma? (y/n):" "n" INSTALL_UPTIMEKUMA + + if [ "$INSTALL_UPTIMEKUMA" = "y" ] || [ "$INSTALL_UPTIMEKUMA" = "Y" ]; then + echo "Installing Uptime Kuma..." + UPTIME_DIR="$DOCKER_DIR/uptime-kuma" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $UPTIME_DIR" + else + mkdir -p "$UPTIME_DIR" + cd "$UPTIME_DIR" + + cat > docker-compose.yml << 'UPTIME_COMPOSE' +name: uptime-kuma + +services: + uptime-kuma: + image: louislam/uptime-kuma:1 + container_name: uptime-kuma + hostname: uptime-kuma + restart: unless-stopped + volumes: + - ./data:/app/data + - /var/run/docker.sock:/var/run/docker.sock:ro + ports: + - "3001:3001" +UPTIME_COMPOSE + + mkdir -p data + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$UPTIME_DIR" + + echo "" + echo "✓ Uptime Kuma configured at $UPTIME_DIR" + echo " Start with: cd $UPTIME_DIR && docker compose up -d" + echo " Access at: http://localhost:3001" + echo "" + fi + fi + + # ---- WG-EASY (WireGuard with Web UI) ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ WG-EASY - WireGuard VPN with web management │" + echo "│ Easy WireGuard setup with QR codes for clients. │" + echo "│ Port: 51821 (web), 51820 (VPN) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install wg-easy? (y/n):" "n" INSTALL_WGEASY + + if [ "$INSTALL_WGEASY" = "y" ] || [ "$INSTALL_WGEASY" = "Y" ]; then + echo "Installing wg-easy..." + WGEASY_DIR="$DOCKER_DIR/wg-easy" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $WGEASY_DIR" + else + mkdir -p "$WGEASY_DIR" + cd "$WGEASY_DIR" + + # Get public IP or hostname + PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "your-public-ip") + WG_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) + + prompt_text "Public IP or hostname for VPN [default: $PUBLIC_IP]:" "$PUBLIC_IP" WG_HOST + + cat > docker-compose.yml << WGEASY_COMPOSE +name: wg-easy + +services: + wg-easy: + image: ghcr.io/wg-easy/wg-easy:latest + container_name: wg-easy + hostname: wg-easy + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + environment: + - WG_HOST=\${WG_HOST} + - PASSWORD=\${WG_PASSWORD} + - WG_DEFAULT_DNS=1.1.1.1 + volumes: + - ./config:/etc/wireguard + ports: + - "51820:51820/udp" + - "51821:51821/tcp" +WGEASY_COMPOSE + + cat > .env << WGEASY_ENV +WG_HOST=$WG_HOST +WG_PASSWORD=$WG_PASSWORD +WGEASY_ENV + + mkdir -p config + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR" + + echo "" + echo "✓ wg-easy configured at $WGEASY_DIR" + echo " Start with: cd $WGEASY_DIR && docker compose up -d" + echo " Web UI: http://localhost:51821" + echo " Password: $WG_PASSWORD (saved in .env)" + echo " VPN Port: 51820/udp (forward this in your router)" + echo "" + fi + fi + + # ---- TRACCAR GPS TRACKING ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ TRACCAR - GPS tracking server │" + echo "│ Track phones, vehicles, assets with OwnTracks/Traccar apps. │" + echo "│ Port: 8082 (web), 5055 (OsmAnd), 5000+ (devices) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Traccar? (y/n):" "n" INSTALL_TRACCAR + + if [ "$INSTALL_TRACCAR" = "y" ] || [ "$INSTALL_TRACCAR" = "Y" ]; then + echo "Installing Traccar..." + TRACCAR_DIR="$DOCKER_DIR/traccar" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $TRACCAR_DIR" + else + mkdir -p "$TRACCAR_DIR" + cd "$TRACCAR_DIR" + + cat > docker-compose.yml << 'TRACCAR_COMPOSE' +name: traccar + +services: + traccar: + image: traccar/traccar:latest + container_name: traccar + hostname: traccar + restart: unless-stopped + volumes: + - ./logs:/opt/traccar/logs:rw + - ./data:/opt/traccar/data:rw + - ./config/traccar.xml:/opt/traccar/conf/traccar.xml:ro + ports: + - "8082:8082" + - "5000-5150:5000-5150" + - "5000-5150:5000-5150/udp" +TRACCAR_COMPOSE + + mkdir -p logs data config + + # Create basic traccar.xml config + cat > config/traccar.xml << 'TRACCAR_XML' + + + + + + ./conf/default.xml + org.h2.Driver + jdbc:h2:/opt/traccar/data/database + sa + + +TRACCAR_XML + + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR" + + echo "" + echo "✓ Traccar configured at $TRACCAR_DIR" + echo " Start with: cd $TRACCAR_DIR && docker compose up -d" + echo " Access at: http://localhost:8082" + echo " Default: admin@admin.com / admin (change immediately!)" + echo "" + fi + fi + + # ---- PORTAINER ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ PORTAINER - Docker management web UI │" + echo "│ Manage containers, images, volumes via browser. │" + echo "│ Port: 9443 (https), 9000 (http) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install Portainer? (y/n):" "n" INSTALL_PORTAINER + + if [ "$INSTALL_PORTAINER" = "y" ] || [ "$INSTALL_PORTAINER" = "Y" ]; then + echo "Installing Portainer..." + PORTAINER_DIR="$DOCKER_DIR/portainer" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $PORTAINER_DIR" + else + mkdir -p "$PORTAINER_DIR" + cd "$PORTAINER_DIR" + + cat > docker-compose.yml << 'PORTAINER_COMPOSE' +name: portainer + +services: + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + hostname: portainer + restart: always + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data:/data + ports: + - "9000:9000" + - "9443:9443" +PORTAINER_COMPOSE + + mkdir -p data + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$PORTAINER_DIR" + + echo "" + echo "✓ Portainer configured at $PORTAINER_DIR" + echo " Start with: cd $PORTAINER_DIR && docker compose up -d" + echo " Access at: https://localhost:9443" + echo " Create admin account on first visit" + echo "" + fi + fi + + # ============================================================================ + # KOPIA BACKUP FOR DOCKER CONTAINERS + # ============================================================================ + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "DOCKER CONTAINER BACKUP (Kopia)" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Backup all Docker containers (configs, databases, app data) to your" + echo "backup drives. Essential for disaster recovery - if your OS drive" + echo "fails, you can restore everything including:" + echo " • Immich memories, facial recognition data" + echo " • Emby/Jellyfin metadata, watch history" + echo " • Minecraft worlds, mods, permissions" + echo " • All app configs, users, and databases" + echo "" + prompt_yn "Set up Kopia container backup? (y/n):" "n" INSTALL_KOPIA + + if [ "$INSTALL_KOPIA" = "y" ] || [ "$INSTALL_KOPIA" = "Y" ]; then + echo "Installing Kopia backup..." + KOPIA_DIR="$DOCKER_DIR/kopia" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $KOPIA_DIR" + else + mkdir -p "$KOPIA_DIR" + cd "$KOPIA_DIR" + + KOPIA_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) + + echo "" + echo "Select backup destination(s):" + echo " Backups will be stored in ~/drives/{backup-drive}/kopia-repo/" + echo "" + + # List available backup drives + echo "Available mount points in ~/drives/:" + ls -1 "$ACTUAL_HOME/drives/" 2>/dev/null | grep -v "^primary$" || echo " (none found - set up drives first)" + echo "" + + prompt_text "Backup drive name [default: backup1]:" "backup1" KOPIA_BACKUP_DRIVE + + KOPIA_REPO="$ACTUAL_HOME/drives/$KOPIA_BACKUP_DRIVE/kopia-repo" + + cat > docker-compose.yml << KOPIA_COMPOSE +name: kopia + +services: + kopia: + image: kopia/kopia:latest + container_name: kopia + hostname: kopia + restart: unless-stopped + privileged: true + devices: + - /dev/fuse:/dev/fuse:rwm + environment: + - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + - KOPIA_PASSWORD=\${KOPIA_PASSWORD} + command: > + server start + --tls-generate-cert + --disable-csrf-token-checks + --address=0.0.0.0:51515 + --server-username=admin + --server-password=\${KOPIA_PASSWORD} + volumes: + - ./config:/app/config + - ./cache:/app/cache + - ./logs:/app/logs + - $DOCKER_DIR:/data/docker:ro + - $KOPIA_REPO:/repository + - ./tmp:/tmp:shared + ports: + - "51515:51515" +KOPIA_COMPOSE + + cat > .env << KOPIA_ENV +KOPIA_PASSWORD=$KOPIA_PASSWORD +KOPIA_ENV + + mkdir -p config cache logs tmp + mkdir -p "$KOPIA_REPO" + + # Create backup script + cat > backup-containers.sh << 'BACKUP_SCRIPT' +#!/bin/bash +# Backup all Docker containers using Kopia + +DOCKER_DIR="$HOME/docker" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) + +echo "=== Docker Container Backup: $TIMESTAMP ===" +echo "" + +# Stop containers before backup for consistency (optional) +read -p "Stop containers during backup for consistency? (y/n): " STOP_CONTAINERS + +if [ "$STOP_CONTAINERS" = "y" ]; then + echo "Stopping containers..." + for dir in "$DOCKER_DIR"/*/; do + if [ -f "$dir/docker-compose.yml" ]; then + echo " Stopping $(basename $dir)..." + (cd "$dir" && docker compose stop) 2>/dev/null + fi + done +fi + +echo "" +echo "Running Kopia backup..." +docker exec kopia kopia snapshot create /data/docker --description "Container backup $TIMESTAMP" + +if [ "$STOP_CONTAINERS" = "y" ]; then + echo "" + echo "Restarting containers..." + for dir in "$DOCKER_DIR"/*/; do + if [ -f "$dir/docker-compose.yml" ]; then + echo " Starting $(basename $dir)..." + (cd "$dir" && docker compose start) 2>/dev/null + fi + done +fi + +echo "" +echo "=== Backup Complete ===" +docker exec kopia kopia snapshot list /data/docker --max-results 5 +BACKUP_SCRIPT + + # Create restore script + cat > restore-containers.sh << 'RESTORE_SCRIPT' +#!/bin/bash +# Restore Docker containers from Kopia backup + +echo "=== Docker Container Restore ===" +echo "" +echo "Available snapshots:" +docker exec kopia kopia snapshot list /data/docker + +echo "" +echo "To restore a specific snapshot:" +echo " docker exec kopia kopia restore /tmp/restore" +echo " Then copy files from ~/docker/kopia/tmp/restore/ to ~/docker/" +echo "" +echo "To mount snapshots for browsing:" +echo " docker exec kopia kopia mount all /tmp/mnt &" +echo " Then browse: ~/docker/kopia/tmp/mnt/" +RESTORE_SCRIPT + + chmod +x backup-containers.sh restore-containers.sh + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$KOPIA_DIR" + + echo "" + echo "✓ Kopia backup configured at $KOPIA_DIR" + echo " Start Kopia: cd $KOPIA_DIR && docker compose up -d" + echo " Web UI: https://localhost:51515" + echo " Username: admin" + echo " Password: $KOPIA_PASSWORD (saved in .env)" + echo "" + echo " Repository at: $KOPIA_REPO" + echo "" + echo " Backup now: cd $KOPIA_DIR && ./backup-containers.sh" + echo " Restore: cd $KOPIA_DIR && ./restore-containers.sh" + echo "" + echo " ⚠️ SAVE YOUR KOPIA PASSWORD! Without it, backups cannot be restored." + echo "" + fi + fi + + # ============================================================================ + # IMPORT/RESTORE CONTAINERS + # ============================================================================ + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "IMPORT CONTAINERS FROM BACKUP" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "If you have a previous Kopia backup, you can import/restore your" + echo "Docker containers from a backup drive." + echo "" + prompt_yn "Import containers from a Kopia backup? (y/n):" "n" IMPORT_CONTAINERS + + if [ "$IMPORT_CONTAINERS" = "y" ] || [ "$IMPORT_CONTAINERS" = "Y" ]; then + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would prompt for backup location and restore containers" + else + echo "" + prompt_text "Path to Kopia repository [e.g., /home/user/drives/backup1/kopia-repo]:" "" IMPORT_REPO + + if [ -d "$IMPORT_REPO" ]; then + echo "" + echo "Repository found. To restore:" + echo "" + echo "1. Start Kopia with this repository:" + echo " docker run -it --rm -v $IMPORT_REPO:/repository -v $DOCKER_DIR:/restore kopia/kopia repository connect filesystem --path=/repository" + echo "" + echo "2. List available snapshots:" + echo " docker run -it --rm -v $IMPORT_REPO:/repository kopia/kopia snapshot list" + echo "" + echo "3. Restore a snapshot:" + echo " docker run -it --rm -v $IMPORT_REPO:/repository -v $DOCKER_DIR:/restore kopia/kopia restore /restore" + echo "" + echo "4. Start your containers:" + echo " for dir in ~/docker/*/; do (cd \"\$dir\" && docker compose up -d); done" + echo "" + else + echo "Repository not found at $IMPORT_REPO" + echo "Make sure your backup drive is mounted and try again." + fi + fi + fi + echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Docker applications configured in: $DOCKER_DIR"