Dlaczego kontenery i po co w ogóle orkiestracja
Czym jest kontener i czym różni się od maszyny wirtualnej
Kontener to izolowane środowisko uruchomieniowe działające na wspólnym jądrze systemu operacyjnego. Aplikacja w kontenerze widzi swój własny system plików, własne procesy i własną sieć, ale w rzeczywistości współdzieli kernel z hostem.
Maszyna wirtualna (VM) emuluje cały system – łącznie z wirtualnym BIOS-em, sterownikami, pełnym systemem operacyjnym. Kontener pomija tę warstwę i korzysta bezpośrednio z jądra hosta, co sprawia, że:
- startuje znacznie szybciej (sekundy zamiast minut),
- zajmuje mniej zasobów, bo nie dubluje całego systemu,
- łatwiej nim zarządzać i przenosić go między środowiskami.
Dla początkującego praktyczna różnica jest taka: VM-y są dobre, gdy trzeba odseparować całe systemy (np. Linux i Windows na jednym fizycznym serwerze), a kontenery – gdy chcesz spakować aplikację z jej zależnościami i uruchamiać ją w powtarzalny sposób na różnych maszynach.
Typowe problemy bez kontenerów
Tradycyjny sposób wdrażania aplikacji sprowadza się do instalowania bibliotek, języków i usług bezpośrednio na serwerze. Po kilku miesiącach powstaje „zupa zależności”, którą boi się dotknąć każdy administrator. Pojawiają się dobrze znane kłopoty:
- „U mnie działa” – programista ma inną wersję Node/Pythona/Javy niż serwer produkcyjny.
- Konflikt zależności – jedna aplikacja potrzebuje MySQL 5.7, inna 8.0, a wszystko ma się uruchamiać na tym samym serwerze.
- Ręczne wdrożenia – kopiowanie plików przez SSH, samodzielne odpalanie procesów, własne skrypty restartujące.
- Trudny rollback – powrót do poprzedniej wersji oznacza cofanie konfiguracji, pakietów, plików; często kończy się reinstalacją serwera.
Kontenery upraszczają to podejście. Aplikacja trafia do obrazu Docker ze wszystkimi zależnościami. Serwer musi „tylko” umieć uruchamiać kontenery. Znika większość różnic między środowiskami, bo wszędzie uruchamiany jest ten sam obraz.
Kiedy pojedynczy docker run jeszcze wystarcza
Na samym początku konteneryzacji wielu osobom wystarcza pojedyncze polecenie docker run. Dla małych zastosowań to jest wręcz rozsądny start. Pojedynczy kontener jest w zupełności OK, gdy:
- masz jedną prostą aplikację webową (np. małe API),
- odpalasz jedną bazę danych tylko dla siebie (testy/development),
- pracujesz wyłącznie lokalnie jako programista i nie dzielisz środowiska z innymi.
Problemy zaczynają się, gdy aplikacja składa się z kilku usług: frontend, backend, baza, może jeszcze osobny worker do przetwarzania zadań w tle i broker kolejki. Nagle potrzebujesz:
- pamiętać kolejność uruchamiania kontenerów,
- mapować porty, żeby usługi się „widziały”,
- ręcznie wpisywać te same zmienne środowiskowe przy każdym uruchomieniu.
W tym miejscu prosty docker run przestaje być wygodny. Pojawia się potrzeba narzędzia do opisu całego zestawu kontenerów – tutaj na scenę wchodzi Docker Compose, a później Kubernetes.
Po co orkiestracja: skalowanie, aktualizacje, awarie
Orkiestracja kontenerów to nie marketingowy slogan, ale odpowiedź na trzy konkretne problemy:
- Skalowanie – gdy ruch rośnie, chcesz szybko uruchomić więcej instancji backendu lub frontendu, bez ręcznego stawiania kolejnych maszyn.
- Aktualizacje – wdrażanie nowej wersji aplikacji bez wyłączania całego systemu i z możliwością bezpiecznego wycofania zmian.
- Awarie – automatyczne restartowanie padniętych kontenerów i przerzucanie ruchu na zdrowe instancje.
Docker Compose rozwiązuje część tych problemów na pojedynczej maszynie – pomaga zdefiniować, uruchomić i zarządzać kilkoma kontenerami naraz. Kubernetes robi to na poziomie całego klastra (wiele maszyn), dodając autoskalowanie, rolling update, zaawansowany routing sieciowy, integrację ze storage i wieloma pluginami.
Przykład: mały projekt vs rosnąca aplikacja SaaS
Wyobraź sobie prostą aplikację: pojedyncze API dla klienta, mała baza, kilka endpointów. Lokalnie wystarczą:
- jeden kontener z API,
- jeden kontener z bazą danych,
- prosty
docker compose upna laptopie.
Po kilku miesiącach ta sama aplikacja zamienia się w SaaS. Dochodzą:
- frontend SPA serwowany z Nginx,
- backend rozbity na kilka mikroserwisów,
- kolejka zadań (RabbitMQ, Kafka),
- osobny serwis do generowania raportów,
- kilka instancji bazy lub klaster bazy.
Na jednej maszynie zaczyna brakować zasobów, więc trzeba dołożyć kolejne serwery. A tu pojawia się pytanie: jak rozłożyć kontenery na wiele maszyn, jak trzymać spójną konfigurację, jak sprawnie robić rolling upgrade? W tym punkcie Compose staje się niewygodny, a realna orkiestracja klastrem (Kubernetes) zaczyna mieć sens.

Docker Compose i Kubernetes w jednym zdaniu – intuicyjne porównanie
Docker Compose jako prosty „pilot” do kilku kontenerów
Docker Compose to narzędzie, które traktuje zestaw kontenerów na jednej maszynie jak jeden projekt. Zamiast uruchamiać wszystko ręcznie, opisujesz usługi w pliku docker-compose.yml i zarządzasz nimi jednym poleceniem.
Najbardziej trafne porównanie: Docker Compose jest jak pilot do domowego kina. Steruje głośnikami, telewizorem, dekoderem – ale wszystko stoi w jednym salonie. Nie interesuje go, co dzieje się w innych pokojach czy budynkach.
Compose:
- uruchamia i zatrzymuje kilka kontenerów razem,
- tworzy lokalną sieć dla usług i ustawia DNS po nazwach serwisów,
- tworzy wolumeny dla danych,
- pozwala wygodnie ustawiać zmienne środowiskowe, porty, depends_on.
Czego nie robi Compose:
- nie skaluje dynamicznie w zależności od ruchu (brak autoskalowania),
- nie zarządza wieloma maszynami – działa na jednym hoście (lub jednorazowo na swarmie, ale to ślepy zaułek),
- nie ma zaawansowanego self-healing – jak proces się wysypie, trzeba zadbać o restart (lub użyć dodatkowych mechanizmów, np. restart policies Dockera),
- nie dostarcza natywnego load balancingu między wieloma hostami.
Kubernetes jako platforma do zarządzania klastrem
Kubernetes można opisać jako system operacyjny dla klastra. Nie interesuje go jedna konkretna maszyna, tylko cała pula zasobów – wszystkie węzły (nodes), na których może uruchamiać kontenery (dokładniej: Pody).
W praktyce Kubernetes:
- rozmieszcza kontenery na wielu maszynach,
- pilnuje, aby zawsze działała zadeklarowana liczba replik,
- może automatycznie skalować aplikację na podstawie CPU/RAM lub metryk niestandardowych,
- obsługuje rolling updates, canary, blue-green (przez odpowiednie konfiguracje),
- integruje się z różnymi typami storage i mechanizmami sieciowymi.
Jednocześnie wiele osób ma wobec Kubernetes nierealne oczekiwania. K8s nie zrobi za nikogo:
- dobrego projektu sieci i polityk bezpieczeństwa,
- monitoringu – trzeba skonfigurować Prometheus/Grafana lub inne narzędzia,
- backupów baz danych – od tego są osobne rozwiązania,
- samego builda obrazów – wciąż potrzebne są pipeline’y CI/CD.
Kubernetes jest potężny, ale wymaga zrozumienia sieci, storage’u i podstaw działania klastra. To nie jest „droższy Compose”, tylko osobna kategoria narzędzia.
Kiedy różnica ma znaczenie, a kiedy jest przedwczesną komplikacją
Popularna rada w sieci brzmi: „od razu ucz się Kubernetesa”. To jest dobra wskazówka tylko, gdy:
- przewidujesz, że aplikacja szybko urośnie do wielu usług i wielu środowisk,
- firma już ma klaster Kubernetes i dołączasz do istniejącej infrastruktury,
- chcesz zawodowo zajmować się DevOps/SRE i wiesz, że K8s będzie codziennym narzędziem.
Gdy tworzysz mały projekt, mikro-SaaS lub uczysz się konteneryzacji, przeskok prosto w Kubernetes często kończy się frustracją. W takim przypadku Docker Compose:
- pozwala szybciej zrozumieć podstawy kontenerów,
- uczciwie pokrywa większość potrzeb lokalnego developmentu,
- jest znacznie prostszy w konfiguracji i debugowaniu.
Dobrym kompromisem bywa podejście krokami: Docker → Docker Compose → Kubernetes. Dzięki temu każde narzędzie rozwiązuje realny problem, zamiast być wprowadzone tylko dlatego, że jest modne.
Podstawowe pojęcia – Docker, obrazy, kontenery, sieć, wolumeny
Obraz, kontener, registry i Dockerfile
Docker opiera się na kilku kluczowych pojęciach:
- Obraz (image) – szablon zawierający aplikację, system plików i wszystkie zależności. Jest niezmienny; z jednego obrazu można tworzyć wiele kontenerów.
- Kontener – działająca instancja obrazu. Ma własne procesy, swoje ID i cykl życia (start, stop, restart).
- Registry – repozytorium obrazów (np. Docker Hub, GitHub Container Registry, prywatny registry).
- Dockerfile – plik opisujący, jak zbudować obraz (FROM, COPY, RUN, CMD itp.).
Z punktu widzenia Compose i Kubernetes najważniejsze jest zrozumienie, że obie platformy konsumują obrazy. To, jak zbudujesz obraz, jest bardzo podobne w obu przypadkach. Ta sama aplikacja spakowana w obraz Docker może zostać uruchomiona przez Compose i przez K8s.
Tagi obrazów i wersjonowanie
Każdy obraz ma tag, np. myapp:1.0.0. Typowe praktyki:
:latest– domyślny tag, jeśli nie podasz innego. Wygodny na początku, ale nieprzewidywalny w produkcji.- tag semantyczny, np.
:1.2.3– dobry do jednoznacznego identyfikowania wersji. - tag gałęzi, np.
:dev,:staging– przydatne w pipeline’ach CI/CD.
Dla Kubernetesa i Compose tag ma kluczowe znaczenie przy rolloutach. Podmiana wersji w manifeście lub w pliku docker-compose.yml steruje tym, który obraz będzie uruchomiony. Zbyt częste używanie :latest utrudnia odtwarzanie dokładnego stanu systemu.
Sieci w Dockerze: bridge, port mapping, DNS po nazwach usług
Domyślny tryb sieci w Dockerze to bridge. Tworzona jest wirtualna sieć, w której kontenery mają swoje adresy IP. Aby udostępnić usługę na zewnątrz hosta, stosuje się mapowanie portów:
-p 8080:80oznacza „port 80 w kontenerze widoczny jako 8080 na hoście”.
Docker Compose idzie krok dalej i tworzy wspólną sieć dla projektu. Dzięki temu kontenery mogą się widzieć po nazwach usług zdefiniowanych w pliku YAML, np.:
- backend odwołuje się do bazy pod adresem
db:5432, gdziedbjest nazwą usługi w Compose.
Ta konwencja później naturalnie przekłada się na Kubernetesa, gdzie Service zapewnia stabilny DNS w stylu postgres.default.svc.cluster.local. Rozumienie prostego DNS w Compose ułatwia przejście do bardziej rozbudowanego modelu sieci w K8s.
Wolumeny i persystencja danych
Kontener z definicji jest ulotny. Po jego usunięciu dane w środku znikają, chyba że są zapisane we wspólnym wolumenie. Docker wspiera kilka mechanizmów przechowywania danych:
- Bind mount – mapowanie katalogu z hosta do kontenera (
/host/path:/container/path). Dobre do developmentu i debugowania. - Named volume – wolumen zarządzany przez Dockera, przechowywany w jego katalogach. Wygodny dla baz danych w środowisku lokalnym/małej produkcji.
Wolumeny a migracja między środowiskami
Przy prostych projektach najczęściej wystarcza lokalny named volume lub bind mount. Schody zaczynają się, gdy środowisk jest kilka (dev, test, prod), a dane nie mogą być przypadkowo nadpisane. Wtedy różnice między Compose a Kubernetes stają się praktycznie odczuwalne.
W Compose volume zwykle oznacza „coś na lokalnym dysku hosta”. To wystarcza, gdy:
- masz jedną maszynę (lub jedną maszynę na środowisko),
- dane nie muszą się przemieszczać między serwerami,
- backup ogarniasz osobnym skryptem lub narzędziem.
W Kubernetes wolumen rzadziej jest „lokalnym katalogiem”, częściej abstrakcją nad zewnętrznym storage’em (EBS, Ceph, NFS, CSI). Manifesty nie mówią już „trzymaj w /var/lib/docker na tym konkretnym serwerze”, ale „podłącz mi wolumen typu X o nazwie Y”. Dzięki temu Pod może zostać przeniesiony na inny węzeł, a dane jadą za nim (o ile driver storage to wspiera).
Konkretny efekt dla developera: lokalnie na Compose możesz bezrefleksyjnie użyć bind mounta z katalogu projektu. W klastrze Kubernetes takie podejście zwykle jest pułapką. Podczas migracji na K8s trzeba wtedy:
- przenieść dane do zewnętrznego storage (np. S3, RDS, managed PostgreSQL),
- zmienić konfigurację z „lokalny volume” na „usługa zewnętrzna + sekrety/ConfigMapy”.

Docker Compose w praktyce – od pierwszego pliku YAML do prostego stacka
Minimalny przykład: aplikacja + baza danych
Najprostszy, ale bardzo reprezentatywny scenariusz to aplikacja webowa plus baza. W Compose może to wyglądać tak:
version: "3.9"
services:
app:
image: myorg/myapp:1.0.0
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:pass@db:5432/mydb
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
volumes:
- mydb_data:/var/lib/postgresql/data
volumes:
mydb_data:
Kilka obserwacji, które przydadzą się później przy K8s:
- aplikacja nie zna IP bazy, używa tylko nazwy serwisu
db, - konfiguracja aplikacji siedzi w zmiennych środowiskowych (DATABASE_URL),
- dane bazy są w named volume
mydb_data, a nie w samym kontenerze.
Profile i różne konfiguracje środowisk
Często powtarzana rada brzmi: „miej jeden plik Compose na wszystko”. Działa na początku, ale szybko się rozjeżdża, gdy:
- dev wymaga dodatkowych narzędzi (np. pgAdmin, MailHog),
- prod potrzebuje innych limitów zasobów czy innych obrazów (np. z włączonym security hardening).
Compose ma profile i mechanizm override, które pozwalają uniknąć kopiowania całych plików. Przykład z profilami:
services:
app:
image: myorg/myapp:1.0.0
ports:
- "8080:8080"
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles:
- dev
Domyślnie odpali się tylko app. Dla developmentu:
docker compose --profile dev upTaki sam wzorzec łatwo potem przenieść do Kubernetesa, gdzie role profili pełnią różne manifesty lub kustomize/Helm values dla konkretnych środowisk.
Rozszerzanie stacka o dodatkowe usługi
Gdy aplikacja rośnie, pojawia się pokusa, żeby „na szybko” dołożyć kolejne serwisy w Compose: cache, kolejkę, panel administracyjny. Taki monolityczny docker-compose.yml zaczyna się wtedy rozrastać i trudno go utrzymać.
Praktyczniejsza strategia:
- podziel plik na kilka logicznych modułów, np.
compose.app.yml,compose.db.yml,compose.tools.yml, - łącz je przy starcie:
docker compose -f compose.app.yml -f compose.db.yml up, - traktuj narzędzia pomocnicze (np. adminer, pgAdmin, Redis Commander) jako osobne profile.
Takie uporządkowanie szczególnie pomaga, gdy później część usług (np. baza) migruje do zarządzanych usług chmurowych, a inne wędrują do Kubernetesa. Zamiast przepisywać gigantyczny plik, zamieniasz tylko konkretny moduł.
Limit zasobów i „symulacja” środowiska produkcyjnego
Compose pozwala zadeklarować limity CPU i RAM, ale wiele zespołów z tego nie korzysta. Efekt: aplikacja lokalnie działa „zawsze dobrze”, robiąc pełny użytek z zasobów developera, a w produkcji dławi się na ograniczonej maszynie lub w klastrze.
Przykład prostych limitów:
services:
app:
image: myorg/myapp:1.0.0
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
Na pojedynczym hoście Docker potraktuje to orientacyjnie, ale już samo „dławienie” aplikacji lokalnie pomaga wyłapać problemy z wydajnością. Później ten sam sposób myślenia przekłada się na rezerwacje i limity w Kubernetes (requests / limits).

Kubernetes od zera – najważniejsze klocki bez przeładowania teorią
Pod, ReplicaSet, Deployment – minimalny zestaw do aplikacji
W dokumentacji K8s łatwo utonąć w typach obiektów. W praktyce, żeby uruchomić standardową aplikację HTTP, potrzebujesz na początek tylko kilku:
- Pod – najmniejsza jednostka uruchomieniowa, zwykle 1 kontener (czasem kilka),
- ReplicaSet – pilnuje, żeby działała zadana liczba Podów,
- Deployment – zarządza ReplicaSetami i rolloutami nowych wersji.
W codziennej pracy bezpośrednio tworzysz Deployment, reszta dzieje się pod spodem. Przykład minimalnego Deploymentu odpowiadającego serwisowi app z Compose:
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: myorg/myapp:1.0.0
ports:
- containerPort: 8080
W porównaniu z Compose brak tu jeszcze sieci i konfiguracji, ale już widać kluczową różnicę: replicas. W Compose skalowanie to dodatek (docker compose up --scale app=3), w Kubernetes jest pierwszoplanowym elementem definicji.
Service – stały adres dla zmiennych Podów
Pody są efemeryczne: mogą zostać restarowane, przeniesione na inne węzły, zmienić IP. Aplikacja kliencka nie może więc łączyć się z Podem „po adresie”. Potrzebny jest stały endpoint, który przeżyje rotację Podów. To rola obiektu Service.
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
app: app
ports:
- name: http
port: 80
targetPort: 8080
Dzięki temu inne Pody w tym samym namespace mogą używać adresu http://app, bardzo podobnie jak app w Compose. Różnica: Service jest load balancerem dla wielu Podów z etykietą app=app, więc bez dodatkowego wysiłku otrzymujesz rozłożenie ruchu i podstawową odporność na awarie pojedynczej repliki.
ConfigMap i Secret – odpowiednik zmiennych środowiskowych, ale z wersjonowaniem
Popularnym podejściem w Compose jest trzymanie konfiguracji bezpośrednio w pliku YAML. Ma to jedną słabą stronę: zmiana konfiguracji wymaga zwykle przebudowania obrazu lub przynajmniej modyfikacji pliku, który bywa wspólny dla wielu środowisk.
W Kubernetes konfiguracja jest wyniesiona do osobnych obiektów:
- ConfigMap – niesekretne wartości konfiguracyjne,
- Secret – hasła, klucze API itp. (zakodowane base64, często dodatkowo szyfrowane przez narzędzia zewnętrzne).
Prosty przykład ConfigMapy oraz użycia w Deployment:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: "production"
LOG_LEVEL: "info"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: myorg/myapp:1.0.0
envFrom:
- configMapRef:
name: app-config
Takie podejście wymusza rozdzielenie: obraz ≠ konfiguracja. W Compose też można to osiągnąć (.env, osobne pliki z env), ale w K8s jest to wzorzec bazowy, nie „miły dodatek”. Dzięki temu łatwiej utrzymać różne konfiguracje dla wielu środowisk przy jednej wersji obrazu.
PersistentVolume i PersistentVolumeClaim – abstrakcja nad storage’em
Odpowiednikiem named volume z Dockera jest w Kubernetes zestaw PersistentVolume (PV) + PersistentVolumeClaim (PVC). PV opisuje rzeczywisty kawałek storage’a, a PVC jest „zamówieniem” na ten storage przez aplikację.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
env:
- name: POSTGRES_USER
value: user
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
W klastrze tworzonym w chmurze najczęściej nie definiujesz PV ręcznie – robi to za ciebie StorageClass. Aplikacja widzi tylko PVC, nie musi wiedzieć, czy stoi pod tym EBS, dysk sieciowy czy lokalny wolumen. To fundamentalna różnica w porównaniu z Compose, gdzie często twardo wiążesz się z lokalnym katalogiem lub konkretnym typem volume drivera.
Ingress – ekspozycja usług na zewnątrz klastra
Jedna z popularnych rekomendacji brzmi: „wystaw Service typu LoadBalancer dla każdej aplikacji”. W chmurze publicznej szybko kończy się to lawiną load balancerów, kosztów i chaosu w routingu.
Kubernetes ma obiekt Ingress, który pełni rolę warstwy HTTP/HTTPS przed wieloma usługami. Przykład:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 80
W praktyce to odpowiednik Nginx/Traefika, który w Compose stawiasz jako osobny kontener reverse proxy. W Kubernetesa ta rola jest „wbudowana” w model obiektów klastra, chociaż konkretny kontroler Ingress (nginx, Traefik, HAProxy) i tak działa jako Deployment w klastrze.
Docker Compose vs Kubernetes na konkretnych przykładach
Przykład 1: proste środowisko developerskie
Załóżmy, że zespół backendowy ma jedną aplikację API i jedną bazę danych. Główne potrzeby:
- łatwy start środowiska na laptopie (
docker compose up), - debugowanie kodu z IDE,
- możliwość wyczyszczenia danych i startu „od zera”.
Compose sprawdza się tu lepiej niż Kubernetes z kilku przyczyn:
- nie wymaga lokalnego klastra ani narzędzi typu kind/k3d/minikube,
- restart stacka jest szybki, plik
docker-compose.ymljest łatwy do zrozumienia dla całego zespołu, - mapowanie katalogu kodu z hosta do kontenera ułatwia hot-reload.
W tym scenariuszu Kubernetes jako środowisko developerskie częściej przeszkadza niż pomaga: start klastra trwa dłużej, debugowanie wymaga dodatkowych narzędzi (kubectl port-forward, skaffold, tilt), a zysk z „realistyczności” jest mizerny przy jednej usłudze.
Przykład 2: mikroserwisy z kilkoma zespołami
Inny biegun: firma ma kilkanaście mikroserwisów, każdy rozwijany przez osobny zespół. Jest kilka środowisk (dev, staging, prod), część usług jest wspólna (np. centralne auth), część zależy od feature flag. W tym kontekście Compose na poziomie organizacji zaczyna się sypać:
- trudno utrzymać spójność wersji między zespołami (każdy ma swój
docker-compose.yml), - brak centralnego sposobu na rollouty i rollbacki,
- monitoring każdego stacka „po swojemu” multiplikuje narzędzia.
Przykład 3: pierwsze wdrożenie produkcyjne „na poważnie”
Dość częsty scenariusz: aplikacja monolityczna, kilka środowisk, rosnący ruch, wymagania typu „zero-downtime podczas deployu” i „monitoring z prawdziwego zdarzenia”. Na tym etapie wiele zespołów próbuje „dociągnąć” środowisko produkcyjne na Docker Compose, bo przecież „działa na stagingu”.
Przez jakiś czas się uda – osobny VM na każdy stack, Compose jako init system, manualne deploye przez SSH. Granica pojawia się przy takich momentach:
- konieczność automatycznego rollbacku po nieudanym deployu,
- skalowanie w poziomie w reakcji na ruch (bez nocnego dyżuru „pod F5”),
- wymóg separacji zasobów między usługami (np. limity CPU/RAM).
To jest ten punkt, w którym Kubernetes zaczyna mieć przewagę nie dlatego, że jest „modny”, tylko dlatego, że rozwiązuje powtarzalne problemy infrastruktury z pudełka. Deployment z strategy: RollingUpdate, limity zasobów w specyfikacji Podów, automatyczny reschedule przy awarii węzła – to wszystko składa się na mniejsze ryzyko „niespodzianek” w produkcji.
Compose można próbować łatać zewnętrznymi narzędziami (systemd, ansible, własne skrypty health-checków), ale każda taka łatka jest customowa. W K8s dostajesz ustandaryzowany język opisu aplikacji, który rozumieją inni inżynierowie, konsultanci czy vendorzy.
Przykład 4: hybryda – Compose lokalnie, Kubernetes w chmurze
Najczęściej spotykany i najbardziej pragmatyczny model to połączenie obu światów. Na laptopie programisty – Docker Compose. Na staging/produkcji – Kubernetes. Z pozoru powstają dwa równoległe światy, w praktyce można je zbliżyć.
Klucz leży w tym, żeby:
- te same obrazy były używane lokalnie i w klastrze,
- konfigurację rozdzielić od obrazu (env, ConfigMap/Secret),
- ograniczyć logikę „infrastrukturalną” w Compose do minimum.
Lokalny docker-compose.yml pełni wtedy funkcję „adaptera developerskiego”: spina kontenery, wystawia porty, czasem dołącza lokalne wolumeny z kodem. Po stronie Kubernetesa te same usługi opisujesz przez Deployment/Service/Ingress, ale bez debug-friendly dodatków typu bind mount z katalogu źródeł.
Popularna rada brzmi: „najpierw naucz się Kubernetes, potem wracaj do Compose”. Odwrócenie tego porządku działa często lepiej. Najpierw składnia Compose, proste zależności serwisów, zobaczenie jak aplikacja zachowuje się w kontenerach. Dopiero wtedy wejście w K8s – już z intuicją, co tak naprawdę orkiestrujesz.
Przykład 5: systemy batchowe i zadania cykliczne
Na etapie pierwszego kontaktu z K8s wiele osób sprowadza temat do „aplikacje HTTP”. Tymczasem, jeśli system ma dużo batchy, ETL-i, cronów, kontrast między Compose a Kubernetes robi się wyjątkowo wyraźny.
W Compose typowe podejścia to:
- skrypty cron na hoście, które wywołują
docker run ..., - jeden kontener z crond w środku i kolekcją skryptów,
- osobny „worker” kontener nasłuchujący kolejki i uruchamiający zadania.
Każde z nich działa, dopóki nie trzeba:
- precyzyjnie śledzić historii i statusu poszczególnych uruchomień,
- skalować liczbę równoległych zadań w zależności od obciążenia,
- łatwo wstrzymać/odtworzyć pojedynczy job.
W Kubernetes do tej kategorii służą obiekty CronJob i Job. Każde wywołanie CronJoba tworzy Job, a ten – Poda, który wykonuje jedno zadanie i kończy się. Status tego Poda jest widoczny w kubectl, logi łatwo zebrać centralnie, a regulacja konkurencji (np. concurrencyPolicy: Forbid) jest częścią definicji, nie zestawem shellowych trików.
Tu Kubernetes ma prawie bezdyskusyjną przewagę. Da się zbudować elegancki system batchowy na Compose, ale zwykle kończy się to własnym mini-orkiestratorem, który rozwiązuje problemy, jakie K8s rozwiązał już lata temu.
Przykład 6: środowiska efemeryczne dla gałęzi / PR-ów
Coraz popularniejsza praktyka: dla każdego Pull Requesta powstaje osobne środowisko, na którym można „poklikać” zmianę razem z frontendem, backendem i zależnymi usługami. Te środowiska żyją kilka godzin lub dni i są potem automatycznie usuwane.
Na Compose jest to możliwe, ale zwykle kończy się w ten sposób:
- pipeline CI tworzy nową maszynę (lub stack na tej samej),
- generuje
docker-compose.ymlze zmienionymi portami/nazwami, - czyści zasoby skryptem na końcu.
Działa, dopóki liczba równoległych środowisk jest mała. Przy większej skali łatwo wpaść w pułapkę „zombie stacków”, które przeżyły swój PR, bo pipeline się wysypał przed cleanupem.
W K8s każde takie efemeryczne środowisko to osobny namespace, który można związać etykietą (np. pr-id=1234). Pipeline tworzy namespace, stosuje manifesty (lub Helm chart), a na końcu usuwa namespace jednym poleceniem. Jeśli cleanup się nie wykona, prosty kubectl get ns -l pr-id ujawni „sieroty”.
To jest przykład miejsca, gdzie rada „Compose jest prostszy, używaj go zawsze, gdy możesz” przestaje się bronić. Przy dynamicznej, krótkotrwałej infrastrukturze język deklaratywny K8s (namespace’y, etykiety, selektory) wygrywa z ręcznie zarządzanymi stackami.
Jak myśleć o migracji: odwzorowanie pojęć Compose → Kubernetes
Naturalny tok myślenia przy pierwszym kontakcie z K8s to „jak przerobić mojego docker-compose.yml na manifesty”. Samo przepisywanie składni jest najmniej ciekawsze. Więcej zyskasz, traktując migrację jak zmianę modelu, nie tylko narzędzia.
Ogólna mapa pojęć wygląda następująco:
- service w Compose → Deployment + Service (czasem też Ingress),
- depends_on → brak bezpośredniego odpowiednika, raczej
readinessProbe+livenessProbe, - ports →
containerPortw Podzie +port/targetPortw Service, - volumes →
volumesw Podzie + PVC (jeśli dane mają przetrwać Pod), - environment →
env,envFromz ConfigMap/Secret.
Zależności między usługami są częstym miejscem błędnych oczekiwań. W Compose depends_on bywa używane jako „poczekaj na bazę danych”. W K8s nie ma natywnego „uruchom X dopiero, gdy Y jest gotowe”. Zamiast tego aplikacja musi sama poradzić sobie z niedostępnością zależności (retry, backoff), a K8s jedynie dba o to, żeby kontener żył dopiero wtedy, gdy jego własny readinessProbe przejdzie.
To ważny mentalny skok: odpada złudzenie, że orkiestrator „załatwi” za nas złożoną kolejność startu. Kubernetes zapewnia infrastrukturę, ale odporność na błędy między usługami jest dalej odpowiedzialnością aplikacji.
Najczęstsze pułapki przy przejściu z Compose na Kubernetes
Przy pierwszych wdrożeniach do K8s powtarza się kilka typowych wpadek. Część wynika z bezrefleksyjnego „przepisania” Compose 1–1.
- Brak limitów zasobów – w Compose kontener rzadko „gryzie” innych, bo często ma maszynę tylko dla siebie. W K8s brak
resources.requests/limitsoznacza potencjalne problemy z sąsiadami w klastrze. - Przeniesienie bind mountów 1:1 – lokalnie
./src:/appułatwia development. W klastrze taki mount byłby zależnością od konkretnego węzła i łamie ideę „pet vs cattle”. Tam miejsce ma PVC, a nie katalog z NFS-a podpięty na chybił trafił. - Używanie Init Containerów zamiast poprawienia aplikacji – kuszące jest zbudowanie skomplikowanych initów, które „zaczekają” na bazę, zrobią migracje, wypełnią cache. Część z tych zadań powinna jednak trafić do samej aplikacji (idempotentne migracje, retry), nie do otoczki.
- Jedna ConfigMap „do wszystkiego” – odpowiednik jednego ogromnego
.envw Compose. Przy pierwszej zmianie w stagingu, która nie powinna trafić do produkcji, zaczyna się ręczne rozdzielanie. Lepiej od razu rozciąć konfigurację po domenach (np.app-config,db-config,feature-flags).
Zamiast kopiować rozwiązania 1–1, bezpieczniej jest zadać pytanie: „jaki problem próbujemy rozwiązać tym hackiem w Compose?” i sprawdzić, czy K8s nie ma na to natywnego obiektu lub wzorca.
Kiedy zostać przy Compose, mimo „presji na Kubernetes”
Obiegowa narracja bywa prosta: „Compose jest do zabawy, Kubernetes do produkcji”. Rzeczywistość jest mniej binarna. Są sytuacje, w których siłowe wdrażanie K8s jest drogą donikąd.
Przykładowe sygnały, że Compose w produkcji nadal ma sens:
- jeden lub dwa serwisy, bez planów rozbudowy architektury,
- brak dedykowanego zespołu SRE/DevOps, a inżynierowie aplikacyjni nie mają czasu na naukę K8s,
- brak wymogów typu multitenancy, autoscaling czy złożone polityki sieciowe.
W takim kontekście próba postawienia własnego klastra, budowania CI/CD, chartów, obserwowalności i polityk może zjeść więcej czasu niż sama budowa aplikacji. Jeżeli problemy, które rozwiązuje Kubernetes, jeszcze się nie pojawiły, można świadomie zostać przy Compose i inwestować w jakość kodu, testy, automatyzację backupów czy monitoringu na poziomie hosta.
Sensowną alternatywą bywa PaaS (np. Heroku- podobne platformy, Cloud Run, App Service), gdzie kontener jest jednostką wdrożenia, ale orkiestrator jest abstrahowany. Dla małych zespołów bywa to bardziej racjonalny krok niż bezpośredni skok w Kubernetesa.
Kiedy Compose zaczyna „trzeszczeć” i warto szukać Kubernetesa
Z drugiej strony, są symptomy, że obecne środowisko na Compose osiągnęło granice skalowalności organizacyjnej lub technicznej. Zamiast kierować się modą, lepiej szukać konkretnych sygnałów.
- Manualne operacje podczas deployów – jeśli część kroku wdrożenia to wciąż „zaloguj się na serwer X, zrestartuj Y, zmień Z”, to znaczy, że brakuje deklaratywnego modelu infrastruktury.
- Konflikty o porty, nazwy hostów, zasoby – rosnąca liczba usług na tych samych hostach zaczyna kolidować, a obejścia to rosnące rozbudowane skrypty.
- Problemy z izolacją środowisk – jeśli staging i produkcja dzielą ten sam serwer „bo tak wyszło”, a Compose tylko zmienia kilka portów i zmiennych, każdy błąd w konfiguracji jest potencjalnie produkcyjny.
- Coraz większa liczba cronów, workerów, batchy – każda kolejna klasa zadań to kolejny „specjalny” kontener i dodatkowa logika w skryptach.
W takim momencie K8s nie jest „fajnym gadżetem”, tylko narzędziem porządkującym chaos. Dzięki namespace’om, etykietom, Deploymentom i Jobom udaje się zredukować ilość „niemożliwych do odtworzenia ręcznie” konfiguracji.
Jak stopniowo oswajać Kubernetes, nie porzucając Compose
Rady w stylu „przepisz wszystko od razu na Kubernetesa” brzmią efektownie, ale często kończą się paraliżem. Bezpieczniejsza ścieżka to ewolucja.
Praktyczny plan etapów:
- Ustabilizuj obrazy – upewnij się, że aplikacje mają sensowne Dockerfile, nie zawierają środowiskowej konfiguracji „na twardo” i działają przewidywalnie pod
docker runbez Compose. - Wyodrębnij konfigurację – przenieś większość ustawień do zmiennych środowiskowych / plików konfiguracyjnych. W Compose użyj
env_file, lokalnych.env. To uprości późniejsze mapowanie na ConfigMap/Secret. - Zbuduj pierwszy mini-klaster – k3d, kind lub managed cluster w chmurze. Uruchom w nim jedną małą usługę, najlepiej taką, która ma mało zależności.
- Odwzoruj tylko część stacka – np. API w K8s, ale baza nadal jako zarządzana usługa (RDS, Cloud SQL) albo klasyczny VM. Dzięki temu zmniejszasz liczbę ruchomych części na start.
Najczęściej zadawane pytania (FAQ)
Czym różni się Docker Compose od Kubernetesa w praktyce?
Docker Compose zarządza zestawem kontenerów na jednej maszynie. Opisujesz usługi w pliku docker-compose.yml, a potem jednym poleceniem je uruchamiasz, zatrzymujesz, tworzysz im sieć i wolumeny. Dobrze sprawdza się do lokalnego developmentu i prostych wdrożeń na pojedynczym serwerze.
Kubernetes zarządza całym klastrem maszyn. Sam decyduje, na którym węźle uruchomić kontener, pilnuje liczby replik, potrafi skalować aplikację na podstawie metryk i robić rolling updates. To bardziej „system operacyjny dla klastra” niż „wygodny wrapper na Dockera”.
Kiedy wystarczy Docker Compose, a kiedy trzeba myśleć o Kubernetes?
Compose wystarczy, gdy aplikacja składa się z kilku usług na jednej maszynie: małe API, baza danych, może kolejka lub worker. Typowy scenariusz: lokalne środowisko programisty, staging na jednym serwerze, mały projekt klienta bez dużych wymagań wydajnościowych.
Kubernetes zaczyna mieć sens, gdy pojawia się potrzeba: uruchamiania wielu instancji tej samej usługi na różnych maszynach, automatycznego skalowania pod ruch, niezawodności na poziomie „data center” oraz standardu wdrożeń dla wielu zespołów. Jeśli Twoja aplikacja i tak mieści się wygodnie na jednym serwerze, K8s częściej będzie nadmiarem niż pomocą.
Czy do nauki kontenerów lepiej od razu zaczynać od Kubernetes?
Popularna rada „od razu ucz się Kubernetesa” sprawdza się wyłącznie wtedy, gdy wchodzisz do firmy, która już ma klaster, albo celujesz w rolę DevOps/SRE i wiesz, że to będzie Twoje codzienne narzędzie. Wtedy ma sens przeskoczenie poziomu Compose, bo i tak większość pracy wykonasz w K8s.
Jeśli dopiero ogarniasz, czym jest obraz, kontener, wolumen i sieć, znacznie szybciej zrozumiesz to na Dockerze i Compose. Kubernetes na starcie miesza warstwę orkiestracji z podstawami, przez co łatwo zniechęca. Lepsza ścieżka dla wielu osób to: Docker → Docker Compose → dopiero potem Kubernetes.
Czy Kubernetes zastępuje Docker Compose?
Kubernetes nie jest „Compose na sterydach”. Rozwiązuje inny problem: nie skupia się na opisaniu kilku usług na jednym serwerze, tylko na zarządzaniu zasobami całego klastra. Sam fakt, że coś jest w kontenerze, nie oznacza, że trzeba to od razu wrzucać do Kubernetes.
W praktyce Compose i K8s często współistnieją. Wielu programistów dalej używa Docker Compose lokalnie (proste docker compose up), a dopiero na środowiskach testowych/produkcyjnych aplikacja ląduje w Kubernetes. To dwa narzędzia do różnych etapów i skal.
Czy na produkcji można zostać przy Docker Compose?
Tak, pod warunkiem że akceptujesz jego ograniczenia. Na pojedynczym, dobrze monitorowanym serwerze z niewielkim ruchem Compose potrafi być mniej skomplikowanym i tańszym rozwiązaniem niż cały klaster K8s. Dla małego SaaS-a, aplikacji wewnętrznej czy MVP to często rozsądny kompromis.
Problemy zaczynają się, gdy potrzebujesz: wysokiej dostępności przy awarii serwera, automatycznego skalowania, izolacji na poziomie wielu węzłów czy standardowego sposobu wdrażania dla kilku zespołów. Wtedy sam Compose stanie się wąskim gardłem i ręczną orkiestracją.
Dlaczego w ogóle potrzebna jest orkiestracja kontenerów?
Dopóki uruchamiasz pojedynczy kontener jednym docker run, orkiestracja wygląda jak przerost formy. Gdy tylko aplikacja składa się z kilku usług (frontend, backend, baza, kolejka, worker), pojawia się konieczność pilnowania kolejności startu, sieci między kontenerami, wspólnych zmiennych środowiskowych i restartów po awarii.
Orkiestracja (Compose na jednej maszynie, Kubernetes na klastrze) rozwiązuje trzy realne problemy: skalowanie (więcej instancji bez ręcznego klikania), aktualizacje (rolling update, rollback bez reinstalacji serwera) i awarie (automatyczne podnoszenie padniętych instancji). Bez tego prędzej czy później kończysz z ręcznie utrzymywaną „zupą kontenerów”.
Czy kontenery zastępują maszyny wirtualne?
Kontenery i VM-y rozwiązują inne problemy. Maszyna wirtualna emuluje cały system operacyjny z własnym kernelem, co daje mocną izolację kosztem większych zasobów i dłuższego startu. Dobrze nadaje się do uruchamiania zupełnie różnych systemów (np. Windows i Linux) na jednym serwerze.
Kontener korzysta z jądra hosta, więc startuje szybciej, zajmuje mniej miejsca i łatwiej go przenosić między środowiskami. Świetnie nadaje się do pakowania aplikacji z zależnościami. W wielu środowiskach oba podejścia działają równolegle: na VM-ach stoi klaster, a na nim dopiero uruchamiane są kontenery.






