Files
hurricane/netcup/Dockerfile

363 lines
14 KiB
Docker
Raw Normal View History

2026-03-05 21:35:59 +00:00
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"]