Kurs NestJS · Moduł 12: Konteneryzacja i CI/CD
Projekt główny - konteneryzacja i CI/CD Pipeline
W tej lekcji7
Cały moduł składał się z osobnych narzędzi: obraz, Compose, pipeline, sekrety, metryki, tracing, plan infrastruktury. Teraz zbierzemy je w jedną konfigurację - i przy okazji przypomnimy sobie, dlaczego każda decyzja wygląda tak, a nie inaczej. Bo to właśnie te uzasadnienia odróżniają konfigurację skopiowaną od zrozumianej.
Obraz - dwa etapy
1FROM node:20-alpine AS builder
2WORKDIR /app
3COPY package.json yarn.lock ./
4RUN yarn install --frozen-lockfile
5COPY . .
6RUN yarn build
7
8FROM node:20-alpine AS production
9WORKDIR /app
10RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
11COPY /app/dist ./dist
12COPY /app/node_modules ./node_modules
13USER nestjs
14EXPOSE 4000
15HEALTHCHECK \
16 CMD curl -f http://localhost:4000/health || exit 1
17CMD ["node", "dist/main"]Trzy decyzje w tym pliku warto umieć uzasadnić.
COPY --from=builder przenosi wynik z pierwszego etapu do drugiego. Kolejność członów jest stała: COPY, --from=builder, /app/dist (skąd), ./dist (dokąd). Dzięki temu w obrazie produkcyjnym nie ma kompilatora, źródeł ani zależności deweloperskich - zostaje sam wynik.
USER nestjs - użytkownika bez uprawnień root tworzymy, aby ograniczyć potencjalne szkody w przypadku włamania. Nie po to, żeby kontener startował szybciej, żeby obraz był mniejszy ani żeby umożliwić wielowątkowość. Proces działający jako root wewnątrz kontenera, po wyrwaniu się z izolacji, jest rootem także na hoście.
HEALTHCHECK mówi Dockerowi, jak sprawdzić, czy aplikacja żyje naprawdę, a nie tylko czy proces istnieje. Kolejność: healthcheck:, test: curl -f http://localhost:4000/health, interval: 30s, retries: 3.
Compose - trzy serwisy
W pliku Compose sprawdź dwie rzeczy, które najczęściej się myli.
Kontener api łączy się z bazą przez nazwę serwisu: mongodb:27017 - nie przez adres IP kontenera, nie przez localhost:27017 i nie przez zewnętrzny DNS. localhost wewnątrz kontenera to ten sam kontener, więc wskazywałby na API, nie na bazę.
Flaga -v w docker compose down -v usuwa wolumeny wraz z danymi. Nie włącza trybu szczegółowych logów, nie waliduje pliku i nie wypisuje wersji. To polecenie kasuje bazę - używaj go świadomie.
Wdrożenie - kolejność
Pełna droga zmiany na produkcję ma cztery etapy:
- Push kodu do gałęzi main.
- CI: lint, test, build.
- Docker build & push image.
- Smoke tests i monitoring po wdrożeniu.
Czwarty etap bywa pomijany, a jest tym, który odróżnia „wdrożone" od „działa". Smoke tests to garść sprawdzeń najważniejszych ścieżek zaraz po wdrożeniu - czy strona się otwiera, czy logowanie przechodzi, czy zdrowie odpowiada. Pipeline mógł przejść na zielono, a aplikacja i tak nie wstać, bo zabrakło zmiennej środowiskowej, której na CI nikt nie potrzebował.
Kubernetes - dwa polecenia
Gdy obraz jest gotowy, klaster przyjmuje opis zasobów z pliku:
1kubectl apply -f deployment.yamlKolejność członów: kubectl, apply, -f, deployment.yaml. Flaga -f wskazuje plik z opisem; apply doprowadza klaster do stanu w nim zapisanego - tworzy, czego brakuje, i zmienia, co się różni.
Skalowanie to osobne polecenie:
1kubectl scale deployment roman-api --replicas=5Tu kolejność brzmi: kubectl scale, deployment roman-api, --replicas=5. Pięć replik znaczy pięć równoległych kopii aplikacji, między które klaster rozdzieli ruch.
Terraform - moduły
Ostatnia rzecz z warstwy infrastruktury. Moduły w Terraform to reużywalne bloki infrastruktury, które można parametryzować - nie pluginy do komunikacji z chmurą (tym są providery), nie testy jednostkowe i nie pliki logów.
Sens jest ten sam co przy funkcji w kodzie: opisujesz raz „serwer aplikacyjny z bazą i siecią", a potem wywołujesz to trzy razy dla środowiska testowego, przedprodukcyjnego i produkcyjnego, zmieniając tylko parametry.
Checklista projektu
Zbierz komplet i sprawdź każdy punkt:
- Docker - obraz wieloetapowy, użytkownik bez roota,
HEALTHCHECK, - Docker Compose - trzy serwisy, łączenie po nazwach, wolumen na dane bazy,
depends_onzservice_healthy, - CI/CD - workflow na
pushipull_request, joby w kolejności lint → build → test → docker, sekrety w ustawieniach repozytorium, - Sekrety - żadnego hasła w repozytorium;
.envtylko lokalnie, - Observability - endpoint
/metrics, metryki RED, tracing zspan.end()wfinally, - Infrastructure as Code - plan w plikach
.tf, stan w backendzie zdalnym, nie w Gicie.
Podsumowanie
Kampania gotowa do wymarszu:
COPY --from=builderprzenosi wynik budowania:COPY,--from=builder, źródło, cel,- użytkownika bez roota tworzymy, aby ograniczyć szkody przy włamaniu - nie dla szybkości ani rozmiaru obrazu,
HEALTHCHECKw kolejności:healthcheck:,test:,interval:,retries:,- kontenery łączą się przez nazwę serwisu (
mongodb:27017), nie przez IP anilocalhost, docker compose down -vusuwa wolumeny wraz z danymi,- kolejność wdrożenia: push → CI (lint, test, build) → docker build & push → smoke tests i monitoring,
kubectlapply-fdeployment.yamlwprowadza opis do klastra,kubectl scaledeployment nazwa--replicas=5zmienia liczbę kopii,- moduły Terraform to reużywalne, parametryzowalne bloki infrastruktury - nie providery, nie testy, nie logi.
To ostatnia lekcja modułu. Umiesz teraz zbudować obraz, opisać środowisko, przepuścić zmianę przez pipeline, ukryć sekrety, zobaczyć, co dzieje się na produkcji, i odtworzyć całą infrastrukturę z pliku. A na razie zapamiętaj: każdy element tej konfiguracji ma uzasadnienie - i to jego znajomość, a nie sam plik, przenosi się na następny projekt.
Kod do tej lekcji: src/deployment-pipeline.ts
1// Projekt Glowny - Kompletny Deployment Pipeline
2console.log("=== DROGI IMPERIUM - DEPLOYMENT PIPELINE ===\n");
3
4interface ChecklistItem {
5 category: string;
6 items: { name: string; status: boolean }[];
7}
8
9const checklist: ChecklistItem[] = [
10 {
11 category: 'Docker',
12 items: [
13 { name: 'Multi-stage Dockerfile', status: true },
14 { name: 'Non-root user', status: true },
15 { name: '.dockerignore', status: true },
16 { name: 'HEALTHCHECK w Dockerfile', status: true },
17 ],
18 },
19 {
20 category: 'Docker Compose',
21 items: [
22 { name: 'API + MongoDB + Redis', status: true },
23 { name: 'Health checks na serwisach', status: true },
24 { name: 'Wolumeny dla danych', status: true },
25 { name: 'Izolowana siec', status: true },
26 ],
27 },
28 {
29 category: 'CI/CD',
30 items: [
31 { name: 'Lint + Test + Build', status: true },
32 { name: 'Docker build & push', status: true },
33 { name: 'GitHub Secrets', status: true },
34 ],
35 },
36 {
37 category: 'Security',
38 items: [
39 { name: 'Helmet', status: true },
40 { name: 'CORS', status: true },
41 { name: 'Rate limiting', status: true },
42 { name: 'ValidationPipe', status: true },
43 { name: 'Compression', status: true },
44 ],
45 },
46 {
47 category: 'Monitoring',
48 items: [
49 { name: 'Health check endpoint', status: true },
50 { name: 'Structured logging', status: true },
51 { name: 'Log rotation', status: true },
52 ],
53 },
54];
55
56checklist.forEach(cat => {
57 console.log(`\n${cat.category}:`);
58 cat.items.forEach(item => {
59 const icon = item.status ? 'DONE' : 'TODO';
60 console.log(` [${icon}] ${item.name}`);
61 });
62});
63
64const total = checklist.reduce((s, c) => s + c.items.length, 0);
65const done = checklist.reduce(
66 (s, c) => s + c.items.filter(i => i.status).length, 0
67);
68console.log(`\nStatus: ${done}/${total} elementow gotowych`);
69console.log("\nDrogi Imperium gotowe - od kodu do produkcji!");
70Widzisz błąd w tej lekcji?