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

