Docker для разработчика: запускаем приложение в контейнере с нуля
Пошаговый гид по Docker без лишней теории: пишем Dockerfile, поднимаем сервис, разбираемся с volumes и сетями. После прочтения вы сможете контейнеризировать любой свой проект.
Содержание
Docker для разработчика: запускаем приложение в контейнере с нуля
Каждый разработчик хотя бы раз сталкивался с ситуацией: код работает локально, но падает на сервере. Или наоборот — коллега клонирует репозиторий, запускает проект и получает ошибки зависимостей, которых у вас нет. Классическое «у меня работает» — это не шутка, а реальная проблема, которую Docker решает системно.
Docker — не просто модный инструмент из DevOps-арсенала. Для разработчика это способ зафиксировать окружение: версию языка, библиотеки, конфиги, переменные среды — и передать всё это в виде одного воспроизводимого артефакта. В этой статье мы пройдём весь путь с нуля: от установки до работающего многоконтейнерного приложения с базой данных, volumes и сетью.
Что такое контейнер и почему это не виртуальная машина
Прежде чем писать первый Dockerfile, стоит понять, с чем именно мы работаем.
Виртуальная машина эмулирует целый компьютер: у неё есть своё ядро ОС, драйверы, системные процессы. Это мощно, но тяжело — VM весит гигабайты и стартует минутами.
Контейнер устроен иначе. Он использует ядро хостовой операционной системы, но изолирует процессы через механизмы Linux: namespaces (изоляция PID, сети, файловой системы) и cgroups (ограничение ресурсов). Контейнер — это, по сути, изолированный процесс с собственным окружением. Он стартует за секунды и весит мегабайты.
Docker — это не единственный инструмент для работы с контейнерами (есть Podman, containerd, LXC), но именно он стал стандартом де-факто для разработки благодаря удобному CLI и экосистеме Docker Hub.
Установка и первый запуск
На macOS и Windows проще всего установить Docker Desktop — он включает Docker Engine, CLI и графический интерфейс. На Linux устанавливаем через пакетный менеджер:
# Ubuntu/Debian
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
sudo apt-get install docker-ce docker-ce-cli containerd.io
# Добавляем пользователя в группу docker, чтобы не использовать sudo
sudo usermod -aG docker $USER
После установки проверяем:
docker --version
# Docker version 25.0.3, build 4debf41
docker run hello-world
Команда docker run hello-world скачает образ с Docker Hub и запустит контейнер. Если вы видите сообщение «Hello from Docker!» — всё работает.
Анатомия Dockerfile
Dockerfile — это текстовый файл с инструкциями для сборки образа. Образ (image) — это шаблон, из которого запускаются контейнеры. Разберём структуру на реальном примере.
Допустим, у нас есть простое веб-приложение на Python/Flask:
my-app/
├── app.py
├── requirements.txt
└── Dockerfile
app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return {"status": "ok", "message": "Hello from Docker!"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
requirements.txt:
flask==3.0.0
Dockerfile:
# Базовый образ: официальный Python 3.12 на slim-варианте Debian
FROM python:3.12-slim
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Сначала копируем только файл зависимостей — для кэширования слоёв
COPY requirements.txt .
# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Копируем остальной код
COPY . .
# Указываем порт, который будет слушать приложение
EXPOSE 5000
# Команда запуска
CMD ["python", "app.py"]
Почему порядок инструкций важен
Docker строит образ послойно. Каждая инструкция (FROM, RUN, COPY) создаёт новый слой. Если слой не изменился — Docker берёт его из кэша. Именно поэтому мы сначала копируем requirements.txt и устанавливаем зависимости, а потом копируем код. Зависимости меняются редко, код — часто. Такой порядок экономит время при пересборке: при изменении app.py Docker не будет заново устанавливать все пакеты.
Сборка и запуск
# Собираем образ и даём ему тег
docker build -t my-flask-app:1.0 .
# Запускаем контейнер
# -d — в фоне (detached mode)
# -p 8080:5000 — пробрасываем порт хоста 8080 на порт контейнера 5000
docker run -d -p 8080:5000 --name flask-app my-flask-app:1.0
Открываем http://localhost:8080 — приложение работает в контейнере.
Полезные команды для работы с запущенными контейнерами:
docker ps # список запущенных контейнеров
docker logs flask-app # логи контейнера
docker exec -it flask-app sh # зайти внутрь контейнера
docker stop flask-app # остановить
docker rm flask-app # удалить
Volumes: как не потерять данные
Контейнер по природе своей эфемерен. Когда вы его удаляете, все данные внутри исчезают. Для приложений с состоянием — баз данных, файловых хранилищ — это критично. Здесь на помощь приходят volumes.
Типы монтирования
Bind mount — монтируем директорию с хоста в контейнер. Удобно при разработке: изменения в коде сразу видны внутри контейнера.
docker run -d \
-p 8080:5000 \
-v $(pwd):/app \
--name flask-dev \
my-flask-app:1.0
Named volume — Docker сам управляет хранилищем. Подходит для баз данных: данные переживают удаление контейнера.
# Создаём volume
docker volume create postgres-data
# Запускаем PostgreSQL с volume
docker run -d \
--name postgres-db \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
Теперь даже если удалить контейнер postgres-db и создать новый с тем же volume — данные сохранятся.
docker volume ls # список volumes
docker volume inspect postgres-data # детали
docker volume rm postgres-data # удалить (только если не используется)
Сети в Docker: как контейнеры общаются между собой
По умолчанию Docker создаёт три сети: bridge, host и none. Для большинства задач разработки используется bridge — изолированная виртуальная сеть, в которой контейнеры могут общаться по имени.
Создадим пользовательскую сеть и запустим в ней несколько контейнеров:
# Создаём сеть
docker network create app-network
# Запускаем PostgreSQL в этой сети
docker run -d \
--name postgres-db \
--network app-network \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=myapp \
-v postgres-data:/var/lib/postgresql/data \
postgres:16
# Запускаем Flask-приложение в той же сети
docker run -d \
--name flask-app \
--network app-network \
-p 8080:5000 \
my-flask-app:1.0
Внутри app-network контейнер flask-app может обращаться к базе данных по имени postgres-db — Docker автоматически резолвит имена контейнеров в IP-адреса. В коде приложения строка подключения будет выглядеть так:
DATABASE_URL = "postgresql://postgres:secret@postgres-db:5432/myapp"
Docker Compose: оркестрация для разработки
Управлять несколькими контейнерами через CLI быстро становится неудобным. docker-compose.yml — это декларативное описание всего стека приложения.
Вот полноценный пример для Flask + PostgreSQL + Redis:
# docker-compose.yml
version: "3.9"
services:
web:
build: .
ports:
- "8080:5000"
environment:
- DATABASE_URL=postgresql://postgres:secret@db:5432/myapp
- REDIS_URL=redis://cache:6379
volumes:
- .:/app
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
networks:
- app-network
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
cache:
image: redis:7-alpine
networks:
- app-network
volumes:
postgres-data:
networks:
app-network:
driver: bridge
Запуск всего стека одной командой:
docker compose up -d # поднять в фоне
docker compose logs -f web # следить за логами сервиса web
docker compose down # остановить и удалить контейнеры
docker compose down -v # то же + удалить volumes
Обратите внимание на healthcheck для базы данных и depends_on с условием service_healthy. Без этого Flask может попытаться подключиться к PostgreSQL раньше, чем та успеет запуститься — типичная ошибка при первом знакомстве с Compose.
Тем, кто хочет разобраться в Docker системно — не только в синтаксисе, но и в архитектурных решениях, CI/CD-интеграции и работе с реестрами образов — стоит посмотреть на структурированные курсы. Один из вариантов — курс по Docker от «sudo teach IT», где практика идёт параллельно с теорией.
Типичные ошибки и подводные камни
Запуск от root внутри контейнера
По умолчанию процессы в контейнере работают от пользователя root. Это удобно при разработке, но небезопасно в продакшене. Добавьте в Dockerfile:
# Создаём непривилегированного пользователя
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
# Переключаемся на него
USER appuser
Хранение секретов в образе
Никогда не кладите пароли, токены и ключи API прямо в Dockerfile или в docker-compose.yml в открытом виде. Используйте переменные окружения через .env-файл (который добавлен в .gitignore) или Docker Secrets для продакшена.
# .env файл
POSTGRES_PASSWORD=supersecret
API_KEY=abc123
# docker-compose.yml читает его автоматически
# или явно:
docker compose --env-file .env up
Игнорирование .dockerignore
Без .dockerignore команда COPY . . затащит в образ node_modules, .git, виртуальное окружение Python и прочий мусор. Это раздувает образ и замедляет сборку.
# .dockerignore
.git
.gitignore
__pycache__
*.pyc
.env
node_modules
venv
.venv
*.log
Использование тега latest
FROM python:latest — плохая практика. latest — это движущаяся цель: сегодня это Python 3.12, завтра — 3.13 с breaking changes. Всегда фиксируйте версию: FROM python:3.12-slim.
Один процесс — один контейнер
Не запускайте в одном контейнере nginx, приложение и базу данных. Это нарушает принцип единственной ответственности и усложняет масштабирование, логирование и обновление компонентов.
Многоэтапная сборка (multi-stage build)
Для компилируемых языков (Go, Rust, Java) или фронтенда с этапом сборки используют многоэтапные Dockerfile. Это позволяет получить минимальный финальный образ без инструментов сборки.
Пример для Go-приложения:
# Этап сборки
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
# Финальный образ
FROM alpine:3.19
WORKDIR /app
# Копируем только скомпилированный бинарник из этапа builder
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
Образ на этапе сборки весит ~350 МБ (Go toolchain), финальный — около 15 МБ. Разница принципиальная, особенно если образы хранятся в реестре и деплоятся по сети.
Что дальше
После того как вы освоили базовый цикл — Dockerfile, docker compose, volumes и сети — открывается следующий уровень:
- Docker Registry: публикация образов на Docker Hub или в приватный реестр (GitLab Registry, AWS ECR, GitHub Container Registry)
- CI/CD интеграция: автоматическая сборка и публикация образов при пуше в репозиторий
- Kubernetes: оркестрация контейнеров в продакшене, когда
docker composeуже недостаточно - Оптимизация образов: инструменты вроде
diveдля анализа слоёв, дистроless-образы - Безопасность: сканирование образов на уязвимости через Trivy или Snyk
Вывод
Docker стал стандартом не потому что это модно, а потому что решает реальную проблему воспроизводимости окружения. Написав Dockerfile один раз, вы получаете гарантию: приложение запустится одинаково у любого члена команды, в CI-пайплайне и на продакшен-сервере.
Ключевые вещи, которые стоит запомнить: порядок слоёв в Dockerfile влияет на скорость сборки; volumes — единственный надёжный способ хранить данные; пользовательские сети дают контейнерам возможность общаться по имени; docker compose — ваш главный инструмент для локальной разработки многокомпонентных приложений.
Если хотите пройти этот путь более системно — с разбором архитектурных решений, практикой деплоя и интеграцией в реальные пайплайны — можно посмотреть на курс по Docker от «sudo teach IT»: там практика выстроена вокруг реальных сценариев, а не синтетических примеров.
Контейнеризация — это навык, который окупается быстро. Один раз разобравшись с Docker, вы перестанете тратить время на «а у меня не запускается» и начнёте тратить его на то, что действительно важно.
Комментарии
Пока нет комментариев