Jak zacząć z Dockerem: praktyczny przewodnik dla programistów i administratorów Linux

0
55

Nawigacja:

Po co ci Docker i kiedy ma to w ogóle sens

Klasyczne bóle: „u mnie działa”, chaos wersji i ręczne wdrożenia

Jeśli pracujesz jako programista lub administrator Linux, pewnie przechodziłeś już przez scenariusz: na twoim laptopie aplikacja działa idealnie, na serwerze testowym zaczyna się sypać, a na produkcji wychodzą kompletne dziwactwa. Różne wersje bibliotek, inne ustawienia systemu, brakujące pakiety, konflikt Python 3.8 vs 3.11 albo Node 16 vs 20 – ten sam kod, inne środowisko, inne efekty. Przy kilku usługach jeszcze da się nad tym panować, przy kilkunastu – zaczyna się walka z wiatrakami.

Docker wprowadza porządek w tym bałaganie, bo pozwala zamknąć aplikację razem z całym środowiskiem w obrazie, który można uruchomić w przewidywalny sposób na wielu hostach. Zamiast instrukcji „zainstaluj to, tamto, zmień to w konfiguracji, a potem się pomódl” masz jeden zdefiniowany opis środowiska i prosty sposób na jego uruchomienie.

Dla admina to z kolei mniej „ręcznie robionych” serwerów. Zamiast każdej aplikacji w osobnym, żmudnie konfigurowanym VM, można mieć jednego mocnego hosta i wiele odseparowanych kontenerów, które startują w kilka sekund. To nie usuwa potrzeby znajomości Linuksa, ale redukuje liczbę zmiennych, z którymi trzeba walczyć przy każdym deployu.

Co Docker rozwiązuje, a czego nie dotyka

Docker rozwiązuje problem standaryzacji środowiska: to, co masz w obrazie, uruchamia się tak samo na twoim laptopie, na serwerze staging i na produkcji – pod warunkiem, że host ma sensowny kernel i zasoby. Pomaga też w szybkim skalowaniu: łatwiej powielić kontener niż ręcznie konfigurować kolejne VM-ki. Dla developerów to swoboda: możesz mieć na jednym hoście kilka wersji tej samej bazy, różne wersje języka, osobne środowiska do testów regresyjnych.

Jednocześnie Docker nie naprawi złej architektury aplikacji. Jeśli twoja aplikacja wycieka pamięć, będzie ją wyciekać również w kontenerze. Jeśli SQL jest pisany bez indeksów, kontener nie przyspieszy zapytań. Docker nie zastępuje też monitoringu, backupów, backupów konfiguracji czy normalnej administracji Linuksem. To narzędzie, które porządkuje jeden konkretny obszar – środowisko wykonawcze – ale resztę i tak trzeba mieć opanowaną.

Nie jest też „srebrną kulą” dla bezpieczeństwa. Kontener to nie jest pełna wirtualna maszyna; kernel jest wspólny. Błędy w konfiguracji, np. uruchamianie wszystkiego jako root, otwarte porty, brak limitów zasobów – to nadal twoja odpowiedzialność.

Kontenery vs wirtualne maszyny – kiedy co wybrać

Wirtualna maszyna (VM) symuluje cały system: od BIOS/UEFI, przez wirtualny hardware, po kernel i userland. Kontener korzysta z wspólnego kernela hosta, ma własną przestrzeń procesów, sieci, mountów, ale nie wnosi własnego kernela. To kluczowa różnica. VM startuje w dziesiątki sekund lub minuty, kontener najczęściej w ułamku sekundy. VM ma zwykle większy narzut zasobów, kontener jest lżejszy.

Kontener jest idealny, kiedy:

  • chcesz szybko stawiać i niszczyć środowiska developerskie lub testowe,
  • aplikacja ma działać na Linuksie i nie wymaga pełnej izolacji hardware’u,
  • liczy się możliwość gęstszego upakowania usług (mikroserwisy, małe serwisy web, joby batchowe).

Wirtualna maszyna nadal ma sens, gdy:

  • potrzebujesz innego systemu operacyjnego niż host (np. Windows na Linuxie),
  • klient lub regulacje wymagają twardej izolacji na poziomie całego systemu,
  • aplikacja używa specyficznych sterowników lub konfiguracji kernela, których nie chcesz mieszać na jednym hostcie.

W praktyce rozsądne środowiska łączą oba podejścia: mniejsza liczba VM-ów, na których działa wiele kontenerów.

Typowe scenariusze użycia Dockera w codziennej pracy

Najczęstszy scenariusz to lokalne środowiska developerskie. Zamiast instalować lokalnie MySQL, Redis, RabbitMQ, Elastic, zaczynasz od zestawu kontenerów, które uruchamiasz jednym poleceniem. Kiedy projekt się kończy albo zmieniasz wersję biblioteki, usuwasz kontenery i obrazy – system pozostaje czysty.

Drugi scenariusz to testy i CI/CD. W pipeline CI (GitLab CI, GitHub Actions, Jenkins) uruchamiasz testy jednostkowe, integracyjne czy nawet E2E w izolowanych kontenerach. W ten sposób każdy pipeline dostaje świeże środowisko, niezależne od poprzednich buildów.

Trzeci przypadek to małe usługi i narzędzia pomocnicze: cron-joby, skrypty backupowe, małe API, narzędzia administratorskie. Nie wymagasz tu całego VM-a, a chcesz mieć powtarzalność i łatwą aktualizację. Kontener sprawdza się świetnie.

Coraz częściej Docker pojawia się też w obszarach „około-infrastrukturalnych”: narzędzia typu reverse proxy, serwery logów, narzędzia monitoringu. Wiele projektów open source publikuje gotowe obrazy, dzięki czemu konfiguracja sprowadza się do pliku YAML w Docker Compose zamiast ręcznego instalowania plików binarnych.

Kluczowe pojęcia bez marketingowego żargonu

Kontener, obraz i warstwy – realny obraz tego, jak to działa

W świecie Dockera obraz (image) to szablon, a kontener to uruchomiona instancja tego szablonu. Najbliższa analogia z programowania: klasa vs obiekt. Klasa definiuje, jak obiekt ma wyglądać i co potrafić, ale dopiero obiekt istnieje w pamięci. Obraz określa, jakie pliki, pakiety i polecenie startowe ma środowisko. Kontener to proces (a często kilka procesów) działający na jego bazie.

Obrazy składają się z warstw (layers). Każda instrukcja w Dockerfile tworzy warstwę – np. instalacja pakietów, kopiowanie plików. Warstwy są niemodyfikowalne (read-only) i współdzielone między obrazami. Na górze tego stosu jest cienka warstwa zapisu specyficzna dla kontenera (copy-on-write). Dzięki temu dwa kontenery oparte na tym samym obrazie bazowym nie dublują fizycznie wszystkich danych na dysku.

Taki mechanizm wpływa mocno na wydajność i praktykę: przemyślany Dockerfile jest krótszy, szybszy i bezpieczniejszy. Instrukcje, które rzadziej się zmieniają (np. instalacja systemowych pakietów), warto trzymać na dole pliku, a częściej zmieniające się (np. kopiowanie kodu aplikacji) – wyżej. Dzięki temu Docker wykorzystuje cache i przy ponownym buildzie nie odtwarza wszystkiego od zera.

Od strony systemu Linux kontener to zbiór mechanizmów izolacji: namespaces (izolacja PID-ów, sieci, mountów, użytkowników) oraz cgroups (ograniczanie CPU, RAM, IO). Do tego dochodzą capabilities, czyli wybrane uprawnienia kernela. Nie trzeba znać ich wszystkich na pamięć, ale świadomość, że kontener to „sprytnie ograniczony proces” działający na wspólnym kernelu, pomaga przy debugowaniu trudniejszych problemów.

Rejestry obrazów, tagi i repozytoria

Obrazy Dockera trzymane są w tzw. rejestrach (registry). Publicznym domyślnym jest Docker Hub, ale istnieją też inne: GitHub Container Registry, GitLab Registry, Google Artifact Registry, AWS ECR i własne prywatne rejestry w firmach. Rejestr to po prostu serwer, który przechowuje obrazy i pozwala je pobierać oraz wysyłać.

Każdy obraz ma postać nazwa_repozytorium:tag. Tag to nie jest „gałąź Gita”, ale raczej etykieta wersji. Często używa się:

  • wersji semantycznych, np. 1.2.3,
  • tagów środowiskowych, np. dev, staging, prod,
  • kombinacji, np. 1.2.3-prod lub 1.2.3-alpine.

Tag latest to tylko umowa – obraz, który maintainer oznaczył jako „domyślny”. Nie oznacza automatycznie najnowszej wersji w sensie semantycznym; to zwykła etykieta. Świadome używanie konkretnych tagów (np. postgres:16 zamiast postgres:latest) jest jednym z prostszych sposobów, by uniknąć niespodziewanych zmian po aktualizacji.

Repozytorium obrazu, jak library/nginx czy mojefirma/api, to zbiór tagów dotyczących konkretnego projektu. Możesz traktować to jak nazwę projektu w rejestrze. Warto przyjąć jasną konwencję w zespole i trzymać się jej, żeby po kilku miesiącach było wiadomo, co jest czym.

Docker Engine, daemon i klient CLI

Pod spodem Docker składa się z dwóch istotnych części: dockerd (daemon) i klienta CLI (docker). Demon nasłuchuje na socketcie (zwykle /var/run/docker.sock) i wykonuje operacje na obrazach, kontenerach, sieciach, wolumenach. Klient CLI wysyła do niego żądania, np. „zbuduj obraz”, „uruchom kontener”, „pokaż logi”.

Po wpisaniu w terminalu komendy docker run ... klient nie zaczyna nagle robić magii – po prostu wysyła odpowiednio zbudowane zapytanie do demona. Dzięki temu możliwy jest też dostęp zdalny (przez TCP), choć dla bezpieczeństwa domyślnie komunikuje się po unix socketcie.

Docker wymaga zwykle uprawnień roota, bo manipuluje namespaces, siecią, systemem plików. Aby nie używać za każdym razem sudo, tworzona jest grupa docker i dodaje się do niej użytkowników. Członek tej grupy praktycznie ma uprawnienia root względem hosta, bo może wykonywać operacje przez dockerd. Z perspektywy bezpieczeństwa to istotne: dodawanie kogokolwiek do grupy docker powinno być świadomą decyzją, bo to równoważnik dostępu administracyjnego, tylko ubrany w inną nazwę.

Programista pracujący przy dwóch monitorach w przyciemnionym pokoju
Źródło: Pexels | Autor: cottonbro studio

Instalacja Dockera na Linuksie bez magii i skrótów

Oficjalne paczki Dockera a paczki z dystrybucji

Docker na Linuksie najczęściej instaluje się na dwa sposoby: z oficjalnych repozytoriów Docker (pakiety docker-ce) albo z repozytoriów dystrybucji (np. docker.io na Debian/Ubuntu). Różnica to głównie tempo aktualizacji i wsparcie. Oficjalne pakiety mają zazwyczaj nowszą wersję Dockera i są wspierane bezpośrednio przez firmę Docker. Dystrybucyjne bywają bardziej konserwatywne i lepiej wpasowane w filozofię danej dystrybucji, ale czasem są wyraźnie w tyle z wersją.

Na systemach serwerowych, gdzie Docker jest kluczowym elementem, często stosuje się oficjalne repozytoria Docker – łatwiej kontrolować, jaką dokładnie wersję się ma, i śledzić zmiany z changelogów Dockera. Na desktopowych instalacjach, środowiskach testowych czy tam, gdzie wymagasz stabilności powiązanej z dystrybucją, paczki z oficjalnych repo dystrybucji są zwykle wystarczające.

Przed instalacją warto sprawdzić, czy system nie ma przypadkiem zainstalowanych starszych, kolidujących paczek (np. dawny docker vs docker-ce) i usunąć je, żeby nie mieszać konfiguracji. Oficjalna dokumentacja Dockera dokładnie opisuje, jak to zrobić dla poszczególnych dystrybucji.

Instalacja krok po kroku na popularnych dystrybucjach

Na Debianie i Ubuntu typowa procedura instalacji z oficjalnych repozytoriów wygląda tak (skrótowo, bez wszystkich niuansów):

Jeśli chcesz pogłębić temat i zobaczyć więcej przykładów z tej niszy, zajrzyj na Informatyka, Nowe technologie, AI.

  • Odinstalowanie starych paczek: sudo apt remove docker docker-engine docker.io containerd runc.
  • Dodanie klucza GPG i repozytorium Dockera (komendy z dokumentacji odpowiedniej wersji systemu).
  • Instalacja pakietów: sudo apt install docker-ce docker-ce-cli containerd.io.
  • Sprawdzenie statusu usługi: sudo systemctl status docker.

Na Fedorze/CentOS/RHEL kroki są podobne, ale z użyciem dnf lub yum i dedykowanego repozytorium Dockera dla tych systemów. W świecie Arch/Manjaro Docker jest dostępny z oficjalnych repozytoriów, więc instalacja sprowadza się do pacman -S docker i włączenia usługi.

Po instalacji praktycznym krokiem jest dodanie siebie do grupy docker:

sudo usermod -aG docker <twoj_uzytkownik>

Po wylogowaniu i zalogowaniu można wykonywać komendy docker bez sudo. W środowiskach produkcyjnych często zostawia się wymaganie sudo, aby lepiej kontrolować, kto faktycznie może uruchamiać kontenery.

Na koniec prosty test: docker run hello-world. Jeśli kontener pobierze się z rejestru i poprawnie się uruchomi, środowisko jest w podstawowej formie gotowe.

Pierwsze sprawdzenie środowiska i diagnoza problemów

Po instalacji warto szybko rzucić okiem na kilka komend:

  • docker version – wersja klienta i serwera, od razu widać, czy demon działa.
  • docker info – szczegóły środowiska: używany driver storage, liczba obrazów, kontenerów, limity.
  • Diagnostyka: gdy coś nie działa po instalacji

    Po świeżej instalacji Docker potrafi kaprysić: brak dostępu do socketu, konflikty z wcześniejszymi narzędziami (np. Podman), problemy z siecią. Zamiast błądzić, lepiej przejść prostą listę kontrolną.

  • Sprawdź usługę: sudo systemctl status docker – jeśli widzisz inactive lub failed, spróbuj sudo systemctl restart docker i przejrzyj logi: journalctl -u docker -xe.
  • Socket i uprawnienia: jeśli dostajesz błąd typu permission denied while trying to connect to the Docker daemon socket, upewnij się, że:
    • albo używasz sudo docker ...,
    • albo jesteś w grupie docker i wylogowałeś się / zalogowałeś ponownie.
  • Konflikt narzędzi: jeśli wcześniej był zainstalowany Podman lub inna platforma kontenerowa z własną konfiguracją cgroups i sieci, warto sprawdzić, czy ich usługi nie walczą o te same zasoby.
  • Sieć i DNS: kiedy docker run hello-world nie może pobrać obrazu, sprawdź, czy host w ogóle ma dostęp do internetu (np. ping 8.8.8.8) oraz czy DNS działa (np. ping google.com).

Jeśli Docker startuje, ale kontenery dziwnie się zachowują, docker info i docker inspect <id_kontenera> zwykle szybciej naprowadzi na trop niż bezradne patrzenie w terminal. Nie trzeba od razu rozumieć wszystkich pól – wystarczy wyłapać podstawy: driver storage, driver sieci, limity zasobów.

Pierwsze kontenery: od „hello-world” do prostego serwisu web

Uruchomienie pierwszego kontenera w praktyce

Pierwsze odpalenie kontenera ma pokazać dwie rzeczy: że środowisko działa oraz jak wygląda podstawowy cykl run -> logi -> zatrzymanie -> usunięcie. Najprostszy przykład to klasyczny hello-world:

docker run hello-world

Co się wtedy dzieje w tle?

  • Docker sprawdza, czy lokalnie istnieje obraz hello-world:latest.
  • Jeśli nie – łączy się z domyślnym rejestrem (Docker Hub) i pobiera obraz.
  • Tworzy kontener na bazie obrazu i uruchamia zdefiniowane w nim polecenie startowe.
  • Kontener wypisuje komunikat i kończy działanie, więc przechodzi w stan exited.

Ten przykład jest jednorazowy – kontener nic nie serwuje, tylko wykonuje polecenie i się kończy. To normalne i na początku bywa dezorientujące: „kontener mi się wyłącza, coś jest zepsute”. Nie, po prostu proces wewnątrz kontenera zakończył pracę.

Tryb interaktywny i jednorazowe zadania

Aby poczuć, że kontener to faktycznie proces w odizolowanym środowisku, dobrze jest wejść do środka jak do tymczasowej maszyny:

docker run -it --rm ubuntu:22.04 bash

Flagi robią swoje:

  • -i – zostawia STDIN otwarty,
  • -t – przydziela pseudo-tty, więc terminal działa „normalnie”,
  • --rm – po wyjściu z kontenera (np. komendą exit) Docker od razu go usuwa.

W środku możesz zrobić zwykłe rzeczy: apt update, cat /etc/os-release, sprawdzić, jaki ma hostname, jakie procesy działają. To bezpieczne pole do eksperymentów – po wyjściu kontener znika, host pozostaje nienaruszony.

Statyczne a działające kontenery: proces ma znaczenie

Kontener żyje tak długo, jak długo działa proces, który jest jego „PID 1”. Jeśli proces jest jednorazowy (np. skrypt), kontener się kończy. Jeśli proces to serwer (np. nginx), kontener będzie działał, dopóki ten serwer odpowiada.

Dlatego uruchamiając cokolwiek jako usługę, potrzebujesz procesu na pierwszym planie (foreground). Przykładowo, aby uruchomić nginx tak, by nie przeszedł w tło, stosuje się tryb -g 'daemon off;' lub wbudowane polecenie startowe obrazu, które już ma to ustawione.

Pierwszy prosty serwis web w kontenerze

Jeśli celem jest szybki serwis HTTP, klasyczny przykład to nginx:

docker run -d --name moj-nginx -p 8080:80 nginx:stable

Co tu się dzieje:

  • -d – tryb odłączony (detached), kontener działa w tle,
  • --name moj-nginx – nadanie czytelnej nazwy, zamiast losowego ID,
  • -p 8080:80 – mapowanie portu hosta na port kontenera (host:kontener),
  • nginx:stable – wybrany tag obrazu, nie latest, żeby uniknąć przypadkowych niespodzianek po aktualizacji.

Po uruchomieniu w przeglądarce można wejść na http://localhost:8080 i obejrzeć stronę startową nginx. W tle kontener nadal działa:

docker ps

Jeśli chcesz podejrzeć logi serwera:

docker logs moj-nginx

Zatrzymanie i usunięcie tego kontenera to:

docker stop moj-nginx
docker rm moj-nginx

Z biegiem czasu takie cykle (uruchom – sprawdź – zatrzymaj – usuń) staną się naturalne jak praca z usługami systemdowymi.

Mapowanie katalogów hosta: szybki podgląd zmian

Przy serwisach webowych szybko pojawia się potrzeba „wstrzyknięcia” własnych plików HTML, logów czy konfiguracji. Służą do tego volume bind mounty, które mapują katalog z hosta do katalogu w kontenerze. Dla nginx może to wyglądać tak:

mkdir -p ~/docker-web/html
echo "<h1>Witaj w moim kontenerze</h1>" > ~/docker-web/html/index.html

docker run -d --name moj-nginx-dev 
  -p 8081:80 
  -v ~/docker-web/html:/usr/share/nginx/html:ro 
  nginx:stable

Użyty parametr:

  • -v ~/docker-web/html:/usr/share/nginx/html:ro – montuje lokalny katalog do katalogu z zawartością serwowaną przez nginx; :ro oznacza tryb tylko do odczytu.

Teraz każda zmiana w ~/docker-web/html od razu jest widoczna w kontenerze pod http://localhost:8081. Dla dewelopera frontendu to najprostszy sposób, żeby szybko „podpiąć” swój kod pod kontenerowy nginx, bez przebudowywania obrazu przy każdej zmianie.

Wchodzenie do działającego kontenera

Czasem trzeba zajrzeć do środka działającego kontenera, żeby podglądnąć log, konfigurację lub sprawdzić procesy. Służy do tego docker exec:

docker exec -it moj-nginx-dev bash

Jeśli obraz nie zawiera bash (częste w minimalistycznych dystrybucjach), można spróbować sh:

docker exec -it moj-nginx-dev sh

To bardzo wygodne przy diagnozie: można sprawdzić, czy pliki są tam, gdzie trzeba, jak wygląda /etc/nginx/nginx.conf, czy aplikacja ma dostęp do zasobów. Po wyjściu kontener nadal działa, nic się nie restartuje.

Zarządzanie cyklem życia: zatrzymywanie, usuwanie, porządki

Przy pierwszych eksperymentach system szybko zapełnia się kontenerami w stanie exited i nieużywanymi obrazami. Krótkie porządki robią dużą różnicę, zwłaszcza na małych dyskach SSD.

Podstawowe komendy porządkowe:

  • docker ps -a – wszystkie kontenery, także zakończone,
  • docker rm <id|nazwa> – usunięcie pojedynczego kontenera,
  • docker rmi <obraz> – usunięcie obrazu (gdy nie ma już zależnych kontenerów),
  • docker system df – podgląd zajętości dysku przez obrazy, kontenery, wolumeny.

Gdy środowisko jest zaśmiecone, a wszystkie kontenery są jedynie testowe, można użyć „miotły”:

docker system prune

Ta komenda usuwa zatrzymane kontenery, nieużywane sieci i dangling images. Istnieje też ostrzejsza wersja z --volumes, ale na niej łatwo stracić dane zapisane w wolumenach, więc lepiej najpierw zrozumieć, co w tych wolumenach trzymasz.

Obrazy: skąd je brać i jak je świadomie wybierać

Oficjalne, community, firmowe – rodzaje obrazów

Rejestry obrazów są pełne gotowych rozwiązań: baz danych, kolejek, serwerów HTTP, systemów CI. Zanim pobierzesz pierwszy lepszy obraz, dobrze jest rozróżnić kilka kategorii:

  • Obrazy oficjalne (official images) – utrzymywane lub nadzorowane przez Docker / zespół projektu; zwykle oznaczone na Docker Hub specjalną etykietą, mają rozbudowaną dokumentację i przegląd bezpieczeństwa.
  • Obrazy community – tworzone przez społeczność lub pojedynczych maintainerów; potrafią być świetne, ale poziom utrzymania i bezpieczeństwa bywa różny.
  • Obrazy firmowe / prywatne – budowane i publikowane przez organizacje na potrzeby własne lub swoich klientów; to często miejsce na twoje własne obrazy.

Na start bezpieczniej oprzeć się na obrazach oficjalnych i popularnych (np. nginx, postgres, redis, node, python) niż na pierwszym wyniku wyszukiwarki z losową nazwą użytkownika. Zwłaszcza w środowiskach produkcyjnych brak zaufanego źródła obrazu to otwarte drzwi do kłopotów.

Jak czytać stronę obrazu w rejestrze

Każdy sensowny obraz ma stronę w rejestrze z opisem. Kilka elementów, na które dobrze spojrzeć przed użyciem:

  • Opis i przykład użycia – czy jest wyjaśnione, jak uruchomić kontener, jakie zmienne środowiskowe są wspierane, jakie porty są wystawione.
  • Lista tagów – czy istnieją wersje z konkretnymi numerami (np. 16, 16-alpine), czy tylko latest.
  • Informacje o bezpieczeństwie – skan podatności, oznaczenia dotyczące wsparcia; przy poważniejszych projektach często są osobne raporty CVE.
  • AKTYWNOŚĆ – kiedy ostatnio aktualizowano obraz, czy tagi są w miarę świeże.

Jeśli opis jest lakoniczny, brak przykładów i wersje nie były odświeżane od lat, lepiej obejrzeć alternatywy. Zwykle nie jesteś pierwszą osobą, która potrzebuje danego typu usługi w kontenerze – warto wykorzystać doświadczenie społeczności.

W tym miejscu przyda się jeszcze jeden praktyczny punkt odniesienia: Git od zera: commit, branch i pull request na przykładach z życia.

Wielkość obrazu a wybór dystrybucji bazowej

Jedna z pierwszych decyzji przy budowie własnego obrazu to wybór obrazu bazowego: pełny Debian/Ubuntu, a może minimalistyczny Alpine? Kusi, żeby od razu iść w najmniejsze możliwe obrazy, ale sytuacja bywa bardziej złożona.

Porównanie podstawowe:

  • Pełniejsze dystrybucje (debian, ubuntu) – wygodne do debugowania (znane narzędzia, pełen ekosystem pakietów), zazwyczaj stabilne; w zamian większy rozmiar obrazu.
  • Dystrybucje minimalistyczne (alpine) – bardzo małe, szybciej się pobierają; dla niektórych języków (np. Go, Rust) świetne; czasem trudniejsze przy budowaniu i debugowaniu, biblioteki glibc vs musl potrafią sprawić kłopot.

Dla deweloperskich zastosowań często wygodniejsze są większe, ale bardziej „przyjazne” obrazy bazowe. Przy produkcji i skalowaniu na dziesiątki węzłów minimalizacja rozmiaru zyskuje na znaczeniu, ale nie zawsze jest to priorytet numer jeden.

Świadome korzystanie z tagów wersji

Używanie wyłącznie :latest bywa kuszące, bo „zawsze ma być nowa wersja”. Ale dokładnie ta „nowość” potrafi nagle wywrócić usługę, jeśli maintainer zmieni konfiguracje domyślne lub podbije główną wersję (np. z 15 do 16 w PostgreSQL).

Kilka praktycznych wzorców:

  • Środowisko dev – można pozwolić sobie na luźniejsze tagi, np. postgres:16 (bez .x) i aktualizować je częściej, testując zmiany.
  • Środowisko prod – lepiej trzymać się konkretnych wersji (np. 16.2) i wykonywać aktualizacje świadomie, po testach.
  • Sprawdzanie, co naprawdę siedzi w obrazie

    Zanim obraz trafi do produkcji, dobrze wiedzieć, co dokładnie w nim jest. Sama nazwa i opis na Docker Hubie to za mało, szczególnie jeśli pracujesz z wrażliwymi danymi.

    Na początek można zajrzeć do warstw obrazu:

    docker image history --no-trunc nginx:stable
    

    Wynik pokazuje polecenia, z których zbudowano obraz (warstwa po warstwie). Jeśli widać tam dziwne rzeczy typu curl <losowy-url> | sh, to ostrzegawczy sygnał. Przy oficjalnych obrazach zwykle zobaczysz dość przewidywalny zestaw: instalacja pakietów, czyszczenie cache, kopiowanie plików.

    Dla dokładniejszej inspekcji można też użyć:

    docker image inspect nginx:stable
    

    JSON z tej komendy zawiera m.in. domyślnego użytkownika (Config.User), zdefiniowane wolumeny, porty, zmienne środowiskowe. Jeśli obraz uruchamia się jako root bez potrzeby, lepiej otoczyć go dodatkową ostrożnością (np. narzucić innego użytkownika przy uruchomieniu).

    Podstawowe praktyki bezpieczeństwa przy wyborze obrazu

    Bezpieczeństwo kontenerów nie jest czarną magią, sporo ryzyka da się zredukować kilkoma nawykami. Nie wszystkie musisz wdrażać od razu – najlepiej zacząć od prostych kroków i stopniowo dokładać kolejne.

  • Ograniczone uprawnienia – jeśli to możliwe, korzystaj z obrazów, które domyślnie nie działają jako root, lub nadpisuj użytkownika przy starcie:
    docker run -u 1000:1000 ... <obraz>

    Nawet prosty mapping na zwykłego użytkownika z hosta zmniejsza skutki ewentualnego włamania.

  • Zaufane źródła – trzymaj się oficjalnych rejestrów lub prywatnych, kontrolowanych registry; kopiowanie „obrazów z internetu” bywa jak instalowanie losowych .exe.
  • Minimalna powierzchnia ataku – unikaj obrazów bazowych z tuzinami niepotrzebnych usług. Jeśli potrzebujesz tylko Pythona, nie ładuj pełnego „desktopowego” systemu.
  • Regularne aktualizacje – zamiast żyć z jednym wiekowym tagiem, lepiej mieć ustalony rytm aktualizacji (np. raz na kwartał przebudowanie obrazów z aktualnymi pakietami).

Skany podatności: automatyczne sprawdzanie obrazów

Kiedy aplikacja dojrzewa, sama inspekcja „na oko” już nie wystarcza. Przydają się automatyczne skanery podatności. Wiele rejestrów (Docker Hub, GitLab Container Registry, GitHub Container Registry) ma skanowanie wbudowane – wystarczy włączyć je w ustawieniach projektu.

Przykładowo, lokalnie można użyć narzędzia takiego jak trivy:

trivy image nginx:stable

Wynik pokaże listę znanych podatności (CVE) wraz z poziomem ważności. Na początku ta lista potrafi przestraszyć – ważne, żeby skupić się na najważniejszych (HIGH/CRITICAL) i zaplanować ich usunięcie poprzez aktualizację obrazu bazowego czy pakietów.

Dla wielu zespołów sensowne jest podejście: dev skanuje obrazy „informacyjnie”, a pipeline CI/CD wymusza brak krytycznych podatności w obrazach trafiających do produkcji. Dzięki temu nie musisz ręcznie pilnować każdego CVE – pilnuje tego automat.

Programistka pisze kod Dockera na laptopie w biurze
Źródło: Pexels | Autor: MART PRODUCTION

Budowanie własnych obrazów: pierwszy Dockerfile bez bólu

Prosty serwis HTTP w Pythonie jako przykład

Łatwiej zrozumieć budowanie obrazów na małym, konkretnym przykładzie. Załóżmy, że masz prosty serwis HTTP w Pythonie (np. Flask) i chcesz go zamknąć w kontenerze.

Struktura katalogu aplikacji może wyglądać tak:

moja-aplikacja/
  app.py
  requirements.txt

Plik app.py (minimalny przykład):

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Witaj z Dockera!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

requirements.txt:

flask==2.3.2

Pierwszy Dockerfile krok po kroku

W tym samym katalogu tworzysz plik Dockerfile:

FROM python:3.11-slim

# Katalog roboczy wewnątrz kontenera
WORKDIR /app

# Kopiowanie zależności w pierwszej kolejności
COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

# Kopiowanie reszty kodu aplikacji
COPY . .

# Port, na którym nasłuchuje aplikacja
EXPOSE 5000

# Domyślna komenda uruchomieniowa
CMD ["python", "app.py"]

Najważniejsze rzeczy, które dzieją się w tym pliku:

  • FROM python:3.11-slim – wybór obrazu bazowego; wariant slim jest lżejszy niż pełny Debian, a nadal wygodny do debugowania,
  • WORKDIR /app – ustawienie bieżącego katalogu roboczego; kolejne polecenia RUN, COPY, CMD odnoszą się do niego,
  • najpierw kopiowany jest tylko requirements.txt i instalowane są pakiety – to pozwala na lepsze buforowanie warstw i szybsze przebudowy przy zmianach w kodzie,
  • EXPOSE 5000 – informacja „dokomentacyjna” o porcie; prawdziwe mapowanie robisz i tak przy docker run -p ...,
  • CMD – domyślna komenda, która startuje proces wewnątrz kontenera.

Budowanie i uruchamianie własnego obrazu

Mając gotowy Dockerfile, budujesz obraz lokalnie:

cd moja-aplikacja

docker build -t moja-apka:dev .

Znaczenie parametrów:

  • -t moja-apka:dev – nadaje nazwę i tag obrazowi,
  • . – kontekst budowania, czyli katalog, z którego Docker ma skopiować pliki (w tym Dockerfile).

Po zbudowaniu możesz go uruchomić:

docker run -d --name moja-apka-kontener 
  -p 5000:5000 
  moja-apka:dev

Pod http://localhost:5000 zobaczysz odpowiedź serwisu. Jeśli coś nie działa, najpierw warto zajrzeć w logi:

docker logs moja-apka-kontener

Typowe błędy przy pierwszych Dockerfile’ach

Na początku łatwo wpaść w kilka powtarzalnych pułapek. Sporo nerwów oszczędza ich świadomość:

  • Bardzo ogólne obrazy bazowe – np. FROM ubuntu:latest plus ręczna instalacja wszystkiego. Tak da się zrobić, ale obrazy rosną, a ty sam musisz dbać o aktualizacje pakietów.
  • Kopiowanie całego katalogu z „śmieciami”COPY . . bez ignorowania .git, node_modules itp. Rozwiązaniem jest .dockerignore.
  • Instalowanie pakietów przy każdym buildzie – brak rozdzielenia kroków na osobne warstwy (osobno zależności, osobno kod) spowalnia buildy, bo Docker nie może ich dobrze zbuforować.
  • Brak jawnego portu i komendy – niby wszystko „da się nadpisać”, ale w praktyce czytelny EXPOSE i CMD ułatwiają życie wszystkim, którzy po tobie użyją obrazu.

.dockerignore – mały plik, duży efekt

Tak jak .gitignore chroni repo przed śmieciami, tak .dockerignore chroni obrazy. W katalogu aplikacji dodaj plik:

.git
.gitignore
__pycache__
*.pyc
.env
node_modules
dist
build

Przy budowaniu obrazów te pliki i katalogi nie będą kopiowane do kontekstu builda. Efekt: szybsze buildy, mniejsze obrazy, mniej przypadkowych sekretów w środku (np. lokalne pliki .env).

Wielostopniowe budowanie (multi-stage builds) – prosty przykład

Przy aplikacjach kompilowanych (Go, Rust, Java z Maven/Gradle) lub frontowych (npm, yarn) dobrze sprawdza się wzorzec „build w jednym etapie, uruchamianie w drugim”. Kod frontowy to dobre, intuicyjne demo:

FROM node:20-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

FROM nginx:stable-alpine

COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

W pierwszym etapie używany jest Node do zbudowania statycznych plików, które w drugim etapie trafiają do minimalnego serwera nginx. Końcowy obraz nie zawiera Node’a ani narzędzi deweloperskich – tylko serwer HTTP i statyczne zasoby. Różnica w rozmiarze i powierzchni ataku bywa ogromna.

Praca z danymi: wolumeny i persystencja

Czym różni się dane w kontenerze od danych na hoście

Przy pierwszych testach wiele osób jest zaskoczonych, że po usunięciu kontenera z bazą danych wszystko znika. Z punktu widzenia Dockera to normalne: system plików kontenera jest z natury efemeryczny, żyje tyle, co kontener.

Żeby dane przetrwały dłużej, masz dwie główne opcje:

  • bind mount – katalog z hosta podpinany w kontenerze (jak przy przykładzie z nginx i HTML-em),
  • wolumen Dockera (volume) – zarządzany przez Dockera, trzymany w specjalnej lokalizacji (np. /var/lib/docker/volumes).

Bind mount jest przejrzysty – widzisz dane normalnie w systemie plików. Wolumeny Dockera są wygodniejsze, kiedy nie chcesz mieszać danych aplikacji z innymi plikami na hoście albo gdy hostów jest wiele, a storage zapewnia zewnętrzny system (NFS, Ceph, chmura).

Named volumes na przykładzie PostgreSQL

Dla bazy danych najczęściej używa się „nazwanych” wolumenów. Prosty przykład PostgreSQL:

docker volume create pgdata

docker run -d --name moj-postgres 
  -e POSTGRES_PASSWORD=sekretnehaslo 
  -v pgdata:/var/lib/postgresql/data 
  -p 5432:5432 
  postgres:16

Kiedy zatrzymasz i usuniesz kontener:

docker stop moj-postgres
docker rm moj-postgres

dane nadal będą w wolumenie pgdata. Możesz postawić nowy kontener na tym samym wolumenie i baza ruszy z istniejącymi tabelami.

Podgląd wolumenów:

docker volume ls
docker volume inspect pgdata

Bind mount vs volume – kiedy co wybrać

Oba podejścia mają sens, ale w trochę innych sytuacjach:

  • Bind mount – dobry do dewelopki (chcesz mieć pliki „pod ręką”), do dzielenia logów z hostem, do szybkich eksperymentów.
  • Volume – wygodniejszy do usług produkcyjnych, gdzie Docker lub orchestrator (np. Kubernetes) zarządzają storage’em za ciebie; łatwiej też podpiąć zewnętrzne pluginy (np. do chmury).

Przy pierwszym kontakcie możesz trzymać się prostego schematu: konfiguracje i kod – bind mount; dane baz i cache’y – wolumeny.

Kopie zapasowe danych z kontenerów

Backupy kontenerów robi się tak samo jak backupy zwykłych usług – kluczem są dane, nie sam kontener. Prostą kopię wolumenu można zrobić z użyciem tymczasowego kontenera:

Na koniec warto zerknąć również na: mmWave w 5G: dlaczego w Europie wciąż rzadko je widzisz? — to dobre domknięcie tematu.

docker run --rm 
  -v pgdata:/data 
  -v $(pwd):/backup 
  alpine tar czf /backup/pgdata-backup.tar.gz -C /data .

Tworzy to archiwum z zawartością wolumenu pgdata w bieżącym katalogu na hoście. Przy odtwarzaniu robisz operację odwrotną (na pustym wolumenie) lub korzystasz z narzędzi samej bazy (dump/restore). Przy produkcyjnych bazach zazwyczaj używa się natywnych mechanizmów (np. pg_dump, pg_basebackup), ale ten schemat pomaga zrozumieć, że dane zawsze siedzą w zwykłym systemie plików.

Konfiguracja przez zmienne środowiskowe i pliki

Parametryzowanie kontenerów bez przebudowy obrazu

Obrazy powinny być możliwie uniwersalne, a różnice między środowiskami (dev/test/prod) najlepiej wyrażać przez konfigurację. Docker sprzyja prostemu podejściu: zmienne środowiskowe.

Większość oficjalnych obrazów wspiera własny zestaw zmiennych – jak w PostgreSQL:

docker run -d --name db-dev 
  -e POSTGRES_PASSWORD=devpass 
  -e POSTGRES_DB=moja_baza 
  -p 5432:5432 
  postgres:16

Twoje własne aplikacje też możesz napisać tak, żeby czytały ustawienia z env (np. DATABASE_URL, REDIS_HOST). Dzięki temu ten sam obraz da się uruchomić z innymi parametrami bez dotykania Dockerfile’a.

Pliki .env i unikanie wycieków sekretów

Najczęściej zadawane pytania (FAQ)

Po co mi Docker, skoro „jakoś” ogarniam środowiska na VM-kach?

Docker upraszcza to, co dziś robisz ręcznie: instalowanie pakietów, dopasowywanie wersji bibliotek, przenoszenie aplikacji między serwerami. Zamiast utrzymywać kilka VM-ek z różnymi konfiguracjami, tworzysz jeden opis środowiska (Dockerfile, docker-compose) i uruchamiasz go w przewidywalny sposób na dowolnym hoście z Linuksem i Dockerem.

Dla programisty oznacza to koniec sytuacji „u mnie działa, na serwerze już nie”. Dla administratora – mniej żmudnego klikania i konfigurowania pojedynczych VM-ek, szybsze wdrożenia i łatwiejsze odtwarzanie usług po awarii.

Czy Docker zastępuje wirtualne maszyny?

Nie, Docker nie zastępuje całkowicie VM-ek, tylko uzupełnia je. Kontener korzysta ze wspólnego kernela hosta i izoluje głównie procesy, sieć i system plików. VM symuluje cały system operacyjny od kernela w górę, dzięki czemu zapewnia mocniejszą izolację i umożliwia uruchamianie zupełnie innych systemów (np. Windows na Linuxie).

Praktyczne podejście to: kilka VM-ek jako „fizyczne” węzły (np. w chmurze), a na każdej z nich wiele lekkich kontenerów. VM zostaje tam, gdzie liczy się twardsza izolacja, specyficzny system lub kernel; kontenery przejmują lekkie usługi, mikroserwisy, narzędzia developerskie i środowiska testowe.

Kiedy lepiej użyć Dockera, a kiedy jednak pozostać przy VM-kach?

Docker sprawdza się najlepiej, gdy:

  • potrzebujesz szybko stawiać i usuwać środowiska developerskie lub testowe,
  • aplikacja ma działać na Linuksie i nie wymaga pełnego odseparowania hardware’u,
  • chcesz „upakować” wiele małych usług na jednym hoście.

Po VM-ki sięgaj wtedy, gdy:

  • potrzebujesz innego systemu niż host (np. Windows, BSD),
  • regulacje lub polityki bezpieczeństwa wymagają mocniejszej izolacji na poziomie całego systemu,
  • aplikacja wymaga specyficznego kernela lub sterowników, których nie chcesz mieszać z innymi usługami.

Czy Docker poprawia bezpieczeństwo aplikacji?

Docker sam z siebie nie jest gwarancją bezpieczeństwa. Kontener to nie pełna wirtualna maszyna – kernel jest wspólny dla wszystkich kontenerów i hosta. Jeśli uruchamiasz wszystko jako root, otwierasz niepotrzebne porty albo nie ustawiasz limitów zasobów, to ryzyko pozostaje, tylko w trochę innym opakowaniu.

Docker pomaga w porządkowaniu środowisk (oddzielne kontenery, mniejsze obrazy, jasne konfiguracje), co pośrednio ułatwia „uszczelnienie” całości. Jednak nadal potrzebujesz: sensownych uprawnień użytkowników, ograniczeń zasobów (cgroups), aktualizacji obrazów, monitoringu i backupów. Kontener nie naprawi zaniedbań w tych obszarach.

Czy Docker rozwiąże problemy z wydajnością mojej aplikacji?

Jeśli aplikacja jest źle napisana – wycieka pamięć, robi pełne scany tabel bez indeksów, ma blokujące zapytania – to w kontenerze będzie zachowywać się tak samo. Docker nie przyspiesza kodu ani baz danych; on tylko pakuje je w powtarzalne środowisko.

Korzyść pojawia się gdzie indziej: łatwiej stworzyć spójne środowiska testowe, porównać różne konfiguracje, szybko postawić dodatkowe instancje pod większe obciążenie. To ułatwia diagnozowanie i łagodzenie problemów z wydajnością, ale ich przyczyna leży w kodzie i architekturze, nie w braku kontenerów.

Co to jest obraz Dockera i jak różni się od kontenera?

Obraz (image) to wzorzec środowiska – zawiera system plików, zainstalowane pakiety i komendę startową. Kontener to uruchomiona instancja takiego obrazu, czyli realny proces (lub kilka procesów) działający na hoście. Najprostsza analogia: obraz to klasa, kontener to obiekt.

Obrazy składają się z warstw: każda instrukcja w Dockerfile tworzy nową, niemodyfikowalną warstwę, którą można współdzielić między różnymi obrazami. Kontener dostaje dodatkową cienką warstwę zapisu. Dzięki temu kilka kontenerów może korzystać z tych samych danych bazowych bez ich kopiowania na dysk za każdym razem.

Co oznaczają tagi obrazów Dockera i czy mogę bezpiecznie używać „latest”?

Tag to etykieta wersji obrazu, np. 1.2.3, dev, prod lub 16-alpine. Sam tag nie ma „magii” – to po prostu nazwa nadana konkretnej wersji. Jeden projekt może mieć wiele tagów wskazujących na różne warianty (inne wersje języka, systemu, zależności).

Tag latest bywa mylący: nie musi oznaczać najnowszej wersji w sensie semantycznym, to tylko domyślna etykieta ustawiona przez autora obrazu. W projektach produkcyjnych bezpieczniej jest wskazywać konkretne tagi (np. postgres:16 zamiast postgres:latest), żeby uniknąć niespodziewanych zmian po zwykłym docker pull.

Najważniejsze wnioski

  • Docker porządkuje chaos środowisk („u mnie działa”) przez zamknięcie aplikacji razem z bibliotekami, zależnościami i konfiguracją w jednym obrazie, który uruchamia się przewidywalnie na różnych hostach.
  • Dla administratorów kontenery oznaczają mniej ręcznie dłubanych serwerów i VM-ek: jeden mocny host może utrzymywać wiele lekkich, szybko startujących usług zamiast dziesiątek ciężkich wirtualek.
  • Docker nie naprawia złej aplikacji ani złej administracji – wycieki pamięci, kiepskie zapytania SQL, brak monitoringu, backupów czy sensownych limitów zasobów pozostają problemem niezależnie od kontenera.
  • Kontenery są lżejsze i szybsze od maszyn wirtualnych, ale współdzielą kernel hosta, więc lepiej nadają się do izolacji usług na Linuksie niż do scenariuszy wymagających pełnej separacji systemu operacyjnego lub specyficznych sterowników.
  • Największą wartość Docker daje w codziennej pracy: szybkie środowiska developerskie (bazy, kolejki, cache w kontenerach), powtarzalne testy w CI/CD oraz lekkie wdrażanie małych usług i narzędzi administracyjnych.
  • Rozsądnym podejściem jest łączenie VM-ów i kontenerów: na kilku wirtualkach utrzymywać „szkielet” infrastruktury, a w kontenerach uruchamiać poszczególne aplikacje i komponenty mikroserwisów.
  • Obrazy Dockera są budowane warstwowo, a kontener to tylko proces korzystający z tych warstw; wspólne, niemodyfikowalne warstwy i cienka warstwa zapisu dla kontenera oszczędzają miejsce na dysku i przyspieszają uruchamianie wielu instancji tej samej aplikacji.