docker-volume-backup — kopie wolumenów Dockera na S3, WebDAV i SSH
Kontener-towarzysz w Go, który pakuje wolumeny Dockera do archiwum tar i wysyła je na S3, WebDAV, SSH, Azure, Dropbox albo Dysk Google — z rotacją, szyfrowaniem i powiadomieniami. Pokazujemy pełną konfigurację dla stacku Laravel Sail: logiczny dump bazy przez etykiety cyklu życia, trzy poziomy retencji z katalogu conf.d, szyfrowanie age i ograniczenie dostępu do gniazda Dockera. Plus rozdział o odtwarzaniu, bo backup, którego nikt nie odtworzył, nie jest backupem.
docker volume ma pięć podkomend: create, inspect, ls, prune i rm. Nie ma backup. Dane, które w typowym wdrożeniu Dockera są jedyną rzeczą naprawdę nie do odtworzenia — baza, pliki wgrane przez użytkowników, indeks wyszukiwania, certyfikaty — leżą w katalogu /var/lib/docker/volumes i nie interesują żadnego standardowego narzędzia do backupu, bo z perspektywy hosta to nieopisany zbiór katalogów o nazwach będących skrótami.
Wychodzi z tego zawsze ten sam skrypt. Wpis w cronie na hoście, w nim docker run --rm -v dane:/dane -v /backup:/out alpine tar czf /out/dane.tar.gz /dane, potem aws s3 cp, a jeśli ktoś był sumienny — find /backup -mtime +7 -delete. Działa. Do momentu, w którym zaczyna się pytać o szczegóły:
- Czy archiwum bazy danych, spakowane w trakcie zapisu, da się w ogóle odtworzyć? (Zwykle nie, a dowiadujemy się o tym w dniu awarii.)
- Kto zauważy, że
tarod trzech tygodni wychodzi z błędem, jeśli cron wysyła pocztę na adres, którego nikt nie czyta? - Gdzie leżą klucze do S3 i czy ktoś, kto ma dostęp do hosta, ma automatycznie dostęp do wszystkich kopii?
- Czy rotacja usuwa stare archiwa również ze zdalnego składu, czy tylko lokalnie?
- Co się stanie, gdy dwa harmonogramy nałożą się na siebie na dużym wolumenie?
docker-volume-backup (github.com/offen/docker-volume-backup) odpowiada na te pytania w formie, która pasuje do świata Dockera: to nie narzędzie do zainstalowania na hoście, tylko kontener dopisany do compose.yaml obok pozostałych usług. Montujesz mu wolumeny do odczytu, podajesz zmienne środowiskowe i dostajesz cykliczne archiwa w miejscu, które wskażesz. Poniżej: jak to działa, jak to skonfigurować pod stack Laravel Sail, gdzie są pułapki (jest ich kilka poważnych, głównie w rotacji i w dostępie do gniazda Dockera) i czego to narzędzie świadomie nie robi.
Czym to jest i w jakim stanie
Obraz offen/docker-volume-backup to napisany w Go program uruchamiany jako kontener-towarzysz (companion container). Obsługuje cykliczne i jednorazowe kopie wolumenów Dockera do katalogu lokalnego oraz na dowolny skład zgodny z S3, WebDAV, Azure Blob Storage, Dropbox, Dysk Google albo SSH — także na kilka z nich jednocześnie. Do tego rotacja starych kopii, szyfrowanie GPG albo age i powiadomienia o przebiegach.
Stan repozytorium na 10 września 2026, wprost z API GitHuba:
- 4004 gwiazdki i 152 forki — projekt niszowy w porównaniu z gwiazdami pierwszej ligi, ale w swojej kategorii jeden z najczęściej używanych,
- repozytorium założone 2 kwietnia 2021, kod w Go, ostatni commit z 8 września 2026,
- 23 commity w ostatnim miesiącu i 25 otwartych zgłoszeń — tempo spokojne, ale ciągłe; to nie jest projekt, który stanął,
- najnowsze wydanie
v2.49.0z 6 września 2026, poprzedniev2.48.2z 27 czerwca. Kadencja wydań to kilka tygodni do kilku miesięcy, czyli rytm narzędzia, które robi jedną rzecz i nie ma powodu zmieniać się co tydzień, - licencja MPL-2.0 — do konsekwencji wracamy na końcu, ale uprzedzając: dla komercyjnego użycia jest to licencja spokojna,
- utrzymywane przez offen.software, z opcją wykupienia godziny konsultacji przez GitHub Sponsors, jeśli wdrożenie w konkretnym środowisku okaże się nieoczywiste.
Obraz jest publikowany równolegle na Docker Hubie i w rejestrze GitHuba, więc offen/docker-volume-backup:v2 i ghcr.io/offen/docker-volume-backup:v2 są wymienne. Waży około 25 MB w postaci skompresowanej i wspiera architektury amd64, arm64 oraz arm/v7 — to ostatnie ma znaczenie, jeśli backupujesz coś, co chodzi na Raspberry Pi.
Projekt jest jawnie zainspirowany starszym jareware/docker-volume-backup i różnice względem niego dobrze opisują, po co powstał: tamten obraz oparty jest na Ubuntu i waży dwadzieścia razy więcej, jest napisany w powłoce, a rotację kopii oddaje politykom cyklu życia S3 — co działa na AWS i nie działa na MinIO ani na dysku lokalnym. Tutaj rotacja jest częścią programu, więc obejmuje wszystkie skonfigurowane cele. Doszła też obsługa trybu Swarm, powiadomień i uwierzytelniania przez profile instancji IAM.
Twórcy jasno zaznaczają, że narzędzie jest rozwijane i testowane wyłącznie przeciw silnikowi Docker CE. Na Balena Engine czy innych implementacjach może działać, ale nikt tego nie obiecuje.
Cykl życia jednego przebiegu
Warto zrozumieć cztery fazy, bo cała elastyczność narzędzia opiera się na możliwości wpięcia własnych komend przed każdą z nich i po każdej z nich:
archive— zawartość katalogu/backupw kontenerze (albo tego, co wskazujeBACKUP_SOURCES) trafia do archiwum tar,process— archiwum jest przetwarzane, czyli w praktyce szyfrowane; faza opcjonalna,copy— archiwum jest kopiowane na wszystkie skonfigurowane składy,prune— stare kopie są usuwane zgodnie z regułą retencji; faza opcjonalna.
To, co wyląduje w archiwum, wynika wyłącznie z tego, co zamontujesz pod /backup. Nie ma tu żadnej magii ani odpytywania Dockera o listę wolumenów: wolumen dane zamontowany jako dane:/backup/dane-aplikacji:ro pojawi się w archiwum jako katalog backup/dane-aplikacji. Ta oczywistość ma znaczenie przy odtwarzaniu — trzeba wiedzieć, ile poziomów ścieżki obciąć.
Minimalna konfiguracja
Kanoniczny przykład ze dokumentacji, czyli usługa backup dopisana do istniejącego compose:
services:
volume-consumer:
build:
context: ./my-app
volumes:
- data:/var/my-app
labels:
# Kontener zostanie zatrzymany na czas backupu, żeby archiwum
# było spójne. Etykietę można pominąć, jeśli zatrzymywanie
# nie jest potrzebne.
- docker-volume-backup.stop-during-backup=true
backup:
# Na produkcji przypnij konkretne wydanie zamiast `latest`.
image: offen/docker-volume-backup:v2
restart: always
env_file: ./backup.env
volumes:
- data:/backup/my-app-backup:ro
# Gniazdo Dockera pozwala zatrzymywać kontenery i czytać ich
# etykiety. Do konsekwencji wracamy w rozdziale o bezpieczeństwie.
- /var/run/docker.sock:/var/run/docker.sock:ro
# Katalog zamontowany pod /archive dostaje lokalną kopię.
- /path/to/local_backups:/archive
volumes:
data:Bez żadnej dodatkowej konfiguracji kontener wykona backup codziennie o północy (domyślne @daily) i zapisze go pod nazwą w rodzaju backup-2026-09-10T04-00-00.tar.gz. Wolumen montujemy tylko do odczytu — narzędzie nigdy nie musi pisać do źródła, a :ro chroni przed pomyłką w innej konfiguracji.
Jednorazowy przebieg bez dopisywania czegokolwiek do compose:
docker run --rm \
-v data:/backup/data \
--env AWS_ACCESS_KEY_ID="<xxx>" \
--env AWS_SECRET_ACCESS_KEY="<xxx>" \
--env AWS_S3_BUCKET_NAME="<xxx>" \
--entrypoint backup \
offen/docker-volume-backup:v2Uruchomienie backupu poza harmonogramem w już działającym kontenerze:
docker exec <kontener> backupA gdy trzeba sprawdzić, co narzędzie właściwie wczytało z konfiguracji — bo przy trzech źródłach zmiennych to pytanie pojawia się szybko:
docker exec <kontener> backup print-configDwie uwagi do print-config: wypisuje sekrety w postaci jawnej, więc nie należy tego wklejać do zgłoszenia na GitHubie, i jest oznaczone w dokumentacji jako funkcja w rozwoju, która może się jeszcze zmienić.
Harmonogram, nazwy plików, kompresja
Harmonogram to wyrażenie cron w BACKUP_CRON_EXPRESSION, z pięcioma albo sześcioma polami — dodatkowe, pierwsze pole to sekundy. Nazwa pliku w BACKUP_FILENAME przyjmuje tokeny w stylu strftime oraz szablon {{ .Extension }}, który rozwinie się do tar.gz, tar.zst albo .tar zależnie od wybranej kompresji. Domyślnie:
BACKUP_CRON_EXPRESSION="@daily"
BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"
BACKUP_COMPRESSION="gz"Cztery ustawienia z tej grupy warto znać, bo rozwiązują konkretne problemy:
BACKUP_COMPRESSION="zst"— Zstandard zamiast gzipa. Przy dużych archiwach różnica w czasie i rozmiarze jest odczuwalna; opcjanonedaje czysty tar, sensowny, gdy skład i tak kompresuje albo gdy dane są nieściśliwe,GZIP_PARALLELISM="0"— zrównoleglenie kompresji gzipem na wszystkie dostępne wątki. Wpływa tylko na kompresję, nie na dekompresję,BACKUP_JITTER="30s"— losowe opóźnienie z podanego przedziału przed każdym przebiegiem. Przy dziesięciu hostach zaplanowanych na tę samą godzinę oszczędza składowi zdalnemu dziesięciu równoczesnych klientów,BACKUP_EXCLUDE_REGEXP— wyrażenie regularne (składnia z biblioteki standardowej Go) wykluczające z archiwum pliki, których pełna ścieżka pasuje do wzorca. Klasyczne zastosowanie:\.log$, żeby nie wozić na S3 logów, które i tak są w innym systemie.
Jest jeszcze BACKUP_LATEST_SYMLINK, tworzący przy kopii lokalnej dowiązanie w rodzaju backup.latest.tar.gz. Ułatwia skrypty odtwarzania, ale działa wyłącznie lokalnie — na zdalnych składach nie ma odpowiednika.
Spójność danych: zatrzymywanie kontenerów
Spakowanie katalogu bazy danych w trakcie jej pracy daje plik, który wygląda jak backup i nim nie jest. Narzędzie oferuje na to dwie odpowiedzi, a wybór między nimi to najważniejsza decyzja w całej konfiguracji.
Odpowiedź pierwsza: zatrzymać kontener na czas archiwizacji. Kontener oznaczony etykietą docker-volume-backup.stop-during-backup=true jest zatrzymywany przed fazą archive i wznawiany po niej. Gdy działa kilka instancji backupu na różnych harmonogramach, wartość etykiety można dowolnie zmienić i dopasować przez BACKUP_STOP_DURING_BACKUP_LABEL:
services:
app:
labels:
- docker-volume-backup.stop-during-backup=service1
backup:
image: offen/docker-volume-backup:v2
environment:
BACKUP_STOP_DURING_BACKUP_LABEL: service1
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:roEtykieta Dockera trzyma jedną wartość na klucz, więc kontener nie mógł dotąd należeć do dwóch instancji backupu naraz. To zmieniło wydanie v2.49.0 z 6 września 2026: ustawienie BACKUP_LABEL_MATCH_BEHAVIOR=one-of każe rozbić wartość etykiety po przecinku i dopasować, jeśli którykolwiek element się zgadza. Kontener z etykietą stop-during-backup=service1,service2 zostanie więc zatrzymany zarówno przez instancję skonfigurowaną na service1, jak i na service2. Separator zmienia BACKUP_LABEL_MATCH_SEPARATOR, jeśli same wartości zawierają przecinki. Domyślne match zachowuje dawne zachowanie, czyli równość.
Osobna etykieta docker-volume-backup.stop-during-backup-no-restart zatrzymuje kontener i nie wznawia go po backupie. Przydaje się, gdy usługę i tak startuje zewnętrzny proces albo harmonogram. Obie etykiety wykluczają się wzajemnie.
W trybie Swarm
Przy Dockerze w trybie Swarm mechanizm ma dwa warianty i trzeba wybrać dokładnie jeden — kontener oznaczony etykietą, którego usługa nadrzędna też jest oznaczona, kończy przebieg wczesnym błędem.
Wariant pierwszy: etykieta w sekcji deploy. Usługa jest skalowana do zera replik, powstaje archiwum, usługa wraca do poprzedniej liczby replik. Działa tylko dla usług w trybie replicated. Wariant drugi: etykieta na kontenerze, co pomija usługi i zatrzymuje kontenery bezpośrednio — ale ponieważ Swarm natychmiast restartuje ręcznie zatrzymane kontenery, wymaga to polityki restartu on-failure. Z always nie jest kompatybilne.
Dwa dodatkowe ograniczenia Swarma: kontener backupu musi stać na węźle menedżerskim (na węźle robotniczym zachowuje się tak, jakby Swarma nie było), a komendy z etykiet cyklu życia widzi tylko na własnym węźle — oznaczone kontenery na innych węzłach są dla niego niewidoczne. Dokumentacja zaleca też twardy limit pamięci na usługę backupu, około 25 MB, z zastrzeżeniem, że przy archiwach powyżej pół gigabajta trzeba go podnieść, bo inaczej przebieg skończy się jako Killed. Nieoczywisty, ale realny scenariusz: limit ustawiony trzy lata temu, wolumen, który tymczasem spuchł, i backupy, których od miesięcy nie ma.
Bazy danych: logiczny dump przez etykiety cyklu życia
Odpowiedź druga na spójność jest lepsza dla baz danych: nie zatrzymywać niczego, tylko poprosić bazę o wykonanie zrzutu, a potem spakować zrzut. Służą do tego etykiety docker-volume-backup.[faza]-[pre|post], wykonywane w kontekście kontenera docelowego, nie kontenera backupu.
services:
database:
image: mariadb
volumes:
- backup_data:/tmp/backups
labels:
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --all-databases > /tmp/backups/dump.sql'
- docker-volume-backup.archive-post=/bin/sh -c 'rm -f /tmp/backups/dump.sql'
volumes:
backup_data:Cztery rzeczy, na których łatwo się tu przejechać:
- Przekierowania wymagają jawnej powłoki. Ograniczenie Dockera, nie narzędzia:
mysqldump > dump.sqlnie zadziała,/bin/sh -c 'mysqldump > dump.sql'zadziała. Dotyczy każdego potoku i każdego przekierowania, - Zrzut musi wylądować w miejscu, które backup widzi. Stąd wspólny wolumen pośredni
backup_data, zamontowany w bazie pod/tmp/backupsi w kontenerze backupu pod/backup/.... Zrzut zapisany do katalogu, którego nikt nie montuje, zniknie razem z kontenerem, - Kolejność komend nie jest określona. Narzędzie gwarantuje, że poczeka na wszystkie komendy
preprzed przejściem dalej i na wszystkiepostprzed zakończeniem — ale nie to, w jakiej kolejności je uruchomi, ani że nie zrobi tego równolegle. Komenda zależna od wyniku innej komendy musi być jedną komendą, - Domyślnie komenda działa jako użytkownik z obrazu docelowego. Gdy trzeba inaczej, jest etykieta
docker-volume-backup.[faza]-[pre|post].user— użytkownik musi istnieć wpasswdwewnątrz kontenera docelowego.
Przykład z dokumentacji dla Gitei pokazuje oba mechanizmy naraz:
labels:
- docker-volume-backup.archive-pre.user=git
- docker-volume-backup.archive-pre=/bin/bash -c 'cd /tmp; /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini -R -f dump.zip'Gdy w środowisku działa więcej niż jedna instancja backupu albo więcej niż jeden harmonogram, komendy trzeba zawężyć. Ustawienie EXEC_LABEL=database w kontenerze backupu sprawia, że wykona on tylko komendy z kontenerów oznaczonych dodatkowo docker-volume-backup.exec-label=database. Tu czai się pułapka, którą dokumentacja wyróżnia osobnym ostrzeżeniem: przy wielu konfiguracjach w katalogu conf.d EXEC_LABEL musi być ustawiony w każdej z nich. Konfiguracja, która go nie ustawia, wykona komendy ze wszystkich oznaczonych kontenerów — niezależnie od tego, jaką wartość exec-label mają.
Warto też wiedzieć, że starsze etykiety exec-pre i exec-post są przestarzałe od wersji 2.19.0 — odpowiadają dzisiejszym archive-pre i archive-post. Podobnie przestarzałe jest BACKUP_FROM_SNAPSHOT, zastąpione właśnie parą archive-pre/archive-post i wolumenem pośrednim, oraz BACKUP_STOP_CONTAINER_LABEL, przemianowane w 2.36.0 na BACKUP_STOP_DURING_BACKUP_LABEL. Wszystkie trzy działają nadal i wypisują ostrzeżenie przy każdym przebiegu, ale mają zniknąć w następnej wersji głównej. Jeśli konfiguracja jest starsza niż rok, opłaca się przejrzeć ją teraz, a nie po aktualizacji, która się wywali.
Gdzie to trafia
Cele składowania konfiguruje się niezależnie i można ich włączyć kilka jednocześnie — jeden przebieg wyśle to samo archiwum wszędzie, gdzie mu wskażesz.
S3 i wszystko, co je udaje
AWS_S3_BUCKET_NAME="backup-bucket"
AWS_S3_PATH="produkcja/wolumeny"
AWS_ACCESS_KEY_ID="..."
AWS_SECRET_ACCESS_KEY="..."
AWS_ENDPOINT="minio.example.com"
AWS_ENDPOINT_PROTO="https"Wszystkie klucze mają przedrostek AWS_, ale konfiguracja działa z dowolnym składem zgodnym z S3 — MinIO, Filebase, Backblaze, Scaleway, cokolwiek. Poza podstawami warto znać:
AWS_IAM_ROLE_ENDPOINT— zamiast statycznych kluczy można użyć profilu instancji:http://169.254.169.254na EC2,http://169.254.170.2na ECS. Najlepszy sposób na to, żeby w konfiguracji nie było w ogóle sekretu,AWS_PART_SIZE— rozmiar części przy wysyłaniu, domyślnie 16 MB. Dokumentacja ostrzega konkretnie: Scaleway ma licznik części ustawiony na 1000, a MinIO na 10 000, więc przy naprawdę dużych archiwach trzeba tę wartość podnieść, bo inaczej wysyłka nie przejdzie,AWS_STORAGE_CLASS="GLACIER"— nagłówek klasy składowania. Przy archiwach miesięcznych, po które sięga się raz na nigdy, różnica w rachunku jest wyraźna. Przy dziennych, po które sięga się w panice, klasa z opóźnieniem odtworzenia jest złym pomysłem,AWS_S3_BUCKET_LOOKUP— druga nowość z v2.49.0. Domyślneautoużywa stylu wirtualnego hosta tylko dla AWS S3, GCS i Aliyun OSS;dnswymusza go zawsze (potrzebne np. dla Tencent COS),pathwymusza styl ścieżkowy. Ustawienie, którego nie tknie się nigdy — do dnia, w którym klient S3 nagle nie widzi kubełka,AWS_ENDPOINT_CA_CERT— ścieżka do certyfikatu CA w formacie PEM albo sam certyfikat jako wartość, dla składów z własnym CA. Jest teżAWS_ENDPOINT_INSECURE, który wyłącza weryfikację certyfikatu i którego po prostu nie należy używać.
WebDAV, SSH, Azure, Dropbox, Dysk Google, dysk lokalny
WebDAV wymaga czterech zmiennych (WEBDAV_URL, WEBDAV_PATH, WEBDAV_USERNAME, WEBDAV_PASSWORD) i jest najkrótszą drogą do wysyłania kopii na Nextcloud albo na przestrzeń u operatora hostingu. SSH/SFTP potrzebuje SSH_HOST_NAME, SSH_USER, SSH_REMOTE_PATH i klucza — domyślnie czytanego z /root/.ssh/id_rsa, więc wystarczy zamontować klucz w tym miejscu:
services:
backup:
image: offen/docker-volume-backup:v2
environment:
SSH_HOST_NAME: server.local
SSH_PORT: 2222
SSH_USER: user
SSH_REMOTE_PATH: /data
volumes:
- data:/backup/my-app-backup:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /path/to/private_key:/root/.ssh/id_rsaAzure Blob Storage przyjmuje albo nazwę konta z kluczem, albo AZURE_STORAGE_CONNECTION_STRING, i pozwala ustawić poziom dostępu przez AZURE_STORAGE_ACCESS_TIER. Dropbox działa na tokenie odświeżania z własnej aplikacji (osobna instrukcja w dokumentacji, bo samo wyklikanie tokenu jest najbardziej pracochłonnym elementem) i ma regulowany DROPBOX_CONCURRENCY_LEVEL, domyślnie 6. Dysk Google używa danych konta usługowego w GOOGLE_DRIVE_CREDENTIALS_JSON, z opcjonalnym identyfikatorem folderu, dysku zespołu i podszywaniem się pod użytkownika w domenie.
Kopia lokalna to po prostu katalog albo wolumen zamontowany pod /archive (ścieżkę zmienia BACKUP_ARCHIVE). Jest tu jedno zachowanie, które warto znać, bo cichnie zamiast krzyczeć: jeśli w kontenerze nie ma tego katalogu, kopie lokalne są po prostu pomijane. Literówka w ścieżce montowania nie da żadnego błędu — dostaniesz przebieg oznaczony jako udany i zero plików w miejscu, w którym ich szukasz.
Rotacja i jej ostre krawędzie
Rotację włącza BACKUP_RETENTION_DAYS — liczba dni, po których kopia jest usuwana, na wszystkich skonfigurowanych składach naraz. Domyślne -1 oznacza brak rotacji. I tu jest miejsce, w którym dokumentacja krzyczy wielkimi literami, więc powtórzymy to samo:
Mechanizm rotacji nie jest wyrafinowany i domyślnie stosuje swoje reguły do wszystkich plików w katalogu docelowym. Jeśli trzymasz kopie obok innych plików, te inne pliki również zostaną usunięte.
Innymi słowy: BACKUP_RETENTION_DAYS=7 wskazane na kubełek albo katalog, w którym leży cokolwiek jeszcze, usunie z niego wszystko starsze niż siedem dni. Nie tylko archiwa tego narzędzia — wszystko. Zabezpieczenia są dwa i nie są opcjonalne w żadnym poważnym wdrożeniu:
- osobny katalog albo kubełek wyłącznie na te kopie, albo
BACKUP_PRUNING_PREFIXustawiony na nieparametryzowaną część nazwy pliku. PrzyBACKUP_FILENAME="db-backup-%Y-%m-%dT%H-%M-%S.tar.gz"prefiksdb-backup-zawęża rotację wyłącznie do plików, które ta instancja sama utworzyła.
Dwa pozostałe ustawienia z tej grupy: BACKUP_PRUNING_LEEWAY (domyślnie minuta) to zapas czasowy chroniący przed wyścigiem, w którym kopia stojąca dokładnie na krawędzi okna zostaje usunięta zaraz po utworzeniu — przy przebiegach o mocno zmiennym czasie trwania warto go zwiększyć. A BACKUP_SKIP_BACKENDS_FROM_PRUNE=s3,webdav wyłącza wskazane składy z rotacji, co pozwala trzymać krótkie okno lokalnie i pełną historię zdalnie.
Trzeba przy tym powiedzieć jasno, czego tu nie ma: retencja jest wyłącznie czasowa. Nie da się powiedzieć „trzymaj ostatnie dziesięć kopii”, nie ma schematu dziadek–ojciec–syn ani niczego, co przypomina polityki retencji z restica czy Borga. Jest jedna liczba dni na konfigurację — i to prowadzi wprost do następnego rozdziału.
Wiele harmonogramów w jednym kontenerze
Kilka harmonogramów o różnej konfiguracji uzyskuje się przez zamontowanie dowolnej liczby plików w formacie .env do katalogu /etc/dockervolumebackup/conf.d. Dla każdego pliku powstaje osobne zadanie cron, a wartości z pliku mają pierwszeństwo nad globalnym środowiskiem.
To jest sposób na zbudowanie poziomów retencji, których sama BACKUP_RETENTION_DAYS nie daje. Kopie dzienne przez tydzień, tygodniowe przez miesiąc, miesięczne na zawsze — trzy pliki:
# 01daily.conf
BACKUP_FILENAME="daily-backup-%Y-%m-%dT%H-%M-%S.tar.gz"
BACKUP_CRON_EXPRESSION="0 2 * * *"
BACKUP_PRUNING_PREFIX="daily-backup-"
BACKUP_RETENTION_DAYS="7"# 02weekly.conf
BACKUP_FILENAME="weekly-backup-%Y-%m-%dT%H-%M-%S.tar.gz"
BACKUP_CRON_EXPRESSION="0 3 * * 1"
BACKUP_PRUNING_PREFIX="weekly-backup-"
BACKUP_RETENTION_DAYS="31"# 03monthly.conf
BACKUP_FILENAME="monthly-backup-%Y-%m-%dT%H-%M-%S.tar.gz"
BACKUP_CRON_EXPRESSION="0 4 1 * *"Trzeci plik nie ustawia retencji, więc kopie miesięczne zostają. Rozdzielne prefiksy sprawiają, że każda reguła rotacji widzi tylko własne pliki — bez nich rotacja dzienna zjadłaby archiwa miesięczne przy pierwszym przebiegu.
Cztery szczegóły działania tego mechanizmu:
- Komenda
backupdziała na wyłącznej blokadzie. Nakładające się harmonogramy nie odpalą się równolegle — poczekają w kolejce. Kolejność przebiegów o identycznym wyrażeniu cron nie jest określona, LOCK_TIMEOUTto domyślnie 60 minut. Przebieg, który nie zdoła zdobyć blokady w tym czasie, kończy się błędem. Przy dużych wolumenach i kolidujących harmonogramach ta wartość jest pierwszą rzeczą do podniesienia,BACKUP_SOURCESna plik konfiguracji pozwala rozdzielić, który podzbiór zamontowanych wolumenów obejmuje dany harmonogram:BACKUP_SOURCES=/backup/app1_dataw pierwszym pliku,/backup/app2_dataw drugim,- Zmiana konfiguracji wymaga ręcznego restartu kontenera. Pliki nie są przeładowywane na gorąco.
Jeśli harmonogramy naprawdę muszą działać równolegle, nie ma innej drogi niż osobny kontener na każdy z nich. Wyzwolenie konkretnej konfiguracji ręcznie wygląda tak:
docker exec <kontener> /bin/sh -c 'set -a; source /etc/dockervolumebackup/conf.d/01daily.conf; set +a && backup'Szyfrowanie
Kopia leżąca na cudzym dysku powinna być zaszyfrowana, zwłaszcza gdy zawiera dane osobowe — a wolumen bazy produkcyjnej prawie zawsze zawiera. Narzędzie daje dwie metody, wzajemnie wykluczające się: GPG albo age. Wybiera się jedną, przez podanie jednej z czterech zmiennych:
GPG_PASSPHRASE— GPG symetrycznie, hasłem. Archiwum dostaje rozszerzenie.gpg,GPG_PUBLIC_KEY_RING— GPG asymetrycznie, kluczami publicznymi (wartość może być wielolinijkowa),AGE_PASSPHRASE— age symetrycznie. Archiwum dostaje rozszerzenie.age,AGE_PUBLIC_KEYS— age asymetrycznie, lista kluczy publicznych po przecinku; przyjmuje zarówno kluczeage, jak i klucze SSH.
Wariant asymetryczny jest wyraźnie lepszy operacyjnie: host, który tworzy kopie, potrzebuje wyłącznie klucza publicznego, a klucz prywatny może leżeć zupełnie gdzie indziej. Włamanie na serwer aplikacyjny nie daje wtedy dostępu do historii backupów. To jednocześnie miejsce, w którym trzeba mieć procedurę na klucz prywatny — zaszyfrowana kopia bez klucza to plik binarny bez wartości, a klucz przechowywany wyłącznie na maszynie, którą backupujesz, nie chroni przed niczym.
Odszyfrowanie kopii GPG to jedna komenda (system zapyta o hasło):
gpg -o backup.tar.gz -d backup.tar.gz.gpgPowiadomienia
Backup bez powiadomień to backup, o którego awarii dowiadujesz się w najgorszym możliwym momencie. Powiadomienia konfiguruje się przez NOTIFICATION_URLS — listę adresów w formacie biblioteki shoutrrr, oddzieloną przecinkami. Poczta wygląda tak:
NOTIFICATION_URLS="smtp://uzytkownik:haslo@smtp.example.com:587/?fromAddress=no-reply@example.com&toAddresses=ops@example.com"
NOTIFICATION_LEVEL="info"Domyślny NOTIFICATION_LEVEL to error, czyli powiadomienie tylko o nieudanych przebiegach. Ustawienie info daje wiadomość po każdym. Wybór nie jest oczywisty: cisza przy poziomie error jest nieodróżnialna od kontenera, który w ogóle nie działa. Sensowny kompromis to error plus zewnętrzny martwy człowiek — monitoring, który krzyczy, jeśli w kubełku nie pojawił się nowy plik.
Shoutrrr obsługuje też Slacka, Discorda, Telegram, Gotify, ntfy i kilkanaście innych usług; ich składnię trzeba sprawdzić w dokumentacji biblioteki. Adresy zawierające znaki specjalne (przecinki) wymagają zakodowania procentowego, a do wygenerowania poprawnego adresu jest gotowe narzędzie:
docker run --rm -ti ghcr.io/nicholas-fedor/shoutrrr generate smtpTreść powiadomień da się przepisać w całości. Pliki z szablonami Go montuje się do /etc/dockervolumebackup/notifications.d/, a nadpisać można cztery zagnieżdżone szablony: title_success, body_success, title_failure i body_failure:
{{ define "body_success" -}}
▶️ Start: {{ .Stats.StartTime | formatTime }}
⏹️ Koniec: {{ .Stats.EndTime | formatTime }}
⌛ Czas: {{ .Stats.TookTime }}
🛑 Zatrzymane kontenery: {{ .Stats.Containers.Stopped }}/{{ .Stats.Containers.All }} ({{ .Stats.Containers.StopErrors }} błędów)
⚖️ Rozmiar: {{ .Stats.BackupFile.Size | formatBytesBin }}
🗑️ Usunięte kopie: {{ .Stats.Storages.S3.Pruned }}/{{ .Stats.Storages.S3.Total }}
{{- end }}Do szablonu trafia obiekt Config (nazwy zmiennych środowiskowych przepisane na PascalCase, więc BACKUP_STOP_DURING_BACKUP_LABEL to BackupStopDuringBackupLabel), Error przy nieudanym przebiegu i rozbudowany Stats: czasy rozpoczęcia i zakończenia, czas trwania, czas oczekiwania na blokadę (LockedTime — przydatne przy diagnozowaniu kolidujących harmonogramów), pełny log przebiegu, statystyki zatrzymanych kontenerów i usług, nazwa oraz rozmiar pliku, a dla każdego składu liczba plików, liczba usuniętych i liczba błędów usuwania. Do formatowania są funkcje formatTime, formatBytesBin, formatBytesDec, env, toJson i toPrettyJson.
Osobna uwaga: starsza konfiguracja poczty przez EMAIL_SMTP_HOST, EMAIL_NOTIFICATION_RECIPIENT i pokrewne zmienne jest przestarzała i zniknie w następnej wersji głównej. Nowe wdrożenia powinny od razu używać NOTIFICATION_URLS.
Bezpieczeństwo: gniazdo Dockera to nie szczegół
Zamontowanie /var/run/docker.sock do kontenera daje temu kontenerowi kontrolę nad demonem Dockera, a więc — w praktyce — uprawnienia roota na hoście. Przyrostek :ro w montowaniu tego nie zmienia; ogranicza zapis do pliku gniazda, nie zakres API, do którego gniazdo daje dostęp. Warto wiedzieć, czym się płaci i co za to dostajemy: bez gniazda narzędzie nadal spakuje wolumeny i wyśle je na skład, ale nie zatrzyma kontenerów i nie wykona komend z etykiet cyklu życia, bo nie zobaczy nawet etykiet.
Jeśli te funkcje są potrzebne, dostęp można zawęzić. Dokumentacja podaje pełną listę wywoływanych API Dockera, co pozwala postawić przed gniazdem serwer proxy w rodzaju docker-socket-proxy i wskazać go przez DOCKER_HOST:
DOCKER_HOST=tcp://docker_socket_proxy:2375Wymagane uprawnienia proxy zależą od tego, z czego korzystasz: INFO i CONTAINERS zawsze; POST przy zatrzymywaniu kontenerów albo komendach z etykiet; EXEC przy samych komendach; SERVICES, NODES i TASKS w trybie Swarm. Przy instalacji Dockera bez roota wystarczy zamontować gniazdo użytkownika: /run/user/1000/docker.sock:/var/run/docker.sock:ro.
Sam kontener backupu domyślnie działa jako root, ale można to zmienić opcją user: 1000:1000 w compose. Ograniczenie jest oczywiste, gdy się je nazwie: nie-root nie zatrzyma kontenerów, jeśli demon Dockera na hoście działa jako root. Wybór jest więc realny — albo mniej uprawnień w kontenerze, albo zatrzymywanie usług na czas backupu. Przy logicznych zrzutach bazy przez etykiety pierwsza opcja jest zwykle do przyjęcia.
Sekrety da się trzymać poza konfiguracją: każdy klucz z dokumentacji przyjmuje wariant z przyrostkiem _FILE, który każe wczytać wartość z pliku. To jest droga do Docker Secrets:
services:
backup:
image: offen/docker-volume-backup:v2
environment:
AWS_ENDPOINT: minio.example.com
AWS_S3_BUCKET_NAME: backup-bucket
AWS_ACCESS_KEY_ID_FILE: /run/secrets/minio_access_key
AWS_SECRET_ACCESS_KEY_FILE: /run/secrets/minio_secret_key
secrets:
- minio_access_key
- minio_secret_keyDokumentacja dodaje przy tym drobiazg, który potrafi zająć pół godziny na diagnozie: sekrety nie są obcinane z białych znaków na początku i na końcu. Znak nowej linii, który wpadł do pliku z sekretem przy jego tworzeniu, będzie częścią hasła.
Strefa czasowa
Kontener startuje w UTC. Skoro harmonogram jest wyrażeniem cron, strefa decyduje o tym, kiedy backup faktycznie się wykona — a „druga w nocy” w UTC to w Polsce czwarta rano latem. Właściwe rozwiązanie to zmienna TZ:
services:
backup:
image: offen/docker-volume-backup:v2
environment:
TZ: Europe/WarsawStarsza metoda, czyli montowanie /etc/timezone, /etc/localtime i /usr/share/zoneinfo z hosta, jest oznaczona jako przestarzała. Działa nadal, o ile TZ nie jest ustawione — przy ustawionym TZ zmienna ma pierwszeństwo. Nieprawidłowa wartość TZ cofa kontener do UTC, cicho.
Odtwarzanie, czyli jedyna część, która się liczy
Archiwum jest zwykłym tarem, więc odtworzenie nie wymaga tego narzędzia — wystarczy tar i kontener tymczasowy. Dokumentacja podaje dwie procedury i różnica między nimi jest istotna.
Wariant pierwszy, dokładanie plików do istniejącego wolumenu:
tar -C /tmp -xvf backup.tar.gz
docker run -d --name temp_restore_container -v data:/backup_restore alpine
docker cp /tmp/backup/data-backup temp_restore_container:/backup_restore
docker stop temp_restore_container
docker rm temp_restore_containerWariant drugi, wycofanie całego wolumenu do stanu z archiwum — zalecany dla wolumenów baz danych, bo nie zostawia mieszanki starych i nowych plików:
docker volume ls
docker volume rm data
docker run --rm -it -v data:/backup/my-app-backup -v /sciezka/do/kopii:/archive:ro \
alpine tar -xvzf /archive/backup-2026-09-10T02-00-00.tar.gzObie procedury wymagają wcześniejszego zatrzymania kontenerów korzystających z wolumenu. I obie wymagają uwagi na jeden szczegół, który psuje pierwszą próbę odtworzenia u każdego: trzeba trafić w odpowiedni poziom ścieżki. To, ile elementów wiodących obciąć, zależy wyłącznie od tego, jak wolumeny były zamontowane w kontenerze backupu. Dlatego pierwsze tar -tvf na archiwum jest tańsze niż zgadywanie.
Backup, którego nikt nigdy nie odtworzył, nie jest backupem — jest plikiem. Odtworzenie na czystym środowisku, z pomiarem czasu i zapisaną procedurą, to jedyny dowód, że system kopii działa. Termin na wykonanie takiej próby jest zawsze ten sam: przed awarią.
Praktyczna wskazówka do tej próby: odtwarzaj z archiwum pobranego ze zdalnego składu, nie z kopii lokalnej. Test, który potwierdza wyłącznie, że lokalny plik daje się rozpakować, pomija dokładnie te elementy, które w dniu awarii mogą nie działać — dostęp do kubełka, klucz szyfrujący i to, czy ktokolwiek pamięta hasło.
W stacku Laravela
W typowym wdrożeniu aplikacji w Laravelu na Dockerze jest zwykle trzy rodzaje danych wartych kopii: wolumen bazy, katalog storage/app z plikami użytkowników (jeśli nie jest na S3) i wolumen wyszukiwania — Meilisearch albo Typesense. Redis w roli cache i kolejki jest odtwarzalny, więc zwykle nie ma powodu go backupować.
Konfiguracja, która to obsługuje, opiera się na logicznym zrzucie bazy przez etykietę i wolumenie pośrednim — nie zatrzymujemy niczego, więc aplikacja pozostaje dostępna:
services:
mysql:
image: mysql/mysql-server:8.0
volumes:
- sail-mysql:/var/lib/mysql
- dumps:/dumps
labels:
- docker-volume-backup.exec-label=laravel
- docker-volume-backup.archive-pre=/bin/sh -c 'mysqldump --single-transaction --quick --routines --events --all-databases -uroot -p"$$MYSQL_ROOT_PASSWORD" > /dumps/mysql.sql'
- docker-volume-backup.archive-post=/bin/sh -c 'rm -f /dumps/mysql.sql'
backup:
image: offen/docker-volume-backup:v2
restart: always
environment:
TZ: Europe/Warsaw
EXEC_LABEL: laravel
BACKUP_CRON_EXPRESSION: "0 2 * * *"
BACKUP_FILENAME: "laravel-%Y-%m-%dT%H-%M-%S.{{ .Extension }}"
BACKUP_COMPRESSION: zst
BACKUP_PRUNING_PREFIX: "laravel-"
BACKUP_RETENTION_DAYS: "14"
AWS_ENDPOINT: minio.example.com
AWS_S3_BUCKET_NAME: laravel-backup
AWS_S3_PATH: produkcja
AWS_ACCESS_KEY_ID_FILE: /run/secrets/s3_key
AWS_SECRET_ACCESS_KEY_FILE: /run/secrets/s3_secret
AGE_PUBLIC_KEYS: "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
NOTIFICATION_URLS: "smtp://user:pass@smtp.example.com:587/?fromAddress=backup@example.com&toAddresses=ops@example.com"
volumes:
- dumps:/backup/mysql:ro
- sail-storage:/backup/storage:ro
- sail-meilisearch:/backup/meilisearch:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
secrets:
- s3_key
- s3_secret
volumes:
sail-mysql:
sail-storage:
sail-meilisearch:
dumps:Kilka decyzji w tej konfiguracji jest celowych i warto je nazwać:
- Backupujemy zrzut, nie katalog
/var/lib/mysql. Wolumen bazy nie jest tu wcale montowany do kontenera backupu — do archiwum trafia wolumendumps, w którymmysqldumpzostawił jeden spójny plik SQL. Kopia katalogu danych działającego MySQL-a bez zatrzymania serwera to loteria, --single-transactionnie jest ozdobą. Bez tej flagimysqldumpna InnoDB blokuje tabele; z nią robi zrzut ze spójnego snapshotu transakcyjnego, bez blokowania zapisów. Do tego--routinesi--events, bo bez nich procedury składowane i zdarzenia po prostu nie wejdą do zrzutu,- Podwójny znak dolara w
$$MYSQL_ROOT_PASSWORDto ucieczka wymagana przez Compose, żeby zmienna została rozwinięta wewnątrz kontenera bazy, a nie podstawiona przez Compose przy wczytywaniu pliku, EXEC_LABELiexec-labelzawężają wykonywanie komend do kontenerów tego wdrożenia. Na hoście z kilkoma projektami bez tego jedna instancja backupu wywoła zrzuty we wszystkich,archive-postusuwa zrzut zaraz po spakowaniu, żeby jawny plik SQL nie leżał na dysku między przebiegami,- Szyfrowanie kluczem publicznym age — serwer produkcyjny nie ma klucza prywatnego, więc nawet pełny dostęp do niego nie daje dostępu do historii kopii,
- Prefiks rotacji zgodny z nazwą pliku — kubełek może być współdzielony, a rotacja nie tknie plików, które nie zaczynają się od
laravel-.
W wariancie z Postgresem różni się właściwie tylko etykieta:
- docker-volume-backup.archive-pre=/bin/sh -c 'pg_dumpall -U postgres > /dumps/postgres.sql'Jedna rzecz, której to narzędzie nie zastępuje w kontekście Laravela: spatie/laravel-backup działa na innym poziomie — zna aplikację, umie zrzucić bazę i wybrane katalogi z perspektywy frameworka i zintegrować się z powiadomieniami Laravela. docker-volume-backup działa poziom niżej, na wolumenach, i nie wie nic o aplikacji. To nie są konkurenci: jeden chroni dane aplikacji, drugi — stan całego wdrożenia, włącznie z wolumenami usług, których aplikacja nawet nie widzi. W małych projektach wybiera się jeden. W większych sensowny jest podział: logiczne zrzuty z poziomu aplikacji do szybkiego odtworzenia danych i kopie wolumenów do odtworzenia całego środowiska.
Czego to narzędzie nie robi
Lista ograniczeń jest tu krótka, ale trzeba ją znać przed wdrożeniem, bo dwa punkty z niej wykluczają całe klasy zastosowań:
- Brak kopii przyrostowych i deduplikacji. Każdy przebieg tworzy pełne archiwum tar. Przy wolumenie o rozmiarze 50 GB i kopii dziennej z retencją dwutygodniową to 700 GB na składzie, niezależnie od tego, że zmieniło się w nich kilka megabajtów. Tu leży granica między tym narzędziem a restikiem albo Borgiem: one deduplikują i dzięki temu skalują się na wielkie zbiory danych, ale wymagają własnego repozytorium, procesu i zrozumienia. docker-volume-backup jest świadomie prostszy i sprawdza się do dziesiątek gigabajtów, nie do terabajtów,
- Retencja tylko czasowa. Brak reguł typu „zachowaj ostatnie N kopii”. Poziomy retencji buduje się ręcznie z plików w
conf.d, jak wyżej, - Brak weryfikacji archiwum. Narzędzie nie sprawdza, czy powstały plik daje się rozpakować ani czy zrzut SQL jest kompletny. Ta weryfikacja jest po stronie wdrażającego i nikt jej nie zrobi za niego,
- Rotacja obejmuje wszystkie pliki w katalogu docelowym, o ile nie zawęzi jej prefiks. Powtarzamy to trzeci raz, bo jest to jedyne ustawienie w całym narzędziu, które przy błędnej konfiguracji usuwa cudze dane,
- Bez interfejsu graficznego i bez centralnego widoku. Cała konfiguracja to zmienne środowiskowe, cały stan to logi i powiadomienia. Przy trzydziestu hostach potrzebna jest do tego warstwa monitoringu z zewnątrz,
- W Swarmie komendy z etykiet widzą tylko własny węzeł, a kontener backupu musi stać na węźle menedżerskim.
Krótko: to narzędzie jest właściwym wyborem dla wdrożeń opartych o Docker Compose na jednym albo kilku hostach, gdzie dane liczy się w gigabajtach, a nie w terabajtach, i gdzie chodzi o to, żeby kopie po prostu były, były zaszyfrowane i było wiadomo, gdy przestaną powstawać. Do wielkich zbiorów danych, kopii przyrostowych albo skomplikowanych polityk retencji trzeba czegoś innego.
Podsumowanie
docker-volume-backup zamyka lukę, którą Docker zostawia od zawsze: wolumeny są jedyną nieodtwarzalną częścią wdrożenia i jedyną, o której polecenia Dockera nie mają nic do powiedzenia. Zamiast skryptu w cronie na hoście dostajemy usługę w tym samym pliku compose, co reszta stacku, konfigurowaną tak samo jak reszta stacku. Co z tego wynika przy planowaniu wdrożenia:
- Projekt jest zdrowy — 4004 gwiazdki, ostatnie wydanie
v2.49.0z 6 września 2026, commity w bieżącym tygodniu, 25 otwartych zgłoszeń, obraz na Docker Hubie i w ghcr.io dlaamd64,arm64iarm/v7. Około 25 MB, napisany w Go, bez zależności do zainstalowania na hoście, - Przypnij konkretne wydanie, nie
latest— dokumentacja mówi to wprost, a przy narzędziu, które usuwa pliki zgodnie z regułą retencji, niespodziewana zmiana zachowania jest droga, - Do baz danych używaj logicznego zrzutu przez
archive-pre, nie kopii katalogu danych. Wolumen pośredni na zrzut,--single-transactionprzy MySQL-u,archive-postdo usunięcia jawnego pliku, - Ustaw
BACKUP_PRUNING_PREFIXalbo daj kopiom osobny kubełek. Rotacja bez prefiksu usuwa wszystkie pliki starsze niż okno retencji, także te, które nie mają z backupem nic wspólnego, - Poziomy retencji buduj z plików w
conf.d— dzienne, tygodniowe, miesięczne, każdy z własnym prefiksem i własnym harmonogramem. Pamiętaj oLOCK_TIMEOUT, jeśli harmonogramy mogą się nałożyć, i o restarcie kontenera po zmianie konfiguracji, - Szyfruj kluczem publicznym (age albo GPG), nie hasłem trzymanym obok kopii, i zapisz gdzieś procedurę na klucz prywatny,
- Sekrety podawaj przez warianty
_FILEi Docker Secrets — z pamięcią o tym, że białe znaki w pliku z sekretem nie są obcinane, - Traktuj gniazdo Dockera poważnie — jego zamontowanie to uprawnienia roota na hoście, także z
:ro. Gdy potrzebne jest zatrzymywanie kontenerów albo komendy z etykiet, rozważdocker-socket-proxyiDOCKER_HOST. Kontener jako nie-root nie zatrzyma kontenerów, - Ustaw
TZ, bo domyślne UTC przesuwa nocny harmonogram o dwie godziny, - Włącz powiadomienia i osobny monitoring braku nowej kopii. Domyślny poziom
errormilczy identycznie przy sukcesie i przy zatrzymanym kontenerze, - Przećwicz odtworzenie, ze zdalnego składu, na czystym środowisku, z zapisaną procedurą. To jedyny test, który cokolwiek dowodzi,
- Przejrzyj przestarzałe ustawienia, jeśli konfiguracja jest starsza:
BACKUP_FROM_SNAPSHOT,exec-pre/exec-post,BACKUP_STOP_CONTAINER_LABELi zmienneEMAIL_*działają nadal, ale mają zniknąć w następnej wersji głównej.
Licencja: projekt jest rozpowszechniany na Mozilla Public License 2.0 — pełnoprawnej licencji open source zatwierdzonej przez OSI, z copyleftem na poziomie pliku. W praktyce oznacza to, że komercyjne użycie obrazu w dowolnej skali, u siebie i u klientów, jest bez zastrzeżeń: MPL nie stawia warunków korzystaniu z programu, a wymóg publikacji dotyczy wyłącznie zmian wprowadzonych w plikach objętych licencją. Zbudowanie własnego forka z poprawkami wymaga udostępnienia tych poprawek na MPL-2.0, ale sam fakt, że narzędzie tworzy kopie danych zamkniętej aplikacji, nie ma na tę aplikację żadnego wpływu — copyleft nie „przechodzi” na oprogramowanie, które jedynie działa obok. To wyraźnie łagodniejszy reżim niż AGPL, którą znamy z MinIO, i jeden z powodów, dla których ten obraz spokojnie ląduje w komercyjnych wdrożeniach.