#!/usr/bin/env bash # # Kopia zapasowa instancji: baza PostgreSQL i pliki MinIO. # # Uruchomienie z katalogu projektu: # ./scripts/backup.sh # ./scripts/backup.sh /mnt/backup/webinar # # Na serwerze deweloperskim z Windows i Git Bash trzeba wylaczyc konwersje # sciezek, inaczej tar dostanie sciezke windowsowa: # MSYS_NO_PATHCONV=1 COMPOSE_FILE=docker-compose.yml ENV_FILE=.env ./scripts/backup.sh # # Domyslnie zapisuje do ./backups. Kazda kopia trafia do osobnego katalogu # nazwanego znacznikiem czasu, zeby przypadkiem nie nadpisac poprzedniej. # # Odtworzenie: patrz scripts/restore.sh oraz sekcja w README. set -euo pipefail COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.prod.yml}" ENV_FILE="${ENV_FILE:-.env.production}" BACKUP_ROOT="${1:-./backups}" # ile kopii zachowac, starsze sa usuwane KEEP_LAST="${KEEP_LAST:-14}" # Kopia bez plikow MinIO: SKIP_MINIO=1 # # Nagrania stanowia prawie caly rozmiar kopii (400 MB do 1 GB na godzine), # a pliki w MinIO sa NIEZMIENNE PO WGRANIU: kopia sprzed tygodnia zawiera # dokladnie te same nagrania co dzisiejsza. Przy ograniczonym miejscu warto # robic codziennie sama baze, a calosc raz w tygodniu. # # Zrzut samej bazy wystarcza do odtworzenia konfiguracji, zapisow, kursow # i wynikow. Zginie wylacznie TRESC plikow: wpisy w bazie zostana i beda # wskazywac na nieistniejace obiekty. SKIP_MINIO="${SKIP_MINIO:-0}" TIMESTAMP="$(date +%Y%m%d-%H%M%S)" TARGET="${BACKUP_ROOT}/${TIMESTAMP}" compose() { docker compose -f "${COMPOSE_FILE}" --env-file "${ENV_FILE}" "$@" } log() { echo "[$(date +%H:%M:%S)] $*" } if [ ! -f "${ENV_FILE}" ]; then echo "BLAD: brak pliku ${ENV_FILE}. Uruchom skrypt z katalogu projektu." >&2 exit 1 fi # nazwa bazy i uzytkownika z konfiguracji instancji DB_NAME="$(grep -E '^DB_NAME=' "${ENV_FILE}" | cut -d= -f2-)" DB_USER="$(grep -E '^DB_USER=' "${ENV_FILE}" | cut -d= -f2-)" if [ -z "${DB_NAME}" ] || [ -z "${DB_USER}" ]; then echo "BLAD: nie udalo sie odczytac DB_NAME lub DB_USER z ${ENV_FILE}" >&2 exit 1 fi mkdir -p "${TARGET}" log "Kopia zapasowa do katalogu ${TARGET}" # --- baza danych --- # pg_dump w formacie custom, mniejszy i pozwala na wybiorcze odtwarzanie log "Zrzut bazy ${DB_NAME}..." compose exec -T postgres pg_dump \ -U "${DB_USER}" \ -d "${DB_NAME}" \ --format=custom \ --no-owner \ > "${TARGET}/baza.dump" DB_SIZE="$(du -h "${TARGET}/baza.dump" | cut -f1)" log "Baza zapisana (${DB_SIZE})" # --- pliki MinIO --- # Obraz MinIO nie zawiera tar, wiec archiwizujemy wolumen z pomocniczego # kontenera alpine podpietego do tego samego wolumenu w trybie tylko do odczytu. if [ "${SKIP_MINIO}" = "1" ]; then log "Pomijam pliki MinIO (SKIP_MINIO=1)" MINIO_SIZE="pominieto" else log "Kopia plikow MinIO..." # Nazwe wolumenu bierzemy prosto z dzialajacego kontenera, zeby nie zgadywac # przedrostka projektu ani nie parsowac konfiguracji compose. MINIO_CONTAINER="$(compose ps -q minio)" if [ -z "${MINIO_CONTAINER}" ]; then echo "BLAD: kontener minio nie dziala, nie mozna wykonac kopii plikow" >&2 exit 1 fi MINIO_VOLUME="$(docker inspect "${MINIO_CONTAINER}" \ --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}')" if [ -z "${MINIO_VOLUME}" ]; then echo "BLAD: nie udalo sie ustalic wolumenu MinIO" >&2 exit 1 fi log " wolumen: ${MINIO_VOLUME}" docker run --rm \ -v "${MINIO_VOLUME}:/data:ro" \ alpine:3.20 tar czf - -C /data . > "${TARGET}/minio.tar.gz" # pusty albo podejrzanie maly plik oznacza, ze archiwizacja sie nie udala if [ ! -s "${TARGET}/minio.tar.gz" ] || [ "$(stat -c%s "${TARGET}/minio.tar.gz")" -lt 100 ]; then echo "BLAD: archiwum MinIO jest puste, kopia jest niekompletna" >&2 exit 1 fi MINIO_SIZE="$(du -h "${TARGET}/minio.tar.gz" | cut -f1)" log "Pliki MinIO zapisane (${MINIO_SIZE})" fi # --- konfiguracja instancji --- # .env.production zawiera sekrety, wiec kopia musi byc chroniona uprawnieniami cp "${ENV_FILE}" "${TARGET}/env.production.kopia" chmod 600 "${TARGET}/env.production.kopia" log "Konfiguracja zapisana" # --- metryczka, zeby przy odtwarzaniu bylo wiadomo co to jest --- cat > "${TARGET}/INFO.txt" </dev/null | head -c 200 || echo 'nieznana') Rodzaj kopii: $([ "${SKIP_MINIO}" = "1" ] && echo 'TYLKO BAZA (bez plikow MinIO)' || echo 'pelna') Rozmiar MinIO: ${MINIO_SIZE} Zawartosc: baza.dump zrzut PostgreSQL w formacie custom (pg_restore) minio.tar.gz zawartosc wolumenu MinIO (brak przy SKIP_MINIO=1) env.production.kopia konfiguracja instancji, ZAWIERA SEKRETY Odtworzenie: ./scripts/restore.sh ${TARGET} EOF chmod 700 "${TARGET}" # --- rotacja starych kopii --- if [ "${KEEP_LAST}" -gt 0 ]; then COUNT="$(find "${BACKUP_ROOT}" -maxdepth 1 -mindepth 1 -type d | wc -l)" if [ "${COUNT}" -gt "${KEEP_LAST}" ]; then REMOVE=$((COUNT - KEEP_LAST)) log "Usuwam ${REMOVE} najstarszych kopii (zachowuje ${KEEP_LAST})" find "${BACKUP_ROOT}" -maxdepth 1 -mindepth 1 -type d \ | sort \ | head -n "${REMOVE}" \ | while read -r old; do rm -rf "${old}" log " usunieto $(basename "${old}")" done fi fi log "Gotowe: ${TARGET}"