FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && \ apt-get install -y --no-install-recommends \ openssh-client \ openssl \ curl \ sshfs \ restic \ fuse3 \ sshpass && \ rm -rf /var/lib/apt/lists/* WORKDIR /app COPY id_ed25519.enc /app/id_ed25519.enc RUN cat > /app/run.sh << 'SCRIPT' #!/bin/bash set -euo pipefail # ══════════════════════════════════════════════════════════════════════════════ # VALIDATION # ══════════════════════════════════════════════════════════════════════════════ REQUIRED_VARS=(KEY_PASSWORD REMOTE_HOST RESTIC_PASSWORD SSH_PASSWORD) for var in "${REQUIRED_VARS[@]}"; do if [[ -z "${!var:-}" ]]; then echo "❌ Error: Missing required variable: $var" exit 1 fi done MODE="${MODE:-BACKUP}" MOUNT_REMOTE="${MOUNT_REMOTE:-root@n.h-y.st:/mnt/data}" MOUNT_POINT="/mnt/data" RESTIC_REPO="$MOUNT_POINT/restic-repo" COMPOSE_DIR="${COMPOSE_DIR:-/root/docker}" echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🚀 Backup/Restore Tool Mode : $MODE Target : root@$REMOTE_HOST Compose : $COMPOSE_DIR Mount : $MOUNT_REMOTE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ " # ══════════════════════════════════════════════════════════════════════════════ # CLEANUP TRAP # ══════════════════════════════════════════════════════════════════════════════ cleanup() { echo "🧹 Cleaning up local secrets..." rm -f ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub } trap cleanup EXIT # ══════════════════════════════════════════════════════════════════════════════ # DECRYPT SSH KEY # ══════════════════════════════════════════════════════════════════════════════ echo "🔑 Decrypting SSH key..." mkdir -p ~/.ssh && chmod 700 ~/.ssh if ! openssl enc -d -aes-256-cbc -pbkdf2 \ -in /app/id_ed25519.enc \ -out ~/.ssh/id_ed25519 \ -pass pass:"$KEY_PASSWORD" 2>/dev/null; then echo "❌ Failed to decrypt SSH key — check KEY_PASSWORD" exit 1 fi chmod 600 ~/.ssh/id_ed25519 ssh-keygen -y -f ~/.ssh/id_ed25519 > ~/.ssh/id_ed25519.pub 2>/dev/null || true # ══════════════════════════════════════════════════════════════════════════════ # SSH HELPERS # ══════════════════════════════════════════════════════════════════════════════ KEY_SSH() { ssh \ -o StrictHostKeyChecking=no \ -o ConnectTimeout=10 \ -o BatchMode=yes \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=3 \ -i ~/.ssh/id_ed25519 \ "$@" } PASS_SSH() { sshpass -p "$SSH_PASSWORD" ssh \ -o StrictHostKeyChecking=no \ -o ConnectTimeout=10 \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=3 \ "$@" } # ══════════════════════════════════════════════════════════════════════════════ # CONNECTIVITY + AUTH BOOTSTRAP # ══════════════════════════════════════════════════════════════════════════════ echo "🔍 Testing connectivity to $REMOTE_HOST..." if KEY_SSH "root@$REMOTE_HOST" exit 2>/dev/null; then echo "✅ Key-based auth succeeded" SSH_CONNECT() { KEY_SSH "root@$REMOTE_HOST" "$@"; } else echo "⚠️ Key auth failed — attempting password auth..." if ! PASS_SSH "root@$REMOTE_HOST" exit 2>/dev/null; then echo "❌ Cannot connect to $REMOTE_HOST — both auth methods failed" exit 1 fi echo "✅ Password auth succeeded" SSH_CONNECT() { PASS_SSH "root@$REMOTE_HOST" "$@"; } echo "🔑 Installing SSH public key for future runs..." sshpass -p "$SSH_PASSWORD" ssh-copy-id \ -o StrictHostKeyChecking=no \ -i ~/.ssh/id_ed25519 \ "root@$REMOTE_HOST" && \ echo "✅ Key installed — password auth won't be needed next run" || \ echo "⚠️ ssh-copy-id failed — continuing with password auth" fi # ══════════════════════════════════════════════════════════════════════════════ # CAPTURE PRIVATE KEY FOR REMOTE INJECTION # ══════════════════════════════════════════════════════════════════════════════ PRIVATE_KEY_CONTENTS=$(cat ~/.ssh/id_ed25519) # ══════════════════════════════════════════════════════════════════════════════ # REMOTE SESSION # ══════════════════════════════════════════════════════════════════════════════ echo "🚀 Starting remote session on $REMOTE_HOST..." SSH_CONNECT bash << EOF set -euo pipefail COMPOSE_DIR="$COMPOSE_DIR" MOUNT_POINT="$MOUNT_POINT" MOUNT_REMOTE="$MOUNT_REMOTE" RESTIC_REPO="$RESTIC_REPO" MODE="$MODE" export RESTIC_PASSWORD="$RESTIC_PASSWORD" export RESTIC_REPOSITORY="\$RESTIC_REPO" log() { echo " \$1"; } step() { echo ""; echo "▶ \$1"; } has_compose() { [ -f "\$1/docker-compose.yml" ] || [ -f "\$1/compose.yml" ] } find_compose_dir() { if has_compose "\$COMPOSE_DIR"; then echo "\$COMPOSE_DIR" elif has_compose "/home/zeshan/docker"; then echo "/home/zeshan/docker" else echo "" fi } # ── 1. Install Dependencies ─────────────────────────────────────────────────── step "Installing dependencies" log "📦 Updating package lists..." apt-get update -qq log "📦 Installing packages..." apt-get install -y --no-install-recommends \ curl \ wget \ ca-certificates \ bash \ coreutils \ procps \ openssh-server \ sshfs \ restic \ fuse3 log "✅ Packages installed" # ── 2. Docker ───────────────────────────────────────────────────────────────── step "Docker" if ! command -v docker &>/dev/null; then log "🐋 Installing Docker..." curl -fsSL https://get.docker.com | sh systemctl enable --now docker log "✅ Docker installed" else log "✅ Docker already installed (\$(docker --version))" fi # ── 3. Migrate legacy zeshan paths ─────────────────────────────────────────── step "Path migration" if [ -d "/home/zeshan/docker" ] && [ ! -d "/root/docker" ]; then log "📦 Migrating /home/zeshan/docker → /root/docker..." cp -r /home/zeshan/docker /root/docker log "✅ Migration complete" elif [ -d "/root/docker" ]; then log "✅ /root/docker already exists" else log "⚠️ No compose directory found yet" fi # ── 4. FUSE ─────────────────────────────────────────────────────────────────── step "FUSE config" grep -q "^user_allow_other" /etc/fuse.conf 2>/dev/null || \ echo "user_allow_other" >> /etc/fuse.conf log "✅ FUSE configured" # ── 5. Storage Mount ────────────────────────────────────────────────────────── step "Storage mount" if mountpoint -q "\$MOUNT_POINT"; then log "⚠️ Already mounted — unmounting first..." umount -l "\$MOUNT_POINT" fi mkdir -p "\$MOUNT_POINT" MOUNT_KEY="\$(mktemp /root/.ssh/mount_key.XXXXXX)" chmod 600 "\$MOUNT_KEY" cat > "\$MOUNT_KEY" << 'PRIVKEY' $PRIVATE_KEY_CONTENTS PRIVKEY chmod 600 "\$MOUNT_KEY" trap 'rm -f \$MOUNT_KEY' EXIT log "🔗 Mounting \$MOUNT_REMOTE..." sshfs \ -o StrictHostKeyChecking=no \ -o IdentityFile="\$MOUNT_KEY" \ -o allow_other \ -o reconnect \ -o ServerAliveInterval=15 \ -o ServerAliveCountMax=3 \ "\$MOUNT_REMOTE" "\$MOUNT_POINT" rm -f "\$MOUNT_KEY" if ! mountpoint -q "\$MOUNT_POINT"; then echo "❌ Mount failed!" exit 1 fi log "✅ \$MOUNT_POINT mounted from \$MOUNT_REMOTE" # ── 6. Persist mount in fstab ───────────────────────────────────────────────── step "Persisting mount in fstab" PERSISTENT_KEY="/root/.ssh/sshfs_mount_key" cat > "\$PERSISTENT_KEY" << 'PRIVKEY' $PRIVATE_KEY_CONTENTS PRIVKEY chmod 600 "\$PERSISTENT_KEY" if grep -q "n.h-y.st:/mnt/data" /etc/fstab; then log "✅ fstab entry already exists — skipping" else echo "root@n.h-y.st:/mnt/data /mnt/data fuse.sshfs IdentityFile=/root/.ssh/sshfs_mount_key,StrictHostKeyChecking=no,allow_other,reconnect,ServerAliveInterval=15,ServerAliveCountMax=3,_netdev,x-systemd.automount 0 0" >> /etc/fstab log "✅ fstab entry added" fi grep "n.h-y.st" /etc/fstab # ── 6. Restic Repo ──────────────────────────────────────────────────────────── step "Restic repository" if ! restic snapshots &>/dev/null; then log "📦 Initialising new restic repository..." restic init log "✅ Repository initialised" else log "✅ Repository already exists" restic snapshots --compact fi # ── 7. Backup or Restore ────────────────────────────────────────────────────── step "Task: \$MODE" if [ "\$MODE" == "RESTORE" ]; then log "📋 Available snapshots:" restic snapshots log "⚠️ Restoring in 5 seconds — Ctrl+C to abort..." sleep 5 log "⏬ Restoring latest snapshot..." mkdir -p /root/docker /var/lib/docker/volumes restic restore latest --target / if [ -d "/home/zeshan/docker" ] && [ ! -d "/root/docker" ]; then log "📦 Moving restored zeshan paths to /root/docker..." cp -r /home/zeshan/docker /root/docker fi log "✅ Restore complete" ACTIVE_COMPOSE="\$(find_compose_dir)" if [ -n "\$ACTIVE_COMPOSE" ]; then log "🐳 Starting Docker services from \$ACTIVE_COMPOSE..." cd "\$ACTIVE_COMPOSE" && docker compose up -d log "✅ Services started" docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" else log "⚠️ No compose file found — skipping docker compose up" fi else # BACKUP ACTIVE_COMPOSE="\$(find_compose_dir)" if [ -n "\$ACTIVE_COMPOSE" ]; then log "⏸️ Stopping services for consistent backup..." cd "\$ACTIVE_COMPOSE" && docker compose stop else log "⚠️ No compose file found — skipping stop" fi log "💾 Running backup..." BACKUP_PATHS="/var/lib/docker/volumes" [ -d "/root/docker" ] && BACKUP_PATHS="\$BACKUP_PATHS /root/docker" [ -d "/home/zeshan/docker" ] && BACKUP_PATHS="\$BACKUP_PATHS /home/zeshan/docker" restic backup \ --tag automated \ --tag "\$(date +%Y-%m-%d)" \ --exclude="*.log" \ --exclude="*.tmp" \ --exclude="*.cache" \ \$BACKUP_PATHS log "✂️ Pruning old snapshots..." restic forget \ --tag automated \ --keep-daily 7 \ --keep-weekly 4 \ --keep-monthly 3 \ --prune log "🔍 Verifying repository integrity..." restic check --read-data-subset=5% if [ -n "\$ACTIVE_COMPOSE" ]; then log "▶️ Restarting services..." cd "\$ACTIVE_COMPOSE" && docker compose start log "✅ Services restarted" docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" fi fi # ── 8. Cleanup ──────────────────────────────────────────────────────────────── step "Cleanup" umount "\$MOUNT_POINT" && log "✅ \$MOUNT_POINT unmounted" echo " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ✅ \$MODE completed successfully ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ " EOF SCRIPT RUN chmod +x /app/run.sh ENTRYPOINT ["/app/run.sh"]