Если вы хоть раз слышали фразу «у меня работает, не знаю почему у тебя не работает» — Docker решает именно эту проблему. Он упаковывает приложение вместе со всем окружением: версией языка, зависимостями, переменными среды. Куда бы вы ни перенесли этот пакет — на другую машину, на сервер, в облако — он запустится одинаково.

Что такое контейнер и при чём тут Docker

Контейнер — это изолированный процесс со своей файловой системой. Не виртуальная машина: у виртуалки своё ядро ОС, свои гигабайты оперативки. Контейнер использует ядро хостовой системы и весит в десятки раз меньше.

Docker — самый популярный инструмент для работы с контейнерами. Он даёт CLI, daemon, реестр образов (Docker Hub) и удобный способ описать окружение через текстовый файл.

Образ (image) — это шаблон. Контейнер — это запущенный экземпляр образа. Один образ можно запустить хоть сто раз.

Установка и первые шаги

На Linux ставится через пакетный менеджер:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER

После перелогина команда docker будет доступна без sudo.

На macOS и Windows — Docker Desktop, у него графический интерфейс, но в работе вы всё равно будете использовать терминал.

Проверить установку:

docker run hello-world

Docker скачает образ hello-world с Docker Hub и запустит контейнер. Если в терминале появилась приветственная надпись — всё работает.

Dockerfile: описываем окружение

Docker-образ строится по инструкции из файла Dockerfile. Вот простой пример для Node.js-приложения:

FROM node:20-alpine

WORKDIR /app

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

COPY . .

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

Что здесь происходит:

  • FROM node:20-alpine — берём базовый образ Node.js 20 на Alpine Linux (весит ~50 МБ против ~900 МБ у образа на Debian)
  • WORKDIR /app — рабочая директория внутри контейнера
  • COPY package*.json ./ — копируем только манифесты зависимостей, не весь код
  • RUN npm ci — устанавливаем зависимости; этот слой кешируется, пока package.json не изменится
  • COPY . . — копируем остальной код
  • EXPOSE 3000 — документируем порт (не открывает его наружу, это просто метаданные)
  • CMD — команда запуска контейнера

Собрать образ:

docker build -t my-app:latest .

Запустить:

docker run -p 3000:3000 my-app:latest

Флаг -p 3000:3000 — пробрасываем порт: хост:контейнер.

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

По аналогии с .gitignore, файл .dockerignore исключает лишнее из контекста сборки:

node_modules
.git
*.log
.env
dist

Без него Docker скопирует node_modules внутрь образа, сборка замедлится, а размер образа вырастет.

Docker Compose: несколько сервисов вместе

Реальные проекты редко состоят из одного контейнера. Обычно есть приложение, база данных, кеш, очередь. Docker Compose позволяет описать всё это в одном файле и запустить одной командой.

Пример compose.yml для PHP-приложения с MySQL и Redis:

services:
  app:
    build: .
    ports:
      - "8080:80"
    environment:
      DB_HOST: db
      DB_NAME: myapp
      DB_USER: user
      DB_PASS: secret
      REDIS_HOST: redis
    depends_on:
      - db
      - redis

  db:
    image: mysql:8
    environment:
      MYSQL_DATABASE: myapp
      MYSQL_USER: user
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: rootsecret
    volumes:
      - db_data:/var/lib/mysql

  redis:
    image: redis:7-alpine

volumes:
  db_data:

Запустить всё:

docker compose up -d

Остановить:

docker compose down

Флаг -d — detached mode, контейнеры работают в фоне. Логи смотрим через docker compose logs -f.

Обратите внимание: имя хоста для базы данных — db, потому что Docker Compose создаёт внутреннюю сеть, где сервисы доступны по именам из compose.yml.

Volumes: данные между перезапусками

Контейнер по природе эфемерен. Остановили — данные внутри пропали. Для постоянных данных используют volumes.

В примере выше db_data:/var/lib/mysql — именованный volume. Docker хранит его на хосте, данные сохраняются между перезапусками контейнера.

Для разработки удобны bind mounts — пробрасываем директорию с хоста внутрь контейнера:

volumes:
  - ./src:/app/src

Теперь изменения в коде на хосте сразу видны в контейнере — не нужно пересобирать образ при каждом изменении.

Многоэтапная сборка

Один из мощных приёмов — multi-stage build. Позволяет собрать проект в одном образе, а в финальный образ скопировать только результат. Финальный образ получается маленьким.

Пример для Go-приложения:

# Этап сборки
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o server ./cmd/server

# Финальный образ
FROM alpine:latest
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
CMD ["./server"]

Финальный образ весит ~15 МБ вместо ~300 МБ с Go-тулчейном.

Та же идея работает для фронтенда: в первом этапе собираем React-приложение через npm run build, во втором — кладём результат в Nginx.

Полезные команды, которые реально нужны

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

# Все контейнеры, включая остановленные
docker ps -a

# Зайти в контейнер
docker exec -it <container_id> sh

# Логи контейнера
docker logs -f <container_id>

# Остановить контейнер
docker stop <container_id>

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

# Список образов
docker images

# Удалить образ
docker rmi <image_id>

docker system prune — спасает, когда диск начинает заканчиваться из-за накопившихся образов. Запускайте периодически.

Переменные окружения: правильный подход

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

# .env
DB_PASSWORD=supersecret
API_KEY=abc123

Compose автоматически подхватывает .env из текущей директории. Добавьте .env в .gitignore — он не должен попасть в репозиторий.

Если в команде несколько разработчиков — храните .env.example с незаполненными значениями и документацией, что куда ставить.

Docker в CI/CD

Где Docker раскрывается по-настоящему — это CI/CD пайплайны. На GitHub Actions, GitLab CI, любой другой платформе вы описываете шаги: собрать образ, прогнать тесты, запушить в реестр, задеплоить на сервер.

Пример GitHub Actions для деплоя:

- name: Build and push image
  run: |
    docker build -t ghcr.io/myorg/myapp:${{ github.sha }} .
    docker push ghcr.io/myorg/myapp:${{ github.sha }}

- name: Deploy
  run: |
    ssh user@server "docker pull ghcr.io/myorg/myapp:${{ github.sha }} && \
      docker stop myapp || true && \
      docker run -d --name myapp -p 80:3000 \
      ghcr.io/myorg/myapp:${{ github.sha }}"

Каждый деплой — это конкретный образ с конкретным тегом (хешем коммита). Откат — просто запустить предыдущий образ.

Типичные ошибки новичков

Запускают всё от root. По умолчанию процесс в контейнере работает от root — это риск безопасности. Добавьте в Dockerfile:

RUN addgroup -S app && adduser -S app -G app
USER app

Не используют .dockerignore. Контекст сборки раздувается, сборка замедляется.

Кладут секреты в образ через ARG/ENV. Они видны в docker history. Передавайте секреты в runtime через переменные окружения или Docker secrets.

Один большой контейнер вместо нескольких маленьких. Если запихнуть nginx, php-fpm и mysql в один контейнер — теряется весь смысл изоляции и масштабирования.

Игнорируют теги образов. latest — не версия, это ловушка. Через месяц latest может означать совсем другой образ. Фиксируйте конкретные версии: node:20.11-alpine, mysql:8.0.36.

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

Небольшой статический сайт или простой лендинг — там Docker скорее лишняя сложность. Такие проекты прекрасно живут на обычном shared-хостинге или простом VPS с минимальной конфигурацией.

Docker оправдывает себя, когда у вас несколько окружений (dev, staging, prod), команда больше одного человека, приложение состоит из нескольких сервисов, или вы хотите автоматизировать деплой.

В REEXY, например, Docker используется в проектах от корпоративных сайтов и выше — там, где важно воспроизводимое окружение и быстрый деплой без «а у меня локально работало».

С чего начать прямо сейчас

  1. Установите Docker Desktop или Docker Engine.
  2. Возьмите один из своих проектов и напишите для него Dockerfile.
  3. Добавьте compose.yml, если проекту нужна база данных.
  4. Попробуйте собрать образ и запустить его.
  5. Настройте .dockerignore.

Не пытайтесь сразу освоить Kubernetes и Docker Swarm. Сначала научитесь уверенно работать с одним контейнером и Compose — это покрывает 90% задач большинства веб-проектов.