Modul 7 — Virtualizace a kontejnery: Docker

31. Best practices pro psaní Dockerfile (velikost image, vrstvy, bezpečnost)

Praktická doporučení pro menší, rychlejší a bezpečnější Docker images — multi-stage build, cachování vrstev, nepoužívání rootu a další zásady.

Odhadovaná délka studia: 60 minut · Stav: nehotovo

Technologie: Docker

Úvod a kontext

Funkční Dockerfile z lekce o základech Dockeru stačí k tomu, aby aplikace
běžela — ale "funguje" a "je dobře napsaný pro produkci" jsou dvě různé
věci. Špatně napsaný Dockerfile vede ke zbytečně velkým images (pomalé
stahování a nasazování), pomalým buildům (dlouhé čekání při každé změně
kódu) a zbytečným bezpečnostním rizikům (kontejner běžící jako root,
zbytečné nástroje uvnitř image usnadňující útočníkovi po případném
průniku). Tato lekce shrnuje osvědčená pravidla, která se v praxi
používají téměř univerzálně.

Teorie

Volba základního image

Menší základní image znamená menší výsledný image, rychlejší stahování a
menší plochu pro bezpečnostní zranitelnosti:

  • alpine varianty (postavené na Alpine Linux) jsou řádově menší
    (jednotky MB) než plné distribuce, ale používají musl místo glibc,
    což může u některých binárních závislostí (zejména v Pythonu/Node.js s
    nativními rozšířeními) způsobit kompatibilní problémy.
  • slim varianty (u oficiálních Python/Node.js images) jsou
    kompromis — menší než plný image, ale pořád glibc-kompatibilní.
  • distroless images (od Google) obsahují jen samotnou aplikaci a
    její běhové závislosti, bez shellu, balíčkovacího systému nebo
    jakýchkoliv dalších nástrojů — nejmenší plocha pro útok, ale hůř se v
    nich ladí (není sh, do kterého by šlo vlézt přes docker exec).

Pořadí instrukcí a cachování vrstev

Docker cachuje výsledek každé vrstvy (instrukce) a při dalším buildu ji
znovu nepočítá, pokud se nezměnil vstup dané instrukce a instrukce
předtím. Z toho plyne klíčové pravidlo: řadit instrukce od
nejméně po nejčastěji se měnící
. Špatný příklad:

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

Zde COPY . . zkopíruje celý zdrojový kód (včetně requirements.txt),
takže i drobná změna v aplikačním kódu zneplatní cache a vynutí si
kompletní přeinstalování všech závislostí při každém buildu — pomalé.
Správně:

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

Teď se requirements.txt kopíruje a závislosti instalují zvlášť, dřív
než zbytek kódu — dokud se soubor se závislostmi nezmění, Docker použije
cachovanou vrstvu s nainstalovanými balíčky a přeskočí pip install
úplně, i když se změnil zbytek aplikace.

Multi-stage build

Multi-stage build řeší situaci, kdy sestavení aplikace potřebuje nástroje
(kompilátor, build systém), které ve finálním image vůbec nejsou potřeba
za běhu — jen zbytečně zvětšují image a rozšiřují plochu pro
zranitelnosti:

# stage 1: sestavení
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# stage 2: finální image jen s výsledkem
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

První stage (build) obsahuje celé Node.js prostředí s nástroji
potřebnými pro sestavení frontendové aplikace. Druhá stage vychází z
mnohem menšího nginx:alpine a instrukcí COPY --from=build si z první
stage vezme jen výsledný zkompilovaný obsah (dist) — samotné Node.js,
zdrojové soubory ani build nástroje se do finálního image vůbec
nedostanou.

.dockerignore

Podobně jako .gitignore u Gitu, soubor .dockerignore vyloučí soubory
z tzv. build contextu (co je vůbec dostupné instrukcím COPY/ADD):

.git
node_modules
__pycache__
*.pyc
.env

Kromě zrychlení buildu (méně dat se posílá do Docker démona) je to i
bezpečnostní opatření — zabrání náhodnému zabalení souborů s tajnými
klíči (.env) do image.

Nespouštět kontejner jako root

Ve výchozím nastavení běží procesy uvnitř kontejneru jako root. Pokud
útočník najde zranitelnost v aplikaci a získá spuštění kódu uvnitř
kontejneru, root uvnitř kontejneru mu usnadňuje případný únik z izolace
kontejneru (container escape) na hostitelský systém. Doporučená praxe je
vytvořit a používat nepřivilegovaného uživatele:

FROM python:3.12-slim
RUN useradd --create-home appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["python", "app.py"]

Explicitní verze a nepoužívání latest

Tag latest je pohyblivý cíl — co dnes znamená latest, se zítra může
změnit na zcela jinou verzi, což vede k neopakovatelným buildům a
překvapením v produkci. Vždy uvádět konkrétní verzi (python:3.12.4-slim
místo python:latest), ideálně i konkrétní hash image pro nejvyšší
reprodukovatelnost v kritických prostředích.

Skenování zranitelností

Base images i nainstalované balíčky mohou obsahovat známé bezpečnostní
zranitelnosti. Nástroje jako docker scout, Trivy nebo Snyk umí image
oskenovat a nahlásit zranitelné závislosti ještě před nasazením do
produkce (podrobněji v modulu o DevSecOps).

Praktický příklad

Shrnutí všech výše uvedených zásad v jednom realistickém Dockerfile pro
Python aplikaci:

FROM python:3.12.4-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

FROM python:3.12.4-slim
RUN useradd --create-home appuser
WORKDIR /app
COPY --from=build /root/.local /home/appuser/.local
COPY --chown=appuser:appuser . .
ENV PATH=/home/appuser/.local/bin:$PATH
USER appuser
CMD ["python", "app.py"]

Odpovídající .dockerignore:

.git
__pycache__
*.pyc
.env
tests/

Kontrola velikosti výsledného image a porovnání s naivní jednostage
verzí:

docker build -t appka:optimalizovana .
docker images appka:optimalizovana

Oskenování image nástrojem Trivy (pokud je nainstalovaný) na známé
zranitelnosti:

trivy image appka:optimalizovana

Shrnutí

Menší a bezpečnější Docker images vznikají kombinací několika návyků:
volbou malého základního image, řazením instrukcí od nejméně po
nejčastěji se měnící kvůli efektivnímu cachování, použitím multi-stage
buildu k oddělení sestavovacích nástrojů od finálního běhového prostředí,
vyloučením zbytečných souborů přes .dockerignore, spouštěním aplikace
pod nepřivilegovaným uživatelem místo rootu a používáním explicitních
verzí místo pohyblivého tagu latest. Pravidelné skenování images na
známé zranitelnosti doplňuje tato opatření o kontrolu závislostí.

Kontrolní otázky

  1. Proč se COPY requirements.txt . a instalace závislostí obvykle
    umísťují před COPY . ., které kopíruje zbytek aplikace?
  2. K čemu slouží multi-stage build a jaký konkrétní problém řeší?
  3. Proč je nebezpečné spouštět procesy uvnitř kontejneru jako root, a jak
    se tomu dá v Dockerfile předejít?
  4. Upravte ukázkový Dockerfile z lekce tak, aby místo python:3.12.4-slim
    používal python:3.12.4-alpine, a vysvětlete jedno riziko této změny.

Lekce na sebe nejsou zamčené — libovolnou lekci můžete otevřít i označit jako hotovou v jakémkoliv pořadí.