← Назад к вопросам
Как разворачиваешь Node.js приложения в Docker и какие best practices знаешь?
2.0 Middle🔥 151 комментариев
#DevOps и инфраструктура
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Docker для Node.js приложений: best practices
За 10+ лет я разворачивал Node.js приложения на Docker в разных сценариях: от простых Express приложений до сложных микросервисных архитектур. Дам полный гайд с best practices.
1. Multi-stage Docker build (Production-grade)
Это основа. Многоступенчатая сборка даёт малый image, быстрый старт, безопасность.
# Dockerfile (production-optimized)
FROM node:20-alpine AS builder
WORKDIR /app
# Копируем package files
COPY package*.json ./
COPY yarn.lock ./
# Устанавливаем зависимости (все, включая dev)
RUN yarn install --frozen-lockfile
# Копируем исходный код
COPY . .
# Собираем TypeScript
RUN yarn build
# ============================================
# Stage 2: Runtime (minimal image)
# ============================================
FROM node:20-alpine AS runtime
WORKDIR /app
# Копируем package files
COPY package*.json ./
COPY yarn.lock ./
# Устанавливаем ТОЛЬКО production зависимости
RUN yarn install --frozen-lockfile --production && \
yarn cache clean
# Копируем только собранный код из builder
COPY --from=builder /app/dist ./dist
# Создаём непривилегированного пользователя
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
EXPOSE 3000
# Запуск
CMD ["node", "dist/index.js"]
Почему это works:
- Builder stage: устанавливаем ALL зависимости для сборки TypeScript
- Runtime stage: копируем только compiled code + production-only packages
- Результат: image ~200MB вместо ~600MB с dev зависимостями
- Security: непривилегированный пользователь
- Health check: Docker знает, как приложение на самом деле
2. .dockerignore (очень важно)
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
.env.local
.DS_Store
.vscode
.idea
dist
coverage
.next
build
Вообще не копируем:
node_modules(переустановим в контейнере).env(security!).git(не нужен)- Тесты, документация (не нужны в production)
3. Оптимизация размера image
# ❌ Плохо: большой image
FROM node:20
RUN apt-get update && apt-get install -y git curl
# ✅ Хорошо: минимальный image
FROM node:20-alpine
RUN apk add --no-cache curl
# ✅ Ещё лучше: multi-stage
FROM node:20-alpine AS build
RUN apk add --no-cache git # нужен только для сборки
# ... build stage ...
FROM node:20-alpine
COPY --from=build /app/dist ./dist
# curl уже не нужен? не копируем!
Размеры image:
node:20= 1GB (полный OS)node:20-alpine= 170MB (минимальный)- Multi-stage + production deps = 120-150MB
4. Кэширование layers
# ❌ Неправильно: копируем всё сразу
FROM node:20-alpine
COPY . .
RUN yarn install
RUN yarn build
# Проблема: любое изменение кода = переустановка всех зависимостей!
# ✅ Правильно: кэшируем зависимости
FROM node:20-alpine
COPY package*.json yarn.lock ./
RUN yarn install --frozen-lockfile # кэшируется если package.json не изменился
COPY . .
RUN yarn build # пересобирается только если код изменился
Результат: rebuild времени снизился с 2 минут на 20 секунд.
5. NODE_ENV в production
FROM node:20-alpine
# ✅ КРИТИЧНО: NODE_ENV=production
ENV NODE_ENV=production
# Это делает:
# - npm/yarn пропускает dev зависимости
# - Express/Fastify удаляет debug middleware
# - Производительность растёт на 30-50%
COPY package*.json ./
RUN yarn install # установит только production
6. Process signals (graceful shutdown)
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
# ✅ exec форма (позволяет Node.js получить signals)
CMD ["node", "dist/index.js"]
# ❌ shell форма (Node.js не получит SIGTERM)
# CMD node dist/index.js
Код в приложении:
import express from 'express';
const app = express();
const server = app.listen(3000);
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(async () => {
// Close DB connections
await db.close();
// Close Redis
await redis.quit();
process.exit(0);
});
// Force exit after 30 seconds
setTimeout(() => {
console.error('Forced shutdown (timeout)');
process.exit(1);
}, 30000);
});
7. Минимальные привилегии
FROM node:20-alpine
WORKDIR /app
# ✅ Непривилегированный пользователь
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
COPY --chown=nodejs:nodejs . .
RUN yarn install
USER nodejs # Переключаемся ДО CMD
CMD ["node", "dist/index.js"]
Важно: если привилегии нужны для создания директорий, делаем это ДО USER переключения.
8. Environment переменные
FROM node:20-alpine
WORKDIR /app
# ✅ Build-time переменные (известны при сборке)
ARG BUILD_NUMBER=unknown
ARG GIT_COMMIT=unknown
# ✅ Runtime переменные (задаются при запуске)
ENV NODE_ENV=production
ENV LOG_LEVEL=info
ENV PORT=3000
COPY . .
RUN echo "Build: ${BUILD_NUMBER}, Commit: ${GIT_COMMIT}" > /app/build-info.txt
CMD ["node", "dist/index.js"]
При запуске:
docker run \
--env NODE_ENV=production \
--env DATABASE_URL=postgres://... \
--env API_KEY=secret \
my-app:latest
9. docker-compose для разработки
# docker-compose.yml (development)
version: '3.9'
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
environment:
NODE_ENV: development
DATABASE_URL: postgres://user:pass@db:5432/mydb
volumes:
- ./src:/app/src # live reload
- /app/node_modules # docker volume для node_modules
depends_on:
- db
- redis
command: yarn dev
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
Запуск:
docker compose up --build
10. Health checks в Kubernetes/Swarm
# Production Dockerfile с health check
FROM node:20-alpine
WORKDIR /app
# Health check endpoint
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
COPY . .
RUN yarn install --production && yarn build
CMD ["node", "dist/index.js"]
Endpoint в приложении:
app.get('/health', (req, res) => {
if (db.isConnected && redis.isConnected) {
res.json({ status: 'ok', uptime: process.uptime() });
} else {
res.status(503).json({ status: 'unhealthy' });
}
});
11. Логирование (structured logging)
// В приложении
import winston from 'winston';
const logger = winston.createLogger({
format: winston.format.json(),
transports: [
new winston.transports.Console(),
],
});
logger.info('Application started', {
port: 3000,
env: process.env.NODE_ENV,
version: process.env.BUILD_NUMBER,
});
# Dockerfile
FROM node:20-alpine
# ✅ stdout/stderr перенаправляется в Docker logs
RUN ln -sf /proc/$$/fd/1 /var/log/app.log
CMD ["node", "dist/index.js"]
Просмотр логов:
docker logs -f container_id
docker logs --tail 100 container_id
12. Security best practices
# ✅ Сканируем image на уязвимости
FROM node:20-alpine
RUN apk add --no-cache curl
# ✅ Обновляем packages
RUN apk update && apk upgrade
# ✅ Непривилегированный пользователь
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
# ✅ Read-only filesystem если возможно
# (в Kubernetes или docker run --read-only)
Сканирование image:
trivy image my-app:latest
docker scout cves my-app:latest
13. CI/CD pipeline (GitHub Actions пример)
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: [v*]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
myregistry/my-app:latest
myregistry/my-app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILD_NUMBER=${{ github.run_number }}
GIT_COMMIT=${{ github.sha }}
14. Развёртывание на Kubernetes
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: myregistry/my-app:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
# Readiness probe
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
# Liveness probe
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
# Graceful shutdown
terminationGracePeriodSeconds: 30
15. Реальный пример: NestJS приложение
# Dockerfile для NestJS
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN yarn install --frozen-lockfile --production && yarn cache clean
COPY --from=builder /app/dist ./dist
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"
EXPOSE 3000
CMD ["node", "dist/main.js"]
Best Practices Checklist
- ✅ Multi-stage build (dev deps не в production image)
- ✅ Alpine Linux (.dockerfile (small image)
- ✅ .dockerignore (не копируем лишнее)
- ✅ NODE_ENV=production
- ✅ Кэширование layers (package.json ДО исходного кода)
- ✅ Health checks
- ✅ Graceful shutdown (exec форма CMD)
- ✅ Непривилегированный пользователь
- ✅ Structured logging (stdout/stderr)
- ✅ Security updates (apk upgrade)
- ✅ Правильные resource limits
- ✅ Secrets не в image (env переменные)