Kontenery — rewolucja w deploymencie

"U mnie działa" — to chyba najczęstszy żart (i problem) w branży IT. Aplikacja działa na laptopie programisty, ale po wrzuceniu na serwer produkcyjny coś się psuje. Inna wersja biblioteki, brakujący pakiet, inna konfiguracja systemu. Docker eliminuje ten problem, pakując aplikację wraz z całym jej środowiskiem w przenośny kontener.

Kontenery vs maszyny wirtualne

Często spotykamy pytanie: czym kontenery różnią się od maszyn wirtualnych? Różnica jest fundamentalna.

Maszyna wirtualna (VM) emuluje cały komputer — ma swój system operacyjny, jądro, sterowniki. Uruchomienie VM z Ubuntu na serwerze z Ubuntu oznacza dwa pełne systemy operacyjne w pamięci. Typowa VM zajmuje gigabajty pamięci i minuty na uruchomienie.

Kontener dzieli jądro systemu z hostem i izoluje tylko procesy aplikacji. Nie potrzebuje własnego systemu operacyjnego — korzysta z mechanizmów izolacji wbudowanych w jądro Linux (namespaces i cgroups). Efekt? Kontener startuje w milisekundach, zajmuje megabajty pamięci i jest praktycznie tak samo wydajny jak natywna aplikacja.

W praktyce oznacza to, że na jednym serwerze możesz uruchomić dziesiątki kontenerów tam, gdzie zmieściłoby się kilka maszyn wirtualnych.

Dockerfile — przepis na kontener

Dockerfile to plik tekstowy opisujący, jak zbudować obraz kontenera. Oto przykład dla prostej aplikacji webowej w Go:

# Etap budowania
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .

# Etap produkcyjny
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /server
COPY --from=builder /app/templates /templates
COPY --from=builder /app/static /static
EXPOSE 8080
CMD ["/server"]

Ten Dockerfile wykorzystuje wzorzec multi-stage build — w pierwszym etapie kompilujemy aplikację z pełnym toolchainem Go, a w drugim kopiujemy tylko gotowy plik binarny do lekkiego obrazu Alpine. Wynikowy obraz waży kilkanaście megabajtów zamiast setek.

Kluczowe instrukcje Dockerfile:

  • FROM — bazowy obraz, na którym budujemy
  • WORKDIR — katalog roboczy wewnątrz kontenera
  • COPY — kopiowanie plików z hosta do kontenera
  • RUN — wykonanie polecenia podczas budowania obrazu
  • EXPOSE — informacja, na którym porcie nasłuchuje aplikacja
  • CMD — domyślne polecenie uruchamiane przy starcie kontenera

Docker Compose — orkiestracja wielu kontenerów

Typowa aplikacja webowa to nie jeden kontener — to serwer aplikacji, baza danych, cache, reverse proxy. Docker Compose pozwala zdefiniować wszystkie te usługi w jednym pliku docker-compose.yml:

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres
      - DB_PORT=5432
    depends_on:
      - postgres

  postgres:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD_FILE=/run/secrets/db_password

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    depends_on:
      - app

volumes:
  pgdata:

Jedno polecenie docker compose up -d uruchamia cały stos. Kontenery widzą się po nazwach usług (np. aplikacja łączy się z bazą danych pod adresem postgres), bo Docker Compose tworzy dla nich wspólną sieć.

Kluczowe koncepcje do zapamiętania

Obraz vs kontener — obraz to szablon (jak klasa w programowaniu), kontener to uruchomiona instancja (jak obiekt). Z jednego obrazu możesz uruchomić wiele kontenerów.

Wolumeny (volumes) — kontenery są efemeryczne — po usunięciu dane znikają. Wolumeny pozwalają przechowywać dane trwale, niezależnie od cyklu życia kontenera. Kluczowe dla baz danych.

Sieci (networks) — Docker pozwala tworzyć izolowane sieci, w których kontenery mogą się komunikować. Domyślnie Docker Compose tworzy sieć dla każdego projektu.

Warstwy (layers) — każda instrukcja w Dockerfile tworzy warstwę. Docker cachuje warstwy — jeśli instrukcja się nie zmieniła, Docker używa wcześniej zbudowanej warstwy. Dlatego warto najpierw kopiować pliki zależności (go.mod, package.json), a dopiero potem kod źródłowy — zmiana kodu nie wymusza ponownego pobierania zależności.

Praktyczne wskazówki na start

Zacznij od gotowych obrazów — nie musisz od razu pisać własnych Dockerfile'ów. Oficjalne obrazy PostgreSQL, Redis, Nginx i wielu innych narzędzi działają out-of-the-box.

Nie przechowuj sekretów w obrazie — hasła, klucze API i inne wrażliwe dane powinny być przekazywane przez zmienne środowiskowe lub Docker Secrets, nigdy umieszczane w Dockerfile czy kodzie.

Używaj .dockerignore — tak jak .gitignore dla Gita, .dockerignore wyklucza pliki z kontekstu budowania. Dodaj tam node_modules, .git, .env i inne pliki, które nie są potrzebne w obrazie.

Taguj obrazy — zamiast polegać na tagu latest, używaj konkretnych wersji (myapp:1.2.3 lub myapp:2024-05-29). To ułatwia rollback i śledzenie, która wersja działa na produkcji.

Podsumowanie

Docker nie jest już narzędziem tylko dla DevOpsów — to codzienne narzędzie pracy web developera. Pozwala na powtarzalne środowisko developerskie, proste wdrożenia i izolację usług. Nauka podstaw — Dockerfile, docker-compose, wolumeny i sieci — to inwestycja, która szybko się zwraca w postaci mniejszej liczby problemów z deploymentem i łatwiejszego zarządzania infrastrukturą.