Czym jest Docker i po co go używać? Najprostsze wyjaśnienie dla początkujących

0
53
Rate this post

Nawigacja:

O co w ogóle chodzi z Dockerem? Proste wyjaśnienie po ludzku

Docker w jednym zdaniu: o co tu chodzi

Docker to narzędzie, które pozwala uruchamiać aplikacje w kontenerach – czyli w lekkich, odizolowanych „pudełkach”, które mają w sobie wszystko, czego dana aplikacja potrzebuje: odpowiednią wersję języka, bibliotek, narzędzi i konfiguracji. Dzięki temu aplikacja działa tak samo na każdym komputerze, na którym działa Docker.

Najprościej: zamiast instalować na swoim systemie milion bibliotek, serwerów i zależności, tworzysz lub pobierasz kontener i mówisz: „uruchom to”. Docker zajmuje się resztą. Ty widzisz działającą aplikację, system operacyjny pozostaje względnie czysty, a większość problemów typu „u mnie działa, u ciebie nie” znika.

Kontener jako pudełko z aplikacją i jej środowiskiem

Intuicyjna metafora: kontener to pudełko z gotowym środowiskiem. W środku masz aplikację oraz wszystkie składniki, których potrzebuje:

  • konkretną wersję języka (np. Python 3.11 zamiast 3.8),
  • konkretne biblioteki systemowe,
  • konfigurację serwera (np. Nginx, Apache),
  • podstawowe pliki systemowe niezbędne do działania.

Z zewnątrz (z punktu widzenia twojego komputera) kontener jest po prostu procesem, który izoluje się od reszty systemu. Jeśli ktoś przygotuje dla ciebie obraz Dockera z aplikacją, to uruchamiasz ją jednym poleceniem, bez ręcznego ustawiania wszystkiego krok po kroku.

„Zainstaluj wszystko” vs „odpal kontener”

Klasyczne podejście: chcesz przetestować nową wersję bazy danych lub serwera WWW. Co robisz?

  • szukasz paczki instalacyjnej dla swojego systemu,
  • instalujesz, konfigurujesz, walczysz z zależnościami,
  • otwierasz porty, sprawdzasz logi,
  • po zakończeniu testów próbujesz to wszystko odinstalować, a część rzeczy i tak zostaje w systemie.

Podejście z Dockerem: uruchamiasz:

docker run --name moja-baza -e POSTGRES_PASSWORD=haslo -p 5432:5432 -d postgres:16

i masz gotową bazę PostgreSQL w kontenerze. Gdy skończysz:

docker stop moja-baza
docker rm moja-baza

i baza znika. System pozostaje praktycznie nietknięty – nie zmieniły się globalne pakiety, usługi, katalogi systemowe.

Problem „u mnie działa” i jak Docker go rozwiązuje

Najbardziej odczuwalna korzyść: spójność środowiska. Bez Dockera każdy developer, tester i serwer produkcyjny może mieć:

  • inną wersję systemu operacyjnego,
  • inne biblioteki,
  • inaczej ustawione zmienne środowiskowe,
  • zainstalowane różne wersje tego samego języka (np. PHP, Node.js).

Skutek: programista mówi, że wszystko działa, tester widzi błędy, na produkcji pojawiają się dziwne problemy. Docker pozwala zapakować całą aplikację w jedno powtarzalne środowisko. Ten sam obraz kontenera uruchamiasz:

  • na swoim laptopie,
  • na maszynie testowej,
  • na serwerze produkcyjnym w chmurze.

Jeśli wszystkim trzem miejscom podasz ten sam obraz Dockera, aplikacja zachowa się identycznie. Różnice środowiskowe sprowadzają się do minimum, a debugowanie staje się prostsze, bo mniej rzeczy jest „magicznie” zależnych od konkretnego systemu.

Kontener, obraz, host – najważniejsze pojęcia bez żargonu

Host: gdzie to wszystko się uruchamia

Host to po prostu system operacyjny, na którym działa Docker. Może to być:

  • Linux (Ubuntu, Debian, Fedora itd.),
  • Windows z Docker Desktop,
  • macOS z Docker Desktop.

Host udostępnia zasoby: procesor, pamięć RAM, dysk i sieć. Docker korzysta z tych zasobów i uruchamia na nich kontenery. W praktyce: jeśli twój komputer jest hostem, kontenery nigdy nie są „mocniejsze” od niego, zawsze dzielą się ograniczonymi zasobami komputera lub serwera.

Obraz Dockera – szablon, z którego powstaje kontener

Obraz Dockera (image) to szablon, z którego tworzone są kontenery. Można to porównać do:

  • pliku ISO z systemem operacyjnym (ale lżejszego),
  • „zamrożonego” stanu systemu z zainstalowaną aplikacją,
  • przepisu kulinarnego – kontener to gotowe danie, obraz to przepis + składniki.

Obraz jest niezmienny (ang. immutable): po zbudowaniu nie wprowadza się w nim zmian „na żywo”. Zmiany powstają przez zbudowanie nowego obrazu z aktualnym kodem lub konfiguracją. Typowe przykłady obrazów:

  • nginx:latest – serwer WWW Nginx,
  • postgres:16 – baza danych PostgreSQL,
  • node:20-alpine – Node.js na lekkim systemie Alpine,
  • python:3.12 – Python z podstawowym systemem.

Kontener – działająca instancja obrazu

Kontener to działająca instancja obrazu. Jeśli obraz jest szablonem, to kontener jest jego uruchomionym egzemplarzem. Z jednego obrazu możesz uruchomić:

  • jeden kontener (np. jedną bazę danych do testów),
  • kilkanaście kontenerów (np. kilkanaście identycznych instancji serwera WWW za load balancerem).

Przykład:

docker run --name serwer-www -p 8080:80 -d nginx:latest

Tutaj obraz to nginx:latest, a kontener ma nazwę serwer-www. Obraz pozostaje niezmienny, kontener żyje swoim życiem: ma własne logi, bieżące procesy, tymczasowe pliki i stan działania. Po zatrzymaniu i usunięciu kontenera obraz nadal jest dostępny i możesz z niego zrobić kolejny kontener.

Rejestry obrazów: Docker Hub i prywatne repozytoria

Rejestr (registry) to miejsce, w którym przechowywane są obrazy Dockera. Najpopularniejszy jest:

  • Docker Hub – publiczny serwis z tysiącami gotowych obrazów.

Firmy często korzystają też z własnych, prywatnych rejestrów:

  • GitHub Container Registry,
  • GitLab Container Registry,
  • Amazon ECR, Google Container Registry, Azure Container Registry.

Obrazy pobierasz poleceniem:

docker pull nazwa-obrazu:tag

albo Docker robi to za ciebie przy pierwszym docker run, jeśli obraz nie jest jeszcze obecny lokalnie.

Kontener a zwykły proces – na czym polega izolacja

Z perspektywy systemu operacyjnego kontener jest procesem (lub zestawem procesów) z dodatkowymi ograniczeniami i izolacją. Pod spodem Docker wykorzystuje mechanizmy jądra Linux (namespaces, cgroups, chroot i inne), by:

  • odizolować przestrzeń plików (kontener „widzi” swój system plików),
  • odizolować procesy (procesy w kontenerze „nie widzą” innych procesów systemu),
  • ograniczyć zasoby (RAM, CPU, dysk),
  • odseparować sieć logicznie (kontener ma własny „interfejs sieciowy”).

Dzięki temu można na jednym hoście uruchomić dziesiątki, a nawet setki kontenerów, z których każdy „myśli”, że działa w swoim osobnym środowisku, mimo że współdzielą jądro systemu.

Krótki przykład: obraz Nginx → działający serwer WWW

Praktyczny, minimalny przykład:

docker pull nginx:latest
docker run --name moj-nginx -p 8080:80 -d nginx:latest

Co się dzieje:

  • docker pull pobiera obraz serwera WWW Nginx z Docker Hub,
  • docker run tworzy kontener moj-nginx i uruchamia go w tle,
  • przekierowanie -p 8080:80 oznacza: port 8080 na twoim komputerze trafia na port 80 w kontenerze,
  • w przeglądarce wpisujesz http://localhost:8080 i widzisz stronę powitalną Nginx.

Nie musisz instalować Nginx bezpośrednio w systemie, nie zalegasz potem z konfiguracją i plikami w katalogach systemowych.

Kontenery kontra maszyny wirtualne – gdzie jest zysk?

Maszyna wirtualna – mini-komputer w komputerze

Maszyna wirtualna (VM) to pełny wirtualny komputer działający wewnątrz twojego sprzętu. Ma:

  • własny system operacyjny,
  • własne sterowniki,
  • własne procesy,
  • własną przestrzeń dyskową (plik obrazu dysku).

Do uruchamiania maszyn wirtualnych używa się np. VirtualBox, VMware, Hyper-V, KVM. Uruchomienie VM zwykle trwa od kilkunastu sekund do kilku minut, zużywa sporo RAM i CPU, a każda VM duplikuje cały system operacyjny.

Kontener – skupienie na aplikacji, nie na całym systemie

Kontener działa inaczej: zamiast uruchamiać pełny system operacyjny, współdzieli jądro hosta i dodaje tylko to, co niezbędne do działania aplikacji:

  • kilka plików systemowych,
  • oprogramowanie potrzebne aplikacji,
  • samą aplikację i jej konfigurację.

Efekt:

  • kontener startuje w sekundę lub ułamki sekundy,
  • zużywa mniej RAM i CPU niż pełna VM,
  • obrazy zajmują mniej miejsca niż pełne systemy.

Porównanie: szybkość, zasoby, granice izolacji

Uproszczone zestawienie cech kontenerów i maszyn wirtualnych:

CechaKontenery (Docker)Maszyny wirtualne (VM)
ZakresIzolowana aplikacja + jej środowiskoPełny system operacyjny + aplikacje
Czas uruchomieniaUłamki sekundy – kilka sekundKilkanaście sekund – kilka minut
Zużycie RAM/CPUNiskie – współdzieli jądro hostaWyższe – osobny system operacyjny
Obraz / dyskTypowo setki MB – kilka GBCzęsto kilka – kilkanaście GB
Poziom izolacjiDobra izolacja procesów, współdzielone jądroSilna izolacja – własne jądro systemu
Typowe zastosowanieUruchamianie i skalowanie usług i aplikacjiSymulacja pełnego systemu, testy OS

Kontenery nie zastępują maszyn wirtualnych w każdej sytuacji. To raczej uzupełnienie: VM dla całych systemów, kontenery dla aplikacji.

Kiedy lepszy Docker, a kiedy klasyczna wirtualizacja

Kontenery mają przewagę, gdy:

  • uruchamiasz wiele usług/aplikacji na jednym serwerze,
  • często i szybko wdrażasz nowe wersje,
  • skalujesz serwisy w górę i w dół (więcej/mniej kontenerów),
  • potrzebujesz powtarzalnego środowiska dla dev/test/prod.

Maszyny wirtualne sprawdzą się lepiej, jeśli:

  • musisz uruchomić zupełnie inny system operacyjny (np. Windows na Linuxie),
  • potrzebujesz maksymalnej izolacji poziomu OS (np. wymagania bezpieczeństwa),
  • testujesz upgrade całego systemu, sterowników, kernela.

Częsta praktyka w firmach: na jednym serwerze (fizycznym) działa kilka VM, a na każdej VM działają kontenery Dockera z właściwymi aplikacjami. To łączy elastyczność dwóch podejść.

Po co w ogóle używać Dockera? Konkretne korzyści w praktyce

Spójne środowisko od developera do produkcji

Największy zysk z Dockera to spójność środowiska. Ten sam obraz:

Jedno źródło prawdy dla konfiguracji i zależności

Gdy aplikacja działa w Dockerze, jej zależności i konfiguracja lądują w jednym miejscu – w obrazie i Dockerfile. Znika problem:

  • „u mnie działa na Pythonie 3.10, a na serwerze jest 3.8”,
  • różnych wersji bibliotek systemowych (OpenSSL, glibc itd.),
  • ręcznych zmian w konfiguracji serwera, o których nikt nie pamięta.

Jeśli obraz przechodzi testy na CI i działa na stagingu, to ten sam artefakt trafia na produkcję. Konfiguracja zmienia się tylko przez:

  • nowy obraz (np. zaktualizowana wersja aplikacji),
  • zmienne środowiskowe i zewnętrzne pliki konfiguracyjne (volumes, ConfigMapy w Kubernetesa itd.).

Nie ma już sytuacji, że administrator „podstroił coś na serwerze”, a potem nikt nie wie co i gdzie.

Mniej konfliktów między projektami na jednym komputerze

Na typowym komputerze developera często działa kilka projektów jednocześnie:

  • jeden wymaga Node 18, drugi Node 20,
  • jeden używa PostgreSQL 13, drugi 16,
  • różne wersje Javy, Pythona, PHP.

Bez kontenerów kończy się to żonglowaniem wersjami, przełączaniem środowisk, narzędziami typu pyenv, nvm i podobnymi. Docker rozcina ten problem:

  • każdy projekt ma swój obraz z dokładnie taką wersją runtime, jakiej potrzebuje,
  • lokalne środowisko pozostaje względnie czyste – wystarczy Docker i edytor kodu.

Jeśli jeden projekt „zasyfi” kontener zależnościami, inny na tym nie traci – ma swój obraz i swój zestaw pakietów.

Szybkie wdrażanie i cofanie zmian

Aplikacja w Dockerze wdraża się jako nowy obraz. Typowy scenariusz:

  1. budujesz nowy obraz z wersją 1.2.0,
  2. wypychasz go do rejestru,
  3. na serwerze podmieniasz kontenery, żeby używały nowego tagu obrazu.

Jeśli coś pójdzie źle, cofnięcie zmian to:

  • zatrzymanie kontenerów z nową wersją,
  • uruchomienie kontenerów ze starym obrazem (np. tag 1.1.5).

Nie ma cofania paczek w systemie, przywracania backupu całego serwera czy „naprawiania” środowiska na żywym organizmie. Zmiana polega na zastąpieniu jednego obrazu drugim.

Lepsza skalowalność i automatyzacja

Kontenery są stworzone do tego, żeby je:

  • automatycznie uruchamiać i wyłączać,
  • przenosić między serwerami,
  • monitorować i restartować, jeśli coś padnie.

Na jednym hoście można:

  • uruchomić kilka instancji tej samej aplikacji (np. 5 kontenerów z backendem),
  • w prosty sposób dodać kolejną instancję przy wzroście ruchu,
  • rozproszyć kontenery po klastrze (Kubernetes, Docker Swarm, ECS).

Z punktu widzenia automatyzacji, kontener to precyzyjnie opisany „klocek”, którym zarządza się łatwiej niż ręcznie konfigurowanym serwerem.

Łatwiejsze testowanie i eksperymenty

Docker dobrze nadaje się do krótkich, izolowanych eksperymentów:

  • test nowej bazy danych bez ingerencji w system (np. docker run postgres),
  • sprawdzenie innej wersji biblioteki bez psucia działającego projektu,
  • odpalanie całych stosów technologicznych (np. ELK stack) na lokalnym laptopie.

Po testach kontener można po prostu usunąć:

docker rm -f nazwa-kontenera

Ślady po eksperymencie znikają, a system hosta pozostaje w takim samym stanie jak wcześniej.

Standardowy sposób pakowania aplikacji

Obraz Dockera staje się standardowym formatem „paczki” aplikacji. To:

  • ułatwia wdrażanie na różnych platformach (serwer bare metal, VM, chmura),
  • upraszcza integrację z narzędziami CI/CD (GitHub Actions, GitLab CI, Jenkins),
  • obniża próg wejścia dla nowych członków zespołu (wystarczy znać kilka komend Dockera).

Jeśli aplikacja jest spakowana jako kontener, coraz więcej usług w chmurze potrafi ją przyjąć „wprost”, bez długiej listy dodatkowych wymogów.

Kontenerowiec Evergreen ładowany w porcie w słoneczny dzień
Źródło: Pexels | Autor: David Dibert

Jak działa Docker pod spodem – tylko tyle, ile trzeba wiedzieć na start

Architektura klient–serwer: Docker CLI i Docker Engine

Docker składa się z dwóch głównych elementów:

  • Docker CLI – program, który wywołujesz w terminalu (docker run, docker build itd.),
  • Docker Engine (demon dockerd) – usługa działająca w tle, która faktycznie tworzy obrazy i kontenery.

Gdy wpisujesz komendę:

docker ps

CLI wysyła żądanie do Dockera Engine (po lokalnym API), a ten zwraca listę kontenerów. Taki podział pozwala:

  • sterować zdalnym serwerem Dockera z własnego komputera,
  • oddzielić interfejs użytkownika od części, która zarządza kontenerami.

Warstwowe obrazy i współdzielenie plików

Obraz Dockera składa się z wielu warstw (layers). Każda warstwa to:

  • zestaw plików powstałych na danym etapie budowania obrazu,
  • „różnica” względem poprzedniej warstwy.

Przykład uproszczonego Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Tu powstaną m.in. warstwy:

  1. bazowy obraz python:3.12-slim,
  2. ustawienie katalogu roboczego,
  3. skopiowany plik requirements.txt,
  4. zainstalowane biblioteki Pythona,
  5. skopiowany kod aplikacji.

Gdy zbudujesz nową wersję obrazu po poprawce w kodzie, Docker:

  • użyje ponownie warstw bazowych (system, Python, biblioteki),
  • przebuduje tylko warstwy od momentu, w którym pliki się zmieniły.

To skraca czas budowania i oszczędza miejsce na dysku – wiele obrazów może współdzielić te same warstwy.

System plików kontenera – copy-on-write

Kontener używa systemu plików obrazu, ale nie modyfikuje go bezpośrednio. Zastosowana jest technika copy-on-write:

  • obraz jest traktowany jako tylko do odczytu,
  • kontener dostaje nad nim cienką warstwę zapisu.

Jeśli proces w kontenerze zmienia plik:

  • oryginalny plik z obrazu zostaje w spokoju,
  • modyfikacja trafia do warstwy zapisu kontenera.

Dzięki temu kilka kontenerów może korzystać z tego samego obrazu bez konfliktów. Każdy ma swoją własną przestrzeń na zmiany.

Sieć dokera w skrócie

Docker tworzy własne sieci wirtualne. Domyślnie nowe kontenery lądują w sieci typu bridge:

  • każdy kontener dostaje własny adres IP w tej sieci,
  • kontenery mogą się widzieć po nazwach (DNS wbudowany w Dockera),
  • dostęp z zewnątrz odbywa się zwykle przez mapowanie portów (-p 8080:80).

Można też tworzyć inne typy sieci (np. host, overlay), ale na początek wystarczy zrozumienie, że:

  • kontenery mają logicznie odseparowaną sieć,
  • mapowanie portów decyduje, jakie usługi są dostępne z hosta i z internetu.

Wolumeny – trwałe dane poza kontenerem

Domyślnie dane zapisane w kontenerze znikają, gdy kontener zostanie usunięty. Do trzymania trwałych danych używa się wolumenów (volumes):

  • wolumen to kawałek przestrzeni dyskowej zarządzany przez Dockera,
  • jest podpinany do kontenerów jako katalog (np. /var/lib/postgresql/data),
  • przetrwa restart i usunięcie kontenera.

Tworzenie wolumenu i użycie w kontenerze:

docker volume create pgdata
docker run -d --name baza 
  -v pgdata:/var/lib/postgresql/data 
  postgres:16

Tu dane bazy trafiają do wolumenu pgdata, a kontener można wymienić bez utraty informacji.

Pierwsze kroki: instalacja Dockera i test „Hello World”

Instalacja Dockera na popularnych systemach

Najwygodniej korzystać z oficjalnych pakietów:

  • Windows / macOS – aplikacja Docker Desktop (graficzny panel + Docker Engine),
  • Linux – pakiety docker-ce z oficjalnego repozytorium Dockera.

Na Windows Docker Desktop domyślnie używa wirtualnej maszyny (WSL2 lub Hyper-V), bo kontenery wymagają jądra podobnego do Linuksa. Dla użytkownika wygląda to jednak jak zwykły Docker.

Sprawdzenie, czy Docker działa

Po instalacji pierwszym krokiem jest sprawdzenie wersji:

docker version

Jeśli pojawią się informacje o kliencie i serwerze, Docker jest uruchomiony. Komenda:

docker info

zwraca szczegółowe dane o konfiguracji – ile masz obrazów, ile kontenerów, jaki driver sieciowy i storage są w użyciu.

Pierwszy test: oficjalny „hello-world”

Klasyczny test:

docker run hello-world

Docker zrobi wtedy:

  1. spróbuje znaleźć lokalny obraz hello-world,
  2. jeśli go nie ma – pobierze go z Docker Hub,
  3. uruchomi kontener, który wypisze krótką wiadomość i się zakończy.

Sam komunikat wyjaśnia, co dokładnie zaszło – to pierwszy dowód, że:

  • Docker potrafi pobierać obrazy z rejestru,
  • kontenery się uruchamiają,
  • logi kontenera trafiają na standardowe wyjście (widzisz je w terminalu).

Prosty serwer HTTP w kontenerze

Kolejny krok to serwer WWW. Najprościej:

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

Po chwili:

  • wejdź w przeglądarce na http://localhost:8080,
  • zobaczysz domyślną stronę Nginx.

Zatrzymanie i usunięcie kontenera:

docker stop moj-nginx
docker rm moj-nginx

Port 8080 przestaje nasłuchiwać, a lokalny system pozostaje bez śladów po Nginx (poza obrazem Dockera).

Podstawowe komendy Dockera, które wystarczą na początek

Praca z obrazami: listowanie, pobieranie, usuwanie

Kilka komend dotyczących obrazów:

  • docker images – lista obrazów lokalnie,
  • docker pull nazwa:tag – pobranie obrazu z rejestru,
  • docker rmi nazwa:tag – usunięcie obrazu.

Przykład:

docker pull nginx:latest
docker images
docker rmi nginx:latest

Uruchamianie, zatrzymywanie i podgląd kontenerów

Najważniejsze komendy dotyczące kontenerów:

  • docker run – stworzenie i uruchomienie nowego kontenera,
  • docker ps – lista działających kontenerów,
  • docker ps -a – lista wszystkich (także zatrzymanych),
  • docker stop nazwa – łagodne zatrzymanie,
  • docker kill nazwa – wymuszone zatrzymanie,
  • docker rm nazwa – usunięcie kontenera.

Przykładowy cykl życia:

Interaktywna praca z kontenerem

Do szybkiego debugowania albo eksploracji obrazu przydaje się tryb interaktywny. Zamiast uruchamiać proces w tle, podłączasz się do powłoki w kontenerze:

docker run -it --name test-bash ubuntu:22.04 bash

Dzieje się kilka rzeczy:

  • Docker pobiera obraz ubuntu:22.04 (jeśli go nie masz),
  • tworzy kontener o nazwie test-bash,
  • uruchamia w nim proces bash,
  • łączy Twój terminal z tym procesem (-it).

Po wyjściu z powłoki (np. komendą exit) kontener się zatrzyma, ale nadal będzie istniał. Można go skasować:

docker rm test-bash

Podgląd logów i szczegółów kontenera

Większość aplikacji loguje na standardowe wyjście. Docker przechwytuje te logi i udostępnia prostą komendę:

docker logs nazwa-kontenera

Przykład z Nginx:

docker logs moj-nginx
docker logs -f moj-nginx

Przełącznik -f działa podobnie jak tail -f – śledzi nowe wpisy w logach.

Do diagnostyki przydaje się też:

docker inspect moj-nginx

Zwraca duży obiekt JSON z pełną konfiguracją i bieżącym stanem kontenera: zmienne środowiskowe, mapowania portów, wolumeny, sieć itd. Jeśli trzeba tylko adres IP:

docker inspect -f '{{ .NetworkSettings.IPAddress }}' moj-nginx

Usuwanie „śmieci”: obrazy, kontenery, wolumeny

Praca z Dockerem generuje sporo obiektów: zatrzymane kontenery, nieużywane obrazy, stare wolumeny. Co jakiś czas dobrze jest zrobić porządek.

Podstawowe narzędzia:

  • docker rm – usuwa kontenery,
  • docker rmi – usuwa obrazy,
  • docker volume rm – usuwa wolumeny.

Do automatycznego sprzątania:

docker system prune

Usunie zatrzymane kontenery, nieużywane sieci, „osierocone” warstwy obrazów. Z przełącznikiem:

docker system prune -a

znikną także wszystkie obrazy, których nie używa żaden kontener. Na środowisku developerskim to wygodne, na produkcji – trzeba uważać.

Obrazy i Dockerfile – jak zbudować własny kontener z aplikacją

Obraz jako szablon kontenera

Obraz Dockera to „zamrożony” stan systemu plików + informacja, jaki proces uruchomić domyślnie. Z jednego obrazu można odpalić dowolną liczbę kontenerów:

docker run --name app1 moj-obraz:latest
docker run --name app2 moj-obraz:latest

Oba kontenery działają niezależnie, choć mają ten sam punkt wyjścia.

Co to jest Dockerfile

Dockerfile to tekstowy plik z instrukcjami, jak zbudować obraz. Każda linia opisuje krok: skąd zaczynamy, jakie pliki kopiujemy, co instalujemy, jak uruchamiamy aplikację.

Prosty szkielet:

FROM <bazowy-obraz>
WORKDIR /app
COPY . .
RUN <komendy instalacyjne>
CMD [<program>, <arg1>, ...]

Minimalny przykład: aplikacja w Pythonie

Załóżmy katalog projektu:

app/
├─ main.py
└─ Dockerfile

Plik main.py:

from http.server import HTTPServer, BaseHTTPRequestHandler

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b"Hello from Docker!")

if __name__ == "__main__":
    server = HTTPServer(("", 8000), Handler)
    print("Listening on :8000")
    server.serve_forever()

Prosty Dockerfile:

FROM python:3.12-slim

WORKDIR /app
COPY main.py .

EXPOSE 8000

CMD ["python", "main.py"]

Budowanie obrazu i uruchomienie:

cd app
docker build -t python-hello .
docker run --name python-hello-container -p 8000:8000 python-hello

W przeglądarce pod http://localhost:8000 pojawi się odpowiedź z kontenera.

Typowe instrukcje w Dockerfile

Podstawowy zestaw wystarcza na większość prostych projektów:

  • FROM – wybór obrazu bazowego (np. node:22-alpine, golang:1.22),
  • WORKDIR – ustawienie katalogu roboczego wewnątrz obrazu,
  • COPY / ADD – kopiowanie plików z hosta do obrazu,
  • RUN – wykonanie komendy na etapie budowania (instalacja pakietów, kompilacja),
  • EXPOSE – deklaracja portu, na którym nasłuchuje aplikacja (informacyjnie),
  • ENV – ustawianie zmiennych środowiskowych,
  • CMD – domyślna komenda, która uruchomi się w kontenerze.

CMD zwykle zapisuje się w formie tablicy (tzw. exec form), jak w przykładzie z Pythonem. Dzięki temu Docker nie odpala powłoki pośredniej, a sygnały (np. SIGTERM) trafiają poprawnie do procesu.

.dockerignore – jak przyspieszyć budowanie obrazu

Domyślnie instrukcja COPY . . pakuje cały katalog kontekstu (bieżący katalog) i wysyła go do Demona Dockera. Jeśli w projekcie leżą katalogi typu node_modules, venv czy ciężkie pliki tymczasowe, budowanie mocno zwalnia.

Rozwiązaniem jest plik .dockerignore w katalogu projektu, np.:

venv
__pycache__
*.pyc
.git
node_modules
dist
build

Działa podobnie do .gitignore: wymienione pliki i foldery nie trafią do kontekstu budowania.

Obrazy „ciężkie” i „lekkie” – dobór bazowego systemu

Ten sam kod aplikacji można spakować na wiele sposobów. Wybór obrazu bazowego wpływa na rozmiar i bezpieczeństwo:

  • obrazy pełne (np. ubuntu, debian) – wygodne do debugowania, ale duże,
  • obrazy „slim” (np. python:3.12-slim) – kompromis między rozmiarem a funkcjonalnością,
  • obrazy „alpine” (np. node:22-alpine) – bardzo małe, ale czasem trzeba dostosować zależności (inne glibc/musl).

Przy typowej aplikacji webowej sensowna strategia to:

  1. na etapie developmentu korzystać z obrazu wygodnego do pracy (np. python:3.12),
  2. do obrazu produkcyjnego użyć lżejszej bazy (np. python:3.12-slim albo obraz typu „distroless”).

Multi-stage build – jeden Dockerfile, różne etapy

Gdy aplikacja wymaga kompilacji (Go, Java, Rust, frontend), dobrym rozwiązaniem są multi-stage builds. Jedna definicja obrazu może mieć kilka etapów:

  • etap „builder” – kompilacja, instalacja narzędzi, testy,
  • etap „runtime” – minimalny obraz z gotowym artefaktem.

Przykład dla aplikacji Go:

FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o app

FROM gcr.io/distroless/base-debian12
WORKDIR /app
COPY --from=builder /app/app .
USER nonroot
CMD ["./app"]

Gotowy obraz nie zawiera kompilatora ani narzędzi developerskich – tylko skompilowany binarny program i niezbędne biblioteki.

Parametryzacja obrazu zmiennymi środowiskowymi

Aplikacja zwykle potrzebuje danych konfiguracyjnych: adres bazy, klucze API, tryb debugowania. Można je przekazywać:

  • domyślnie w Dockerfile za pomocą ENV,
  • dynamicznie przy uruchamianiu kontenera przez -e lub plik --env-file.

Przykład:

docker run -d --name app 
  -e APP_ENV=production 
  -e DB_HOST=db 
  -p 8000:8000 
  moj-obraz:latest

W Dockerfile można definiować rozsądne wartości domyślne:

ENV APP_ENV=development
ENV APP_PORT=8000

Na produkcji te wartości da się nadpisać bez budowania nowego obrazu.

Mapowanie katalogów z hosta – bind mounts

Oprócz wolumenów Dockera można podpiąć do kontenera dowolny katalog z hosta. Przydaje się to w developmentcie, gdy kod ma być widoczny natychmiast:

docker run --name node-dev 
  -v $(pwd):/app 
  -w /app 
  -p 3000:3000 
  node:22-alpine 
  npm run dev

Tu:

  • -v $(pwd):/app – katalog bieżący z hosta jest widoczny w kontenerze jako /app,
  • -w /app – ustawia katalog roboczy,
  • wszystkie zmiany w plikach lokalnych są od razu widoczne wewnątrz kontenera.

Tagowanie i wersjonowanie obrazów

Obraz ma nazwę i tag. Bez tagu Docker użyje :latest, ale w praktyce lepiej być bardziej precyzyjnym:

docker build -t moja-aplikacja:1.0.0 .
docker build -t moja-aplikacja:1.0.1 .

W projektach spotyka się m.in. strategie:

  • tagowanie numerem wersji aplikacji (semver),
  • tagowanie skrótem commita (moja-aplikacja:git-sha),
  • utrzymywanie ruchomego taga :latest dla najnowszego builda.

W CI/CD często buduje się obraz z dwoma tagami naraz, np. :1.4.2 i :latest, co ułatwia zarówno odtwarzalność, jak i szybkie wdrażanie najnowszej wersji.

Wysyłanie obrazu do rejestru (Docker Hub, GitHub Container Registry, prywatny)

Aby użyć własnego obrazu na innym serwerze, trzeba go wysłać do rejestru. Przykład z Docker Hub:

  1. Załóż konto na Docker Hub i zaloguj się:
    docker login
  2. Otaguj lokalny obraz z prefiksem nazwy użytkownika:
    docker tag moja-aplikacja:1.0.0 <uzytkownik>/moja-aplikacja:1.0.0
  3. Wyślij obraz:
    docker push <uzytkownik>/moja-aplikacja:1.0.0

Na docelowej maszynie wystarczy:

docker pull <uzytkownik>/moja-aplikacja:1.0.0
docker run -d -p 8000:8000 <uzytkownik>/moja-aplikacja:1.0.0

Zamiast publicznego Docker Hub można użyć prywatnego rejestru (np. GitHub Container Registry, GitLab Container Registry, ECR w AWS). Z punktu widzenia Dockera różni się tylko adres, np. ghcr.io/<org>/moja-aplikacja:tag.

Przykład: mała aplikacja webowa z bazą, cała w Dockerze

Przybliżony scenariusz z codziennej praktyki:

  • aplikacja Node.js,
  • baza PostgreSQL,
  • developer chce szybko uruchomić pełne środowisko lokalnie.

Obraz aplikacji (uproszczony Dockerfile):

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

ENV PORT=3000
EXPOSE 3000

CMD ["npm", "start"]

W tym momencie można:

  1. zbudować obraz:
    docker build -t moja-node-app:dev .
  2. uruchomić bazę:
    docker run -d --name db 
      -e POSTGRES_PASSWORD=haslo 
      -e POSTGRES_USER=app 
      -e POSTGRES_DB=appdb 
      -v pgdata:/var/lib/postgresql/data 
      postgres:16
  3. Bibliografia i źródła

  • Docker Overview. Docker, Inc. – Oficjalne wprowadzenie do Dockera, kontenerów, obrazów i rejestrów
  • Docker Engine Overview. Docker, Inc. – Opis działania silnika Docker, hosta, kontenerów i obrazów
  • PostgreSQL on Docker Official Image Reference. PostgreSQL Global Development Group – Opis oficjalnego obrazu postgres, zmienne środowiskowe, przykłady użycia
  • Nginx Docker Official Image Documentation. F5 NGINX, Inc. – Opis obrazu nginx:latest, podstawowe przykłady docker run i mapowanie portów
  • Kubernetes Documentation – Containers. Cloud Native Computing Foundation – Wyjaśnienie pojęcia kontenera, obrazu i różnic względem VM w kontekście orkiestracji
  • Linux Containers and Docker. Red Hat – Artykuł wyjaśniający kontenery, obrazy, hosta oraz izolację procesów w Linuksie
  • Linux kernel documentation – Control Groups (cgroups). The Linux Foundation – Techniczne podstawy ograniczania zasobów procesów i kontenerów
  • What is Docker?. Microsoft Learn – Wprowadzenie do Dockera dla początkujących, definicje kontenerów i obrazów