Тестування, документування та розгортання застосунків

Docker та контейнеризація

Docker та контейнеризація

🎯 Мета лекції

  • Опанувати концепції Docker контейнеризації для упаковки NestJS застосунків.
  • Навчитися створювати оптимізовані Dockerfile з multi-stage builds для зменшення розміру образів.
  • Освоїти Docker Compose для оркестрації multi-container середовищ (NestJS + PostgreSQL + Redis).
  • Зрозуміти принципи volumes для персистентності даних та networking для комунікації між контейнерами.
  • Впровадити best practices для безпечних та ефективних production-ready Docker образів.

🔑 Ключові терміни

  • Container: ізольоване середовище виконання з власною файловою системою, процесами та мережею.
  • Image: незмінний template для створення контейнерів, містить ОС, залежності та код застосунку.
  • Dockerfile: текстовий файл з інструкціями для побудови Docker image.
  • Multi-stage Build: техніка оптимізації, що використовує кілька базових образів для зменшення final image size.
  • Docker Compose: інструмент для декларативного опису та запуску multi-container застосунків через YAML конфігурацію.

Короткий зміст

У цій лекції вивчається упаковка NestJS застосунку у Docker контейнери для reproducible deployments:

  • Docker концепція — контейнеризація для ізоляції застосунків, образи (images) як templates, контейнери (containers) як running instances, переваги: consistency across environments, easy scaling, dependency isolation
  • Dockerfile для NestJS — інструкції для побудови image: FROM node:18-alpine базовий образ, WORKDIR для робочої директорії, COPY для файлів, RUN для команд (npm install), CMD для запуску застосунку, EXPOSE для порту
  • Multi-stage build — оптимізація розміру образу: stage 1 (builder) для npm install та build, stage 2 (production) лише з dist/ та production dependencies, копіювання артефактів між stages, зменшення final image size з ~1GB до ~200MB
  • .dockerignore — виключення файлів з build context: node_modules/, .git/, .env, *.md, зменшення build time та розміру image
  • Docker Compose — оркестрація multi-container застосунків, docker-compose.yml з services: app (NestJS), db (PostgreSQL), redis, networks для communication, volumes для persistence
  • Volumes — персистентність даних БД через named volumes, bind mounts для development (live reload), anonymous volumes для node_modules
  • Networking — communication між контейнерами через service names, environment variables для connection strings (DATABASE_HOST=db), port mapping для доступу ззовні (-p 3000:3000)
  • Best practices — використання .dockerignore, multi-stage builds, non-root user для security, health checks, minimal base images (alpine), layer caching optimization

Розглядаються практичні приклади: Dockerfile для NestJS з multi-stage, docker-compose.yml для dev environment (NestJS + PostgreSQL + Redis), production-ready setup, debugging у контейнері.


Проблема розгортання без контейнеризації

На попередніх лекціях ми розробили повнофункціональний NestJS застосунок із автентифікацією, тестуванням, документацією та конфігурацією. Проте виникає питання: як розгорнути цей застосунок у production, щоб він працював стабільно та передбачувано?

Традиційний підхід «розгортання на сервері» створює низку проблем:

1. "Works on my machine" синдром. Застосунок працює на локальній машині розробника (macOS з Node.js 18.16.0), але падає на production сервері (Ubuntu з Node.js 16.20.0). Причина — різні версії Node.js, системних бібліотек, залежностей.

2. Складність налаштування середовища. Для запуску застосунку потрібно вручну встановити Node.js, PostgreSQL, Redis, налаштувати environment variables, запустити міграції БД. Процес займає години та схильний до людських помилок.

3. Конфлікти залежностей. На одному сервері розгорнуто два застосунки: один вимагає Node.js 14, інший — Node.js 18. Без контейнеризації це вимагає складних workarounds через nvm або окремі сервери.

4. Відсутність ізоляції. Застосунки на одному сервері ділять файлову систему, мережу, ресурси. Збій одного може впливати на інші (наприклад, memory leak у одному застосунку споживає всю RAM).

5. Складність масштабування. Щоб запустити 3 інстанси застосунку для балансування навантаження, потрібно вручну налаштовувати кожен сервер — довгий та помилконебезпечний процес.

Рішення — Docker контейнеризація. Docker упаковує застосунок разом із усіма залежностями (Node.js, npm packages, системні бібліотеки) в ізольований контейнер, що гарантує однакову поведінку на будь-якому середовищі:

# Локальна машина розробника
docker run -p 3000:3000 my-nestjs-app

# Staging сервер
docker run -p 3000:3000 my-nestjs-app

# Production сервер
docker run -p 3000:3000 my-nestjs-app

# Результат однаковий на всіх середовищах ✅
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "Development" as Dev #DBEAFE {
  card "Розробник" as DevPerson
  card "docker build" as Build
  card "Docker Image" as DevImage #FEF3C7
}

rectangle "Registry" as Registry #DCFCE7 {
  card "Docker Hub\nAWS ECR\nGitHub Registry" as Reg
}

rectangle "Production" as Prod #FEE2E2 {
  card "docker pull" as Pull
  card "Docker Container" as Container #FEF3C7
  card "Running App" as App
}

DevPerson --> Build : "Пише код"
Build --> DevImage : "Створює image"
DevImage --> Reg : "docker push"
Reg --> Pull : "docker pull"
Pull --> Container : "Створює контейнер"
Container --> App : "Запускає застосунок"

@enduml

Переваги Docker:

  1. Consistency across environments: той самий Docker image працює на macOS, Linux, Windows — локально та у production.
  2. Dependency isolation: кожен контейнер має власні залежності, версії Node.js, npm packages — без конфліктів.
  3. Easy scaling: запуск 10 інстансів застосунку — одна команда docker-compose up --scale app=10.
  4. Fast deployment: розгортання застосунку — завантаження Docker image (секунди) замість налаштування сервера (години).
  5. Rollback capability: повернення до попередньої версії — перезапуск контейнера з попереднім image tag.

Docker базові концепції

Перед створенням Dockerfile розберемося з ключовими концепціями Docker.

Image vs Container

Docker Image — незмінний (immutable) template, що містить:

  • Операційну систему (наприклад, Alpine Linux)
  • Runtime середовище (Node.js 18)
  • Залежності (npm packages)
  • Код застосунку (скопійовані файли)
  • Інструкції для запуску (CMD)

Аналогія: Image — це «рецепт» або «blueprint» для створення контейнерів.

Docker Container — запущений екземпляр (running instance) image з власними:

  • Файловою системою (копія з image + зміни)
  • Процесами (Node.js процес)
  • Мережевим інтерфейсом (IP адреса у Docker мережі)
  • Ресурсами (виділена RAM, CPU)

Аналогія: Container — це «готова страва», приготована за «рецептом» (image).

Зв'язок: Один image може створити багато контейнерів:

# Створення image з назвою my-app
docker build -t my-app .

# Запуск 3 контейнерів з одного image
docker run -d -p 3001:3000 my-app  # Контейнер 1
docker run -d -p 3002:3000 my-app  # Контейнер 2
docker run -d -p 3003:3000 my-app  # Контейнер 3

Dockerfile інструкції

Dockerfile — текстовий файл з інструкціями для побудови image. Основні команди:

FROM — базовий image, на якому будується ваш image:

FROM node:18-alpine  # Alpine Linux з Node.js 18 (мінімальний розмір)

WORKDIR — робоча директорія всередині контейнера:

WORKDIR /app  # Всі наступні команди виконуються у /app

COPY — копіювання файлів з host машини у image:

COPY package*.json ./  # Копіювання package.json та package-lock.json
COPY . .                # Копіювання всіх файлів проєкту

RUN — виконання команди під час побудови image:

RUN npm install         # Встановлення залежностей при build
RUN npm run build       # Компіляція TypeScript у JavaScript

CMD — команда, що виконується при запуску контейнера:

CMD ["node", "dist/main.js"]  # Запуск застосунку

EXPOSE — документування порту (не відкриває порт!):

EXPOSE 3000  # Документує, що застосунок слухає на порту 3000

ENV — встановлення environment variables:

ENV NODE_ENV=production
ENV PORT=3000

Порядок виконання: Dockerfile читається зверху вниз, кожна інструкція створює новий layer у image.

Layers та кешування

Docker використовує layer-based architecture для оптимізації:

FROM node:18-alpine      # Layer 1: базовий image
WORKDIR /app             # Layer 2: створення /app директорії
COPY package*.json ./    # Layer 3: копіювання package.json
RUN npm install          # Layer 4: встановлення залежностей
COPY . .                 # Layer 5: копіювання коду
RUN npm run build        # Layer 6: компіляція
CMD ["node", "dist/main.js"]  # Metadata, не layer

Кешування: Якщо layer не змінився, Docker використовує закешовану версію замість повторного виконання:

# Перша побудова — всі layers створюються заново
$ docker build -t my-app .
Step 1/7 : FROM node:18-alpine
 ---> Pulling from library/node
Step 2/7 : WORKDIR /app
 ---> Running in abc123
Step 3/7 : COPY package*.json ./
 ---> Running in def456
Step 4/7 : RUN npm install
 ---> Running in ghi789 (займає 2 хвилини)
...

# Друга побудова — змінено лише код, npm install з кешу
$ docker build -t my-app .
Step 1/7 : FROM node:18-alpine
 ---> Using cache
Step 2/7 : WORKDIR /app
 ---> Using cache
Step 3/7 : COPY package*.json ./
 ---> Using cache
Step 4/7 : RUN npm install
 ---> Using cache (завантажено з кешу за секунди!)
Step 5/7 : COPY . .
 ---> Running in jkl012 (лише цей layer перебудовується)
...

Best practice: Копіюйте package.json окремо перед копіюванням коду, щоб npm install кешувався:

# ✅ ХОРОША ПРАКТИКА: npm install кешується, якщо package.json не змінився
COPY package*.json ./
RUN npm install
COPY . .  # Копіювання коду після npm install

# ❌ ПОГАНА ПРАКТИКА: npm install перевиконується при кожній зміні коду
COPY . .
RUN npm install

Простий Dockerfile для NestJS

Створимо базовий Dockerfile для NestJS застосунку:

# Dockerfile (базова версія)
FROM node:18-alpine

# Встановлення робочої директорії
WORKDIR /app

# Копіювання package.json та package-lock.json
COPY package*.json ./

# Встановлення залежностей
RUN npm install

# Копіювання коду застосунку
COPY . .

# Компіляція TypeScript
RUN npm run build

# Expose порту
EXPOSE 3000

# Запуск застосунку
CMD ["node", "dist/main.js"]

Побудова та запуск

# Побудова Docker image
docker build -t my-nestjs-app .

# Перегляд створених images
docker images
# REPOSITORY        TAG       IMAGE ID       SIZE
# my-nestjs-app     latest    abc123def456   1.2GB

# Запуск контейнера
docker run -p 3000:3000 my-nestjs-app

# Запуск у detached mode (у фоні)
docker run -d -p 3000:3000 --name nestjs-container my-nestjs-app

# Перегляд логів
docker logs nestjs-container

# Зупинка контейнера
docker stop nestjs-container

# Видалення контейнера
docker rm nestjs-container
docker build — Build Output
$ docker build -t my-nestjs-app .
[+] Building 128.4s (12/12) FINISHED
=> [internal] load build definition from Dockerfile
=> => transferring dockerfile: 245B
=> [internal] load .dockerignore
=> [1/6] FROM node:18-alpine
=> [2/6] WORKDIR /app
=> [3/6] COPY package*.json ./
=> [4/6] RUN npm install (120.5s)
=> [5/6] COPY . .
=> [6/6] RUN npm run build (5.2s)
=> exporting to image
=> => exporting layers
=> => writing image sha256:abc123...
=> => naming to docker.io/library/my-nestjs-app
✓ Successfully built my-nestjs-app

Проблеми базового Dockerfile:

  1. Великий розмір image (~1.2GB) — містить development залежності (devDependencies), TypeScript код, тести.
  2. Неоптимальний для production — запускає застосунок як root user (небезпечно).
  3. Повільна побудова — npm install виконується кожного разу навіть при незмінних залежностях.

Multi-stage Build для оптимізації

Multi-stage build розділяє процес побудови на кілька етапів (stages), копіюючи лише необхідні артефакти у final image:

# Dockerfile (multi-stage)

# ===================================
# Stage 1: Builder
# ===================================
FROM node:18-alpine AS builder

WORKDIR /app

# Копіювання dependency files
COPY package*.json ./
COPY tsconfig*.json ./

# Встановлення всіх залежностей (включно з devDependencies)
RUN npm ci

# Копіювання коду
COPY src ./src

# Компіляція TypeScript
RUN npm run build

# ===================================
# Stage 2: Production
# ===================================
FROM node:18-alpine AS production

# Створення non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001

WORKDIR /app

# Копіювання package files
COPY package*.json ./

# Встановлення ЛИШЕ production залежностей
RUN npm ci --omit=dev && npm cache clean --force

# Копіювання скомпільованого коду з builder stage
COPY --from=builder /app/dist ./dist

# Зміна власника файлів на nestjs user
RUN chown -R nestjs:nodejs /app

# Перемикання на non-root user
USER nestjs

# Expose порту
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# Запуск застосунку
CMD ["node", "dist/main.js"]

Що відбувається:

  1. Stage 1 (builder):
    • Встановлює всі залежності (включно з typescript, @types/*, тестові фреймворки).
    • Компілює TypeScript → JavaScript у директорію dist/.
  2. Stage 2 (production):
    • Встановлює лише dependencies (без devDependencies).
    • Копіює скомпільований код з builder stage (COPY --from=builder).
    • Створює non-root user для безпеки.
    • Додає health check для моніторингу.

Результат: final image містить лише те, що потрібно для запуску застосунку — розмір зменшується з ~1.2GB до ~200MB.

docker build — Multi-stage Size Comparison
$ docker images
REPOSITORY TAG SIZE
my-app-basic latest 1.2GB
my-app-multistage latest 187MB
✓ Size reduced by 84%!

.dockerignore файл

Створіть .dockerignore для виключення файлів з build context:

# .dockerignore

# Dependencies
node_modules/
npm-debug.log
package-lock.json  # Використовуємо npm ci

# Build artifacts
dist/
build/

# Tests
*.spec.ts
*.e2e-spec.ts
test/
coverage/

# Environment files
.env
.env.*
!.env.example

# Git
.git/
.gitignore
.github/

# IDE
.vscode/
.idea/
*.swp
*.swo

# Documentation
*.md
docs/

# Docker
Dockerfile
docker-compose*.yml
.dockerignore

# Misc
.DS_Store
.eslintrc.js
.prettierrc

Ефект: зменшення build context з ~500MB до ~50MB, прискорення docker build на 50-70%.


Docker Compose для Multi-Container Setup

Docker Compose дозволяє декларативно описати кілька сервісів (NestJS app, PostgreSQL, Redis) та їх взаємодію у файлі docker-compose.yml.

Базовий docker-compose.yml

# docker-compose.yml
version: '3.8'

services:
  # PostgreSQL Database
  db:
    image: postgres:15-alpine
    container_name: blog-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: blog_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - blog-network

  # Redis Cache
  redis:
    image: redis:7-alpine
    container_name: blog-redis
    restart: unless-stopped
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - blog-network

  # NestJS Application
  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: blog-app
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_NAME: blog_db
      REDIS_HOST: redis
      REDIS_PORT: 6379
    depends_on:
      - db
      - redis
    networks:
      - blog-network

# Named volumes для персистентності даних
volumes:
  postgres_data:
  redis_data:

# Custom network для комунікації між контейнерами
networks:
  blog-network:
    driver: bridge

Команди Docker Compose

# Запуск всіх сервісів у detached mode
docker-compose up -d

# Перегляд статусу сервісів
docker-compose ps

# Перегляд логів всіх сервісів
docker-compose logs -f

# Перегляд логів конкретного сервісу
docker-compose logs -f app

# Зупинка всіх сервісів
docker-compose stop

# Зупинка та видалення контейнерів
docker-compose down

# Зупинка та видалення контейнерів + volumes
docker-compose down -v

# Перебудова образів та запуск
docker-compose up -d --build

# Масштабування сервісу (запуск 3 інстансів app)
docker-compose up -d --scale app=3
docker-compose up — Startup Output
$ docker-compose up -d
[+] Running 5/5
✔ Network blog_blog-network Created
✔ Volume "blog_postgres_data" Created
✔ Volume "blog_redis_data" Created
✔ Container blog-postgres Started
✔ Container blog-redis Started
✔ Container blog-app Started
$ docker-compose ps
NAME IMAGE STATUS PORTS
blog-app blog-app:latest Up 5 seconds 0.0.0.0:3000->3000/tcp
blog-postgres postgres:15-alpine Up 7 seconds 0.0.0.0:5432->5432/tcp
blog-redis redis:7-alpine Up 6 seconds 0.0.0.0:6379->6379/tcp
✓ Application running at http://localhost:3000

Пояснення ключових опцій

services — список контейнерів для запуску:

  • db — PostgreSQL база даних.
  • redis — Redis кеш.
  • app — NestJS застосунок.

image — готовий Docker image з Docker Hub (для db та redis).

build — інструкції для побудови custom image (для app):

build:
  context: .          # Директорія з Dockerfile
  dockerfile: Dockerfile
  target: production  # Multi-stage target

container_name — зручна назва контейнера (замість автогенерованої).

restart: unless-stopped — автоматичний перезапуск контейнера при падінні (крім ручної зупинки).

ports — mapping портів host:container:

ports:
  - "3000:3000"  # localhost:3000 → container:3000

environment — environment variables для контейнера:

environment:
  DATABASE_HOST: db  # Використовуємо service name як hostname!

depends_on — порядок запуску (app запускається після db та redis):

depends_on:
  - db
  - redis
depends_on не чекає готовності сервісу! Він лише забезпечує порядок запуску контейнерів. PostgreSQL контейнер може стартувати, але БД ще не готова приймати з'єднання. Для production використовуйте wait-for-it.sh або retry логіку у застосунку (див. розділ Best Practices).

volumes — персистентність даних:

volumes:
  - postgres_data:/var/lib/postgresql/data  # Named volume

networks — custom мережа для комунікації між контейнерами:

networks:
  - blog-network

Контейнери у одній мережі можуть звертатися один до одного за service name:

// У NestJS застосунку
const dbHost = 'db';  // Замість localhost
const redisHost = 'redis';

Volumes для персистентності даних

Volumes дозволяють зберігати дані поза контейнером, щоб вони не втрачалися при перезапуску або видаленні контейнера.

Типи volumes

1. Named Volumes — Docker керує зберіганням даних:

services:
  db:
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:  # Docker створює та керує цим volume

Дані зберігаються у /var/lib/docker/volumes/ та не видаляються при docker-compose down.

2. Bind Mounts — прив'язка директорії host машини до контейнера:

services:
  app:
    volumes:
      - ./src:/app/src  # Зміни у ./src одразу доступні у контейнері
      - /app/node_modules  # Anonymous volume для node_modules

Корисно для development — зміни коду одразу відображаються без rebuild.

3. Anonymous Volumes — тимчасові volumes без назви:

volumes:
  - /app/node_modules  # Створюється та видаляється з контейнером

Development Setup з Hot Reload

Для локальної розробки з live reload створіть docker-compose.dev.yml:

# docker-compose.dev.yml
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: blog_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_dev_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  app:
    build:
      context: .
      dockerfile: Dockerfile.dev  # Окремий Dockerfile для dev
      target: development
    ports:
      - "3000:3000"
      - "9229:9229"  # Debugger port
    environment:
      NODE_ENV: development
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_NAME: blog_dev
      REDIS_HOST: redis
      REDIS_PORT: 6379
    volumes:
      - ./src:/app/src  # Bind mount для live reload
      - ./test:/app/test
      - /app/node_modules  # Anonymous volume (не override)
    command: npm run start:dev  # Watch mode
    depends_on:
      - db
      - redis

volumes:
  postgres_dev_data:

Dockerfile.dev для development:

# Dockerfile.dev
FROM node:18-alpine AS development

WORKDIR /app

COPY package*.json ./
RUN npm install  # Всі залежності, включно з dev

COPY . .

EXPOSE 3000 9229

CMD ["npm", "run", "start:dev"]

Запуск development середовища:

docker-compose -f docker-compose.dev.yml up -d

Тепер зміни у src/ одразу викликають hot reload у контейнері.


Networking та комунікація між контейнерами

Docker створює ізольовані мережі для груп контейнерів. Контейнери у одній мережі можуть комунікувати за service names.

Service Discovery

У docker-compose.yml кожен сервіс отримує hostname = service name:

services:
  db:  # Hostname: db
    image: postgres:15-alpine

  redis:  # Hostname: redis
    image: redis:7-alpine

  app:  # Hostname: app
    environment:
      DATABASE_HOST: db     # DNS резолвить db → IP адресу контейнера
      REDIS_HOST: redis

У NestJS застосунку:

// config/database.config.ts
export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST || 'localhost',  // 'db' у Docker, 'localhost' локально
  port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  username: process.env.DATABASE_USER || 'postgres',
  password: process.env.DATABASE_PASSWORD,
  name: process.env.DATABASE_NAME || 'blog_db',
}));

Port Mapping

ports відкриває доступ до контейнера ззовні:

services:
  app:
    ports:
      - "3000:3000"  # host:container
  • 3000:3000 — localhost:3000 на host машині → port 3000 у контейнері.
  • 8080:3000 — localhost:8080 на host машині → port 3000 у контейнері.

Без ports контейнер доступний лише всередині Docker мережі:

services:
  redis:
    # Немає ports — доступний лише для app сервісу, не для host машини
    image: redis:7-alpine

Custom Networks

Створіть окремі мережі для ізоляції сервісів:

services:
  app:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend  # DB доступна лише для backend

  nginx:
    networks:
      - frontend  # Nginx не має доступу до DB

networks:
  frontend:
  backend:

Environment Variables у Docker

Є кілька способів передати environment variables у контейнер:

1. Inline у docker-compose.yml

services:
  app:
    environment:
      NODE_ENV: production
      PORT: 3000
      DATABASE_HOST: db

2. Через .env файл

Створіть .env у директорії з docker-compose.yml:

# .env
NODE_ENV=production
DATABASE_PASSWORD=secure-password
JWT_SECRET=generated-jwt-secret

Docker Compose автоматично завантажує .env файл:

services:
  app:
    environment:
      NODE_ENV: ${NODE_ENV}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}

3. Через env_file

services:
  app:
    env_file:
      - .env.production  # Завантажує всі змінні з файлу

4. Через docker run

docker run -e NODE_ENV=production -e PORT=3000 my-app
Не комітьте .env у Git! Для production використовуйте secrets management (Docker Secrets, Kubernetes Secrets, AWS Secrets Manager). Докладніше у розділі Best Practices.

Production-ready Docker Setup

Для production застосуйте додаткові заходи безпеки та надійності:

1. Health Checks

Додайте health check endpoint у NestJS:

// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }
}

Встановіть @nestjs/terminus:

npm install --save @nestjs/terminus

У Dockerfile додайте HEALTHCHECK:

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

Docker автоматично перезапускає контейнер, якщо health check падає.

2. Non-root User

Запуск застосунку як root — небезпечно. Створіть dedicated user:

# Створення non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001

# Зміна власника файлів
RUN chown -R nestjs:nodejs /app

# Перемикання на nestjs user
USER nestjs

3. Secrets Management

Не передавайте secrets через environment variables у production! Використовуйте Docker Secrets (Docker Swarm) або Kubernetes Secrets.

Docker Secrets приклад:

# docker-compose.yml (Docker Swarm)
version: '3.8'

services:
  app:
    image: my-app:latest
    secrets:
      - db_password
      - jwt_secret
    environment:
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
      JWT_SECRET_FILE: /run/secrets/jwt_secret

secrets:
  db_password:
    external: true
  jwt_secret:
    external: true

Створення secrets:

echo "secure-db-password" | docker secret create db_password -
echo "generated-jwt-secret" | docker secret create jwt_secret -

У NestJS зчитуйте secrets з файлів:

// config/database.config.ts
import { readFileSync } from 'fs';

function getSecret(key: string): string {
  const filePath = process.env[`${key}_FILE`];
  if (filePath) {
    return readFileSync(filePath, 'utf8').trim();
  }
  return process.env[key];
}

export default registerAs('database', () => ({
  password: getSecret('DATABASE_PASSWORD'),
}));

4. Resource Limits

Обмежте CPU та RAM для контейнера:

services:
  app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M

5. Logging

Налаштуйте structured logging:

// main.ts
import { Logger } from '@nestjs/common';

const logger = new Logger('Bootstrap');

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'], // Production: лише важливі логи
  });

  await app.listen(3000);
  logger.log(`🚀 Application running on port 3000`);
}

У docker-compose.yml налаштуйте log driver:

services:
  app:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

6. Wait-for-it Script

Додайте скрипт очікування готовності БД:

# scripts/wait-for-it.sh
#!/bin/sh
# wait-for-it.sh

set -e

host="$1"
shift
cmd="$@"

until nc -z "$host" 5432; do
  >&2 echo "Postgres is unavailable - sleeping"
  sleep 1
done

>&2 echo "Postgres is up - executing command"
exec $cmd

У Dockerfile:

COPY scripts/wait-for-it.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/wait-for-it.sh

CMD ["wait-for-it.sh", "db:5432", "--", "node", "dist/main.js"]

Альтернатива — використати готове рішення:

RUN apk add --no-cache netcat-openbsd

CMD ["sh", "-c", "while ! nc -z db 5432; do sleep 1; done && node dist/main.js"]

Debugging у Docker контейнері

Для debugging NestJS застосунку у Docker налаштуйте remote debugging через Chrome DevTools або VS Code.

1. Додайте debugger port у Dockerfile

# Dockerfile.dev
FROM node:18-alpine AS development

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

EXPOSE 3000
EXPOSE 9229  # Debugger port

CMD ["npm", "run", "start:debug"]

2. Налаштуйте start:debug script

// package.json
{
  "scripts": {
    "start:debug": "nest start --debug 0.0.0.0:9229 --watch"
  }
}

0.0.0.0:9229 дозволяє підключатися до debugger ззовні контейнера (замість localhost:9229).

3. Docker Compose з debugger port

# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
      - "9229:9229"  # Debugger port
    volumes:
      - ./src:/app/src
      - /app/node_modules
    command: npm run start:debug

4. VS Code launch.json

// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "attach",
      "name": "Docker: Attach to Node",
      "address": "localhost",
      "port": 9229,
      "restart": true,
      "sourceMaps": true,
      "localRoot": "${workspaceFolder}",
      "remoteRoot": "/app",
      "protocol": "inspector"
    }
  ]
}

5. Запуск та debugging

# Запуск контейнера з debugger
docker-compose -f docker-compose.dev.yml up -d

# У VS Code: Run → Start Debugging (F5)
# Встановлюйте breakpoints у src/ файлах
Chrome DevTools: Відкрийте chrome://inspect → Configure → додайте localhost:9229 → клікніть на "inspect" під Remote Target.

Docker Best Practices

1. Використовуйте .dockerignore

Виключайте непотрібні файли з build context:

node_modules/
dist/
.git/
*.md
.env
test/
coverage/

Ефект: зменшення build time на 50-70%.

2. Multi-stage Builds

Розділяйте build та runtime stages:

FROM node:18-alpine AS builder
# ... build process

FROM node:18-alpine AS production
COPY --from=builder /app/dist ./dist
# Лише production dependencies

Ефект: зменшення image size з ~1.2GB до ~200MB.

3. Оптимізуйте Layer Caching

Копіюйте package.json окремо від коду:

COPY package*.json ./
RUN npm ci
COPY . .  # Копіюється останнім

Ефект: npm install кешується, якщо залежності не змінилися.

4. Використовуйте Alpine Images

FROM node:18-alpine  # ~40MB замість node:18 (~900MB)

Alpine Linux — мінімалістична дистрибуція (5MB), що зменшує розмір final image.

5. Non-root User

RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
USER nestjs

Ефект: підвищення безпеки — застосунок не має root привілеїв.

6. Health Checks

HEALTHCHECK --interval=30s --timeout=3s \
  CMD node -e "require('http').get('http://localhost:3000/health', ...)"

Ефект: автоматичне виявлення та перезапуск нездорових контейнерів.

7. Secrets через Files (не Environment Variables)

secrets:
  - db_password
environment:
  DATABASE_PASSWORD_FILE: /run/secrets/db_password

Ефект: secrets не видно через docker inspect або логи.

8. Resource Limits

deploy:
  resources:
    limits:
      cpus: '1.0'
      memory: 512M

Ефект: запобігання resource exhaustion при memory leaks або CPU spikes.

9. Proper Logging

logging:
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

Ефект: обмеження розміру log файлів, запобігання заповненню диску.

10. Use Specific Image Tags

FROM node:18.16.0-alpine  # ✅ Конкретна версія
# Замість
FROM node:latest  # ❌ Непередбачувані зміни

Ефект: reproducible builds — той самий image на різних середовищах.


Практичний приклад: повний production setup

Повна структура проєкту з Docker:

project/
├── src/
│   ├── auth/
│   ├── users/
│   ├── config/
│   └── main.ts
├── test/
├── scripts/
│   └── wait-for-it.sh
├── Dockerfile
├── Dockerfile.dev
├── docker-compose.yml
├── docker-compose.dev.yml
├── .dockerignore
├── .env.example
└── package.json

Production Dockerfile

# Dockerfile
# ===================================
# Stage 1: Builder
# ===================================
FROM node:18.16.0-alpine AS builder

WORKDIR /app

# Копіювання dependency files
COPY package*.json ./
COPY tsconfig*.json ./

# Встановлення залежностей
RUN npm ci

# Копіювання коду
COPY src ./src

# Компіляція
RUN npm run build

# Видалення devDependencies
RUN npm prune --production

# ===================================
# Stage 2: Production
# ===================================
FROM node:18.16.0-alpine AS production

# Встановлення netcat для wait-for-it
RUN apk add --no-cache netcat-openbsd

# Створення non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nestjs -u 1001

WORKDIR /app

# Копіювання production dependencies
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules

# Копіювання compiled code
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist

# Копіювання package.json для metadata
COPY --chown=nestjs:nodejs package.json ./

# Копіювання scripts
COPY --chown=nestjs:nodejs scripts ./scripts
RUN chmod +x ./scripts/wait-for-it.sh

# Перемикання на non-root user
USER nestjs

# Expose порту
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# Запуск з wait-for-it
CMD ["sh", "-c", "while ! nc -z ${DATABASE_HOST:-db} ${DATABASE_PORT:-5432}; do sleep 1; done && node dist/main.js"]

Production docker-compose.yml

# docker-compose.yml
version: '3.8'

services:
  db:
    image: postgres:15-alpine
    container_name: blog-postgres
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${DATABASE_USER:-postgres}
      POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
      POSTGRES_DB: ${DATABASE_NAME:-blog_prod}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - backend
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-postgres}"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: blog-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    networks:
      - backend
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

  app:
    build:
      context: .
      dockerfile: Dockerfile
      target: production
    container_name: blog-app
    restart: unless-stopped
    ports:
      - "${PORT:-3000}:3000"
    environment:
      NODE_ENV: production
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: ${DATABASE_USER:-postgres}
      DATABASE_PASSWORD: ${DATABASE_PASSWORD}
      DATABASE_NAME: ${DATABASE_NAME:-blog_prod}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      JWT_SECRET: ${JWT_SECRET}
      JWT_EXPIRATION: ${JWT_EXPIRATION:-1h}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - backend
      - frontend
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

  nginx:
    image: nginx:alpine
    container_name: blog-nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    networks:
      - frontend

volumes:
  postgres_data:
  redis_data:

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

.env для production

# .env (не комітити!)
NODE_ENV=production

DATABASE_USER=prod_user
DATABASE_PASSWORD=ultra-secure-production-password
DATABASE_NAME=blog_prod

REDIS_PASSWORD=redis-secure-password

JWT_SECRET=generated-by-openssl-rand-hex-32-see-docs
JWT_EXPIRATION=1h

PORT=3000

Deployment процес

# 1. Клонування репозиторію на production сервер
git clone https://github.com/your-org/blog-api.git
cd blog-api

# 2. Створення .env з production значеннями
nano .env

# 3. Побудова та запуск
docker-compose up -d --build

# 4. Перегляд логів
docker-compose logs -f app

# 5. Перевірка статусу
docker-compose ps

# 6. Запуск міграцій БД (якщо потрібно)
docker-compose exec app npm run migration:run

# 7. Health check
curl http://localhost:3000/health
Production Deployment
$ docker-compose up -d --build
[+] Building 45.2s (18/18) FINISHED
=> [builder 1/6] FROM node:18.16.0-alpine
=> [builder 6/6] RUN npm run build
=> [production 4/5] COPY --from=builder /app/dist ./dist
[+] Running 5/5
✔ Network blog_backend Created
✔ Network blog_frontend Created
✔ Container blog-postgres Healthy
✔ Container blog-redis Healthy
✔ Container blog-app Started
✔ Container blog-nginx Started
$ curl http://localhost:3000/health
{"status":"ok","info":{"database":{"status":"up"}}}
✓ Application deployed successfully!

Висновки

✅ Переваги Docker контейнеризації

  • Consistency: застосунок працює однаково на development, staging, production — "works on my machine" проблема вирішена.
  • Isolation: кожен контейнер має власні залежності, версії Node.js, системні бібліотеки — без конфліктів.
  • Portability: Docker image працює на будь-якій платформі (Linux, macOS, Windows, cloud).
  • Scalability: запуск кількох інстансів застосунку — одна команда docker-compose up --scale app=10.
  • Fast Deployment: розгортання застосунку — завантаження image (секунди) замість налаштування сервера (години).
  • Rollback: повернення до попередньої версії — перезапуск контейнера з попереднім image tag.
  • Development Parity: однакове середовище для всіх розробників — clone repo, docker-compose up, готово.

⚠️ Поширені помилки

  • Великі Docker images — відсутність multi-stage builds призводить до images розміром 1GB+.
  • Відсутність .dockerignore — node_modules/, .git/, test/ додаються до build context, уповільнюючи build.
  • Запуск як root — небезпечно для production, завжди створюйте non-root user.
  • Hardcoded environment variables — використання ENV DATABASE_PASSWORD=secret у Dockerfile замість runtime змінних.
  • Відсутність health checks — Docker не знає, чи застосунок справді працює, навіть якщо контейнер running.
  • Ігнорування depends_on readiness — app стартує до готовності БД, викликаючи connection errors.
  • Не закріплені image tags — FROM node:latest замість FROM node:18.16.0-alpine призводить до непередбачуваних змін.

Що ми розглянули:

  1. Проблема традиційного деплою — "works on my machine", складність налаштування, конфлікти залежностей.
  2. Docker базові концепції — image vs container, Dockerfile інструкції, layers та кешування.
  3. Простий Dockerfile для NestJS — базова структура, побудова та запуск контейнера.
  4. Multi-stage build — оптимізація розміру image з ~1.2GB до ~200MB через розділення build та runtime stages.
  5. .dockerignore — виключення непотрібних файлів для зменшення build time на 50-70%.
  6. Docker Compose — оркестрація multi-container setup (NestJS + PostgreSQL + Redis), services, networks, volumes.
  7. Volumes — персистентність даних через named volumes, bind mounts для development з live reload.
  8. Networking — комунікація між контейнерами через service names, port mapping для зовнішнього доступу.
  9. Production-ready setup — health checks, non-root user, secrets management, resource limits, structured logging.
  10. Debugging у Docker — remote debugging через VS Code або Chrome DevTools на порту 9229.
  11. Best Practices — 10 правил для оптимальних та безпечних Docker образів.
  12. Практичний приклад — повний production setup з Dockerfile, docker-compose.yml, nginx, health checks.

Ключові висновки:

  • Використовуйте multi-stage builds — зменшення розміру image на 80-90%.
  • Створюйте .dockerignore — виключення node_modules/, .git/, test/ прискорює build.
  • Запускайте як non-root user — підвищення безпеки у production.
  • Додайте health checks — автоматичне виявлення та перезапуск нездорових контейнерів.
  • Використовуйте Alpine images — базовий образ ~40MB замість ~900MB.
  • Закріплюйте версії — node:18.16.0-alpine замість node:latest для reproducible builds.
  • Secrets через files — використання Docker Secrets або Kubernetes Secrets замість environment variables.
  • Wait for dependencies — додайте retry логіку або wait-for-it script для готовності БД.

Docker контейнеризація — це стандарт індустрії для розгортання backend застосунків. Вона забезпечує consistency, isolation, portability та easy scaling. У поєднанні з оркестраторами (Kubernetes, Docker Swarm) Docker дозволяє створювати highly available та fault-tolerant системи для production навантаження.


Часті запитання (FAQ)

Додаткові ресурси:

Copyright © 2026