Классическая история: разработчик пишет код, всё работает на его машине. Деплоит на сервер — падает. Причина: разные версии Node, Python, библиотек, системные зависимости. Docker решает эту проблему радикально: весь проект упакован в контейнер вместе с окружением и работает одинаково везде — на ноуте, на CI, на боевом сервере.

Ещё плюсы: изоляция сервисов (каждый в своём контейнере, не мешают друг другу), воспроизводимость (новый разработчик запускает одну команду и всё работает), масштабирование (поднять ещё пять копий сервиса — одна строчка).

Базовые понятия

Image (образ) — шаблон. Снимок файловой системы со всем нужным: OS, рантайм, код, зависимости. Образы неизменяемы.

Container (контейнер) — запущенный образ. Живой процесс с изолированным окружением. Из одного образа можно создать сотню контейнеров.

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

Volume — постоянное хранилище. Данные в контейнере исчезают при его пересоздании, volume живёт отдельно. Туда идут базы данных и загруженные файлы.

Network — виртуальная сеть, в которой контейнеры видят друг друга по именам, а не по IP.

Dockerfile: пишем без глупостей

Пример для Node.js:

FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

Несколько вещей, которые здесь важны.

alpine — минималистичный дистрибутив Linux. Образ весит около 5 МБ вместо 900+ МБ у обычного Debian. Для продакшена почти всегда берите alpine-версии.

Сначала package.json, потом код. Docker кэширует слои. Если скопировать сначала весь код, при каждом изменении любого файла будет заново запускаться npm ci. Разделяя copy, мы используем кэш слоя с зависимостями, пока package.json не изменился. На больших проектах это экономит минуты каждой сборки.

npm ci вместо npm install — ci устанавливает строго по lock-файлу, не изменяет его. Воспроизводимость гарантирована.

Пример для Python/Django:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

--no-cache-dir уменьшает размер образа — pip не сохраняет скачанные пакеты.

.dockerignore — не забудьте

Перед сборкой Docker отправляет весь контекст (папку проекта) на сборщик. Без .dockerignore туда попадут node_modules, .git, .env файлы с секретами — и образ раздуется, а в историю слоёв утекут пароли.

Создайте .dockerignore:

node_modules
.git
.env
*.log
dist
coverage
.DS_Store

Multi-stage build: образ без мусора

Для проектов с компиляцией (TypeScript, Go, React) есть многоэтапная сборка. Один этап компилирует, второй берёт только результат.

Пример для TypeScript backend:

# Этап сборки
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# Этап продакшена
FROM node:20-alpine AS production

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY --from=builder /app/dist ./dist

EXPOSE 3000
CMD ["node", "dist/server.js"]

В итоговый образ попадают только скомпилированные файлы и production-зависимости. node_modules для разработки, исходные .ts файлы — ничего лишнего нет. Образ выходит меньше в три-пять раз.

docker-compose: локальная разработка

Один контейнер — это хорошо. Но реальный проект — это backend, frontend, база данных, Redis, может быть очереди. docker-compose позволяет описать всё это в одном файле и запустить одной командой.

Типичный docker-compose.yml:

version: '3.8'

services:
  backend:
    build: ./backend
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    volumes:
      - ./backend:/app
      - /app/node_modules

  frontend:
    build: ./frontend
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

docker-compose up — всё поднимается. docker-compose up -d — в фоне. docker-compose down — всё останавливается. Данные в базе сохраняются в volume postgres_data.

Важный момент с volumes для разработки: ./backend:/app монтирует локальную папку в контейнер, изменения в коде сразу отражаются без пересборки. А /app/node_modules — трюк, который предотвращает перезапись node_modules из контейнера локальной пустой папкой.

Секреты и переменные окружения

Никогда не хардкодьте секреты в Dockerfile или docker-compose.yml. Для локальной разработки используйте .env файл:

DATABASE_URL=postgresql://user:pass@db:5432/mydb
JWT_SECRET=my-super-secret
AWS_ACCESS_KEY=...

В docker-compose подключайте его:

services:
  backend:
    env_file:
      - .env

.env — в .gitignore и .dockerignore. В репозиторий кладите только .env.example с пустыми значениями как документацию для команды.

Переход в продакшен

Локальная разработка и продакшен — разные конфигурации. Для разработки нужны hot reload, отладочные инструменты, монтирование исходников. В продакшене — нет.

Хороший подход — несколько compose-файлов:

  • docker-compose.yml — базовая конфигурация
  • docker-compose.override.yml — дополнения для разработки (применяется автоматически)
  • docker-compose.prod.yml — конфигурация продакшена

Запуск на сервере:

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

Что меняется в продакшен-конфигурации:

Nginx как reverse proxy. Не открывайте Node или Python напрямую наружу. Nginx принимает запросы, раздаёт статику, терминирует SSL, проксирует на backend.

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certbot/conf:/etc/letsencrypt
    depends_on:
      - backend

Restart policy. Контейнеры должны подниматься автоматически после перезагрузки сервера:

services:
  backend:
    restart: unless-stopped

Resource limits. Ограничивайте потребление ресурсов, чтобы один контейнер не забрал всё:

services:
  backend:
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'

Healthcheck. Docker должен знать, живой ли контейнер:

services:
  backend:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

Логи: не потерять и не захлебнуться

По умолчанию Docker пишет логи в файлы на хосте и потенциально заполняет диск. Ограничьте:

services:
  backend:
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

Смотреть логи в реальном времени:

docker-compose logs -f backend

Для серьёзного мониторинга — связка Prometheus + Grafana или Loki для агрегации логов. Но для старта хватит и структурированных логов с правильным форматированием JSON.

Частые ошибки

Запуск от root. По умолчанию процессы в контейнере работают от root — это риск безопасности. Создавайте отдельного пользователя:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

Данные без volume. База данных без volume — потеря всего при пересоздании контейнера. Всегда используйте volumes для PostgreSQL, MySQL, загруженных файлов.

Образы без версий. image: postgres без тега — это postgres:latest. Однажды latest обновится и что-то сломается. Фиксируйте версии: postgres:16.2-alpine.

Большой контекст сборки. Без .dockerignore сборка тянет всё подряд, включая гигабайты node_modules. С .dockerignore — только то, что нужно.

Секреты в слоях образа. Если в Dockerfile есть RUN curl -H "Authorization: Bearer my-token" ... — токен останется в истории слоёв даже после удаления. Используйте BuildKit secrets:

# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=mytoken \
    TOKEN=$(cat /run/secrets/mytoken) && curl -H "Authorization: Bearer $TOKEN" ...

Полезные команды

# Список запущенных контейнеров
docker ps

# Войти в контейнер
docker exec -it container_name sh

# Статистика ресурсов в реальном времени
docker stats

# Удалить неиспользуемые образы, контейнеры, сети
docker system prune

# Посмотреть размер образов
docker images

# Пересобрать один сервис без остановки остальных
docker-compose up --build --no-deps backend

# Выполнить команду внутри контейнера
docker-compose exec backend npm run migrate

CI/CD: автоматизируем деплой

Ручная сборка образов и деплой — не вариант для серьёзного проекта. Базовый pipeline в GitHub Actions:

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build and push image
        run: |
          docker build -t myapp:${{ github.sha }} .
          docker tag myapp:${{ github.sha }} registry.example.com/myapp:latest
          docker push registry.example.com/myapp:latest

      - name: Deploy to server
        run: |
          ssh user@server 'docker-compose pull && docker-compose up -d'

Каждый пуш в main — автоматическая сборка образа, пуш в registry, деплой на сервер. Ноль ручных операций.

Когда Docker не нужен

Docker — не серебряная пуля. Для простого лендинга или сайта-визитки сложность настройки не оправдана. Если сайт статический или живёт на shared hosting — Docker только добавит головной боли.

Контейнеризация начинает давать реальную отдачу когда: несколько разработчиков в команде, несколько сред (dev/staging/prod), микросервисная архитектура, или когда нужно масштабирование. В веб-студии REEXY Docker применяется на проектах с корпоративными сайтами и интернет-магазинами — там, где инфраструктура должна быть воспроизводимой и предсказуемой, а деплой не должен превращаться в ручной квест.

Kubernetes — следующий шаг?

После Docker часто спрашивают про Kubernetes. Честный ответ: для большинства проектов он избыточен. Docker Compose на одном сервере справляется с нагрузкой, которой большинство сайтов никогда не достигнет.

Kubernetes стоит рассматривать когда: десятки микросервисов, автоматическое масштабирование под нагрузкой, деплой без даунтайма критичен. Если вы только начинаете с Docker — не думайте об этом. Освойте Compose, настройте CI/CD, поймите как работают volumes и сети — этого хватит очень надолго.