Jak czytać to kompendium
mechanizmy, nie listy flagWiększość problemów z Dockerem nie bierze się z nieznajomości komend. Bierze się z błędnego modelu w głowie: „kontener to lekka maszyna wirtualna”, „RUN rm zmniejsza obraz”, „EXPOSE otwiera port”. Każdy rozdział tłumaczy jeden mechanizm, pokazuje go na klikalnym diagramie i celowo go psuje (przycisk 💥), żebyś zobaczył objaw, zanim spotkasz go na produkcji.
Największy nacisk leży na budowaniu obrazów: Dockerfile, kontekst, cache warstw, multi-stage, BuildKit, sekrety, wiele platform. Reszta — runtime, sieci, dane, Compose, rejestry, bezpieczeństwo — jest po to, żeby obraz miał sens w praktyce.
Kompendium ma poziomy. Rozdziały i bloki mają znaczek — L1 podstawy, L2 praktyk, L3 ekspert. Głębokość wybierasz przełącznikiem w lewym dolnym rogu; głębszy materiał chowa się, dopóki go nie zechcesz. Mapa poniżej skacze do dowolnego rozdziału i odhacza przeczytane.
Twój obraz Node.js waży 1,2 GB, a każda zmiana jednej linijki w src/ powoduje 4-minutowy build z pełnym npm install. Co dokładnie poszło nie tak — i ile linijek Dockerfile trzeba zmienić, żeby build trwał sekundy, a obraz ważył kilkadziesiąt MB? Odpowiedź na końcu.
Fundamenty
Czym naprawdę jest kontener, kto go uruchamia i z czego zbudowany jest obraz.
Kontener to nie maszyna wirtualna
to zwykły proces w przebraniuMaszyna wirtualna emuluje sprzęt i uruchamia własne jądro systemu. Kontener niczego nie emuluje. To zwykły proces na jądrze hosta, któremu jądro pokazuje „okrojony świat”: własne drzewo procesów, własny interfejs sieciowy, własny system plików. Robią to dwa mechanizmy jądra Linux.
- Namespaces — izolują to, co proces widzi:
pid(procesy),net(sieć),mnt(punkty montowania),uts(hostname),ipc,user(mapowanie UID). - Control groups (cgroups) — ograniczają to, ile proces może zużyć: pamięć, CPU, I/O, liczbę procesów.
Wspólne jądro to też wspólna powierzchnia ataku. Izolacja kontenera jest słabsza niż izolacja VM. Dlatego w części V mówimy o capabilities, seccomp i trybie rootless.
- Kontener = proces hosta + namespaces (co widzi) + cgroups (ile zużyje).
- Nie ma własnego jądra, więc startuje szybko, ale obraz musi pasować do jądra i architektury hosta.
Kto naprawdę uruchamia kontener
CLI tylko wysyła prośbęDocker to architektura klient–serwer. Polecenie docker to klient. Wysyła żądania przez REST API (domyślnie na gniazdo Unix /var/run/docker.sock) do demona dockerd. Demon zarządza obrazami, sieciami i wolumenami, a samo uruchamianie kontenerów deleguje do containerd. Ten z kolei używa runtime’u OCI runc, który tworzy namespaces i cgroups i odpala proces.
docker run nginx od klawiatury do procesuMiędzy containerd a runc stoi containerd-shim. Shim zostaje rodzicem procesu kontenera po tym, jak runc się zakończy. Dzięki temu restart demona nie zabija kontenerów, o ile włączysz opcję live-restore. Kontekst klienta zmienisz przez docker context use albo zmienną DOCKER_HOST — wtedy ten sam CLI rozmawia ze zdalnym demonem.
Dostęp do /var/run/docker.sock to w praktyce root na hoście: kto może wołać API, może uruchomić kontener z zamontowanym / hosta. Nie montuj gniazda do kontenerów „bo wygodnie”.
docker(CLI) → REST API →dockerd→containerd→runc→ proces.- Brak obrazu lokalnie = najpierw
pullz rejestru (domyślnie Docker Hub).
Obraz, warstwy i warstwa zapisu
kanapka tylko do odczytu + jedna kartka na wierzchuObraz to uporządkowany stos warstw tylko do odczytu plus konfiguracja (domyślne polecenie, zmienne, użytkownik…). Każda warstwa to zestaw zmian w systemie plików względem poprzedniej: dodane, zmienione i usunięte pliki. Sterownik storage (klasycznie overlay2; w świeżych instalacjach Docker Engine 29+ domyślnie containerd image store, również oparty na overlayfs) nakłada je na siebie i pokazuje jako jeden system plików.
Uruchomienie kontenera dodaje na wierzch cienką warstwę zapisu. Wszystkie zmiany kontenera lądują tylko tam. Usunięcie kontenera usuwa tę warstwę — warstwy obrazu zostają nietknięte i współdzielone przez wszystkie kontenery z tego obrazu.
RUN rm w Dockerfile — zobaczysz to w rozdziale o RUN.Pisanie dużo danych do warstwy zapisu jest wolniejsze niż do wolumenu i znika z kontenerem. Logi, bazy danych i uploady trzymaj w wolumenach (rozdział o danych). docker container diff <id> pokazuje, co kontener zmienił: A dodane, C zmienione, D usunięte.
🔬 Głębiej: overlay2 na dysku
OverlayFS łączy katalogi lowerdir (warstwy obrazu, tylko do odczytu), upperdir (warstwa zapisu kontenera) i pokazuje wynik w merged. Pierwszy zapis do pliku z niższej warstwy wywołuje copy_up: cały plik jest kopiowany do upperdir, nawet jeśli zmieniasz jeden bajt. Przy dużych plikach (np. pliki bazy danych) to realny koszt. Usunięcie tworzy w upperdir plik „whiteout”, który ukrywa oryginał. Ścieżki zobaczysz w docker inspect pod GraphDriver.Data (przy klasycznym storage demona).
- Obraz = warstwy tylko do odczytu + konfiguracja. Kontener = obraz + warstwa zapisu.
- Copy-on-write: zmiana pliku kopiuje go do warstwy zapisu; usunięcie tylko go zasłania.
- Wiele kontenerów współdzieli warstwy obrazu — na dysku są raz.
Cykl życia kontenera
created → running → exited → removedKontener żyje tak długo, jak jego główny proces (PID 1 w namespace kontenera). Gdy proces się kończy, kontener przechodzi w stan exited i zachowuje kod wyjścia. Zatrzymany kontener nadal istnieje — razem z warstwą zapisu i logami — dopóki go nie usuniesz.
create albo od razu start (to wtedy jak docker run).restarting i dead; zakładamy, że aplikacja obsługuje SIGTERM. Kod wyjścia 137 = 128+9 (SIGKILL).| Komenda | Co robi | Kiedy |
|---|---|---|
docker run | create + start (+ pull, jeśli brak obrazu) | codziennie |
docker run --rm | usuwa kontener po zakończeniu procesu | jednorazowe zadania, testy |
docker run -d | tryb odłączony, zwraca ID | serwisy w tle |
docker run -it | interaktywny TTY ze stdin | shell, REPL |
docker stop | sygnał stopu (domyślnie SIGTERM), po limicie czasu SIGKILL | łagodne zamknięcie |
docker kill | od razu SIGKILL (lub sygnał z -s) | proces nie reaguje |
docker rm -f | usuwa, także działający (najpierw kill) | sprzątanie |
docker ps -a | lista, także zatrzymanych | „gdzie mój kontener?” |
Uruchamiasz docker run ubuntu i kontener od razu ma status Exited (0). Dlaczego?
Odpowiedź
Domyślne polecenie obrazu ubuntu to bash. Bez -it stdin nie jest otwarty, więc bash od razu kończy pracę z kodem 0. Kontener żyje tyle, co jego główny proces.
- Kontener żyje tyle, co jego PID 1. Koniec procesu =
exited. - Zatrzymany ≠ usunięty: warstwa zapisu i logi zostają do
docker rm.
Dockerfile od podszewki
Serce kompendium: jak powstaje obraz, jak działa cache, multi-stage, BuildKit, sekrety i wiele platform.
Anatomia Dockerfile
przepis, z którego każda linia coś zostawiaDockerfile to plik tekstowy z instrukcjami wykonywanymi po kolei. docker build wysyła go razem z kontekstem (zwykle bieżący katalog) do buildera BuildKit. BuildKit jest domyślnym builderem w Docker Desktop i Docker Engine. Instrukcje dzielą się na dwie grupy: takie, które zmieniają system plików (RUN, COPY, ADD) i tworzą warstwę z zawartością, oraz takie, które zmieniają tylko konfigurację obrazu (ENV, CMD, LABEL, EXPOSE…). Te drugie w docker image history mają rozmiar 0 B.
# syntax nie jest instrukcją.| Instrukcja | Co robi | Kiedy / pułapka |
|---|---|---|
FROM img [AS name] | obraz bazowy, start etapu; FROM scratch = pusty | pinuj tag lub digest; AS w multi-stage |
RUN | wykonuje polecenie w nowej warstwie | łącz powiązane polecenia przez && |
COPY src dst | kopiuje z kontekstu (lub --from etapu/obrazu) | domyślny wybór do plików |
ADD src dst | jak COPY + URL, repo Git, automatyczne rozpakowanie lokalnego tar | tylko gdy potrzebujesz tych funkcji; URL z --checksum |
WORKDIR /app | katalog roboczy dla kolejnych instrukcji; tworzy go, jeśli brak | zamiast RUN cd … |
ENV K=v | zmienna w buildzie i w działającym kontenerze | trwała — nie dla sekretów |
ARG K[=v] | zmienna tylko w czasie budowania | nadpisywana --build-arg; nie dla sekretów |
USER uid[:gid] | użytkownik dla kolejnych RUN, CMD, ENTRYPOINT | uruchamiaj aplikację jako nie-root |
EXPOSE 8080 | dokumentacja portu; niczego nie publikuje | publikujesz przez -p |
CMD | domyślne polecenie / argumenty | nadpisywane argumentami docker run |
ENTRYPOINT | stały program kontenera | exec form; zmiana przez --entrypoint |
HEALTHCHECK | polecenie sprawdzające zdrowie | domyślnie co 30 s, timeout 30 s, 3 próby |
LABEL k=v | metadane obrazu | np. org.opencontainers.image.source |
VOLUME /data | punkt montowania anonimowego wolumenu | zmiany w tej ścieżce po VOLUME w buildzie mogą przepaść |
SHELL ["bash","-c"] | zmienia powłokę dla shell form | np. -o pipefail |
STOPSIGNAL | sygnał wysyłany przez docker stop | aplikacja kończy się np. na SIGQUIT |
ONBUILD | instrukcja odpalana w obrazie potomnym | rzadko; ukryta magia |
Pierwsza linia # syntax=docker/dockerfile:1 każe BuildKitowi pobrać najnowszą stabilną wersję frontendu Dockerfile w gałęzi 1.x. Dzięki temu nowe funkcje (heredoki, COPY --link, opcje RUN --mount) działają niezależnie od wersji demona. Druga dyrektywa, # check=, steruje sprawdzeniami buildu; same sprawdzenia uruchomisz przez docker build --check .. Dyrektywy muszą stać na samej górze, przed jakąkolwiek instrukcją.
ADD archiwum.tar.gz /opt/ rozpakuje lokalne archiwum — czasem to pożądane, czasem niespodzianka. Używaj COPY, chyba że świadomie chcesz rozpakowania, pobrania z URL lub repozytorium Git.
RUN,COPY,ADDzmieniają pliki; reszta zmienia metadane obrazu.EXPOSEto dokumentacja, nie publikacja portu.# syntax=docker/dockerfile:1na górze = aktualny frontend.
Kontekst budowania i .dockerignore
kropka na końcu komendy ma znaczenieW docker build -t app . kropka to kontekst — zbiór plików, do których builder ma dostęp. COPY widzi wyłącznie pliki z kontekstu; COPY ../secret . nie zadziała. Kontekst może być lokalnym katalogiem, adresem repozytorium Git, archiwum tar albo pustym kontekstem (Dockerfile ze stdin: docker build -).
Plik .dockerignore w katalogu głównym kontekstu wyklucza ścieżki z kontekstu. Składnia przypomina .gitignore: wzorce, ** dla dowolnej głębokości, ! dla wyjątków. Dla konkretnego Dockerfile możesz mieć osobny plik, np. build.Dockerfile.dockerignore obok build.Dockerfile.
COPY . . używa wszystkiego.# .dockerignore — przykład dla aplikacji Node.js
.git
node_modules
dist
**/*.log
.env
.env.*
!.env.example
Dockerfile*
.dockerignore
Buildx pozwala dodać kolejne konteksty: docker buildx build --build-context docs=../docs ., a w Dockerfile COPY --from=docs . /docs. Kontekstem może być też obraz (docker-image://alpine:3.20) — w ten sposób podmieniasz obraz bazowy bez edycji Dockerfile.
- Builder widzi tylko kontekst. Ścieżki poza nim są niedostępne.
.dockerignoreprzyspiesza build, stabilizuje cache i chroni przed wyciekiem.envczy.git.
Cache warstw: kolejność to wszystko
jedna zmiana unieważnia wszystko pod niąBuilder przy każdej instrukcji sprawdza, czy ma w cache wynik dla identycznego wejścia. Jeśli tak — używa go (CACHED). Jeśli nie — wykonuje krok i wszystkie kolejne kroki też, bo ich wejściem jest zmieniona warstwa. Reguły:
- RUN — klucz to tekst polecenia (i warstwa pod nim). Builder nie wie, że
apt-get updatedałoby dziś inny wynik. - COPY / ADD — klucz to suma kontrolna zawartości i metadanych kopiowanych plików. Sam czas modyfikacji (
mtime) nie jest brany pod uwagę. - ARG — zmiana wartości unieważnia cache przy pierwszym użyciu zmiennej, nie przy jej deklaracji. Zmiana wartości
ENVw Dockerfile to zmiana samej instrukcji, więc miss zaczyna się od tej linii.
--pull, żeby builder go zauważył.# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./ # zmienia się rzadko
RUN npm ci # drogi krok — chroniony przez cache
COPY . . # zmienia się często
CMD ["node", "src/app.js"]
RUN apt-get update w osobnej linii trafi do cache i zostanie tam na miesiące. Gdy potem dopiszesz pakiet do RUN apt-get install …, instalacja użyje starych list pakietów. Zawsze łącz: RUN apt-get update && apt-get install -y --no-install-recommends pkg && rm -rf /var/lib/apt/lists/*.
docker build --no-cache ignoruje cache całkowicie. --no-cache-filter stage robi to tylko dla wybranych etapów. --pull wymusza sprawdzenie nowszej wersji obrazu bazowego. Podgląd zużycia: docker buildx du, sprzątanie: docker builder prune.
- Od rzadko zmienianego do często zmienianego: zależności przed kodem.
- Miss na kroku N = przebudowa N, N+1, … do końca etapu.
- RUN cache’uje się po tekście polecenia, COPY po zawartości plików.
RUN, łączenie poleceń i rozmiar warstw
rm w następnej linii niczego nie usuwaKażdy RUN to osobna warstwa. Warstwa przechowuje różnicę względem poprzedniej. Jeśli w jednym RUN pobierzesz 300 MB, a w kolejnym je usuniesz, obraz dalej zawiera 300 MB — usunięcie tylko zasłania pliki (whiteout). Sprzątaj w tym samym RUN, w którym śmiecisz.
Shell form, exec form i heredoki
Shell form (RUN npm ci) uruchamia polecenie przez /bin/sh -c, więc działają zmienne, potoki i &&. Exec form (RUN ["npm","ci"]) to tablica JSON uruchamiana bez powłoki — bez podstawiania zmiennych typu $HOME. Tablica musi używać cudzysłowów podwójnych; z pojedynczymi Docker potraktuje ją jak shell form.
# heredoc (Dockerfile 1.4+): wiele poleceń, jedna warstwa, czytelnie
RUN <<EOF
set -e
apt-get update
apt-get install -y --no-install-recommends curl ca-certificates
rm -rf /var/lib/apt/lists/*
EOF
RUN curl -fsSL url | tar xz zakończy się sukcesem, nawet gdy curl zawiedzie — liczy się kod ostatniego polecenia w potoku. Dodaj SHELL ["/bin/bash", "-o", "pipefail", "-c"] (gdy obraz ma bash) albo set -o pipefail na początku polecenia.
- Usuwanie w kolejnej warstwie nie zmniejsza obrazu. Sprzątaj w tym samym RUN.
- Shell form =
/bin/sh -c; exec form = bez powłoki, cudzysłowy podwójne.
CMD kontra ENTRYPOINT
program i jego domyślne argumentyENTRYPOINT określa program, który kontener zawsze uruchamia. CMD dostarcza domyślne argumenty — albo całe domyślne polecenie, gdy ENTRYPOINT nie istnieje. Argumenty podane w docker run obraz arg1 arg2 zastępują CMD, ale nie ENTRYPOINT. Najczęstszy wzorzec: ENTRYPOINT ["app"] + CMD ["--help"].
- ENTRYPOINT = program, CMD = domyślne argumenty.
docker run … argszastępuje CMD. - Używaj exec form dla obu. Shell-form ENTRYPOINT ignoruje CMD i argumenty z
docker run.
PID 1, sygnały i docker stop
dlaczego stop trwa dokładnie 10 sekunddocker stop wysyła do głównego procesu sygnał stopu (domyślnie SIGTERM, zmienisz go przez STOPSIGNAL) i czeka. Dla kontenerów Linux domyślny limit to 10 sekund; po nim leci SIGKILL. Jeśli stop zawsze trwa równo 10 sekund, twoja aplikacja nie dostaje SIGTERM albo go ignoruje.
Dwie typowe przyczyny. Pierwsza: shell form (CMD node app.js). PID 1 to wtedy /bin/sh -c, który nie przekazuje sygnałów dziecku. Druga: proces z PID 1 jest w Linuksie traktowany specjalnie — jądro nie stosuje domyślnej akcji „zakończ” dla sygnałów, na które PID 1 nie ma handlera. Aplikacja bez handlera SIGTERM po prostu go nie zauważy.
exec i wtedy shell form działa przypadkiem — nie polegaj na tym. Czasy zamknięcia aplikacji ilustracyjne.docker run --init wstawia jako PID 1 mały proces init (domyślnie oparty na tini), który przekazuje sygnały i sprząta procesy-zombie. Jeśli masz skrypt entrypoint.sh, kończ go linią exec "$@" — wtedy aplikacja zastępuje powłokę i sama staje się PID 1.
- stop = sygnał stopu → limit czasu (10 s dla Linux) → SIGKILL (kod 137).
- Exec form + obsługa SIGTERM w aplikacji, albo
--init, alboexec "$@"w skrypcie.
ARG, ENV i zakresy zmiennych
kto widzi zmienną i jak długoARG istnieje tylko w czasie budowania, ENV — w czasie budowania i w każdym kontenerze z obrazu. Zakresy mają reguły, które łatwo przeoczyć:
ARGprzed pierwszymFROMjest w zakresie globalnym — działa tylko w liniachFROM. Żeby użyć go w etapie, napisz w nim ponownieARG NAZWA(bez wartości).ARGw etapie żyje do końca tego etapu.ENVo tej samej nazwie coARGwygrywa z nim.- BuildKit udostępnia automatyczne ARG platformy:
TARGETPLATFORM,TARGETOS,TARGETARCH,BUILDPLATFORM… W etapie trzeba je zadeklarować (ARG TARGETARCH).
build i runtime.ARG NODE_VER=22
FROM node:${NODE_VER}-slim AS build # globalny ARG działa w FROM
ARG NODE_VER # ...a w etapie trzeba go powtórzyć
RUN echo "building on node $NODE_VER"
FROM node:${NODE_VER}-slim AS runtime
ENV PORT=8080 # trafi do kontenera
Wartości build-argów i zmiennych środowiskowych mogą trwale zostać w metadanych obrazu, atestacjach provenance i historii obrazu. Token przekazany przez --build-arg to token opublikowany razem z obrazem. Do sekretów służą secret mounty — rozdział o sekretach.
- ARG = build, ENV = build + runtime. Globalny ARG trzeba powtórzyć w etapie.
- Ani ARG, ani ENV nie nadają się na sekrety.
Multi-stage builds
buduj w kuchni, podawaj na talerzuKompilator, nagłówki, cache menedżera pakietów i testy są potrzebne do zbudowania aplikacji, ale nie do jej uruchomienia. Multi-stage build dzieli Dockerfile na etapy: każdy FROM zaczyna nowy etap, a COPY --from=etap przenosi tylko wybrane artefakty. Obraz wynikowy to ostatni etap (albo ten wskazany przez --target) — reszta nie trafia do obrazu.
# syntax=docker/dockerfile:1
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
FROM build AS test
RUN go test ./...
FROM gcr.io/distroless/static-debian12 AS runtime
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
COPY --from działa tylko z etapami?COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf. Możesz też wskazać etap numerem (--from=0), ale nazwy są odporne na zmianę kolejności.Etap deps wspólny dla dev i prod. Etap test uruchamiany w CI przez --target test. Etap export z FROM scratch + docker build --output type=local,dest=out . — wtedy wynikiem buildu są pliki na dysku, a nie obraz.
- Każdy FROM = nowy etap. Do obrazu trafia tylko ostatni (lub
--target). COPY --fromprzenosi artefakty z etapu albo z dowolnego obrazu.
BuildKit: graf, równoległość i RUN --mount
Dockerfile to nie skrypt, tylko grafKlasyczny builder wykonywał Dockerfile linia po linii. BuildKit zamienia go w graf zależności: wykrywa i pomija nieużywane etapy, buduje niezależne etapy równolegle i przesyła z kontekstu tylko pliki, których build używa. Do tego dochodzą RUN --mount — tymczasowe montowania widoczne tylko podczas jednego kroku i nie zapisywane w warstwie.
npm ci przepada.| Typ mountu | Co daje | Kiedy |
|---|---|---|
type=cache,target=… | trwały katalog cache między buildami; opcja sharing: shared (domyślnie), private, locked | npm, pip, go build, apt, Maven |
type=secret,id=… | plik z sekretem, domyślnie w /run/secrets/<id> | tokeny, poświadczenia |
type=ssh | dostęp do agenta SSH hosta | git clone prywatnych repo |
type=bind | montuje pliki z kontekstu lub etapu bez kopiowania do warstwy | kod źródłowy potrzebny tylko do kompilacji |
type=tmpfs | katalog w pamięci na czas kroku | pliki tymczasowe |
RUN --mount=type=cache,target=/root/.npm \
npm ci
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends gcc
RUN --mount=type=bind,target=. \
go build -o /out/app ./cmd/app
Cache warstw decyduje, czy krok się wykona. Cache mount przyspiesza krok, który już się wykonuje. Gdy krok trafia w cache warstw, mount nie jest w ogóle potrzebny.
🔬 Głębiej: LLB, frontend i COPY --link
BuildKit nie rozumie Dockerfile bezpośrednio. Frontend (obraz docker/dockerfile, wskazany dyrektywą # syntax) tłumaczy Dockerfile na LLB — niskopoziomowy graf operacji, który solver BuildKit wykonuje z cache adresowanym zawartością. Dlatego nowe funkcje Dockerfile nie wymagają aktualizacji demona.
COPY --link (Dockerfile 1.4+) tworzy warstwę niezależną od warstw pod nią. Po zmianie obrazu bazowego taka warstwa nie musi być przebudowana, a przy eksporcie z --cache-from BuildKit może przenieść ją na nową bazę bez przebudowy kolejnych kroków.
- BuildKit: graf etapów, równoległość, pomijanie nieużywanych etapów.
RUN --mount: cache, secret, ssh, bind, tmpfs — nic z mountu nie trafia do warstwy.
Sekrety w buildzie
token musi zniknąć, zanim powstanie warstwaBuild często potrzebuje sekretu: tokenu do prywatnego rejestru npm, klucza SSH do prywatnego repo, poświadczeń chmurowych. Każdy sposób, który zapisuje sekret w warstwie lub metadanych, publikuje go razem z obrazem. Dobre sposoby są dwa: secret mount i SSH mount.
docker image history i konfiguracja obrazu; „provenance” = atestacja buildu (w trybie max zawiera build-argi).# Dockerfile
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
RUN --mount=type=secret,id=API_TOKEN,env=API_TOKEN \
./fetch-assets.sh
RUN --mount=type=ssh \
git clone git@github.com:org/private.git
# CLI
docker build --secret id=npmrc,src=$HOME/.npmrc \
--secret id=API_TOKEN,env=API_TOKEN \
--ssh default .
Bez target sekret jest montowany jako plik /run/secrets/<id>. Opcja env= zamiast pliku wystawia go jako zmienną środowiskową na czas jednego RUN. Gdy w --secret podasz tylko id, Buildx szuka najpierw zmiennej środowiskowej o tej nazwie, potem pliku. Zmiana wartości sekretu nie unieważnia cache — jeśli krok ma się wykonać z nowym sekretem, wymuś to inaczej (np. build-argiem z niesekretną wersją).
- ARG, ENV, COPY + rm — sekret zostaje w obrazie lub metadanych.
--mount=type=secret/type=ssh— sekret istnieje tylko podczas jednego kroku.
Mały i bezpieczny obraz
czego nie ma, tego nie da się zhakowaćKażdy pakiet w obrazie to megabajty do pobrania i potencjalne podatności do łatania. Wybór obrazu bazowego to największa pojedyncza decyzja o rozmiarze i powierzchni ataku.
| Praktyka | Jak | Dlaczego |
|---|---|---|
| Nie-root | RUN useradd -r app + USER app | ucieczka z procesu ≠ root w kontenerze |
| Pinowanie | FROM node:22.11-slim@sha256:… | powtarzalność; tag może się przesunąć |
| Bez rekomendowanych | apt-get install --no-install-recommends | mniej pakietów |
| Odświeżanie bazy | docker build --pull, rebuild cykliczny | poprawki bezpieczeństwa w bazie |
| Skan | docker scout cves obraz | widoczność znanych CVE |
| Bez sekretów i śmieci | .dockerignore, secret mounty, multi-stage | nic, czego nie potrzebuje runtime |
Alpine używa biblioteki musl zamiast glibc. Większość rzeczy działa, ale natywne rozszerzenia (np. kompilowane moduły Pythona czy Node.js) mogą wymagać kompilacji ze źródeł albo zachowywać się inaczej. Mniejszy obraz nie zawsze oznacza szybszy build.
Distroless i scratch nie mają shella ani menedżera pakietów. Debugujesz je inaczej: etapem debug w multi-stage, obrazem z wariantem :debug albo poleceniem docker debug, które daje shell z narzędziami w dowolnym kontenerze lub obrazie. Docker oferuje też Docker Hardened Images — minimalne, utrzymywane obrazy bazowe z SBOM i provenance.
- Mniejsza baza = mniej MB i mniej CVE. Multi-stage pozwala użyć najmniejszej bazy w runtime.
- USER nie-root, pinowany digest, cykliczny rebuild z
--pull.
Buildx: wiele platform i eksport cache
jeden tag, wiele architekturLaptop z Apple Silicon to linux/arm64, typowy serwer w chmurze — linux/amd64. Obraz wieloplatformowy to indeks obrazów (manifest list): jeden tag wskazuje kilka manifestów, po jednym na platformę. docker pull sam wybiera manifest pasujący do hosta.
Klasyczny storage obrazów (domyślny w starszych wersjach Docker Engine; nowsze wersje domyślnie używają containerd image store) nie przechowuje obrazów wieloplatformowych. Potrzebujesz containerd image store albo buildera z driverem docker-container (wtedy wynik trzeba jawnie wypchnąć lub załadować).
# cross-kompilacja: etap build działa natywnie na platformie buildera
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app .
FROM alpine:3.20
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
# docker buildx build --platform linux/amd64,linux/arm64 -t reg/app:1.0 --push .
Eksport cache — szybki build w CI
Runner CI startuje zwykle z pustym builderem. Cache można wyeksportować i zaimportować przez --cache-to / --cache-from.
| Backend | Gdzie leży cache | Kiedy |
|---|---|---|
inline | w samym obrazie; tylko tryb min | najprostszy start |
registry | osobny obraz cache w rejestrze; obsługuje mode=max | uniwersalny wybór |
local | katalog na dysku | runner z trwałym dyskiem |
gha | cache GitHub Actions | GitHub Actions |
s3, azblob | bucket / blob storage | własna infrastruktura |
docker buildx build \
--cache-from type=registry,ref=reg/app:buildcache \
--cache-to type=registry,ref=reg/app:buildcache,mode=max \
-t reg/app:1.0 --push .
min (domyślny) eksportuje tylko warstwy, które trafiły do obrazu wynikowego. max eksportuje też warstwy etapów pośrednich, np. etapu build w multi-stage — dzięki temu CI trafia w cache nawet w krokach, których nie ma w obrazie. Domyślny driver docker obsługuje backendy inline, local, registry i gha tylko z włączonym containerd image store.
🔬 Głębiej: Bake
docker buildx bake czyta konfigurację buildów z pliku (HCL, JSON lub Compose — domyślnie m.in. docker-bake.hcl) i uruchamia wiele targetów równolegle. Zamiast długich skryptów z flagami opisujesz targety, platformy, tagi i cache deklaratywnie, z dziedziczeniem i zmiennymi.
group "default" { targets = ["api", "worker"] }
target "api" {
context = "."
target = "runtime"
platforms = ["linux/amd64", "linux/arm64"]
tags = ["reg/api:1.0"]
}
- Obraz wieloplatformowy = indeks obrazów; pull wybiera manifest hosta.
- Cross-kompilacja z
--platform=$BUILDPLATFORMjest zwykle szybsza niż emulacja. - W CI eksportuj cache (
registrylubgha,mode=max).
Uruchamianie: dane, sieć, zasoby, Compose
Obraz jest gotowy. Teraz: gdzie trzymać dane, jak kontenery się widzą, jak je ograniczać i jak zgrać kilka serwisów.
Dane: volumes, bind mounts, tmpfs
warstwa zapisu to nie miejsce na bazę danychWarstwa zapisu kontenera znika razem z nim. Dane, które mają przeżyć kontener, trzymasz poza nią. Docker ma trzy rodzaje montowań:
| Typ | Gdzie leżą dane | Kiedy |
|---|---|---|
-v dane:/var/lib/postgresql/data(volume) | obszar zarządzany przez Dockera | dane aplikacji, bazy — domyślny wybór |
-v "$PWD":/app(bind mount) | dowolna ścieżka hosta | kod źródłowy w dev, pliki konfiguracyjne |
--tmpfs /tmp | pamięć RAM hosta | dane tymczasowe, które nie mają trafić na dysk |
Montowanie na niepustą ścieżkę zasłania pliki, które obraz miał w tej ścieżce. Wyjątek: pusty wolumen montowany w ścieżce z plikami — wtedy Docker kopiuje zawartość obrazu do wolumenu. Bind mount niczego nie kopiuje — pokazuje katalog hosta. Stąd klasyka: -v "$PWD":/app zasłania /app/node_modules zbudowane w obrazie.
--mount type=bind,src=…,dst=…,readonly jest bardziej jawne. Różnica w zachowaniu: -v z nieistniejącą ścieżką hosta tworzy katalog, a --mount type=bind zwraca błąd bind source path does not exist. Montowania tylko do odczytu oznaczasz :ro lub readonly.
- Volume = trwałe dane zarządzane przez Dockera. Bind = katalog hosta. tmpfs = RAM.
- Pusty volume dostaje kopię plików z obrazu; bind mount nie.
Sieci i publikowanie portów
localhost w kontenerze to kontenerKażdy kontener ma własny namespace sieciowy, więc localhost w kontenerze to sam kontener, nie host. Domyślnie kontener trafia do sieci bridge. Kluczowa różnica: sieć zdefiniowana przez użytkownika (docker network create app) daje automatyczny DNS — kontenery widzą się po nazwie. Domyślny bridge tego nie robi.
| Driver / opcja | Co robi | Kiedy |
|---|---|---|
bridge (użytkownika) | prywatna sieć na hoście + DNS po nazwach | domyślny wybór dla kilku kontenerów |
host | kontener używa stosu sieciowego hosta, bez izolacji | wydajność, specjalne przypadki |
none | brak sieci | zadania offline |
overlay | sieć między wieloma hostami (Swarm) | klastry Swarm |
-p 8080:80 | port 8080 hosta → 80 kontenera, na wszystkich interfejsach | dostęp z zewnątrz |
-p 127.0.0.1:8080:80 | tylko z hosta | dev, panele admina |
-P | publikuje wszystkie porty z EXPOSE na losowych portach hosta | szybkie testy |
Dokumentacja ostrzega: opublikowany port jest domyślnie dostępny nie tylko z hosta, ale i ze świata. Reguły Dockera mogą też ominąć prosty firewall hosta (np. ufw). Bazy w dev publikuj z 127.0.0.1: — albo wcale, jeśli łączy się z nimi tylko inny kontener.
host.docker.internal. Na Linuksie dodaj --add-host=host.docker.internal:host-gateway. Kontenery między sobą łącz przez wspólną sieć i nazwy, nie przez porty hosta.- Sieć użytkownika = DNS po nazwach kontenerów. Domyślny bridge — bez DNS.
EXPOSEdokumentuje,-ppublikuje. Kontener↔kontener nie potrzebuje-p.
Limity zasobów, restart i healthcheck
kontener bez limitu może zjeść cały hostBez limitów kontener może użyć tyle pamięci i CPU, ile da jądro hosta. Limity ustawiasz flagami docker run, które tłumaczą się na cgroups. Gdy proces przekroczy twardy limit pamięci, jądro zabija go (OOM kill) — zwykle zobaczysz kod 137 i OOMKilled: true w docker inspect. Polityka restartu decyduje, co dalej.
| Flaga | Co robi | Kiedy |
|---|---|---|
-m 512m / --memory | twardy limit pamięci (minimum 6m) | zawsze na produkcji |
--memory-swap | pamięć + swap łącznie | kontrola swapu |
--cpus=1.5 | ile CPU może użyć kontener | sąsiedzi na hoście |
--pids-limit | maksymalna liczba procesów | ochrona przed fork bombą |
--restart on-failure[:N] | restart przy kodzie ≠ 0, opcjonalnie max N razy | zadania, które mogą chwilowo zawieść |
--restart always | restart zawsze; ręcznie zatrzymany wraca po restarcie demona | serwisy |
--restart unless-stopped | jak always, ale zatrzymany nie wraca po restarcie demona | serwisy, które czasem wyłączasz |
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD curl -fsS http://localhost:8080/health || exit 1
Stan zdrowia ma trzy wartości: starting, healthy, unhealthy. Kontener staje się unhealthy po retries kolejnych nieudanych próbach. Błędy w czasie start-period nie wliczają się do limitu. Sam Docker Engine nie restartuje niezdrowego kontenera — healthcheck czytają orkiestratory i Compose (depends_on: condition: service_healthy). Obraz musi mieć narzędzie, którego używasz (curl często go nie ma w slim/distroless).
- Ustaw
-mi--cpus. OOM kill = kod 137. - Restart:
no(domyślnie),on-failure,always,unless-stopped.
Docker Compose: wiele serwisów, jeden plik
docker compose up i masz całe środowiskoCompose opisuje aplikację wielokontenerową w pliku YAML (compose.yaml). Każdy serwis to kontener (lub kilka replik) z obrazem albo sekcją build. Compose automatycznie tworzy sieć dla projektu, więc serwisy widzą się po nazwach — api łączy się z db:5432.
services:
api:
build: { context: ., target: dev }
ports: ["127.0.0.1:8080:8080"]
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
db: { condition: service_healthy }
migrate: { condition: service_completed_successfully }
develop:
watch:
- { action: sync, path: ./src, target: /app/src }
- { action: rebuild, path: package.json }
migrate:
build: .
command: ["npm", "run", "migrate"]
depends_on: { db: { condition: service_healthy } }
db:
image: postgres:17
environment: { POSTGRES_USER: app, POSTGRES_PASSWORD: app }
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 10
adminer:
image: adminer
profiles: ["tools"]
volumes:
pgdata:
pg_isready przechodzi.| Element | Co robi | Kiedy |
|---|---|---|
depends_on.condition | service_started / service_healthy / service_completed_successfully | kolejność startu i gotowość |
profiles | serwis startuje tylko po włączeniu profilu (--profile tools, COMPOSE_PROFILES) lub gdy wskażesz go z nazwy | narzędzia opcjonalne |
develop.watch | docker compose watch: akcje sync, rebuild, sync+restart | szybka pętla dev |
compose.override.yaml | scalany automatycznie z compose.yaml | lokalne różnice dev |
-f a.yaml -f b.yaml | scalanie plików; późniejszy nadpisuje | środowiska (CI, prod) |
.env | zmienne do interpolacji ${VAR} w pliku Compose | konfiguracja bez edycji YAML |
Od najwyższego: (1) docker compose run -e; (2) environment lub env_file z wartością interpolowaną z shella lub pliku .env; (3) samo environment; (4) env_file; (5) ENV w obrazie. Plik .env w katalogu projektu służy do interpolacji w compose.yaml — sam z siebie nie trafia do kontenerów.
Zmieniasz plik w ./src, a w kontenerze nic się nie dzieje. Używasz docker compose up. Co sprawdzić?
Odpowiedź
Reguły develop.watch działają tylko z docker compose watch (lub docker compose up --watch). Zwykłe up ich nie uruchamia. Dodatkowo proces w kontenerze musi sam przeładować zmienione pliki — sync tylko je kopiuje.
- Compose = serwisy + sieć projektu + wolumeny w jednym pliku.
depends_onbez warunku czeka tylko na start kontenera, nie na gotowość.
Dystrybucja i codzienna praca
Jak obraz trafia do rejestru, czym tag różni się od digestu i jak debugować kontenery.
Rejestry, tagi i digesty
tag to karteczka, digest to odcisk palcaPełna nazwa obrazu to [rejestr/][namespace/]repozytorium[:tag|@digest]. Bez rejestru Docker używa Docker Hub (docker.io), a oficjalne obrazy żyją w namespace library — dlatego debian to w rzeczywistości docker.io/library/debian:latest. Bez tagu Docker przyjmuje latest.
Tag jest zmiennym wskaźnikiem: właściciel może w każdej chwili przypiąć go do innego obrazu. Digest (sha256:…) to skrót zawartości manifestu — ta sama wartość zawsze oznacza ten sam obraz. latest to tylko domyślna nazwa tagu, nie „najnowsza wersja”.
docker login ghcr.io
docker tag app:dev ghcr.io/org/app:1.4.2
docker push ghcr.io/org/app:1.4.2
docker image inspect --format '{{index .RepoDigests 0}}' ghcr.io/org/app:1.4.2
docker pull ghcr.io/org/app@sha256:<digest>
Taguj obraz kilkoma tagami naraz: niezmiennym (1.4.2, SHA commita) i ruchomymi (1.4, 1). Wdrażaj po niezmiennym tagu albo po digeście. Standard OCI opisuje format obrazu i protokół rejestru, więc ten sam obraz działa w Docker Hub, GHCR, ECR, GCR/Artifact Registry czy Harbor.
- Brak rejestru =
docker.io; brak tagu =latest. - Tag jest zmienny, digest niezmienny. Na produkcji: wersja albo digest.
Debugowanie i codzienny workflow
objaw → komendaWiększość problemów z kontenerami diagnozujesz kilkoma komendami. Kliknij objaw, a figura podpowie, od czego zacząć.
| Komenda | Co robi | Kiedy |
|---|---|---|
docker logs -f --tail 100 c | stdout/stderr głównego procesu | pierwszy krok zawsze |
docker exec -it c sh | nowy proces w działającym kontenerze | zajrzeć do środka |
docker inspect c | pełna konfiguracja i stan (JSON); --format | env, mounty, sieci, exit code |
docker cp c:/ścieżka . | kopiowanie plików z/do kontenera | wyciągnąć log, zrzut |
docker stats / docker top c | zużycie zasobów na żywo / procesy | wolno, dużo pamięci |
docker events | strumień zdarzeń demona | „kto restartuje mój kontener?” |
docker system df | miejsce zajęte przez obrazy, kontenery, wolumeny, cache | pełny dysk |
docker system prune | usuwa zatrzymane kontenery, nieużywane sieci, wiszące obrazy, nieużywany cache buildu | sprzątanie (--volumes usuwa też anonimowe wolumeny) |
docker build --progress=plain | pełne logi kroków buildu | debug Dockerfile |
docker init | generuje Dockerfile, compose.yaml i .dockerignore dla projektu | start nowego projektu |
docker logs widzi tylko to, co główny proces pisze na stdout/stderr. Aplikacja, która loguje do pliku w kontenerze, jest „niema”. Loguj na stdout. Domyślny driver json-file nie rotuje logów — ustaw max-size i max-file, albo przejdź na driver local.
- logs → inspect → exec. W tej kolejności rozwiążesz większość zagadek.
docker system dfprzedprune; uważaj z--volumes.
Pod maską i bezpieczeństwo
Namespaces z bliska, hardening runtime’u i łańcuch dostaw obrazów.
Namespaces i cgroups z bliska
każdą ścianę można zburzyć flagąKażdy namespace izoluje jeden aspekt systemu. Docker pozwala selektywnie z nich zrezygnować, np. --network host albo --pid host. To przydatne do diagnostyki i specjalnych przypadków, ale każda taka flaga burzy jedną ścianę izolacji.
--privileged nie wyłącza namespaces, ale daje wszystkie capabilities i dostęp do urządzeń hosta — efektywnie znosi większość izolacji. Wyniki komend przykładowe.Limity z docker run trafiają do cgroup kontenera. docker stats czyta z niej zużycie. Aplikacje, które same dobierają liczbę wątków lub rozmiar sterty do zasobów maszyny, muszą czytać limity cgroup, a nie liczbę CPU i RAM hosta — inaczej przy -m 512m mogą zaplanować więcej pamięci niż limit.
- Flagi
--pid/--network/--uts hostwspółdzielą dany namespace z hostem. --privileged≈ brak izolacji. Nie na produkcji.
Hardening runtime’u
root w kontenerze to wciąż root (prawie)Domyślnie proces w kontenerze działa jako root, ale z okrojonym zestawem capabilities jądra i domyślnym profilem seccomp, który blokuje około 44 z ponad 300 wywołań systemowych. To rozsądna baza. Każda flaga poniżej przesuwa ją w stronę bezpieczniej albo groźniej.
| Mechanizm | Co robi | Kiedy |
|---|---|---|
USER / --user | proces nie jest rootem w kontenerze | zawsze, gdy aplikacja nie potrzebuje roota |
--cap-drop ALL --cap-add … | tylko potrzebne capabilities, np. NET_BIND_SERVICE | usługi produkcyjne |
--read-only + --tmpfs /tmp | system plików kontenera tylko do odczytu | bezstanowe aplikacje |
no-new-privileges | blokuje eskalację przez setuid | ogólne utwardzenie |
| Rootless mode | demon i kontenery działają jako zwykły użytkownik hosta | hosty współdzielone, CI |
| userns-remap | root w kontenerze mapowany na nieuprzywilejowany UID hosta | izolacja przy klasycznym demonie |
Dodanie użytkownika do grupy docker daje mu dostęp do API demona, a to efektywnie uprawnienia roota na hoście. Traktuj tę grupę jak sudo.
- Baza: okrojone capabilities + seccomp. Dokładaj: nie-root, cap-drop, read-only.
- Nigdy:
--privilegedi gniazdo dockera w aplikacjach produkcyjnych.
Łańcuch dostaw: SBOM, provenance, skanowanie
wiedz, co jest w środku i skąd się wzięłoBuildKit może dołączyć do obrazu atestacje: SBOM (lista pakietów w obrazie) i provenance (jak, z czego i czym obraz zbudowano). Atestacja provenance na poziomie mode=min jest dodawana do obrazów domyślnie; SBOM włączasz jawnie (--sbom=true).
docker buildx build --sbom=true --provenance=mode=max \
-t reg/app:1.4.2 --push .
docker buildx imagetools inspect reg/app:1.4.2 --format '{{ json .SBOM }}'
docker scout cves reg/app:1.4.2
Provenance w trybie max zapisuje m.in. wartości build-argów. To kolejny powód, by sekrety nigdy nie szły przez --build-arg.
- SBOM = co jest w środku. Provenance = jak powstało. Skan = znane podatności.
- Nowe CVE w bazie naprawiasz rebuildem z
--pull, nie zmianą kodu.
Egzamin + ściąga
sprawdź, co zostało w głowieŚciąga
| Komenda / instrukcja | Co robi / domyślnie | Kiedy |
|---|---|---|
| # syntax=docker/dockerfile:1 | najnowszy stabilny frontend 1.x | pierwsza linia każdego Dockerfile |
| COPY package*.json ./ → RUN npm ci → COPY . . | zależności przed kodem | stabilny cache |
| RUN a && b && rm -rf cache | sprzątanie w tej samej warstwie | mniejszy obraz |
| FROM … AS build / COPY --from=build | multi-stage | obraz runtime bez narzędzi buildu |
| docker build --target test | buduje do wskazanego etapu | testy w CI |
| RUN --mount=type=cache,target=… | trwały cache menedżera pakietów | npm, pip, go, apt |
| RUN --mount=type=secret,id=x | sekret w /run/secrets/x tylko na czas kroku | tokeny w buildzie |
| ENTRYPOINT ["app"] + CMD ["--flag"] | program + domyślne argumenty | obraz jako narzędzie |
| docker stop -t 30 | sygnał stopu, potem SIGKILL; domyślnie 10 s (Linux) | dłuższe zamykanie |
| docker build --no-cache / --pull | bez cache / świeży obraz bazowy | podejrzany cache, łatki bazy |
| docker buildx build --platform linux/amd64,linux/arm64 | obraz wieloplatformowy | Apple Silicon + serwery x86 |
| --cache-to type=registry,ref=…,mode=max | eksport cache z etapami pośrednimi | CI |
| docker network create app | sieć z DNS po nazwach | kontenery się widzą |
| -p 127.0.0.1:8080:80 | port tylko z hosta | dev, bazy |
| -v dane:/ścieżka | nazwany wolumen | trwałe dane |
| -m 512m --cpus 1 | limity cgroups | produkcja |
| --restart unless-stopped | restart poza ręcznym zatrzymaniem | serwisy |
| depends_on: condition: service_healthy | czeka na healthcheck zależności | Compose: api + db |
| docker compose watch | sync / rebuild / sync+restart | pętla dev |
| image@sha256:… | niezmienna referencja | wdrożenia |
| docker logs / inspect / exec | diagnostyka | zwykle w tej kolejności |
| docker system df / prune | zajętość / sprzątanie | pełny dysk |
| --cap-drop ALL --read-only --user | hardening | produkcja |
| --sbom=true --provenance=mode=max | atestacje obrazu | łańcuch dostaw |
Obraz 1,2 GB z 4-minutowym buildem miał zwykle trzy błędy. (1) COPY . . stało przed npm install, więc każda zmiana w src/ unieważniała cache instalacji. (2) Brak .dockerignore — node_modules i .git lądowały w kontekście i w obrazie. (3) Pełny obraz bazowy z narzędziami buildu w obrazie końcowym. Poprawka to kilka linijek: kopiowanie package*.json przed instalacją, .dockerignore, multi-stage ze slim lub distroless w runtime i — w bonusie — RUN --mount=type=cache,target=/root/.npm. Zmiana w kodzie przebudowuje wtedy tylko kroki od COPY . . w dół (kopiowanie i ewentualny build).
Następny krok: weź swój najstarszy Dockerfile, uruchom docker build --check . i docker image history — i przejdź go z tą ściągą.