Pierwszy raz zobaczyłem Docker w 2019. Kolega z zespołu chwalił się, że “aplikacja odpala się jedną komendą”. Sprawdziłem, faktycznie odpala. Ale nie zrozumiałem, po co, skoro npm start też odpala.

Rok później dołączyłem do zespołu, gdzie każdy programista miał inny setup lokalnie. “U mnie działa” było memem. Deploy na staging to była loteria. Wtedy zrozumiałem Docker.

Dziś każdy projekt w mojej agencji ma Docker Compose dla lokalnego dev’u i containers na produkcji. Bez tego nie wyobrażam sobie pracy zespołowej.

Ten post to praktyczny przewodnik. Kiedy Docker ma sens, kiedy nie, konkretne kompozycje dla mojego stacka, i najczęstsze błędy z produkcji.

Co to jest Docker

Docker to platforma do konteneryzacji. Zamiast instalować aplikację na maszynie, pakujesz ją w kontener - lightweight izolowaną paczkę z kodem, zależnościami, konfiguracją.

Kontener uruchamiasz na każdej maszynie z Dockerem. Ten sam kontener działa lokalnie u Ciebie, u kolegi, na CI, na produkcji. “Works on my machine” znika.

To nie wirtualna maszyna. Kontener dzieli kernel z host’em, jest lekki (setki MB, nie dziesiątki GB), uruchamia się w sekundach.

Kiedy Docker ma sens

Ma sens:

  • Zespół 2+ osób pracujących na tym samym projekcie
  • Aplikacja wymagająca specyficznej wersji Node/Python/PHP/etc.
  • Stack z kilkoma serwisami (backend, frontend, DB, Redis, queue)
  • Deployment poza Vercelem/Netlify (self-hosted, VPS, AWS ECS)
  • Micro-services albo distributed systems
  • Onboarding nowego developera (jeden docker compose up zamiast 2h setupu)

Nie ma sensu:

  • Solo project, jeden serwer, jeden stack
  • Frontend-only na Vercelu
  • Prototyp jednodniowy
  • Statyczna strona (Jekyll, Astro, wysyła się na CDN)

Overhead Docker (build, config, learning curve) nie zawsze się spina. Solo dev na Vercelu spokojnie może pominąć.

Dla mojego mrellwart.com (Jekyll static site) nie używam Dockera. Dla projektów klientów w monorepo Next.js + Strapi + Postgres zawsze.

Podstawowe pojęcia (bez teorii)

Image: template, z którego uruchamiasz kontenery. Jak klasa w OOP.

Container: uruchomiona instancja obrazu. Jak instancja klasy.

Dockerfile: przepis, jak zbudować image. Instrukcje: skopiuj kod, zainstaluj zależności, uruchom.

Docker Compose: definiuje kilka kontenerów razem plus network między nimi. YAML zamiast kupy docker run komend.

Volume: persistent storage. Dane, które przeżywają restart kontenera (np. baza danych).

Network: kontenery komunikują się między sobą. Compose robi to za Ciebie automatycznie.

Reszta (registry, orchestration, Kubernetes) to advanced tematy, których nie potrzebujesz na początek.

Mój Dockerfile dla Next.js

# apps/web/Dockerfile
FROM node:20-alpine AS base

# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# Dependencies layer
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/web/package.json ./apps/web/
COPY packages/types/package.json ./packages/types/
RUN pnpm install --frozen-lockfile

# Build layer
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules
COPY . .
RUN pnpm --filter web build

# Runtime layer
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "apps/web/server.js"]

Kluczowe rzeczy:

  • Multi-stage build: separate stages dla deps, build, runtime. Runtime image jest mały (bez source code, bez dev deps).
  • Layer caching: package.json copiowane pierwsze, dopiero potem source. Docker cache’uje layer z install, jeśli deps się nie zmieniły.
  • Non-root user: security. Nie uruchamiaj kontenerów jako root.
  • Alpine: mały base image (5MB), szybszy download.

Mój Docker Compose dla monorepo

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myapp
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  strapi:
    build:
      context: .
      dockerfile: apps/cms/Dockerfile
    environment:
      DATABASE_CLIENT: postgres
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: myapp
      DATABASE_USERNAME: dev
      DATABASE_PASSWORD: devpassword
      NODE_ENV: development
    ports:
      - "1337:1337"
    volumes:
      - ./apps/cms:/app/apps/cms
      - /app/apps/cms/node_modules
    depends_on:
      postgres:
        condition: service_healthy

  web:
    build:
      context: .
      dockerfile: apps/web/Dockerfile.dev
    environment:
      NEXT_PUBLIC_STRAPI_URL: http://strapi:1337
      DATABASE_URL: postgresql://dev:devpassword@postgres:5432/myapp
    ports:
      - "3000:3000"
    volumes:
      - ./apps/web:/app/apps/web
      - /app/apps/web/node_modules
    depends_on:
      - strapi

volumes:
  postgres_data:
  redis_data:

Uruchamianie: docker compose up. Nowy programista klonuje repo, uruchamia to, wszystko działa w 2 minuty.

Development vs Production Dockerfile

Osobne Dockerfile dla dev i prod. Bo:

Dev:

  • Volume mount source code (żeby hot reload działał)
  • Więcej dev tools (debugging, testing)
  • Verbose logging
  • Nie optymalizowane pod size

Prod:

  • Multi-stage build (mały runtime image)
  • Non-root user
  • Health checks
  • Minimalne dependencies
  • Optymalizowane pod startup time

Konwencja u mnie: Dockerfile dla prod, Dockerfile.dev dla dev.

Volume mounting: jak nie zabić performance

Klasyczna pułapka. Volume mount z source code + node_modules = ekstremalnie wolny build.

Zły:

volumes:
  - ./apps/web:/app/apps/web

To mountuje WSZYSTKO, włącznie z node_modules z Twojego hosta. Jeśli host to Mac, a kontener to Linux, node_modules są niekompatybilne.

Dobry:

volumes:
  - ./apps/web:/app/apps/web
  - /app/apps/web/node_modules

Drugi mount to “anonymous volume” - mówi Dockerowi “użyj tego, co jest w kontenerze, nie w hoście”. node_modules pozostaje kontenerowy.

Bez tego triku dev environment jest 10x wolniejszy.

Zdrowie kontenerów: health checks

Kontener się uruchamia. Ale czy działa? Nie zawsze wiadomo.

Health check w Dockerfile albo Compose:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

Docker sprawdza co 30s. Jeśli 3 fail, kontener uznany za unhealthy.

Load balancery (nginx, Traefik, ECS) używają tego do routing. Unhealthy kontener nie dostaje ruchu.

Dla każdej aplikacji web dodaj /api/health endpoint zwracający 200 OK.

Networking między kontenerami

W Docker Compose kontenery widzą się po nazwie serwisu. W przykładzie wyżej:

  • Web uderza w Strapi przez http://strapi:1337
  • Strapi uderza w Postgres przez postgres:5432

Bez portów ludzkiego typu localhost. Wewnętrzny DNS.

Ports w Compose expose’ują kontener do host (Twojego Maca). Jeśli chcesz otworzyć localhost:3000 w browserze, potrzebujesz ports: - "3000:3000".

Networking pułapka: aplikacja w kontenerze próbuje uderzyć w localhost:5432 (na hoście). Nie działa, bo kontener ma swój localhost. Użyj nazwy serwisu (postgres:5432).

Optymalizacja rozmiaru images

Duży image = wolne push, pull, deploy.

Techniki:

Multi-stage builds. Runtime tylko z tym, co potrzeba. Bez dev deps, source code, build tools.

Alpine base. node:20-alpine (140 MB) vs node:20 (900 MB). 6x mniejszy.

.dockerignore. Nie kopiuj .git, node_modules, .next, .env.

Kolejność COPY. Copy package.json pierwsze, potem source. Layer z install cache’owany.

Distroless (advanced). Google’s distroless images, jeszcze mniejsze niż Alpine, tylko runtime, bez shell. Dla max optymalizacji.

Mój prod image dla Next.js: ~180 MB (vs 900 MB dla naiwnego setupu).

Deployment: docker w produkcji

Jak deployujesz kontener na produkcji, opcje:

Vercel/Netlify. Nie potrzebujesz Docker, oni robią to za Ciebie.

Fly.io. Bardzo dev-friendly, dobrze zintegrowany z Dockerem. Deploy przez fly deploy. Świetny dla małych i średnich apps.

Railway. Podobne do Fly.io, łatwiejsze onboarding, mniej flexibility.

Coolify (self-hosted). Otwiera się poza platformami, na własnym VPS. Wybór, jeśli chcesz kontroli albo prywatności.

Docker Swarm. Kubernetes lite. Dobre dla single-server multi-container. Rzadko używane w 2026 (przegrało z K8s).

Kubernetes (K8s). Standard enterprise. Overkill dla większości projektów. Wymaga dedicated devops.

AWS ECS / Fargate. Managed AWS. Dobre dla teams already on AWS.

Dla klientów mojej agencji: głównie Fly.io albo Coolify. K8s tylko, jeśli klient ma dedicated devops team.

Docker w CI/CD

Podstawowe użycie: build image w CI, push do registry, deploy z registry.

Pisałem szerzej w GitHub Actions CI/CD. Krótki flow:

  1. Push do main branch
  2. GitHub Actions triggeruje
  3. Build Docker image
  4. Run tests w kontenerze
  5. Push image do GitHub Container Registry
  6. Deploy do Fly.io (albo swojego serwera przez SSH)

Cały pipeline: 5-10 minut. Zero manual steps.

Bezpieczeństwo kontenerów

Non-root user. Nigdy USER root w produkcji. Attackers, którzy dostaną się do kontenera, mają root=nothing.

Minimalne dependencies. Im mniej rzeczy w image, tym mniejszy attack surface.

Scanning. Docker Scout, Trivy, Snyk skanują image pod kątem CVE. Włącz w CI.

Secrets nie w image. .env w .dockerignore. Sekrety w environment variables przy deploy.

Updated base images. node:20-alpine dostaje security patches. Rebuild image regularnie.

Read-only filesystem. --read-only flag lub read_only: true w Compose. Kontener nie może modyfikować siebie.

Monitorowanie kontenerów

Kontener działa. Ale czy zdrowy?

Docker stats (basic): CPU, memory per kontener w real-time.

Dozzle (open source): web UI do logów kontenerów. Prosty.

Portainer (open source): full-featured management UI dla Dockera.

Grafana + Prometheus (advanced): metrics stack. Alerty, dashboards, historia.

Sentry (application-level): errors, traces, performance. Independent from Docker.

Mój standard: Dozzle dla logów lokalnie, Fly.io built-in monitoring dla produkcji.

Częste błędy

Ignorowanie warstw. Copy source jako pierwsze = deps re-install przy każdej zmianie kodu. Copy package.json first, source second.

Bloated images. Twój image ma 2 GB. Powód: kopiujesz cały monorepo, dev deps, git history. Multi-stage plus .dockerignore fix.

Missing health checks. Kontener odpala się, ale nie odpowiada. Load balancer nadal wysyła ruch. Users dostają 502. Add health check.

Volume permission issues. Zapisujesz plik jako user X w kontenerze, host widzi jako root. Chown w Dockerfile albo używaj bind mounts z uwagą.

Hardcoded configs. Docker image z hardcoded database URL dla dev. Deploy na prod, aplikacja próbuje uderzyć w dev DB. Environment variables od dnia 1.

Root user w prod. Sec risk. Zawsze non-root user w prod images.

Ignorowanie logów. Kontener padnie w produkcji, nikt nie wie dlaczego. Zbieraj logi (Loki, CloudWatch, Datadog).

Alternatywy dla Dockera

Podman: rootless container runtime, kompatybilny z Docker. Rising alternatywa, głównie dla Red Hat ecosystem.

Nix: reproducible builds, całkiem inna filozofia. Steep learning curve, ale purists lubią.

Native builds (Vercel/Netlify/Cloudflare Workers): dla frontendu Docker nie potrzebny.

Serverless (AWS Lambda, Cloudflare Workers): container-less deployment. Prostsze dla małych functions.

Dla większości projektów Docker to nadal default. Alternatywy mają swoje niche.

Co czytać dalej

  • “Docker Deep Dive” Nigel Poulton. Standard reference. Dobra do systematycznego uczenia.
  • Docker docs. Best-in-class dokumentacja.
  • DevOps for Developers newsletter. Praktyczne case studies.

Nie kupuj kursów Dockera na Udemy. Docs + jeden real project = wystarczy.

Od czego zacząć

Jeśli nie używałeś jeszcze Dockera:

  1. Zainstaluj Docker Desktop. Free dla personal use, płatne dla firm >250 employees.
  2. Uruchom docker run hello-world. Pierwszy kontener.
  3. Zbuduj Dockerfile dla jednego swojego projektu. Prostszego niż full monorepo.
  4. Dodaj docker-compose.yml dla dev environment.
  5. Ucz się przez debugowanie. Container failed to start? Log check, fix, repeat.
  6. Po tygodniu zbuduj multi-service compose (backend + DB + frontend).
  7. Po miesiącu deploy container do produkcji (Fly.io start).

Docker to fundamental skill dla programisty w 2026. Nawet jeśli używasz Vercela dla frontendu (Dockerless), backend, DB, workers wymagają kontenerów.

Programiści bez Dockera skills będą ograniczeni w wyborze projektów i pracodawców. Programiści z Dockerem są przygotowani na cały spektrum, od SaaS na Vercelu do enterprise na K8s.

Pisałem o mojej stack agencji w postach o TypeScript, Next.js, Strapi. Docker to klej, który spina to wszystko na produkcji.