Serwery sieciowe - ilustracja konteneryzacji Docker
DevOps

Docker i konteneryzacja: praktyczny przewodnik dla początkujących

25 maja 2026 9 min czytania

Czym są kontenery i dlaczego zmieniły branżę

Jeśli kiedykolwiek usłyszałeś klasyczne „u mnie działa”, prawdopodobnie spotkałeś się też z konteneryzacją jako odpowiedzią na ten problem. Kontener to odizolowane środowisko uruchomieniowe, które pakuje twoją aplikację razem z wszystkimi zależnościami: bibliotekami systemowymi, runtime’em, plikami konfiguracyjnymi. Dzięki temu ten sam obraz działa identycznie na laptopie, serwerze testowym i w produkcji.

Kontenery różnią się od maszyn wirtualnych poziomem izolacji. VM emuluje cały sprzęt i uruchamia pełny system operacyjny z własnym kernelem — to kilkaset megabajtów albo więcej, a czas startu liczy się w minutach. Kontener współdzieli kernel hosta i korzysta z mechanizmów Linuksa (cgroups, namespaces) do izolacji procesów, systemu plików, sieci i limitów zasobów. W rezultacie obraz kontenera może mieć kilka megabajtów, a start zajmuje ułamki sekundy. Na Windowsie i macOS Docker uruchamia lekką maszynę wirtualną z Linuksem w tle — nie jest to więc „natywna” konteneryzacja, ale z punktu widzenia developera działa identycznie.

Praktyczne korzyści widać od razu. Onboarding nowego programisty skraca się z dnia do godziny: git clone, docker compose up i środowisko jest gotowe. Różnice między maszynami znikają, bo zamiast instrukcji „zainstaluj PostgreSQL 14, Redisa, Node 18…” dostajesz jeden plik, który opisuje całą infrastrukturę. Deploy staje się przewidywalny, bo w produkcji uruchamiasz ten sam obraz, który przetestowałeś lokalnie.

W tym artykule przejdziemy przez praktyczne podstawy Dockera: architekturę, pierwszy Dockerfile, uruchamianie kontenerów, docker-compose dla aplikacji wielokontenerowych, wolumeny, optymalizację obrazów, sieci oraz bezpieczeństwo. Pokażę też, jak wpiąć to w pipeline CI/CD, a na końcu wspomnę o Kubernetesie i sytuacjach, w których konteneryzacja nie jest dobrym rozwiązaniem.

Architektura Dockera

Docker składa się z trzech głównych komponentów. Klient (polecenie docker w terminalu) wysyła żądania przez socket Unix lub HTTP REST API do daemona (dockerd), który zarządza obrazami, kontenerami, sieciami i wolumenami. Registry to zdalny magazyn obrazów — najpopularniejszy to Docker Hub, ale możesz też postawić własny (GitHub Container Registry, GitLab, Harbor, ECR od Amazona).

Warto też rozróżnić dwa pojęcia: obraz (image) to niezmienny szablon opisujący pliki i konfigurację, a kontener to działająca instancja obrazu. Z jednego obrazu możesz uruchomić dowolnie wiele kontenerów — na przykład dziesięć replik tej samej aplikacji za load balancerem. Obrazy są warstwowe: każda instrukcja w Dockerfile tworzy nową warstwę, a warstwy są współdzielone między obrazami. Jeśli dwa obrazy używają tej samej bazy node:20-alpine, pobierzesz ją z registry tylko raz.

Twój pierwszy Dockerfile

Dockerfile to tekstowy przepis na budowę obrazu. Każda instrukcja tworzy nową warstwę, a warstwy są cache’owane — jeśli zmienisz tylko kod aplikacji, Docker nie będzie ponownie pobierał i instalował zależności. To kluczowe dla szybkich buildów.

Dockerfile
# Bazowy obraz - oficjalny Node.js na Alpine Linux
FROM node:20-alpine
# Katalog roboczy wewnątrz kontenera
WORKDIR /app
# Najpierw package.json - lepsze cache'owanie warstw
COPY package*.json ./
RUN npm ci --omit=dev
# Dopiero teraz kopiujemy resztę kodu
COPY . .
# Port, na którym aplikacja nasłuchuje
EXPOSE 3000
# Komenda startowa - forma exec, nie shell
CMD ["node", "server.js"]

Kolejność instrukcji ma znaczenie. Kopiujemy package.json przed resztą kodu, żeby warstwa z npm ci była cache’owana dopóki nie zmienisz zależności. Gdybyś odwrócił tę kolejność, każda zmiana w kodzie źródłowym unieważniałaby cache i wymuszała reinstalację wszystkich paczek. EXPOSE to dokumentacja — sam w sobie nie otwiera portu, ale informuje docker run -P oraz orkiestratorów, co aplikacja publikuje.

Różnica między CMD a ENTRYPOINT bywa myląca. ENTRYPOINT definiuje stałą komendę, do której CMD dodaje domyślne argumenty. Użytkownik może nadpisać CMD przy docker run, a ENTRYPOINT zwykle pozostaje stały. Dla prostych przypadków wystarczy sam CMD w formie exec (tablica stringów) — forma shell uruchamia komendę w /bin/sh -c, co zaburza propagację sygnałów systemowych.

Budowanie i uruchamianie kontenerów

Mając Dockerfile, zbudujesz obraz komendą docker build, a uruchomisz kontener przez docker run. W codziennej pracy będziesz używał też kilku innych komend do inspekcji — ps, logs, exec, inspect.

terminal.sh
# Zbuduj obraz z tagiem myapp:1.0
docker build -t myapp:1.0 .
# Uruchom kontener w tle z mapowaniem portu 8080 -> 3000
docker run -d --name api -p 8080:3000 myapp:1.0
# Lista działających kontenerów
docker ps
# Podgląd logów na żywo
docker logs -f api
# Shell wewnątrz działającego kontenera
docker exec -it api sh
# Zatrzymanie i usunięcie
docker stop api && docker rm api

Flaga -d uruchamia kontener w tle (detached), -p host:container mapuje porty, a --name nadaje mu czytelny identyfikator. Bez nazwy dostaniesz losową etykietę typu sleepy_einstein. Polecenie docker inspect api zwróci pełny JSON z konfiguracją — przydatny do debugowania sieci i zmiennych środowiskowych.

Wskazówka

Kontenery powinny być efemeryczne — traktuj je jak bydło, nie zwierzęta domowe. Nie modyfikuj stanu wewnątrz działającego kontenera. Jeśli coś trzeba zmienić, zmień Dockerfile albo konfigurację i przebuduj obraz. Wszystkie dane, które muszą przetrwać restart, trzymaj w wolumenach.

docker-compose: aplikacje wielokontenerowe

Prawdziwe aplikacje rzadko składają się z jednego kontenera. Typowo potrzebujesz API, bazy danych, cache’a, może też kolejki albo workera. Docker Compose pozwala opisać całą taką konfigurację w jednym pliku YAML i uruchomić wszystko jednym poleceniem docker compose up.

docker-compose.yml
services:
  api:
    build: .
    ports:
      - "8080:3000"
    environment:
      DATABASE_URL: postgres://app:secret@db:5432/app
      REDIS_URL: redis://cache:6379
    depends_on:
      - db
      - cache
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
  cache:
    image: redis:7-alpine
volumes:
  pgdata:

Compose tworzy automatycznie dedykowaną sieć, w której kontenery rozwiązują się po nazwach usług — dlatego API łączy się z bazą jako db, a nie przez adres IP. depends_on określa kolejność startu, ale nie czeka na gotowość aplikacji — do tego potrzebujesz healthchecków lub skryptów typu wait-for-it.

Wolumeny i dane trwałe

W powyższym przykładzie pgdata to named volume — obszar zarządzany przez Dockera, który przetrwa usunięcie kontenera. Alternatywą są bind mounts, które montują konkretny katalog hosta (np. ./src:/app/src) — przydatne w developmencie do hot reloadu, rzadziej w produkcji. Trzeci typ to tmpfs, które trzyma dane tylko w pamięci RAM — nadaje się do cache’a albo danych sesyjnych, które wolno stracić przy restarcie.

Dobra zasada: wszystko, co musi przetrwać deploy — bazy danych, uploady użytkowników, logi długoterminowe — idzie do named volume’u albo storage’u zewnętrznego (S3, EBS). Wolumeny nie są automatycznie backupowane, więc pamiętaj o regularnym pg_dump lub snapshotach.

Optymalizacja obrazów

Obraz ważący 1.2 GB to nie tylko wolny push i pull — to też większa powierzchnia ataku. Trzy techniki dają największy zwrot: małe bazy, multi-stage builds i porządny .dockerignore.

Dockerfile.multistage
# Etap 1: build - potrzebny pełen toolchain
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/api
# Etap 2: minimalny runtime - tylko binarka
FROM gcr.io/distroless/static:nonroot
WORKDIR /app
COPY --from=builder /out/app /app/app
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/app"]

Multi-stage build pozwala kompilować w pełnym środowisku, a do finalnego obrazu skopiować tylko artefakt. Dla Go oznacza to zejście z ~900 MB do kilkunastu MB. Distroless (obrazy Google bez powłoki i menedżera pakietów) i Alpine to dwie popularne drogi — Alpine ma powłokę i musl libc, distroless nie ma właściwie niczego poza runtime’em.

Plik .dockerignore działa jak .gitignore — wyklucza pliki z kontekstu builda. Zawsze wrzuć tam node_modules, .git, .env, katalogi testowe i build output.

Bezpieczeństwo i sieci

Domyślnie kontenery działają jako root — to zły pomysł w produkcji. Dodaj użytkownika o niskich uprawnieniach i przełącz się na niego przed CMD. Skanuj obrazy pod kątem znanych podatności narzędziami typu Trivy albo Grype, najlepiej w pipeline’ie CI — nie ręcznie.

security.sh
# Non-root user w Dockerfile
# RUN addgroup -S app && adduser -S app -G app
# USER app
# Skan obrazu pod kątem CVE
trivy image --severity HIGH,CRITICAL myapp:1.0
# Tylko odczyt systemu plików + bez nowych uprawnień
docker run --read-only --security-opt=no-new-privileges \
  --cap-drop=ALL --name api myapp:1.0
# Dedykowana sieć i limity zasobów
docker network create backend
docker run --network backend --memory=512m --cpus=1.5 \
  --name api myapp:1.0

Domyślna sieć typu bridge wystarcza na start, ale w compose i produkcji warto tworzyć osobne sieci per środowisko lub warstwę (np. frontend, backend). Sekretów nie wrzucaj do obrazu ani do Dockerfile — używaj zmiennych środowiskowych wstrzykiwanych w runtime albo Docker Secrets / managerów sekretów typu Vault, AWS Secrets Manager czy Doppler. Jeśli jednak przypadkiem wbudowałeś sekret w warstwę, samo nadpisanie go w następnym builderze nie wystarczy — stara warstwa zostaje w historii obrazu i każdy, kto zciągnie obraz, może ją odczytać.

Konfiguracja różna dla dev i prod

Typowy błąd początkujących to jeden Dockerfile do wszystkiego. W praktyce development i produkcja mają inne potrzeby: w dev chcesz hot reload, devtools, volume mount z kodem źródłowym. W produkcji chcesz małego obrazu, non-root usera, tylko skompilowany artefakt. Rozwiązanie to dwa warianty — osobny Dockerfile.dev albo multi-stage z targetami (--target development, --target production). Compose obsługuje to przez docker-compose.override.yml, który nakłada się automatycznie w dev, ale jest ignorowany w produkcji.

CI/CD z GitHub Actions

Typowy pipeline wygląda tak: checkout repozytorium, logowanie do registry, build z cache’em warstw, push z dwoma tagami (SHA commita i latest), a na końcu deploy. Action docker/build-push-action zajmuje się większością roboty.

.github/workflows/docker.yml
name: build-and-push
on:
  push:
    branches: [main]
jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: |
            ghcr.io/myorg/myapp:${{ github.sha }}
            ghcr.io/myorg/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Docker Hub jest wygodny dla publicznych obrazów, ale ma limity pulla dla anonimowych użytkowników. Dla projektów komercyjnych rozważ GHCR, ECR, GCR albo samodzielnie hostowany Harbor — zwykle są tańsze i dają lepszą kontrolę dostępu.

Kiedy NIE konteneryzować i co dalej

Docker nie jest srebrną kulą. Jeśli piszesz prostą aplikację desktopową, narzędzie CLI dystrybuowane jako binarka albo mikroserwis na funkcjach serverless (Lambda, Cloud Functions) — konteneryzacja może być niepotrzebnym overheadem. Dla ciężkich workloadów obliczeniowych wymagających dostępu do GPU nadal sprawdza się, ale konfiguracja jest bardziej złożona i wymaga NVIDIA Container Toolkita. Single-binary aplikacje w Go albo Rust często są łatwiejsze do zdeployowania bez kontenera — zwłaszcza jeśli masz prosty VPS i systemd.

Kiedy już opanujesz pojedyncze kontenery i compose, naturalnym następnym krokiem jest Kubernetes — orkiestrator do zarządzania setkami kontenerów na wielu maszynach. K8s dodaje pojęcia takie jak Pody, Deploymenty, Services, Ingresses, ConfigMapy i Secrets, ale fundamentalne zrozumienie Dockera jest konieczne, żeby tam ruszyć. Na małą skalę prostsze alternatywy to Docker Swarm, Nomad albo zarządzane platformy typu Fly.io, Railway, Render czy Google Cloud Run — wszystkie przyjmują obraz Dockera na wejściu i zajmują się resztą.

Następne kroki

Zacznij od skonteneryzowania istniejącego projektu, który znasz. Napisz Dockerfile, dodaj compose z bazą danych, przenieś build do CI. Dopiero gdy to będzie naturalne, patrz na Kubernetes i service mesh. Jeśli chcesz dowiedzieć się więcej o tym, co robimy i jakie tematy omawiamy, zajrzyj na stronę o nas.