Перейти к содержимому

27. Реальный проект: Full-stack

Реальное production-приложение обычно состоит из нескольких сервисов: фронтенд, бэкенд API, база данных, кэш, прокси-сервер. Docker Compose позволяет описать всю эту инфраструктуру в одном файле и управлять ею как единым целым.

Рассмотрим типичный full-stack стек:

Internet → Nginx (80/443) → React SPA (статика)
→ Node.js API (:3000)
↓ ↓
PostgreSQL Redis
(данные) (кэш/сессии)

Nginx выступает точкой входа: отдаёт статические файлы React и проксирует /api/* запросы к Node.js.

myapp/
├── docker-compose.yml
├── .env
├── nginx/
│ ├── Dockerfile
│ └── nginx.conf
├── api/
│ ├── Dockerfile
│ └── src/
└── web/
├── Dockerfile
└── src/
version: '3.9'
services:
# ──────────────────────────────────────
# PostgreSQL — основная база данных
# ──────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: myapp_postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-myapp}
POSTGRES_USER: ${DB_USER:-myapp}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-myapp}"]
interval: 10s
timeout: 5s
retries: 5
# ──────────────────────────────────────
# Redis — кэш и хранилище сессий
# ──────────────────────────────────────
redis:
image: redis:7-alpine
container_name: myapp_redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- backend
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# ──────────────────────────────────────
# Node.js API
# ──────────────────────────────────────
api:
build:
context: ./api
dockerfile: Dockerfile
target: production
container_name: myapp_api
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 3000
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
JWT_SECRET: ${JWT_SECRET}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- backend
- frontend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
# ──────────────────────────────────────
# React frontend (собирается и отдаётся Nginx)
# ──────────────────────────────────────
web:
build:
context: ./web
dockerfile: Dockerfile
target: production
container_name: myapp_web
networks:
- frontend
# ──────────────────────────────────────
# Nginx — reverse proxy + статика
# ──────────────────────────────────────
nginx:
build:
context: ./nginx
dockerfile: Dockerfile
container_name: myapp_nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./ssl:/etc/nginx/ssl:ro
depends_on:
api:
condition: service_healthy
networks:
- frontend
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 5s
retries: 3
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # backend недоступен снаружи напрямую
volumes:
postgres_data:
driver: local
redis_data:
driver: local
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
FROM base AS deps
RUN npm ci --only=production
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build
FROM base AS production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
events { worker_connections 1024; }
http {
upstream api {
server api:3000;
keepalive 32;
}
server {
listen 80;
# Статика React
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
expires 1y;
add_header Cache-Control "public, immutable";
}
# API proxy
location /api/ {
proxy_pass http://api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
}
# WebSocket поддержка
location /ws {
proxy_pass http://api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
}
Окно терминала
# .env (НЕ коммитить в Git!)
DB_NAME=myapp
DB_USER=myapp
DB_PASSWORD=super_secret_db_password
REDIS_PASSWORD=super_secret_redis_password
JWT_SECRET=your_jwt_secret_here_min_32_chars
Окно терминала
# Запуск всех сервисов
docker compose up -d
# Только зависимости (без приложения)
docker compose up -d postgres redis
# Просмотр логов
docker compose logs -f api
# Проверка статуса
docker compose ps
# Выполнение команд внутри контейнера
docker compose exec api node scripts/migrate.js
docker compose exec postgres psql -U myapp -d myapp
# Остановка без удаления данных
docker compose stop
# Полная очистка (ОСТОРОЖНО: удаляет volumes)
docker compose down -v

Docker Compose запускает сервисы с учётом depends_on. Граф зависимостей нашего стека:

postgres ─────┐
├──→ api ──→ nginx
redis ────────┘
web ──────────────→ nginx

condition: service_healthy гарантирует, что сервис не просто запущен, но прошёл health check.