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.
This commit is contained in:
Claude
2026-01-01 21:09:09 +00:00
parent edd870703b
commit 4d04de25b3
2 changed files with 883 additions and 0 deletions
+56
View File
@@ -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)
+827
View File
@@ -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'
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE properties SYSTEM 'http://java.sun.com/dtd/properties.dtd'>
<properties>
<entry key='config.default'>./conf/default.xml</entry>
<entry key='database.driver'>org.h2.Driver</entry>
<entry key='database.url'>jdbc:h2:/opt/traccar/data/database</entry>
<entry key='database.user'>sa</entry>
<entry key='database.password'></entry>
</properties>
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 <snapshot-id> /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 <snapshot-id> /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"