Если вы хоть раз слышали фразу «у меня работает, не знаю почему у тебя не работает» — 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 используется в проектах от корпоративных сайтов и выше — там, где важно воспроизводимое окружение и быстрый деплой без «а у меня локально работало».
С чего начать прямо сейчас
- Установите Docker Desktop или Docker Engine.
- Возьмите один из своих проектов и напишите для него
Dockerfile.
- Добавьте
compose.yml, если проекту нужна база данных.
- Попробуйте собрать образ и запустить его.
- Настройте
.dockerignore.
Не пытайтесь сразу освоить Kubernetes и Docker Swarm. Сначала научитесь уверенно работать с одним контейнером и Compose — это покрывает 90% задач большинства веб-проектов.