Animowane kompendium · Docker

Docker od środka

Wszystko, co developer powinien wiedzieć o Dockerze — z naciskiem na to, jak naprawdę powstaje obraz: warstwy, cache, multi-stage, BuildKit, sekrety i wiele platform. Przewijaj — diagramy ruszają same.

25 rozdziałów · 25 symulacji 🧠
przewiń ↓

Jak czytać to kompendium

mechanizmy, nie listy flag

Wię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.

🧠 Zagadka na start

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.

Część I

Fundamenty

Czym naprawdę jest kontener, kto go uruchamia i z czego zbudowany jest obraz.

Kontener to nie maszyna wirtualna

to zwykły proces w przebraniu

Maszyna 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.

▶ Stos VM kontra stos kontenera uruchom 3 instancje i porównaj
Maszyny wirtualne
Kontenery
Kliknij ➕.
Uproszczenie: pomijamy hypervisor typu 1 vs 2 i Docker Desktop. Czasy startu i rozmiary są ilustracyjne, rząd wielkości — nie pomiar.
❓ Nie ma głupich pytań
P:
To jak działa Docker na Macu albo Windowsie, skoro nie ma tam jądra Linux?
O:
Docker Desktop uruchamia lekką maszynę wirtualną z jądrem Linux, a kontenery żyją w niej. Dlatego bind mount z macOS bywa wolniejszy niż na natywnym Linuksie — pliki przechodzą przez warstwę współdzielenia plików z VM (nowsze mechanizmy, jak virtiofs, mocno to ograniczają).
⚠️ Uwaga

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.

📌 W punktach
  • 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.

▶ Droga polecenia docker run nginx od klawiatury do procesu
Kliknij ▶.
Uproszczenie: pomijamy containerd-shim, sieć (CNI/libnetwork) i snapshotter. Kolejność kroków zgodna z dokumentacją, czasy animacji umowne.
🤓 Dla dociekliwych

Mię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.

⚠️ Uwaga

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”.

📌 W punktach
  • docker (CLI) → REST API → dockerd → containerd → runc → proces.
  • Brak obrazu lokalnie = najpierw pull z rejestru (domyślnie Docker Hub).

Obraz, warstwy i warstwa zapisu

kanapka tylko do odczytu + jedna kartka na wierzchu

Obraz 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.

▶ Copy-on-write w akcji czytaj, zmieniaj, usuwaj plik
Wybierz akcję.
Uproszczenie: warstwy pokazujemy jako listy plików; overlay2 kopiuje cały plik przy pierwszym zapisie (copy-up), a usunięcie oznacza plikiem „whiteout”. Nazwy plików przykładowe.
❓ Nie ma głupich pytań
P:
Skoro kontener może usunąć plik, to czy zmniejsza to obraz?
O:
Nie. Plik dalej leży w niższej warstwie. Warstwa wyżej tylko go „przykrywa” znacznikiem usunięcia. Ta sama zasada dotyczy RUN rm w Dockerfile — zobaczysz to w rozdziale o RUN.
🤓 Dla dociekliwych

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).

📌 W punktach
  • 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 → removed

Kontener ż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.

▶ Maszyna stanów kontenera klikaj komendy, obserwuj stan
$
Zacznij od create albo od razu start (to wtedy jak docker run).
Uproszczenie: pomijamy stany restarting i dead; zakładamy, że aplikacja obsługuje SIGTERM. Kod wyjścia 137 = 128+9 (SIGKILL).
KomendaCo robiKiedy
docker runcreate + start (+ pull, jeśli brak obrazu)codziennie
docker run --rmusuwa kontener po zakończeniu procesujednorazowe zadania, testy
docker run -dtryb odłączony, zwraca IDserwisy w tle
docker run -itinteraktywny TTY ze stdinshell, REPL
docker stopsygnał stopu (domyślnie SIGTERM), po limicie czasu SIGKILLłagodne zamknięcie
docker killod razu SIGKILL (lub sygnał z -s)proces nie reaguje
docker rm -fusuwa, także działający (najpierw kill)sprzątanie
docker ps -alista, także zatrzymanych„gdzie mój kontener?”
✏️ Zaostrz ołówek

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.

📌 W punktach
  • Kontener żyje tyle, co jego PID 1. Koniec procesu = exited.
  • Zatrzymany ≠ usunięty: warstwa zapisu i logi zostają do docker rm.
Część II

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ś zostawia

Dockerfile 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.

▶ Co zostawia każda linia kliknij linię albo zbuduj całość
$
Kliknij dowolną linię.
Rozmiary warstw ilustracyjne. Uproszczenie: pomijamy, że BuildKit może scalać i pomijać kroki oraz że parser directive # syntax nie jest instrukcją.
InstrukcjaCo robiKiedy / pułapka
FROM img [AS name]obraz bazowy, start etapu; FROM scratch = pustypinuj tag lub digest; AS w multi-stage
RUNwykonuje polecenie w nowej warstwiełącz powiązane polecenia przez &&
COPY src dstkopiuje z kontekstu (lub --from etapu/obrazu)domyślny wybór do plików
ADD src dstjak COPY + URL, repo Git, automatyczne rozpakowanie lokalnego tartylko gdy potrzebujesz tych funkcji; URL z --checksum
WORKDIR /appkatalog roboczy dla kolejnych instrukcji; tworzy go, jeśli brakzamiast RUN cd …
ENV K=vzmienna w buildzie i w działającym kontenerzetrwała — nie dla sekretów
ARG K[=v]zmienna tylko w czasie budowanianadpisywana --build-arg; nie dla sekretów
USER uid[:gid]użytkownik dla kolejnych RUN, CMD, ENTRYPOINTuruchamiaj aplikację jako nie-root
EXPOSE 8080dokumentacja portu; niczego nie publikujepublikujesz przez -p
CMDdomyślne polecenie / argumentynadpisywane argumentami docker run
ENTRYPOINTstały program konteneraexec form; zmiana przez --entrypoint
HEALTHCHECKpolecenie sprawdzające zdrowiedomyślnie co 30 s, timeout 30 s, 3 próby
LABEL k=vmetadane obrazunp. org.opencontainers.image.source
VOLUME /datapunkt montowania anonimowego wolumenuzmiany w tej ścieżce po VOLUME w buildzie mogą przepaść
SHELL ["bash","-c"]zmienia powłokę dla shell formnp. -o pipefail
STOPSIGNALsygnał wysyłany przez docker stopaplikacja kończy się np. na SIGQUIT
ONBUILDinstrukcja odpalana w obrazie potomnymrzadko; ukryta magia
🤓 Parser directives

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ą.

⚠️ COPY czy ADD?

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.

📌 W punktach
  • RUN, COPY, ADD zmieniają pliki; reszta zmienia metadane obrazu.
  • EXPOSE to dokumentacja, nie publikacja portu.
  • # syntax=docker/dockerfile:1 na górze = aktualny frontend.

Kontekst budowania i .dockerignore

kropka na końcu komendy ma znaczenie

W 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.

▶ Co trafia do buildera przełącz .dockerignore
Przesłany kontekst
0 MB
.dockerignore: wyłączony.
Rozmiary ilustracyjne. Uproszczenie: BuildKit przesyła kontekst przyrostowo i tylko pliki, których build faktycznie używa, ale COPY . . używa wszystkiego.
# .dockerignore — przykład dla aplikacji Node.js
.git
node_modules
dist
**/*.log
.env
.env.*
!.env.example
Dockerfile*
.dockerignore
🤓 Nazwane konteksty

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.

📌 W punktach
  • Builder widzi tylko kontekst. Ścieżki poza nim są niedostępne.
  • .dockerignore przyspiesza build, stabilizuje cache i chroni przed wyciekiem .env czy .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:

▶ Symulator cache zmień plik i zobacz, co się przebuduje
Dockerfile
Czas buildu
—
Wybierz zmianę.
Czasy kroków ilustracyjne. Uproszczenie: pomijamy cache mounty (rozdział o BuildKit) i to, że nowy digest obrazu bazowego wymaga --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"]
⚠️ Pułapka apt-get update

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/*.

🤓 Sterowanie cache

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.

📌 W punktach
  • 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 usuwa

Każ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.

▶ Śmieci w warstwach porównaj trzy wersje tego samego Dockerfile
Rozmiar obrazu
—
Wybierz wariant.
Rozmiary ilustracyjne. Uproszczenie: rozmiar to suma warstw bez kompresji.

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
🤓 pipefail

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.

📌 W punktach
  • 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 argumenty

ENTRYPOINT 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"].

▶ Kalkulator: co faktycznie się uruchomi wybierz kombinację
ENTRYPOINT
CMD
docker run
Wybierz opcje.
Wyniki odpowiadają tabeli interakcji CMD/ENTRYPOINT z dokumentacji Dockerfile. Uproszczenie: nie pokazujemy CMD dziedziczonego z obrazu bazowego.
❓ Nie ma głupich pytań
P:
Mój obraz bazowy ma CMD. Dodałem własny ENTRYPOINT i CMD zniknęło. Bug?
O:
Nie, tak to działa: ustawienie ENTRYPOINT czyści CMD odziedziczone z obrazu bazowego. Jeśli potrzebujesz CMD, zdefiniuj je ponownie w swoim Dockerfile.
📌 W punktach
  • ENTRYPOINT = program, CMD = domyślne argumenty. docker run … args zastę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 sekund

docker 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.

▶ docker stop na osi czasu porównaj warianty startu
Wybierz wariant.
Oś czasu 0–10 s. Uproszczenie: niektóre powłoki dla pojedynczego polecenia robią exec i wtedy shell form działa przypadkiem — nie polegaj na tym. Czasy zamknięcia aplikacji ilustracyjne.
🤓 --init i skrypty startowe

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.

📌 W punktach
  • stop = sygnał stopu → limit czasu (10 s dla Linux) → SIGKILL (kod 137).
  • Exec form + obsługa SIGTERM w aplikacji, albo --init, albo exec "$@" w skrypcie.

ARG, ENV i zakresy zmiennych

kto widzi zmienną i jak długo

ARG istnieje tylko w czasie budowania, ENV — w czasie budowania i w każdym kontenerze z obrazu. Zakresy mają reguły, które łatwo przeoczyć:

▶ Gdzie widać zmienną kliknij zmienną
Kliknij zmienną.
Uproszczenie: kolumny to miejsca, w których wartość może być widoczna. Obraz w przykładzie ma dwa etapy: 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
⚠️ Sekrety w ARG i ENV

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.

📌 W punktach
  • 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 talerzu

Kompilator, 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.

▶ Od 1 GB do kilkudziesięciu MB zbuduj etapy, wybierz target
Kliknij ▶.
Rozmiary ilustracyjne (rząd wielkości dla aplikacji Go). Uproszczenie: BuildKit buduje tylko etapy, od których zależy target, i niezależne etapy wykonuje równolegle.
# 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"]
❓ Nie ma głupich pytań
P:
Czy COPY --from działa tylko z etapami?
O:
Nie. Możesz kopiować z dowolnego obrazu: 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.
🤓 Wzorce

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.

📌 W punktach
  • Każdy FROM = nowy etap. Do obrazu trafia tylko ostatni (lub --target).
  • COPY --from przenosi artefakty z etapu albo z dowolnego obrazu.

BuildKit: graf, równoległość i RUN --mount

Dockerfile to nie skrypt, tylko graf

Klasyczny 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.

▶ Cache mount kontra zwykły RUN zmień jedną zależność
Pobrane z sieci
—
Scenariusz: zmieniasz jedną zależność w package.json, więc cache warstwy npm ci przepada.
Liczby ilustracyjne. Uproszczenie: cache mount żyje w builderze, a nie w obrazie — na świeżym runnerze CI jest pusty, dopóki go nie wyeksportujesz lub nie zachowasz innym sposobem.
Typ mountuCo dajeKiedy
type=cache,target=…trwały katalog cache między buildami; opcja sharing: shared (domyślnie), private, lockednpm, pip, go build, apt, Maven
type=secret,id=…plik z sekretem, domyślnie w /run/secrets/<id>tokeny, poświadczenia
type=sshdostęp do agenta SSH hostagit clone prywatnych repo
type=bindmontuje pliki z kontekstu lub etapu bez kopiowania do warstwykod źródłowy potrzebny tylko do kompilacji
type=tmpfskatalog w pamięci na czas krokupliki 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 mount ≠ cache warstw

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.

📌 W punktach
  • 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 warstwa

Build 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.

▶ Gdzie zostaje ślad sekretu porównaj metody
Wybierz metodę.
Uproszczenie: „historia” = 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 .
🤓 Szczegóły

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ą).

📌 W punktach
  • 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.

▶ Wybierz bazę dla tej samej aplikacji rozmiar i kompromisy
Wybierz bazę.
Rozmiary ilustracyjne, rząd wielkości. Liczby podatności zależą od dnia skanu — nie podajemy ich. „scratch” dotyczy statycznie skompilowanej binarki (Go, Rust), nie Node.js.
PraktykaJakDlaczego
Nie-rootRUN useradd -r app + USER appucieczka z procesu ≠ root w kontenerze
PinowanieFROM node:22.11-slim@sha256:…powtarzalność; tag może się przesunąć
Bez rekomendowanychapt-get install --no-install-recommendsmniej pakietów
Odświeżanie bazydocker build --pull, rebuild cyklicznypoprawki bezpieczeństwa w bazie
Skandocker scout cves obrazwidoczność znanych CVE
Bez sekretów i śmieci.dockerignore, secret mounty, multi-stagenic, czego nie potrzebuje runtime
⚠️ Alpine i musl

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.

🤓 Obrazy bez powłoki

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.

📌 W punktach
  • 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 architektur

Laptop 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ć).

▶ Trzy strategie budowania dla arm64 + amd64 na hoście amd64
Wybierz strategię.
Czasy ilustracyjne. Uproszczenie: emulacja jest zwykle wolniejsza zwłaszcza przy kompilacji; jej koszt zależy od obciążenia.
# 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.

BackendGdzie leży cacheKiedy
inlinew samym obrazie; tylko tryb minnajprostszy start
registryosobny obraz cache w rejestrze; obsługuje mode=maxuniwersalny wybór
localkatalog na dyskurunner z trwałym dyskiem
ghacache GitHub ActionsGitHub Actions
s3, azblobbucket / blob storagewł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 .
🤓 mode=min a mode=max

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"]
}
📌 W punktach
  • Obraz wieloplatformowy = indeks obrazów; pull wybiera manifest hosta.
  • Cross-kompilacja z --platform=$BUILDPLATFORM jest zwykle szybsza niż emulacja.
  • W CI eksportuj cache (registry lub gha, mode=max).
Część III

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ę danych

Warstwa zapisu kontenera znika razem z nim. Dane, które mają przeżyć kontener, trzymasz poza nią. Docker ma trzy rodzaje montowań:

TypGdzie leżą daneKiedy
-v dane:/var/lib/postgresql/data
(volume)
obszar zarządzany przez Dockeradane aplikacji, bazy — domyślny wybór
-v "$PWD":/app
(bind mount)
dowolna ścieżka hostakod źródłowy w dev, pliki konfiguracyjne
--tmpfs /tmppamięć RAM hostadane tymczasowe, które nie mają trafić na dysk
▶ Co przeżyje docker rm zapisz dane i usuń kontener
Zapisz rekord, usuń kontener, uruchom nowy.
Uproszczenie: cztery kontenery bazy z różnym przechowywaniem danych w tej samej ścieżce. Liczby rekordów przykładowe.
⚠️ Montowanie zasłania

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 czy -v?

--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.

📌 W punktach
  • 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 kontener

Każ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.

▶ api łączy się z db wybierz konfigurację
$
Wybierz scenariusz.
Uproszczenie: pomijamy reguły iptables/nftables i IPv6. Adresy IP przykładowe.
Driver / opcjaCo robiKiedy
bridge (użytkownika)prywatna sieć na hoście + DNS po nazwachdomyślny wybór dla kilku kontenerów
hostkontener używa stosu sieciowego hosta, bez izolacjiwydajność, specjalne przypadki
nonebrak siecizadania offline
overlaysieć między wieloma hostami (Swarm)klastry Swarm
-p 8080:80port 8080 hosta → 80 kontenera, na wszystkich interfejsachdostęp z zewnątrz
-p 127.0.0.1:8080:80tylko z hostadev, panele admina
-Ppublikuje wszystkie porty z EXPOSE na losowych portach hostaszybkie testy
⚠️ Publikowanie jest publiczne

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.

❓ Nie ma głupich pytań
P:
Jak z kontenera połączyć się z usługą na hoście?
O:
W Docker Desktop użyj nazwy 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.
📌 W punktach
  • Sieć użytkownika = DNS po nazwach kontenerów. Domyślny bridge — bez DNS.
  • EXPOSE dokumentuje, -p publikuje. Kontener↔kontener nie potrzebuje -p.

Limity zasobów, restart i healthcheck

kontener bez limitu może zjeść cały host

Bez 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.

▶ Wyciek pamięci kontra limit i polityka restartu wybierz politykę i uruchom
--restart
Pamięć / limit
0 MB
Wybierz politykę i scenariusz.
Uproszczenie: pomijamy rosnące opóźnienie między restartami i to, że restart liczy się dopiero po ≥10 s działania. Zużycie pamięci ilustracyjne.
FlagaCo robiKiedy
-m 512m / --memorytwardy limit pamięci (minimum 6m)zawsze na produkcji
--memory-swappamięć + swap łączniekontrola swapu
--cpus=1.5ile CPU może użyć kontenersąsiedzi na hoście
--pids-limitmaksymalna liczba procesówochrona przed fork bombą
--restart on-failure[:N]restart przy kodzie ≠ 0, opcjonalnie max N razyzadania, które mogą chwilowo zawieść
--restart alwaysrestart zawsze; ręcznie zatrzymany wraca po restarcie demonaserwisy
--restart unless-stoppedjak always, ale zatrzymany nie wraca po restarcie demonaserwisy, które czasem wyłączasz
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
  CMD curl -fsS http://localhost:8080/health || exit 1
🤓 Healthcheck

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).

📌 W punktach
  • Ustaw -m i --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 środowisko

Compose 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:
▶ depends_on: kolejność startu porównaj warunki
Wybierz wariant.
Czasy ilustracyjne. Uproszczenie: „gotowa” baza oznacza, że healthcheck pg_isready przechodzi.
ElementCo robiKiedy
depends_on.conditionservice_started / service_healthy / service_completed_successfullykolejność startu i gotowość
profilesserwis startuje tylko po włączeniu profilu (--profile tools, COMPOSE_PROFILES) lub gdy wskażesz go z nazwynarzędzia opcjonalne
develop.watchdocker compose watch: akcje sync, rebuild, sync+restartszybka pętla dev
compose.override.yamlscalany automatycznie z compose.yamllokalne różnice dev
-f a.yaml -f b.yamlscalanie plików; późniejszy nadpisujeśrodowiska (CI, prod)
.envzmienne do interpolacji ${VAR} w pliku Composekonfiguracja bez edycji YAML
🤓 Pierwszeństwo zmiennych środowiskowych

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.

✏️ Zaostrz ołówek

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.

📌 W punktach
  • Compose = serwisy + sieć projektu + wolumeny w jednym pliku.
  • depends_on bez warunku czeka tylko na start kontenera, nie na gotowość.
Część IV

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 palca

Peł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”.

▶ Tag się przesuwa, digest nie opublikuj nową wersję
Rejestr ma jedną wersję obrazu.
Digesty skrócone i przykładowe. Uproszczenie: pomijamy, że obraz wieloplatformowy ma digest indeksu i osobne digesty manifestów.
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>
🤓 Strategia tagowania

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.

📌 W punktach
  • Brak rejestru = docker.io; brak tagu = latest.
  • Tag jest zmienny, digest niezmienny. Na produkcji: wersja albo digest.

Debugowanie i codzienny workflow

objaw → komenda

Większość problemów z kontenerami diagnozujesz kilkoma komendami. Kliknij objaw, a figura podpowie, od czego zacząć.

▶ Mapa diagnostyczna wybierz objaw
Wybierz objaw.
—
Kolejność komend to sugerowana ścieżka, nie jedyna. Komendy zgodne z dokumentacją CLI Dockera.
KomendaCo robiKiedy
docker logs -f --tail 100 cstdout/stderr głównego procesupierwszy krok zawsze
docker exec -it c shnowy proces w działającym kontenerzezajrzeć do środka
docker inspect cpełna konfiguracja i stan (JSON); --formatenv, mounty, sieci, exit code
docker cp c:/ścieżka .kopiowanie plików z/do kontenerawyciągnąć log, zrzut
docker stats / docker top czużycie zasobów na żywo / procesywolno, dużo pamięci
docker eventsstrumień zdarzeń demona„kto restartuje mój kontener?”
docker system dfmiejsce zajęte przez obrazy, kontenery, wolumeny, cachepełny dysk
docker system pruneusuwa zatrzymane kontenery, nieużywane sieci, wiszące obrazy, nieużywany cache buildusprzątanie (--volumes usuwa też anonimowe wolumeny)
docker build --progress=plainpełne logi kroków buildudebug Dockerfile
docker initgeneruje Dockerfile, compose.yaml i .dockerignore dla projektustart nowego projektu
⚠️ Logi w plikach

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.

📌 W punktach
  • logs → inspect → exec. W tej kolejności rozwiążesz większość zagadek.
  • docker system df przed prune; uważaj z --volumes.
Część V

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.

▶ Co widzi proces w kontenerze przełączaj namespaces
Wszystkie namespaces izolowane.
Uproszczenie: --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.
🤓 cgroups w praktyce

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.

📌 W punktach
  • Flagi --pid/--network/--uts host współ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.

▶ Promień rażenia po przejęciu aplikacji włączaj i wyłączaj flagi
Promień rażenia
Konfiguracja domyślna.
Skala ilustracyjna, nie metryka bezpieczeństwa. Pokazuje kierunek, nie wartość.
MechanizmCo robiKiedy
USER / --userproces nie jest rootem w kontenerzezawsze, gdy aplikacja nie potrzebuje roota
--cap-drop ALL --cap-add …tylko potrzebne capabilities, np. NET_BIND_SERVICEusługi produkcyjne
--read-only + --tmpfs /tmpsystem plików kontenera tylko do odczytubezstanowe aplikacje
no-new-privilegesblokuje eskalację przez setuidogólne utwardzenie
Rootless modedemon i kontenery działają jako zwykły użytkownik hostahosty współdzielone, CI
userns-remaproot w kontenerze mapowany na nieuprzywilejowany UID hostaizolacja przy klasycznym demonie
⚠️ Grupa docker

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.

📌 W punktach
  • Baza: okrojone capabilities + seccomp. Dokładaj: nie-root, cap-drop, read-only.
  • Nigdy: --privileged i gniazdo dockera w aplikacjach produkcyjnych.

Łańcuch dostaw: SBOM, provenance, skanowanie

wiedz, co jest w środku i skąd się wzięło

BuildKit 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).

▶ Od commita do zaufanego obrazu przejdź pipeline
Kliknij ▶.
Uproszczenie: kroki pipeline’u przykładowe; identyfikator CVE i wynik skanu zmyślone dla ilustracji.
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 mode=max

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.

📌 W punktach
  • 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
Wynik: 0 / 0

Ściąga

Komenda / instrukcjaCo robi / domyślnieKiedy
# syntax=docker/dockerfile:1najnowszy stabilny frontend 1.xpierwsza linia każdego Dockerfile
COPY package*.json ./ → RUN npm ci → COPY . .zależności przed kodemstabilny cache
RUN a && b && rm -rf cachesprzątanie w tej samej warstwiemniejszy obraz
FROM … AS build / COPY --from=buildmulti-stageobraz runtime bez narzędzi buildu
docker build --target testbuduje do wskazanego etaputesty w CI
RUN --mount=type=cache,target=…trwały cache menedżera pakietównpm, pip, go, apt
RUN --mount=type=secret,id=xsekret w /run/secrets/x tylko na czas krokutokeny w buildzie
ENTRYPOINT ["app"] + CMD ["--flag"]program + domyślne argumentyobraz jako narzędzie
docker stop -t 30sygnał stopu, potem SIGKILL; domyślnie 10 s (Linux)dłuższe zamykanie
docker build --no-cache / --pullbez cache / świeży obraz bazowypodejrzany cache, łatki bazy
docker buildx build --platform linux/amd64,linux/arm64obraz wieloplatformowyApple Silicon + serwery x86
--cache-to type=registry,ref=…,mode=maxeksport cache z etapami pośrednimiCI
docker network create appsieć z DNS po nazwachkontenery się widzą
-p 127.0.0.1:8080:80port tylko z hostadev, bazy
-v dane:/ścieżkanazwany wolumentrwałe dane
-m 512m --cpus 1limity cgroupsprodukcja
--restart unless-stoppedrestart poza ręcznym zatrzymaniemserwisy
depends_on: condition: service_healthyczeka na healthcheck zależnościCompose: api + db
docker compose watchsync / rebuild / sync+restartpętla dev
image@sha256:…niezmienna referencjawdrożenia
docker logs / inspect / execdiagnostykazwykle w tej kolejności
docker system df / prunezajętość / sprzątaniepełny dysk
--cap-drop ALL --read-only --userhardeningprodukcja
--sbom=true --provenance=mode=maxatestacje obrazułańcuch dostaw
🎓 Gratulacje — odpowiedź na zagadkę

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ą.