Классическая история: разработчик пишет код, всё работает на его машине. Деплоит на сервер — падает. Причина: разные версии 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 и сети — этого хватит очень надолго.