363 lines
14 KiB
Docker
363 lines
14 KiB
Docker
|
|
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"]
|