Skrypt do automatycznego backupu strony www na serwer FTP

0
5
Rate this post

Z tego tekstu dowiesz się...

Po co automatyczny backup strony www i kiedy ma sens

Ręczny backup a automatyczny – różnice w praktyce

Ręczny backup strony www brzmi prosto: zalogować się na hosting, zgrać pliki przez FTP, wyeksportować bazę danych z phpMyAdmin, odłożyć wszystko na dysk lokalny. Tyle że w codziennym rytmie pracy rzadko kto robi to regularnie, a jeszcze rzadziej robi to poprawnie udokumentowane i w kilku wersjach. Przy jednej stronie może się to udawać, przy kilku czy kilkunastu – przestaje być realne.

Automatyczny backup strony www zdejmuje z barków administratora konieczność pamiętania o kopii zapasowej. Skrypt uruchamiany cyklicznie (np. z crona) działa o określonej porze, w powtarzalny sposób, z tymi samymi parametrami. Jeśli zostanie dobrze przetestowany, znacząco redukuje ryzyko błędów ludzkich: pominięcia katalogu, pomyłki w nazwie bazy, nadpisania złej kopii.

Ręczne kopie sprawdzają się przy jednorazowych operacjach: przed dużą migracją, przed wielką aktualizacją, przy zmianie hostingu. Automatyzacja jest kluczowa wtedy, gdy strona żyje: treści się zmieniają, użytkownicy wysyłają formularze, zamówienia trafiają do sklepu. Utrata nawet kilku godzin danych w sklepie internetowym może oznaczać realne straty.

Realne scenariusze utraty danych

Najczęstsze powody, dla których kopia zapasowa na FTP ratuje sytuację, powtarzają się od lat. Zwykle nie chodzi o spektakularne awarie całych centrów danych, lecz o prozaiczne zdarzenia:

  • Aktualizacja CMS lub wtyczki, która „rozsypała” stronę – po aktualizacji WordPressa lub rozszerzeń nagle pojawia się biały ekran, błędy PHP albo nie działają kluczowe funkcje. Bez świeżej kopii trzeba ręcznie cofać zmiany, często po omacku.
  • Włamanie i złośliwe modyfikacje plików – malware dopisane do plików PHP, podmienione szablony, ukryte skrypty wysyłające spam. Odkręcanie tego bez czystej kopii bywa frustrujące i niepewne.
  • Przypadkowe usunięcie plików lub katalogów – jeden nieuważny „Delete” w menedżerze plików hostingu, usunięcie katalogu z mediami albo konfiguracją i strona przestaje działać poprawnie.
  • Błędy na poziomie bazy danych – nieudana migracja, import niewłaściwego dumpa, usunięcie tabel lub rekordów. Bez dumpa bazy wykonanie rollbacku jest w zasadzie niemożliwe.
  • Problemy po stronie hostingu – rzadziej, ale jednak: awarie dysków, błędna migracja konta na nowy serwer, cofnięcie stanu konta do wcześniejszego backupu hostingu (który obejmuje wszystkie strony na raz).

Jeśli istnieje regularna kopia zapasowa na serwer FTP, straty ograniczają się zazwyczaj do czasu koniecznego na przywrócenie kopii i ewentualnych kilku godzin nowych danych. Bez żadnego backupu jedyną opcją bywa „stawianie” wszystkiego od zera.

Dlaczego backup na zewnętrzny serwer FTP, a nie lokalnie

Kopia trzymana na tym samym serwerze, na którym działa strona, jest lepsza niż nic, ale ma istotną wadę: dzieli los tego samego serwera. Jeśli awaria dotyczy całego serwera (dysk, RAID, panel), backup zniknie razem z nim. Podobnie przy poważnym włamaniu – atakujący, mając dostęp do plików, często kasuje również katalogi z kopiami zapasowymi.

Zewnętrzny serwer FTP/FTPS/SFTP izoluje kopie od środowiska produkcyjnego. Nawet jeśli ktoś przejmie konto FTP na hostingu strony, niekoniecznie ma dostęp do drugiego serwera, na który wysyłane są archiwa. Rozdzielenie ról serwera źródłowego i docelowego zmniejsza ryzyko jednoczesnej utraty danych produkcyjnych i backupów.

Dodatkowo osobny serwer FTP ułatwia centralizację. Jeśli zarządzanych jest kilka projektów, można wszystkie kopie odkładać w jednej strukturze katalogów na zewnętrznym zasobie, a następnie np. replikować go dalej (na dysk USB, do chmury obiektowej itp.).

Minimalne wymagania, aby automatyczny backup miał sens

Do uruchomienia prostego mechanizmu automatycznego backupu strony www na serwer FTP potrzebne są głównie cztery elementy:

  • Dostęp do shella (SSH) lub innego środowiska uruchomieniowego – najczęściej będzie to konsola na serwerze z aplikacją lub na oddzielnym serwerze administracyjnym.
  • Możliwość korzystania z narzędzi systemowych – takich jak tar, gzip lub zip (do kompresji) oraz mysqldump (do backupu bazy danych MySQL/MariaDB).
  • Serwer FTP/FTPS/SFTP – z utworzonym kontem, znanym hostem, loginem, hasłem i katalogiem docelowym na kopie zapasowe.
  • Mechanizm harmonogramu – w systemach Linux/UNIX jest to zwykle cron, który uruchamia skrypt w określonych odstępach (np. codziennie w nocy).

Jeśli któryś z tych elementów jest niedostępny (np. brak SSH na tanim hostingu), trzeba szukać alternatyw: skryptów uruchamianych z zewnątrz, gotowych narzędzi backupowych lub backupów realizowanych z poziomu panelu hostingu.

Krótka sytuacja z praktyki

Typowy scenariusz: niewielki sklep na WordPress + WooCommerce działa przez kilka miesięcy bez problemu. Właściciel włącza automatyczne aktualizacje – z czasem jedna z aktualizacji wtyczki e-commerce kończy się błędem krytycznym i białym ekranem. Hosting ma kopie, ale sprzed kilku dni, bo backup wykonywany jest raz na tydzień. Różnica obejmuje zamówienia i nowe produkty. Jeśli natomiast działał skrypt bash backup z codzienną kopią na FTP, strata ogranicza się do kilku godzin – do przywrócenia jest świeży plik archiwum z danej nocy.


Zbliżenie stanowiska programisty z kodem na monitorze i smartfonem
Źródło: Pexels | Autor: Firos nv

Założenia techniczne: co dokładnie będzie backupowane i gdzie

Pliki strony vs. baza danych – dwa różne światy

Kompletna kopia zapasowa strony www musi objąć zarówno pliki, jak i dane trzymane w bazie. Pliki to przede wszystkim:

  • kod aplikacji (np. pliki PHP, biblioteki, szablony),
  • statyczne zasoby: grafiki, pliki CSS/JS, uploady użytkowników,
  • pliki konfiguracyjne (np. wp-config.php, pliki .env),
  • dodatkowe skrypty, cronjoby, pliki narzędziowe.

Baza danych przechowuje to, co najcenniejsze: treści wpisów, informacje o użytkownikach, zamówienia, konfiguracje wtyczek, logi zdarzeń itd. Bez bazy danych wiele nowoczesnych CMS-ów nie jest w stanie w ogóle się uruchomić. Dlatego sensowny backup wymaga obsługi obu elementów i spójności czasowej: dump bazy powinien być wykonany przed zarchiwizowaniem plików lub w miarę blisko tego momentu.

Struktura typowej strony na hostingu współdzielonym

Na standardowym hostingu współdzielonym spotyka się najczęściej podobną strukturę katalogów:

  • katalog główny użytkownika (np. /home/uzytkownik/),
  • katalog publiczny strony (np. /home/uzytkownik/public_html/ lub /home/uzytkownik/www/),
  • dodatkowe katalogi na logi (logs), pliki systemowe, tymczasowe dane.

Niektóre konfiguracje umieszczają pliki konfiguracyjne poza katalogiem publicznym (dla bezpieczeństwa), np. /home/uzytkownik/config/. Projekt skryptu powinien jasno określać które katalogi i pliki są częścią backupu, a które można pominąć (np. ogromne katalogi cache, tymczasowe raporty, sesje PHP). Dzięki temu rozmiar archiwów pozostanie rozsądny.

Serwer FTP, FTPS czy SFTP – porównanie pod kątem bezpieczeństwa

Hasło i dane przekazywane w czystym FTP wędrują przez sieć w postaci niezaszyfrowanej. W nowoczesnych środowiskach jest to rozwiązanie akceptowalne tylko w kontrolowanych sieciach wewnętrznych. Dla backupów wysyłanych przez Internet bezpieczniej jest używać połączeń szyfrowanych.

ProtokółSzyfrowanieTypowe portyPlusyMinusy
FTPBrak21Prosty, szeroko wspieranyNiezabezpieczone hasła i dane, podatny na podsłuch
FTPS (FTP over TLS)Tak21 lub dedykowanySzyfrowanie, kompatybilność z FTPCzasem problemy z firewallami i trybem pasywnym
SFTP (SSH File Transfer)Tak22Stabilny, bezpieczny, działa przez SSHWymaga serwera SSH, inny protokół niż FTP

Bezpieczeństwo danych FTP zależy bezpośrednio od użytego protokołu. Jeśli dostawca docelowego serwera oferuje SFTP, zwykle jest to najlepsza opcja: jedno konto SSH może służyć zarówno do administracji, jak i odbioru backupów, a połączenie jest szyfrowane. W praktyce jednak wiele hostingów udostępnia głównie FTPS – również akceptowalne, o ile połączenie jest poprawnie wymuszone i weryfikowane.

Konsekwencje wyboru protokołu dla skryptu

Decyzja, czy backup trafi na FTP, FTPS czy SFTP, wpływa na dobór narzędzi w skrypcie. Dla FTP i FTPS wygodne jest narzędzie lftp, które obsługuje oba scenariusze i oferuje rozbudowane opcje (np. mirroring, kasowanie starych plików, tryb non-interactive). Dla SFTP można użyć zarówno sftp, jak i scp czy rsync przez SSH.

W prostym podejściu można założyć, że skrypt będzie używał jednego, wybranego trybu. Później możliwe jest rozbudowanie go o przełączniki lub zmienne określające protokół, port i sposób logowania (hasło, klucz SSH).

Ograniczenia hostingu i ich wpływ na projekt backupu

Na serwerach współdzielonych często występują limity, które trzeba uwzględnić:

  • limit czasu wykonania procesów – zbyt długi backup dużej strony może zostać przerwany,
  • brak niektórych narzędzi – np. brak dostępu do mysqldump lub lftp,
  • limity przestrzeni dyskowej – konieczne jest kontrolowanie rozmiaru katalogu z tymczasowymi archiwami,
  • brak dostępu do shella (SSH) – wówczas skrypt musi być uruchamiany gdzie indziej (np. na zewnętrznym serwerze).

Jeśli hosting jest „ciasny”, często lepszym rozwiązaniem jest pull backup: osobny serwer łączy się na FTP/SFTP z hostingiem i pobiera kopię, a następnie wysyła ją w docelowe miejsce. Takie podejście minimalizuje obciążenie hostingu i uniezależnia się od jego ograniczeń.


Wybór języka skryptowego i środowiska uruchomieniowego

Dlaczego Bash/sh na Linuxie jest dobrym punktem startowym

Jeśli serwer z aplikacją działa na Linuksie, najprostszym wyborem jest skrypt Bash lub sh. Shell ma kilka istotnych zalet:

  • jest dostępny praktycznie na każdym serwerze z SSH,
  • nie wymaga doinstalowywania środowisk uruchomieniowych (jak Python/Node/Java),
  • łatwo obsługuje narzędzia systemowe (tar, gzip, mysqldump, rsync, lftp),
  • doskonale współpracuje z cronem.

Do zadań typu kompresja backupu strony, wywołanie dumpa bazy, wysłanie pliku po FTP i usunięcie starych archiwów shell nadaje się w 100%. Dopiero przy zaawansowanych funkcjach (np. integracja z API chmury, szyfrowanie asymetryczne, rozbudowane logowanie strukturalne) może pojawić się sens wyjścia poza Basha.

Alternatywy: Python, PowerShell i gotowe narzędzia

Python daje większe możliwości w zakresie struktury kodu, testów jednostkowych, rozszerzeń i integracji z zewnętrznymi usługami (np. AWS S3, Google Cloud Storage). Biblioteki takie jak paramiko (SFTP/SSH), boto3 (AWS) czy ftplib (FTP) pozwalają zbudować złożony system backupu, ale wymagają instalacji i utrzymania środowiska Pythonowego.

PowerShell jest z kolei naturalnym wyborem w środowiskach Windows, zwłaszcza tam, gdzie i tak istnieje infrastruktura oparta o serwery Windows i Active Directory. Skrypt backupu można wtedy odpalać z Harmonogramu zadań, korzystać z wbudowanych cmdletów do pracy z plikami oraz modułów obsługujących FTP/SFTP czy API chmurowe. Minusem jest mniejsza przenośność na typowe hostingi współdzielone, gdzie PowerShell zwykle nie jest dostępny.

Trzecia grupa rozwiązań to gotowe narzędzia backupowe: dedykowane aplikacje, kontenery Docker, a nawet usługi SaaS. Część z nich potrafi łączyć się po FTP/SFTP, robić migawki filesystemu i trzymać historię wersji. Dają wygodny interfejs i raportowanie, lecz: kosztują, wymagają zaufania do zewnętrznego dostawcy i nie zawsze dobrze współpracują z ograniczonymi hostingami. Skrypt Bashowy, choć prostszy, bywa bardziej elastyczny i łatwiejszy do wpasowania w konkretne ograniczenia serwera.

Dobór języka i środowiska dobrze oprzeć na jednym, prostym kryterium: gdzie fizycznie będzie uruchamiany backup. Jeśli jest dostęp do shella na tym samym serwerze, gdzie działa strona – Bash/sh zazwyczaj wygrywa prostotą. Jeśli backup ma startować z centralnego serwera administracyjnego obejmującego dziesiątki witryn, wtedy Python lub inne „wyżej poziomowe” środowisko da lepszą skalowalność i łatwiejsze utrzymanie większej bazy kodu.

Bez względu na technologię, kluczowy pozostaje schemat działania: stabilny dump bazy, spakowanie najważniejszych plików, wysyłka na zewnętrzny serwer i usuwanie starych archiwów. Gdy ten fundament jest dobrze zaprojektowany, sam wybór narzędzia – Bash, Python, PowerShell czy gotowa aplikacja – staje się głównie kwestią wygody i dopasowania do konkretnej infrastruktury.

Klawisze z napisem BACKUP na koralowym tle
Źródło: Pexels | Autor: Miguel Á. Padriñán

Przygotowanie środowiska: dostęp, narzędzia, konto FTP

Wymagane dostępy i informacje konfiguracyjne

Zanim pojawi się choćby jedna linijka skryptu, trzeba zebrać zestaw konkretnych danych. Bez nich nawet najładniejszy kod będzie bezużyteczny. Minimalny pakiet wygląda tak:

  • dane dostępowe do serwera z aplikacją (SSH lub panel z możliwością utworzenia crona),
  • lokalizacja plików strony (np. /home/uzytkownik/public_html/),
  • lokalizacja ewentualnych plików konfiguracyjnych poza katalogiem publicznym (np. /home/uzytkownik/config/),
  • dane dostępowe do bazy danych (host, nazwa bazy, użytkownik, hasło),
  • dane dostępowe do serwera docelowego (host, port, login, protokół: FTP/FTPS/SFTP),
  • ustalone miejsce, w którym na serwerze aplikacji będą lądować tymczasowe archiwa (np. /home/uzytkownik/backups_tmp/).

Bezpieczniej jest przechowywać hasła i loginy w osobnym pliku konfiguracyjnym, do którego dostęp ma tylko właściciel (uprawnienia 600), niż wpisywać je na sztywno w kodzie skryptu. Ułatwia to również późniejszą zmianę haseł bez dotykania logiki backupu.

Instalacja narzędzi systemowych potrzebnych do backupu

Standardowy skrypt backupu oparty na Bashu opiera się zwykle na kilku poleceniach systemowych. Najczęściej spotykany zestaw to:

  • tar – tworzenie archiwów katalogów i plików,
  • gzip lub xz – kompresja archiwów,
  • mysqldump lub mariadb-dump – zrzut bazy MySQL/MariaDB,
  • lftp – obsługa FTP/FTPS (w trybie skryptowym),
  • ssh/sftp/scp – wysyłka po SFTP (gdy dostępny jest SSH),
  • date, find, df – generowanie znaczników czasu, zarządzanie plikami, kontrola miejsca na dysku.

Na serwerach z własnym dostępem root (VPS, dedyk) pakiety można zwykle doinstalować:

# Debian/Ubuntu
sudo apt install lftp

# CentOS/Rocky/Alma
sudo yum install lftp

Na hostingu współdzielonym sytuacja bywa różna: część narzędzi jest już dostępna, inne mogą być niedostępne na stałe. Warto sprawdzić:

which mysqldump
which lftp
which sftp

Jeśli nie ma dostępu do mysqldump, pozostaje użycie alternatyw (np. panel hostingu generujący dump i zapisujący go na dysku) lub przeniesienie procesu backupu bazy na inny serwer, który potrafi łączyć się z bazą zdalnie.

Konfiguracja konta FTP/SFTP i struktury katalogów docelowych

Na serwerze, który będzie przyjmował backupy, dobrze jest od razu ustalić przejrzystą strukturę katalogów. Pozwala to uniknąć chaosu po kilku miesiącach działania automatu. Sprawdza się podział:

  • po witrynach (np. /backups/mojadomena.pl/, /backups/innyserwis.com/),
  • z dodatkowym rozbiciem na typ zasobu (np. files/, db/) albo trzymaniem wszystkiego w jednym katalogu z sensowną nazwą pliku.

Do przyjmowania kopii warto utworzyć osobne konto FTP/SFTP o ściśle ograniczonych uprawnieniach (tzw. jail do jednego katalogu). Dzięki temu ewentualny wyciek danych dostępowych ze skryptu nie daje napastnikowi pełnego dostępu do całego serwera backupowego.

Planowanie harmonogramu backupów

Sam skrypt to tylko połowa układanki. Druga część to harmonogram. Jego parametry zależą od charakteru serwisu:

  • mała strona-wizytówka – zwykle wystarczy backup raz na dobę, często nawet rzadziej,
  • sklep internetowy – bezpieczeństwo transakcji i zamówień wymaga zrzutu minimum co kilka godzin, czasem co godzinę (często oddzielnie dla bazy i plików),
  • serwis z intensywnym uploadem użytkowników – krytyczne stają się częste kopie plików, ewentualnie w połączeniu z przyrostowym backupem.

Na serwerach linuksowych harmonogram obsłuży cron. Wpis w tablicy crona (np. przez crontab -e) może wyglądać tak:

# Backup codziennie o 3:15 w nocy
15 3 * * * /home/uzytkownik/scripts/backup_www.sh >> /home/uzytkownik/logs/backup.log 2>&1

W przypadku hostingu bez SSH, ale z konfiguracją zadań cyklicznych w panelu (np. „Zadania CRON”), wywołuje się analogiczne polecenie lub adres URL uruchamiający skrypt zewnętrzny.

Projekt skryptu backupu – architektura i logika krok po kroku

Podział na etapy i minimalna funkcjonalność

Skrypt backupu łatwiej utrzymać, jeśli jego działanie podzielone jest na czytelne etapy. Prosty, ale praktyczny szkielet obejmuje:

  1. wczytanie konfiguracji (ścieżki, dane dostępowe, opcje),
  2. sprawdzenie wymagań (narzędzia, miejsce na dysku tymczasowym),
  3. wykonanie dumpu bazy danych (jeśli jest w użyciu),
  4. spakowanie plików serwisu i dumpu bazy,
  5. wysłanie archiwum na serwer FTP/FTPS/SFTP,
  6. posprzątanie lokalnych plików tymczasowych,
  7. (opcjonalnie) usuwanie starych backupów po stronie zdalnej.

Każdy z tych kroków można zaimplementować jako osobną funkcję w Bashu lub przynajmniej wydzielony blok kodu. Ułatwia to diagnozowanie awarii i rozbudowę logiki.

Format nazewnictwa plików backupu

Dobrze zaprojektowany schemat nazw plików archiwów przyspiesza późniejsze operacje. Często stosuje się:

  • znacznik czasu w formacie YYYYMMDD-HHMM – łatwe sortowanie alfabetyczne = sortowanie chronologiczne,
  • oznaczenie typu backupu (np. full, db, files),
  • krótką nazwę projektu lub domeny.

Przykładowa nazwa:

mojadomena_pl-full-20240215-0315.tar.gz

lub przy osobnym backupie bazy i plików:

mojadomena_pl-db-20240215-0315.sql.gz
mojadomena_pl-files-20240215-0315.tar.gz

Strategia retencji: ile kopii trzymać i gdzie ją egzekwować

Nieograniczone gromadzenie archiwów efektywnie zapcha każdy dysk. Trzeba więc z wyprzedzeniem określić zasady retencji. Typowy, prosty model to:

  • przechowywanie lokalnych, tymczasowych archiwów maksymalnie kilka dni (np. 2–3) – tylko na wypadek problemów z wysyłką,
  • na serwerze docelowym: przechowywanie ostatnich N dni backupów (np. 7, 14 lub 30) lub ograniczanie się do kilkunastu-kilkudziesięciu ostatnich plików.

Retencja może być egzekwowana:

  • lokalnie – poprzez find usuwający stare pliki w katalogu tymczasowym,
  • zdalnie – poprzez skrypt lftp lub dodatkową komendę ssh/sftp, która usuwa starsze archiwa po stronie serwera backupowego.

W przypadku hostingu współdzielonego, gdzie miejsce jest najbardziej ograniczone właśnie na serwerze z aplikacją, sens ma agresywne czyszczenie lokalnych plików i łagodniejsza retencja po stronie docelowej.

Obsługa błędów i powiadamianie

Prędzej czy później pojawi się awaria: brak miejsca, błąd logowania na FTP, zmiana hasła do bazy. Skrypt powinien:

  • przerywać działanie po krytycznym błędzie (np. brak dumpu bazy), a nie wykonywać kolejnych kroków „na ślepo”,
  • zwracać sensowny kod wyjścia (exit 0 przy sukcesie, exit >0 przy błędzie),
  • logować komunikaty do pliku lub na standardowe wyjście, przechwytywane przez crona.

Minimum to zbiorczy log tekstowy. Jeśli jest taka możliwość, przydaje się też prosty mechanizm powiadomienia (np. wysłanie maila przez mail lub webhook do narzędzia monitorującego) po nieudanym backupie. W wielu małych wdrożeniach sprawdza się też prosta kontrola logów manualnie raz na jakiś czas – ale wymaga to dyscypliny po stronie administratora.

Zewnętrzny dysk twardy podłączony kablem USB do laptopa na biurku
Źródło: Pexels | Autor: Arina Krasnikova

Implementacja prostego skryptu Bash do backupu plików strony

Założenia dla przykładowego skryptu

Poniższy przykład pokazuje jeden, konkretny wariant:

  • backup plików strony z katalogu /home/uzytkownik/public_html/,
  • backup bazy MySQL jedną komendą mysqldump,
  • tworzenie jednego archiwum .tar.gz zawierającego dump bazy i pliki serwisu,
  • wysłanie archiwum po FTPS na serwer docelowy za pomocą lftp,
  • utrzymywanie lokalnych archiwów do 3 dni,
  • podstawowa obsługa błędów i logów.

Konfiguracja (ścieżki, dane dostępowe) zostanie wyniesiona do sekcji zmiennych na początku skryptu, aby można je było łatwo dostosować do różnych serwisów.

Przykładowy skrypt Bash – pliki + baza + wysyłka po FTPS

Przykład prostego, jednoplikowego skryptu: backup_www.sh.

#!/usr/bin/env bash

set -euo pipefail

############################
# Konfiguracja podstawowa  #
############################

# Nazwa projektu / domeny (bez znaków specjalnych)
PROJECT_NAME="mojadomena_pl"

# Ścieżki lokalne
WEB_ROOT="/home/uzytkownik/public_html"
CONFIG_DIR="/home/uzytkownik/config"
BACKUP_TMP_DIR="/home/uzytkownik/backups_tmp"

# Backup bazy
DB_ENABLE=true
DB_HOST="localhost"
DB_NAME="moja_baza"
DB_USER="uzytkownik_db"
DB_PASS="haslo_db"

# Serwer docelowy (FTPS)
FTP_ENABLE=true
FTP_HOST="backup.example.com"
FTP_PORT="21"
FTP_USER="backupuser"
FTP_PASS="haslo_backup"
FTP_REMOTE_DIR="/backups/${PROJECT_NAME}"

# Retencja lokalna (dni)
LOCAL_RETENTION_DAYS=3

# Opcjonalny log (można też logować przez crona)
LOG_FILE="/home/uzytkownik/logs/backup_www.log"

############################
# Inicjalizacja            #
############################

TIMESTAMP="$(date +%Y%m%d-%H%M)"
BACKUP_NAME="${PROJECT_NAME}-full-${TIMESTAMP}"
BACKUP_ARCHIVE="${BACKUP_TMP_DIR}/${BACKUP_NAME}.tar.gz"
DB_DUMP_FILE="${BACKUP_TMP_DIR}/${PROJECT_NAME}-db-${TIMESTAMP}.sql"

mkdir -p "${BACKUP_TMP_DIR}"

# Logowanie pomocnicze
log() {
  local level="$1"
  shift
  local msg="$*"
  local line="[$(date '+%Y-%m-%d %H:%M:%S')] [${level}] ${msg}"
  echo "${line}"
  if [[ -n "${LOG_FILE}" ]]; then
    echo "${line}" >> "${LOG_FILE}"
  fi
}

############################
# Kontrola wymagań         #
############################

check_requirements() {
  log INFO "Sprawdzanie wymaganych narzędzi..."

  local cmds=("tar" "gzip" "date" "find")
  if [[ "${DB_ENABLE}" == true ]]; then
    cmds+=("mysqldump")
  fi
  if [[ "${FTP_ENABLE}" == true ]]; then
    cmds+=("lftp")
  fi

  for cmd in "${cmds[@]}"; do
    if ! command -v "${cmd}" >/dev/null 2>&1; then
      log ERROR "Brak wymaganego narzędzia: ${cmd}"
      exit 1
    fi
  done

  if [[ ! -d "${WEB_ROOT}" ]]; then
    log ERROR "Katalog WEB_ROOT nie istnieje: ${WEB_ROOT}"
    exit 1
  fi
}

############################
# Backup bazy danych       #
############################

backup_database() {
  if [[ "${DB_ENABLE}" != true ]]; then
    log INFO "Backup bazy danych wyłączony (DB_ENABLE=false)."
    return 0
  fi

  log INFO "Rozpoczynam dump bazy danych: ${DB_NAME}"

  # Używamy zmiennej środowiskowej dla bezpieczeństwa hasła
  MYSQL_PWD="${DB_PASS}" mysqldump 
    --user="${DB_USER}" 
    --host="${DB_HOST}" 
    --single-transaction 
    --quick 
    "${DB_NAME}" > "${DB_DUMP_FILE}"

  if [[ ! -s "${DB_DUMP_FILE}" ]]; then
    log ERROR "Dump bazy danych nie został utworzony lub jest pusty."
    exit 1
  fi

  log INFO "Dump bazy danych zapisany: ${DB_DUMP_FILE}"
}

############################
# Backup plików serwisu    #
############################

create_archive() {
  log INFO "Tworzenie archiwum plików serwisu i dumpu bazy..."

  local tar_args=()

  # Dodajemy katalog z plikami
  tar_args+=("-C" "${WEB_ROOT}" ".")

  # Dodajemy katalog konfiguracyjny, jeśli istnieje
  if [[ -d "${CONFIG_DIR}" ]]; then
    tar_args+=("-C" "${CONFIG_DIR}" ".

.")
  fi

  # Dodajemy dump bazy, jeśli istnieje
  if [[ -f "${DB_DUMP_FILE}" ]]; then
    tar_args+=("-C" "${BACKUP_TMP_DIR}" "$(basename "${DB_DUMP_FILE}")")
  fi

  # Tworzymy archiwum
  tar -czf "${BACKUP_ARCHIVE}" "${tar_args[@]}"

  if [[ ! -s "${BACKUP_ARCHIVE}" ]]; then
    log ERROR "Archiwum nie zostało utworzone lub jest puste: ${BACKUP_ARCHIVE}"
    exit 1
  fi

  log INFO "Archiwum utworzone: ${BACKUP_ARCHIVE}"

  # Opcjonalnie usuwamy surowy dump bazy po zapakowaniu
  if [[ -f "${DB_DUMP_FILE}" ]]; then
    rm -f "${DB_DUMP_FILE}"
    log INFO "Tymczasowy dump bazy usunięty: ${DB_DUMP_FILE}"
  fi
}

############################
# Wysyłka na serwer FTP    #
############################

upload_via_ftp() {
  if [[ "${FTP_ENABLE}" != true ]]; then
    log INFO "Wysyłka na FTP wyłączona (FTP_ENABLE=false)."
    return 0
  fi

  log INFO "Wysyłanie archiwum na serwer FTP: ${FTP_HOST}"

  lftp -u "${FTP_USER}","${FTP_PASS}" -p "${FTP_PORT}" "ftps://${FTP_HOST}" <<EOF
set ssl:verify-certificate no
mkdir -p "${FTP_REMOTE_DIR}"
cd "${FTP_REMOTE_DIR}"
put "${BACKUP_ARCHIVE}"
EOF

  if [[ "$?" -ne 0 ]]; then
    log ERROR "Błąd podczas wysyłki archiwum na serwer FTP."
    exit 1
  fi

  log INFO "Archiwum wysłane na FTP: ${FTP_REMOTE_DIR}/$(basename "${BACKUP_ARCHIVE}")"
}

############################
# Czyszczenie starych kopii#
############################

cleanup_local() {
  log INFO "Czyszczenie lokalnych archiwów starszych niż ${LOCAL_RETENTION_DAYS} dni w ${BACKUP_TMP_DIR}"

  find "${BACKUP_TMP_DIR}" -type f -name "${PROJECT_NAME}-full-*.tar.gz" 
    -mtime +"${LOCAL_RETENTION_DAYS}" -print -delete 2>/dev/null || true
}

############################
# Główna logika skryptu    #
############################

main() {
  log INFO "===== START BACKUP: ${PROJECT_NAME} (${TIMESTAMP}) ====="

  check_requirements
  backup_database
  create_archive
  upload_via_ftp
  cleanup_local

  log INFO "Backup zakończony pomyślnie: ${BACKUP_ARCHIVE}"
  log INFO "===== KONIEC BACKUPU: ${PROJECT_NAME} ====="
}

main "$@"

Uruchamianie z crona i podstawowe modyfikacje

Po zapisaniu skryptu trzeba nadać mu prawa wykonywania i podpiąć go pod harmonogram. Minimalna konfiguracja na koncie użytkownika wygląda następująco:

chmod +x /home/uzytkownik/backup_www.sh

crontab -e

Przykładowy wpis w cronie, wykonujący backup codziennie o 3:15 w nocy:

15 3 * * * /home/uzytkownik/backup_www.sh >> /home/uzytkownik/logs/backup_www.cron.log 2>&1

W prostych wdrożeniach często wystarczy zmiana kilku zmiennych na początku skryptu: ścieżki WEB_ROOT/CONFIG_DIR, dane dostępu do bazy i FTP, ścieżka katalogu tymczasowego oraz liczba dni retencji. Jeśli katalog z konfiguracją znajduje się poza domyślnym CONFIG_DIR, można dodać kolejny blok -C /sciezka "." w tablicy tar_args lub dopisać osobny katalog z logami.

Przeczytaj także:  Skrypty w JavaScript: podstawy tworzenia interaktywnych stron

Gdy pojawia się potrzeba utrzymywania osobnych kopii bazy i plików, najprościej rozdzielić funkcję tworzącą archiwum na dwa warianty lub dodać dodatkowe argumenty wiersza poleceń (np. --only-db, --only-files) i w main() sterować tym, które kroki są wykonywane. W środowiskach z kilkoma serwisami na jednym koncie dobrym podejściem jest trzymanie oddzielnych plików konfiguracyjnych i jednego wspólnego „silnika” backupu wczytującego je przez source.

Jeśli backup ma działać na różnych środowiskach (dev/stage/produkcja), przydaje się wydzielenie parametrów do osobnych plików konfiguracyjnych, np. backup.dev.conf, backup.prod.conf. Sam skrypt może wtedy przyjmować nazwę profilu jako argument (./backup_www.sh prod) i robić prosty source "backup.${PROFILE}.conf". Znika potrzeba utrzymywania kilku kopii skryptu różniących się tylko wartościami zmiennych, a łatwiej jest uniknąć przypadkowego wykonania z nieaktualną konfiguracją.

Dobrą praktyką jest też kontrola miejsca na dysku przed startem backupu. Krótka funkcja sprawdzająca, czy w BACKUP_TMP_DIR oraz w katalogu z logami jest przynajmniej określona liczba wolnych megabajtów, potrafi oszczędzić sporo nerwów. Jeśli backup pęknie w połowie z powodu braku miejsca, logi i niedokończone archiwum rzadko są warte zachodu. Prosty test oparty o df -Pm czy stat i przerwanie skryptu z czytelnym komunikatem w log() w większości przypadków wystarczy.

Przy średniej i większej skali sens ma dodanie prostego monitoringu: e-maila lub webhooka wywoływanego tylko przy błędzie. W najprostszym wariancie wystarczy na końcu main() wysłać mail lub sendmail do administratora w bloku trap, który złapie wyjście niezerowe. Dzięki temu awarie typu „zmieniło się hasło do FTP” czy „zniknął katalog z konfiguracją” nie są odkrywane dopiero podczas przywracania danych.

Jeżeli kopie zaczynają zajmować zbyt dużo miejsca na serwerze backupowym, można rozbudować retencję o poziomy: gęstsze kopie z ostatnich dni, rzadsze z poprzednich tygodni, a najstarsze tylko raz w miesiącu. Tego typu logikę wygodnie zaimplementować już po stronie systemu backupowego (np. w dedykowanym repozytorium kopii), ale nawet proste rozszerzenie find o filtrowanie po prefiksach typu -daily, -weekly istotnie porządkuje bałagan w katalogu z archiwami.

Tak przygotowany skrypt jest dobrym punktem wyjścia: działa w prostym hostingu, na VPS-ie i na serwerze dedykowanym, a jednocześnie łatwo go rozszerzać o kolejne bazy, dodatkowe katalogi czy inne cele niż FTP. Najważniejsze, żeby backup był nie tylko wykonywany automatycznie, ale też regularnie testowany przez rzeczywiste odtworzenie plików i bazy – dopiero wtedy daje realne zabezpieczenie przed utratą danych.

Rozszerzenia skryptu: SFTP, wiele baz i selektywny backup

Prosty scenariusz FTP szybko przestaje wystarczać, gdy pojawia się chęć szyfrowania połączenia kluczem, trzymania kilku baz danych albo wycinania z backupu ciężkich katalogów (np. cache czy uploadów generowanych automatycznie). Wtedy lepiej minimalnie rozbudować skrypt niż utrzymywać kilka zupełnie odrębnych wersji.

Wysyłka przez SFTP zamiast FTP/FTPS

Jeśli serwer docelowy wspiera SSH, wygodniej (i bezpieczniej) jest użyć SFTP z kluczem niż logować się hasłem na FTP. Z poziomu skryptu Bash wystarczy narzędzie sftp lub scp. Przykładowa funkcja, równoległa do upload_via_ftp():

############################
# Wysyłka przez SFTP       #
############################

upload_via_sftp() {
  if [[ "${SFTP_ENABLE}" != true ]]; then
    log INFO "Wysyłka przez SFTP wyłączona (SFTP_ENABLE=false)."
    return 0
  fi

  if [[ ! -f "${SFTP_KEY}" ]]; then
    log ERROR "Brak klucza SFTP: ${SFTP_KEY}"
    exit 1
  fi

  log INFO "Wysyłanie archiwum przez SFTP: ${SFTP_USER}@${SFTP_HOST}:${SFTP_REMOTE_DIR}"

  # Tworzymy katalog po stronie zdalnej (jeśli to OpenSSH >= 5.7)
  sftp -i "${SFTP_KEY}" -P "${SFTP_PORT}" "${SFTP_USER}@${SFTP_HOST}" <<EOF
mkdir ${SFTP_REMOTE_DIR}
cd ${SFTP_REMOTE_DIR}
put ${BACKUP_ARCHIVE}
EOF

  if [[ "$?" -ne 0 ]]; then
    log ERROR "Błąd podczas wysyłki archiwum przez SFTP."
    exit 1
  fi

  log INFO "Archiwum wysłane przez SFTP: ${SFTP_REMOTE_DIR}/$(basename "${BACKUP_ARCHIVE}")"
}

W górnej części skryptu trzeba uzupełnić konfigurację:

SFTP_ENABLE=true
SFTP_HOST="backup.example.com"
SFTP_PORT=22
SFTP_USER="backup"
SFTP_KEY="/home/uzytkownik/.ssh/backup_ed25519"
SFTP_REMOTE_DIR="/backups/${PROJECT_NAME}"

W main() można pozostawić oba warianty wysyłki i sterować nimi zmiennymi konfiguracyjnymi. Jeśli środowisko produkcyjne ma być bardziej restrykcyjne, a dev/test nadal wysyłać na FTP, wystarczy różne pliki backup.<env>.conf z innymi flagami FTP_ENABLE/SFTP_ENABLE.

Obsługa wielu baz danych w jednym przebiegu

Typowa aplikacja PHP potrafi korzystać z kilku baz jednocześnie – osobna do logów, osobna do głównej logiki. Trzymanie ich w jednym dumpie upraszcza odtwarzanie, ale wymaga minimalnej zmiany w funkcji backupu. Najprostszy wariant to lista nazw baz w zmiennej i iteracja.

# Na górze skryptu
DB_ENABLE=true
DB_NAMES=("moja_baza" "moja_baza_logi")
DB_DUMP_FILES=()  # tablica na wszystkie dumpy

backup_database() {
  if [[ "${DB_ENABLE}" != true ]]; then
    log INFO "Backup baz danych wyłączony (DB_ENABLE=false)."
    return 0
  fi

  for db in "${DB_NAMES[@]}"; do
    local dump_file="${BACKUP_TMP_DIR}/${PROJECT_NAME}-${db}-${TIMESTAMP}.sql"
    log INFO "Rozpoczynam dump bazy danych: ${db}"

    MYSQL_PWD="${DB_PASS}" mysqldump 
      --user="${DB_USER}" 
      --host="${DB_HOST}" 
      --single-transaction 
      --quick 
      "${db}" > "${dump_file}"

    if [[ ! -s "${dump_file}" ]]; then
      log ERROR "Dump bazy danych ${db} nie został utworzony lub jest pusty."
      exit 1
    fi

    log INFO "Dump bazy danych zapisany: ${dump_file}"
    DB_DUMP_FILES+=("${dump_file}")
  done
}

W funkcji tworzącej archiwum trzeba zastąpić pojedynczy plik pętlą:

create_archive() {
  log INFO "Tworzenie archiwum plików serwisu i dumpów baz..."

  local tar_args=()

  tar_args+=("-C" "${WEB_ROOT}" ".")

  if [[ -d "${CONFIG_DIR}" ]]; then
    tar_args+=("-C" "${CONFIG_DIR}" ".")
  fi

  # Dumpy wszystkich baz
  for dump in "${DB_DUMP_FILES[@]}"; do
    if [[ -f "${dump}" ]]; then
      tar_args+=("-C" "${BACKUP_TMP_DIR}" "$(basename "${dump}")")
    fi
  done

  tar -czf "${BACKUP_ARCHIVE}" "${tar_args[@]}"

  if [[ ! -s "${BACKUP_ARCHIVE}" ]]; then
    log ERROR "Archiwum nie zostało utworzone lub jest puste: ${BACKUP_ARCHIVE}"
    exit 1
  fi

  log INFO "Archiwum utworzone: ${BACKUP_ARCHIVE}"

  # Usuwamy wszystkie dumpy
  for dump in "${DB_DUMP_FILES[@]}"; do
    if [[ -f "${dump}" ]]; then
      rm -f "${dump}"
      log INFO "Tymczasowy dump bazy usunięty: ${dump}"
    fi
  done
}

Przy kilku bazach hostowanych na tym samym serwerze ważniejszy staje się czas wykonania. Dobrze jest, aby cron nie odpalał równolegle innych ciężkich zadań (np. wolnych raportów SQL), które akurat dociążają ten sam serwer MySQL.

Wykluczanie katalogów i plików z backupu

Nie zawsze sens ma pakowanie wszystkiego. Zawartość katalogu z cache czy tymczasowym uploadem generowanym na bieżąco (np. miniatury obrazów) tylko „zapcha” archiwum. tar pozwala wygodnie je pominąć.

Przykładowa konfiguracja listy wykluczeń:

EXCLUDE_PATHS=(
  "cache"
  "tmp"
  "logs"
  "node_modules"
  "vendor/.cache"
)

Rozszerzenie funkcji create_archive() o opcje --exclude:

create_archive() {
  log INFO "Tworzenie archiwum z wykluczeniami..."

  local tar_args=()

  # Bazowy katalog serwisu
  tar_args+=("-C" "${WEB_ROOT}")

  # Wykluczenia względem WEB_ROOT
  for path in "${EXCLUDE_PATHS[@]}"; do
    tar_args+=("--exclude=${path}")
  done

  tar_args+=(".")

  # Konfiguracja poza WEB_ROOT
  if [[ -d "${CONFIG_DIR}" ]]; then
    tar_args+=("-C" "${CONFIG_DIR}" ".")
  fi

  # Dumpy baz
  for dump in "${DB_DUMP_FILES[@]}"; do
    if [[ -f "${dump}" ]]; then
      tar_args+=("-C" "${BACKUP_TMP_DIR}" "$(basename "${dump}")")
    fi
  done

  tar -czf "${BACKUP_ARCHIVE}" "${tar_args[@]}"

W praktyce dobrze jest najpierw ręcznie uruchomić tar z tym samym zestawem wykluczeń, ale z opcją -tvf, żeby zobaczyć listę plików, jakie realnie trafią do archiwum. Pozwala to wychwycić przypadki typu wycięty z backupu public/uploads, podczas gdy część tych plików wcale nie jest odtwarzana automatycznie.

Bezpieczeństwo kopii: szyfrowanie, uprawnienia i dane wrażliwe

Backupy przechowują pełen obraz serwisu – od kodu źródłowego po hasła do usług zewnętrznych zapisane w konfiguracji. Jeśli trafią w niepowołane ręce, konsekwencje są zwykle poważniejsze niż przy samej utracie dostępności strony.

Szyfrowanie archiwów przed wysyłką

Najprościej zastosować symetryczne szyfrowanie gpg lub openssl. W środowiskach z kilkoma administratorami lepiej sprawdza się wariant z kluczem publicznym – każdy uprawniony ma własny klucz prywatny, a serwer backupowy nie przechowuje hasła.

Przykład szyfrowania GPG z użyciem klucza publicznego:

GPG_ENABLE=true
GPG_RECIPIENT="backup@example.com"

encrypt_archive() {
  if [[ "${GPG_ENABLE}" != true ]]; then
    log INFO "Szyfrowanie archiwum wyłączone (GPG_ENABLE=false)."
    return 0
  fi

  local encrypted="${BACKUP_ARCHIVE}.gpg"

  log INFO "Szyfrowanie archiwum: ${BACKUP_ARCHIVE} -> ${encrypted}"

  gpg --batch --yes --encrypt 
      --recipient "${GPG_RECIPIENT}" 
      --output "${encrypted}" 
      "${BACKUP_ARCHIVE}"

  if [[ "$?" -ne 0 || ! -s "${encrypted}" ]]; then
    log ERROR "Błąd podczas szyfrowania archiwum."
    exit 1
  fi

  # Po zaszyfrowaniu można usunąć surowe archiwum
  rm -f "${BACKUP_ARCHIVE}"
  BACKUP_ARCHIVE="${encrypted}"
}

W main() trzeba wtedy dodać wywołanie encrypt_archive pomiędzy create_archive a wysyłką. Dobrze jest też przetestować pełne odtworzenie: pobranie pliku z serwera backupowego, gpg --decrypt i rozpakowanie archiwum na maszynie deweloperskiej.

Ochrona konfiguracji i haseł w repozytorium

Skrypt backupu często ląduje w repozytorium razem z kodem strony. Dane dostępowe (baza, FTP, SFTP, odbiorcy GPG) nie powinny tam trafiać w postaci jawnej. Rozdzielenie „silnika” i konfiguracji pomaga utrzymać ten podział.

Praktyczny układ plików:

  • backup_www.sh – skrypt z logiką (wersjonowany w Git).
  • backup.prod.conf, backup.dev.conf – pliki konfiguracyjne poza repozytorium (w katalogu domowym użytkownika lub w /etc).
  • .gitignore z wpisem backup.*.conf, aby przypadkowo nie dodać ich do repo.

Przykładowe wczytywanie konfiguracji na początku main():

PROFILE="${1:-prod}"
CONFIG_FILE="${HOME}/backup.${PROFILE}.conf"

if [[ -f "${CONFIG_FILE}" ]]; then
  # shellcheck disable=SC1090
  source "${CONFIG_FILE}"
else
  log ERROR "Brak pliku konfiguracyjnego: ${CONFIG_FILE}"
  exit 1
fi

Pliki konfiguracyjne powinny być ograniczone uprawnieniami do właściciela, np. chmod 600 backup.prod.conf, szczególnie gdy na serwerze loguje się więcej niż jedna osoba.

Testy odtwarzania: jak sprawdzić, czy backup jest coś wart

Samo pojawianie się nowych plików .tar.gz czy .gpg w katalogu backupów nie gwarantuje, że da się z nich odtworzyć serwis. Raz na pewien czas opłaca się zrobić „ćwiczenie ewakuacyjne” – przywrócenie danych na osobnym środowisku.

Scenariusz testowego odtwarzania serwisu

Minimalny plan działań przy teście:

  1. Wybrać świeże archiwum z serwera backupowego i pobrać je na maszynę testową.
  2. Jeśli archiwum jest szyfrowane – odszyfrować je odpowiednim kluczem.
  3. Rozpakować tar.gz w oddzielnym katalogu, np. /srv/restore_test.
  4. Utworzyć nową, pustą bazę danych i odtworzyć do niej dump(y).
  5. Skonfigurować tymczasową instancję serwera www (np. wirtualny host na restore.example.local) wskazującą na rozpakowane pliki i nową bazę.

Jeśli serwis się uruchamia, logowanie działa, a podstawowe operacje (dodanie wpisu, upload pliku) przechodzą bez błędów, backup można uznać za użyteczny. Przy okazji wychodzą na jaw słabe punkty: zakodowane „na sztywno” ścieżki, brak dumpu dodatkowej bazy, brak części plików z powodu zbyt agresywnych wykluczeń w tar.

Półautomatyczny test przywracania

Przy częstszych testach wygodniej jest zbudować prosty skrypt „restore”, który uruchamia powtarzalne kroki. Przykładowy szkielet dla MySQL:

#!/usr/bin/env bash
set -euo pipefail

RESTORE_ARCHIVE="$1"
RESTORE_DIR="/srv/restore_test"
RESTORE_DB_NAME="restore_moja_baza"
DB_USER="root"
DB_PASS="haslo"
DB_HOST="localhost"

log() {
  echo "[$(date +'%F %T')] $*"
}

log "Czyszczenie katalogu testowego..."
rm -rf "${RESTORE_DIR}"
mkdir -p "${RESTORE_DIR}"

log "Rozpakowywanie archiwum..."
tar -xzf "${RESTORE_ARCHIVE}" -C "${RESTORE_DIR}"

log "Tworzenie bazy danych: ${RESTORE_DB_NAME}"
MYSQL_PWD="${DB_PASS}" mysql -u "${DB_USER}" -h "${DB_HOST}" -e 
  "DROP DATABASE IF EXISTS `${RESTORE_DB_NAME}`; CREATE DATABASE `${RESTORE_DB_NAME}`;"

# Założenie: dump ma nazwę zawierającą 'moja_baza'
DUMP_FILE=$(find "${RESTORE_DIR}" -name "*moja_baza*.sql" | head -n1)
if [[ -z "${DUMP_FILE}" ]]; then
  log "Brak pliku dumpu bazy w katalogu restore."
  exit 1
fi

log "Odtwarzanie bazy z ${DUMP_FILE}"
MYSQL_PWD="${DB_PASS}" mysql -u "${DB_USER}" -h "${DB_HOST}" "${RESTORE_DB_NAME}" < "${DUMP_FILE}"

log "Odtwarzanie plików zakończone. Skonfiguruj wirtualny host wskazujący na ${RESTORE_DIR}."

Taki skrypt nie zastąpi pełnego testu manualnego, ale redukuje liczbę kroków i ryzyko pomyłki (np. odtworzenia dumpu do złej bazy). Łatwiej także zautomatyzować okresowe sprawdzanie spójności backupów.

Integracja z systemami kontroli wersji i CI/CD

Na projektach rozwijanych zespołowo sensowne jest włączenie logiki backupu w szerszy proces: repozytorium Gita, pipeline’y CI/CD oraz automatyczną dokumentację infrastruktury.

Skrypt backupu można traktować jak element „infrastruktury jako kodu”: jest wersjonowany, przechodzi code review i ma swój cykl życia obok aplikacji. Dzięki temu widać, kiedy ktoś zmienił harmonogram, dodał nową bazę do dumpu albo zmodyfikował wykluczenia katalogów. Zmiany w logice backupu przestają być „cichym” edytem na serwerze i zaczynają podlegać tym samym regułom jakościowym, co reszta projektu.

Dobrym wzorcem jest trzymanie skryptu w repozytorium aplikacji, ale konfiguracji – już nie. Repo zawiera szablony plików .conf.example oraz dokumentację zmiennych środowiskowych, natomiast konkretne hasła, ścieżki i odbiorców GPG są doklejane przez system wdrożeniowy (Ansible, Terraform, GitLab CI Variables, Secrets w GitHub Actions itp.). Dzięki temu deweloper jest w stanie odtworzyć konfigurację na środowisku testowym, a produkcyjne dane dostępowe nie wypływają poza infrastrukturę.

Integracja z CI/CD przydaje się też do okresowego sanity checku: pipeline może raz na tydzień odpalić testowy backup na środowisku staging, zliczyć wielkość archiwum, sprawdzić obecność plików z kluczowych katalogów i zweryfikować, czy dump bazy da się w ogóle zaimportować. Taki „suchy bieg” szybko wychwyci np. zmianę hasła do bazy, która sprawia, że skrypt backupu od pewnego dnia produkuje puste dumpy, ale wciąż kończy się kodem wyjścia 0.

W bardziej rozbudowanych instalacjach backup staje się jednym z etapów deploymentu: przed dużą migracją schematu bazy pipeline potrafi wymusić wykonanie świeżego backupu, odczekać na jego poprawne zakończenie i dopiero wtedy odpalić migracje. Jeśli backup się nie uda, wdrożenie jest blokowane, co zapobiega sytuacji, w której zmieniamy nieodwracalnie dane bez aktualnej kopii bezpieczeństwa.

Dobrze zaprojektowany, automatyczny backup strony www ratuje skórę nie tylko przy awariach sprzętu, ale też przy ludzkich błędach i nieudanych wdrożeniach. Kilkadziesiąt linijek Bash, sensowna konfiguracja FTP lub SFTP i nawyk okresowego testowania odtwarzania zwykle wystarczą, żeby z kryzysu zrobić planowane, spokojne działania serwisowe zamiast gaszenia pożaru w nocy.

Obsługa wielu środowisk i instancji serwisu

Na jednym serwerze często działa kilka instancji aplikacji – produkcja, staging, czasem dodatkowe strony klientów. Utrzymywanie kilku kopii skryptu backupu dla każdego wariantu prowadzi do bałaganu. Dużo czyściej jest mieć jeden „silnik” i różne profile konfiguracyjne.

Rozszerzenie mechanizmu profili o obsługę wielu serwisów może wyglądać następująco:

PROFILE="${1:-prod}"
SITE="${2:-main}"   # main / blog / client1 itd.

CONFIG_FILE="${HOME}/backup.${PROFILE}.${SITE}.conf"

if [[ -f "${CONFIG_FILE}" ]]; then
  # shellcheck disable=SC1090
  source "${CONFIG_FILE}"
else
  log ERROR "Brak pliku konfiguracyjnego: ${CONFIG_FILE}"
  exit 1
fi

Konfiguracje różnią się wtedy nie tylko danymi dostępowymi, ale też zakresem backupu:

  • innym katalogiem z kodem (/var/www/main vs /var/www/blog),
  • inną bazą danych,
  • innym docelowym katalogiem na serwerze FTP,
  • innym harmonogramem (np. produkcja – codziennie, staging – co tydzień).

Przy większej liczbie serwisów wygodnie jest dorzucić prosty wrapper wywołujący backup seryjnie dla kolejnych instancji:

#!/usr/bin/env bash
set -euo pipefail

PROFILES=("prod" "staging")
SITES=("main" "blog")

for profile in "${PROFILES[@]}"; do
  for site in "${SITES[@]}"; do
    echo "=== Backup: ${profile}/${site} ==="
    /usr/local/bin/backup_www.sh "${profile}" "${site}"
  done
done

Taki układ ułatwia też częściowe wyłączenie backupu, jeśli któreś środowisko jest chwilowo wycofane z użytku – wystarczy usunąć je z listy w wrapperze, zamiast grzebać w samym skrypcie.

Rotacja logów i monitorowanie działania skryptu

Sam backup nie wystarcza, jeśli nie ma sygnału, że coś się zepsuło. Najprostszy poziom kontroli to logi tekstowe plus powiadomienie, gdy skrypt kończy się błędem.

Format i przechowywanie logów

Jeśli skrypt wypisuje komunikaty przez funkcję log, niewielka zmiana pozwala zbudować normalny dziennik zdarzeń:

LOG_FILE="/var/log/backup_www.log"

log() {
  local level="$1"; shift
  local msg="$*"
  local ts
  ts="$(date +'%F %T')"
  echo "[${ts}] [${level}] ${msg}" | tee -a "${LOG_FILE}"
}

Przy pliku w /var/log dobrze jest skonfigurować logrotate, żeby dziennik nie urósł do gigabajtów. Przykładowy wpis:

/var/log/backup_www.log {
    weekly
    rotate 12
    compress
    missingok
    notifempty
    create 640 root adm
}

Dodatkowo, jeśli serwer korzysta z systemd, część informacji można skierować na journal (np. logger -t backup_www ...), co upraszcza podgląd logów z kilku maszyn w jednym miejscu.

Powiadomienia o błędach

Nawet najlepiej pisany log jest bezużyteczny, jeśli nikt go nie czyta. Warto więc zdefiniować prostą ścieżkę powiadamiania – mail, webhook, komunikator zespołowy.

Najprostsza wersja mailowa:

ALERT_EMAIL="admin@example.com"

notify_failure() {
  local subject="Backup WWW FAILED on $(hostname)"
  local body="Czas: $(date)
Host: $(hostname)
Log (last 50 lines):

$(tail -n50 "${LOG_FILE}")"

  echo "${body}" | mail -s "${subject}" "${ALERT_EMAIL}" || 
    log ERROR "Nie udało się wysłać powiadomienia e-mail."
}

W main() wystarczy złapać błąd globalnym trapem:

main() {
  # ...
}

trap 'rc=$?; if [[ $rc -ne 0 ]]; then notify_failure; fi; exit $rc' EXIT

main "$@"

Przy integracji z narzędziami typu Zabbix, Prometheus czy Icinga prostszym podejściem bywa wypisanie krótkiej informacji na stdout i niezerowy kod wyjścia. Agent monitoringu wywołuje skrypt okresowo i traktuje każdy status > 0 jako alarm.

Różnice między FTP, FTPS i SFTP w kontekście backupu

Przy projektowaniu mechanizmu wysyłki na zewnętrzny serwer backupowy kluczowe są szczegóły protokołu. Wiele paneli hostingowych nazywa wszystko „FTP”, ale technicznie mamy co najmniej trzy scenariusze:

  • klasyczny FTP – bez szyfrowania, nieakceptowalny dla danych wrażliwych,
  • FTPS – FTP tunelowany przez TLS (tryb explicit lub implicit),
  • SFTP – protokół plikowy działający na SSH, zupełnie inny niż FTP.

Jeśli serwer backupowy udostępnia SFTP, to zwykle jest to najlepszy wybór: jeden port, proste otwieranie połączeń z firewalla i możliwość użycia kluczy SSH bez haseł.

Przykład implementacji wysyłki SFTP z użyciem klucza

Prosta funkcja w skrypcie Bash może opierać się o sftp w trybie batch:

SFTP_HOST="backup.example.com"
SFTP_PORT="22"
SFTP_USER="backup"
SFTP_KEY="${HOME}/.ssh/backup_ed25519"
SFTP_TARGET_DIR="/backups/www"

upload_sftp() {
  local src_file="$1"
  log INFO "Wysyłanie archiwum na SFTP: ${SFTP_HOST}:${SFTP_TARGET_DIR}"

  sftp -i "${SFTP_KEY}" -P "${SFTP_PORT}" 
    "${SFTP_USER}@${SFTP_HOST}" <<EOF
mkdir ${SFTP_TARGET_DIR} 2>/dev/null
cd ${SFTP_TARGET_DIR}
put ${src_file}
bye
EOF

  if [[ "$?" -ne 0 ]]; then
    log ERROR "Błąd podczas wysyłania pliku na SFTP."
    exit 1
  fi
}

Przy FTPS dochodzi więcej parametrów: tryb pasywny/aktywny, weryfikacja certyfikatu, port SSL. Wygodnym narzędziem bywa curl albo lftp. Dla klasycznego FTP ten sam kod da się uprościć, ale trzeba mieć świadomość, że login i dane lecą „na czysto”. Jeżeli nie ma absolutnie żadnej możliwości szyfrowania połączenia, pozostaje szyfrować archiwum (GPG) i akceptować, że sam ruch nie jest tajny, ale zawartość pliku już tak.

Backup przyrostowy a pełny – praktyczne kompromisy

Pełne archiwum całego katalogu strony za każdym razem bywa wygodne, ale przy większych zasobach szybko rośnie czas i rozmiar backupu. Przy statycznych plikach (np. duże katalogi z uploadami) rozsądnym kompromisem jest podzielenie backupu na segmenty.

Segmentacja na warstwy

Typowy podział:

  • warstwa aplikacji – kod, konfiguracje, skrypty,
  • warstwa danych plikowych – uploady użytkowników, media, cache,
  • warstwa bazy – dumpy SQL.

Warstwa aplikacji i baza zmieniają się dynamicznie, więc tam pełne backupy są uzasadnione. Warstwa plikowa bywa ogromna, ale przyrost dzienny jest niewielki. Dla niej można stosować przyrosty oparte na rsync po SFTP albo lokalny snapshot (np. rsnapshot, Btrfs/ZFS) i dopiero potem wysyłkę „zeskładowanego” stanu.

Wykorzystanie rsync do zmniejszenia transferu

Jeśli serwer backupowy udostępnia SSH, można zrezygnować z klasycznego FTP i skorzystać z rsync z opcją --link-dest (twarde dowiązania do poprzedniego backupu). Skrypt Bash nie musi wtedy kompresować całego katalogu, a jedynie oddelegowuje tę logikę do rsync:

RSYNC_USER="backup"
RSYNC_HOST="backup.example.com"
RSYNC_BASE="/backups/www"
RSYNC_SSH_KEY="${HOME}/.ssh/backup_ed25519"

rsync_backup() {
  local src_dir="$1"
  local date_tag
  date_tag="$(date +'%F_%H-%M-%S')"

  local dest_dir="${RSYNC_BASE}/${date_tag}"
  local latest_link="${RSYNC_BASE}/latest"

  local link_dest=""
  if ssh -i "${RSYNC_SSH_KEY}" "${RSYNC_USER}@${RSYNC_HOST}" "[ -d '${latest_link}' ]"; then
    link_dest="--link-dest=${latest_link}"
  fi

  rsync -a --delete ${link_dest} 
    -e "ssh -i ${RSYNC_SSH_KEY}" 
    "${src_dir}/" 
    "${RSYNC_USER}@${RSYNC_HOST}:${dest_dir}/"

  ssh -i "${RSYNC_SSH_KEY}" "${RSYNC_USER}@${RSYNC_HOST}" 
    "rm -f '${latest_link}'; ln -s '${dest_dir}' '${latest_link}'"
}

Po stronie backupu ma się wtedy katalogi przypominające pełne snapshoty, ale większość plików współdzieli fizyczne bloki z poprzednimi kopiami. Z punktu widzenia odtwarzania używa się po prostu najnowszego katalogu.

Bezpieczeństwo kluczy i haseł używanych w backupie

Skrypt backupu operuje na materiale wrażliwym – hasłach do baz, kontach FTP/SFTP, kluczach GPG i SSH. Utrata kontroli nad tymi danymi bywa gorsza niż sama awaria serwisu.

Separacja użytkownika systemowego

Użytkownik wykonujący backup powinien być ograniczony do minimum:

  • osobne konto systemowe, np. backup,
  • brak shellowego dostępu z zewnątrz lub dostęp z bardzo mocnymi ograniczeniami,
  • minimum uprawnień do katalogów aplikacji i baz (np. sudo tylko do poleceń mysqldump).

Pliki konfiguracyjne (hasła, klucze) zapisane w katalogu domowym tego użytkownika, z maską 600 i należące tylko do niego.

Ograniczone uprawnienia po stronie serwera backupowego

Konto FTP/SFTP nie powinno być w stanie „grzebać” w cudzych backupach. Dobrą praktyką jest:

  • chroot do katalogu backupowego,
  • blokada logowania interaktywnego (dla SFTP: ForceCommand internal-sftp),
  • brak możliwości wykonywania poleceń poza operacjami na plikach.

Jeśli po SFTP używane są klucze SSH, można je jeszcze dodatkowo ograniczyć na poziomie authorized_keys:

command="internal-sftp",no-port-forwarding,no-X11-forwarding,no-agent-forwarding 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... backup-key

Takie wpisy sprawiają, że nawet kradzież klucza nie pozwoli na normalne zalogowanie przez SSH – połączenie jest natychmiast przełączane na SFTP z ograniczeniami.

Typowe problemy i odporność skryptu na błędy

Przy seryjnym działaniu przez miesiące najczęściej pojawiają się drobne, powtarzalne problemy: brak miejsca na dysku, zmiana hasła do bazy, wygasły certyfikat serwera FTPS. Skrypt powinien umieć je wykryć i zakończyć się jasno zdefiniowaną porażką, zamiast generować uszkodzone archiwa.

Kontrola wolnego miejsca

Przed rozpoczęciem kompresji dobrze jest sprawdzić, ile wolnego miejsca jest w /tmp lub w katalogu roboczym. Przykładowa funkcja:

check_free_space() {
  local dir="$1"
  local required_mb="$2"

  local available_mb
  available_mb=$(df -Pm "${dir}" | awk 'NR==2 {print $4}')

  if (( available_mb < required_mb )); then
    log ERROR "Za mało miejsca w ${dir}: dostępne ${available_mb} MB, wymagane ${required_mb} MB."
    exit 1
  fi
}

W main() można ją użyć np. z założeniem „wymagamy co najmniej dwukrotności rozmiaru katalogu strony”, mierzonej przez du -sm.

Walidacja dumpu bazy

Samo wykonanie mysqldump nie gwarantuje poprawności pliku. W razie błędów połączenia czy braku uprawnień narzędzie często produkuje plik z komunikatem błędu, który na pierwszy rzut oka wygląda jak zwykły SQL. Minimalne sprawdzenie może polegać na szukaniu frazy „Dump completed”:

create_db_dump() {
  # ...
  mysqldump ... > "${dump_file}" 2>"${dump_file}.log"

  if ! grep -q "Dump completed" "${dump_file}" && ! grep -q "MySQL dump" "${dump_file}"; then
    log ERROR "Dump bazy wygląda podejrzanie, sprawdź ${dump_file}.log"
    exit 1
  fi
}

Bardziej zaawansowane podejście to próbny import dumpu do „pustej” bazy testowej, ale to ma sens głównie przy okazjonalnych testach, a nie przy każdym nightly backupie.

Wersjonowanie struktury katalogów backupu

Struktura katalogów na serwerze backupowym powinna być stała, ale technologie i potrzeby się zmieniają. Z czasem może pojawić się chęć dołożenia kolejnego elementu (np. logów aplikacyjnych) albo zmiany nazw plików. Dobrze jest założyć prosty schemat wersjonowania.

Znacznik wersji w nazwie lub metadanych

Najczęściej wystarczy dodać prosty numer wersji do nazwy archiwum lub katalogu:

Przykłady:

  • www-backup_v1_2024-04-25_01-00.tar.gz – początkowy format, bez szyfrowania,
  • www-backup_v2_2024-08-10_01-00.enc.tar.gz – druga wersja, z GPG i dodatkowymi metadanymi,
  • www-backup_v3_2024-11-01_01-00_full.tar.zst – kolejna ewolucja, inna kompresja, zmieniony zestaw katalogów.

Dzięki takiemu znacznikowi od razu widać, które backupy są zgodne z aktualnym skryptem, a w razie migracji można utrzymać stary mechanizm odtwarzania tylko dla starszych wersji.

Alternatywą dla wersjonowania w samej nazwie jest prosty plik metadanych dołączany do każdego katalogu lub archiwum, np. BACKUP_META.json. Można w nim zapisać:

  • wersję formatu,
  • nazwę hosta i ścieżkę źródłową,
  • wersję skryptu backupu,
  • sumy kontrolne kluczowych plików (np. dumpów SQL).

Taki metaplik bywa bezcenny przy odtwarzaniu z dawnej kopii, kiedy nie pamięta się już, jak dokładnie wyglądał system sprzed kilku lat albo którą wersją skryptu ją wykonano.

Przy aktualizacji formatu dobrze jest zachować jedną prostą zasadę: nowe wersje nie psują starych backupów. Jeżeli zmieniana jest struktura katalogów lub nazewnictwo, lepiej wprowadzić to jako nową gałąź (np. v3/ w drzewie katalogów) i jednocześnie utrzymać możliwość odczytu starych kopii. Czasem wystarczy dodatkowy, mały skrypt restore_v1.sh czy restore_v2.sh, który zna specyfikę konkretnego formatu.

Dobrze zaprojektowany skrypt backupu przestaje być chaotyczną łatką „na szybko”, a staje się narzędziem, na którym można się oprzeć w krytycznym momencie. Jasne logowanie, przewidywalna struktura katalogów, rozsądna rotacja i kilka przetestowanych scenariuszy odtworzenia dają sporą pewność, że awaria serwera nie zamieni się w długi przestój lub utratę danych. Skrypt w Bashu można potem stopniowo rozwijać lub przepisać na inne technologie, ale fundament – procedura, ścieżki, sposób przechowywania – zostaje ten sam.

Bibliografia

  • The Linux Command Line. No Starch Press (2012) – Podstawy powłoki, tar, gzip, cron, automatyzacja w Linuksie
  • UNIX and Linux System Administration Handbook (5th Edition). Pearson (2017) – Praktyki administrowania, backup, cron, bezpieczeństwo serwerów
  • Backup and Recovery. O’Reilly Media (2007) – Strategie kopii zapasowych, wersjonowanie, scenariusze utraty danych
  • MySQL 8.0 Reference Manual. Oracle – Oficjalna dokumentacja mysqldump, eksport i import baz danych
  • WordPress Codex: Backing Up Your Database and Files. WordPress Foundation – Zalecenia backupu plików i bazy WordPress, typowe scenariusze awarii
  • NIST Special Publication 800-34 Rev.1: Contingency Planning Guide for Federal Information Systems. National Institute of Standards and Technology (2010) – Zalecenia dot. planowania ciągłości działania i kopii zapasowych

Poprzedni artykułProgramowanie jako wspólnota – jak budować dobre praktyki
Leszek Czarnecki

Leszek Czarnecki to webmaster i developer PHP, który łączy techniczną dokładność z podejściem „ma działać, być bezpieczne i łatwe do rozwijania”. Na porady-it.pl tworzy poradniki o skryptach dla nowoczesnych stron: od poprawnej obsługi formularzy i sesji, przez pracę z bazami danych (PDO, przygotowane zapytania), po integracje z API, automatyzacje i optymalizację wydajności. Zwraca uwagę na detale, które robią różnicę w praktyce: logowanie błędów, walidację danych, porządną strukturę projektu i unikanie rozwiązań, które później trudno utrzymać. Pisze jasno, krok po kroku, z przykładami gotowymi do wdrożenia.

Kontakt: leszek_czarnecki@porady-it.pl