#!/usr/bin/env bash # /opt/docker/backup.sh # Backs up all Docker service databases and named volumes to /opt/backups/ # Run as root via cron. Keeps 7 daily backups. set -euo pipefail BACKUP_DIR="/opt/backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_PATH="${BACKUP_DIR}/${TIMESTAMP}" LOG="${BACKUP_DIR}/backup.log" KEEP_DAYS=7 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; } mkdir -p "$BACKUP_PATH" log "=== Backup started: ${TIMESTAMP} ===" # ── Matrix PostgreSQL ────────────────────────────────────────────── log "Dumping Matrix PostgreSQL..." if docker ps --format '{{.Names}}' | grep -q '^matrix-postgres$'; then docker exec matrix-postgres pg_dump -U synapse synapse \ | gzip > "${BACKUP_PATH}/matrix-postgres-${TIMESTAMP}.sql.gz" \ && log " Matrix Postgres: OK ($(du -sh "${BACKUP_PATH}/matrix-postgres-${TIMESTAMP}.sql.gz" | cut -f1))" \ || log " Matrix Postgres: FAILED" else log " matrix-postgres not running — skipping" fi # ── Rocket.Chat MongoDB ──────────────────────────────────────────── log "Dumping Rocket.Chat MongoDB..." if docker ps --format '{{.Names}}' | grep -q '^mongodb$'; then docker exec mongodb mongodump \ -u admin -p CHANGE_ME_MONGO_ADMIN_PASSWORD \ --authenticationDatabase admin \ --db rocketchat \ --archive \ | gzip > "${BACKUP_PATH}/rocketchat-mongo-${TIMESTAMP}.archive.gz" \ && log " MongoDB: OK ($(du -sh "${BACKUP_PATH}/rocketchat-mongo-${TIMESTAMP}.archive.gz" | cut -f1))" \ || log " MongoDB: FAILED" else log " mongodb not running — skipping" fi # ── Named Docker volumes ─────────────────────────────────────────── log "Backing up Docker volumes..." for VOLUME in synapse-media rocketchat-uploads; do if docker volume ls --format '{{.Name}}' | grep -q "^matrix_${VOLUME}\|^rocketchat_${VOLUME}\|^${VOLUME}$"; then ACTUAL_VOL=$(docker volume ls --format '{{.Name}}' | grep "${VOLUME}" | head -1) docker run --rm \ -v "${ACTUAL_VOL}:/volume:ro" \ -v "${BACKUP_PATH}:/backup" \ alpine \ tar czf "/backup/${VOLUME}-${TIMESTAMP}.tar.gz" -C /volume . \ && log " Volume ${VOLUME}: OK" \ || log " Volume ${VOLUME}: FAILED" else log " Volume ${VOLUME}: not found — skipping" fi done # ── Config files (bind mounts) ───────────────────────────────────── log "Backing up config directories..." tar czf "${BACKUP_PATH}/configs-${TIMESTAMP}.tar.gz" \ /opt/docker/traefik/traefik.yml \ /opt/docker/traefik/config/ \ /opt/docker/matrix/docker-compose.yml \ /opt/docker/matrix/element-config/ \ /opt/docker/matrix/synapse-config/homeserver.yaml \ /opt/docker/matrix/synapse-config/matrix.example.com.log.config \ /opt/docker/rocketchat/docker-compose.yml \ 2>/dev/null && log " Configs: OK" || log " Configs: partial (some files missing)" # IMPORTANT: signing key is sensitive — back up separately with tight perms if [ -f /opt/docker/matrix/synapse-config/matrix.example.com.signing.key ]; then cp /opt/docker/matrix/synapse-config/matrix.example.com.signing.key \ "${BACKUP_PATH}/synapse-signing-key-${TIMESTAMP}.key" chmod 600 "${BACKUP_PATH}/synapse-signing-key-${TIMESTAMP}.key" log " Synapse signing key: backed up (600)" fi # ── Minecraft server ─────────────────────────────────────────────── log "Backing up Minecraft server..." if docker ps --format '{{.Names}}' | grep -q '^minecraft-mc$'; then # Server is running - create consistent world snapshot docker exec minecraft-mc bash -c \ "cd /data && tar czf /tmp/mc-world-backup-${TIMESTAMP}.tar.gz world/ world_nether/ world_the_end/ 2>/dev/null" && \ docker cp minecraft-mc:/tmp/mc-world-backup-${TIMESTAMP}.tar.gz "${BACKUP_PATH}/" && \ docker exec minecraft-mc rm -f /tmp/mc-world-backup-${TIMESTAMP}.tar.gz && \ log " Minecraft world: OK ($(du -sh "${BACKUP_PATH}/mc-world-backup-${TIMESTAMP}.tar.gz" | cut -f1))" \ || log " Minecraft world: FAILED" # Backup configs and plugins tar czf "${BACKUP_PATH}/minecraft-configs-${TIMESTAMP}.tar.gz" \ /opt/docker/minecraft/server.properties \ /opt/docker/minecraft/purpur.yml \ /opt/docker/minecraft/spigot.yml \ /opt/docker/minecraft/paper-*.yml \ /opt/docker/minecraft/bukkit.yml \ /opt/docker/minecraft/ops.json \ /opt/docker/minecraft/banned-*.json \ /opt/docker/minecraft/eula.txt \ 2>/dev/null && \ log " Minecraft configs: OK" \ || log " Minecraft configs: partial (expected)" else # Server is stopped - backup everything directly tar czf "${BACKUP_PATH}/minecraft-full-backup-${TIMESTAMP}.tar.gz" \ /opt/docker/minecraft/world/ \ /opt/docker/minecraft/world_nether/ \ /opt/docker/minecraft/world_the_end/ \ /opt/docker/minecraft/plugins/ \ /opt/docker/minecraft/server.properties \ /opt/docker/minecraft/purpur.yml \ /opt/docker/minecraft/spigot.yml \ 2>/dev/null && \ log " Minecraft (full, offline): OK ($(du -sh "${BACKUP_PATH}/minecraft-full-backup-${TIMESTAMP}.tar.gz" | cut -f1))" \ || log " Minecraft (offline): partial" fi "${BACKUP_PATH}/synapse-signing-key-${TIMESTAMP}.key" chmod 600 "${BACKUP_PATH}/synapse-signing-key-${TIMESTAMP}.key" log " Synapse signing key: backed up (600)" fi # ── Prune old backups ────────────────────────────────────────────── log "Pruning backups older than ${KEEP_DAYS} days..." find "$BACKUP_DIR" -maxdepth 1 -type d -mtime "+${KEEP_DAYS}" -exec rm -rf {} + 2>/dev/null || true find "$BACKUP_DIR" -maxdepth 1 -name "*.log" -mtime +30 -delete 2>/dev/null || true BACKUP_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1) log "=== Backup complete: ${BACKUP_PATH} (${BACKUP_SIZE}) ==="