Docker: контейнеризация

Концепции

Dockerfile  →  Image  →  Container
 (рецепт)     (снимок)   (запущенный процесс)

Image: неизменяемый снимок файловой системы + метаданные (CMD, ENV, порты). Состоит из слоёв (layers). Каждая инструкция в Dockerfile = слой. Container: запущенный экземпляр image. Изолированный процесс со своей FS, сетью, PID-пространством. Эфемерный, можно удалить и поднять заново из того же image. Volume: постоянное хранилище данных, живёт независимо от контейнера. Network: виртуальная сеть, контейнеры в одной сети видят друг друга по имени. Registry: хранилище image-ей (Docker Hub, GitHub Container Registry, self-hosted).

Registry (Docker Hub)
  └── Repository (nginx)
       └── Tag (1.27, latest, alpine)
            └── Image (sha256:abc...)
                 └── Layers (слои, переиспользуются между image-ами)

Установка

# Arch
sudo pacman -S docker docker-compose
sudo systemctl enable --now docker
sudo usermod -aG docker $USER     # чтобы не писать sudo (перелогиниться)

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

# macOS
brew install --cask docker        # Docker Desktop

Основные команды

Image

docker build -t myapp:latest .              # собрать image из Dockerfile
docker build -t myapp:latest -f prod.Dockerfile .  # указать файл
docker images                                # список image-ей
docker image ls                              # то же самое
docker rmi <image>                           # удалить image
docker image prune                           # удалить неиспользуемые image
docker pull nginx:1.27-alpine                # скачать image
docker tag myapp:latest registry/myapp:v1    # добавить тег
docker push registry/myapp:v1               # отправить в registry
docker history <image>                       # слои image-а
docker inspect <image>                       # полная информация (JSON)

Container

docker run nginx                             # запустить контейнер
docker run -d nginx                          # в фоне (detached)
docker run -d --name web nginx               # с именем
docker run -d -p 8080:80 nginx               # проброс порта (хост:контейнер)
docker run -d -p 127.0.0.1:8080:80 nginx     # только localhost
docker run --rm nginx                        # удалить после остановки
docker run -it ubuntu bash                   # интерактивный режим с терминалом
docker run -e DB_HOST=localhost myapp         # переменные окружения
docker run --env-file .env myapp             # ENV из файла
docker run -v ./data:/app/data myapp         # bind mount (хост:контейнер)
docker run -v pgdata:/var/lib/postgresql/data postgres  # named volume

docker ps                                    # запущенные контейнеры
docker ps -a                                 # все контейнеры (включая остановленные)
docker stop <container>                      # остановить
docker start <container>                     # запустить остановленный
docker restart <container>                   # перезапустить
docker rm <container>                        # удалить
docker rm -f <container>                     # остановить + удалить
docker logs <container>                      # логи
docker logs -f <container>                   # логи в реальном времени
docker logs --tail 100 <container>           # последние 100 строк
docker exec -it <container> bash             # зайти внутрь контейнера
docker exec -it <container> sh               # если bash нет (alpine)
docker cp <container>:/path/file ./local     # скопировать файл из контейнера
docker stats                                 # мониторинг ресурсов (CPU, RAM)
docker top <container>                       # процессы внутри контейнера
docker inspect <container>                   # полная информация (JSON)

Volume

docker volume ls                             # список
docker volume create mydata                  # создать
docker volume inspect mydata                 # информация
docker volume rm mydata                      # удалить
docker volume prune                          # удалить неиспользуемые

Network

docker network ls                            # список сетей
docker network create mynet                  # создать сеть
docker network inspect mynet                 # информация
docker run -d --network mynet --name api myapp   # запустить в сети
docker run -d --network mynet --name db postgres  # другой контейнер в той же сети
# api может обращаться к db по имени: postgres://db:5432

Очистка

docker system df                             # сколько места занято
docker system prune                          # удалить остановленные контейнеры, неиспользуемые сети, dangling images
docker system prune -a --volumes             # полная очистка (осторожно)

Dockerfile

Базовый пример

FROM node:22-alpine

WORKDIR /app

# Сначала зависимости (кешируется, если package.json не менялся)
COPY package.json package-lock.json ./
RUN npm ci

# Потом код (меняется часто)
COPY . .

EXPOSE 3000

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

Инструкции

ИнструкцияЧто делает
FROMБазовый image
WORKDIRРабочая директория (создаётся автоматически)
COPYКопировать файлы из контекста в image
ADDКак COPY, но умеет распаковывать архивы и качать URL
RUNВыполнить команду при сборке (создаёт слой)
CMDКоманда по умолчанию при запуске контейнера
ENTRYPOINTГлавный процесс контейнера (CMD становится аргументами)
ENVПеременная окружения
ARGПеременная времени сборки (docker build --build-arg)
EXPOSEДокументация порта (не пробрасывает, только метаданные)
VOLUMEОбъявить точку монтирования
USERПереключить пользователя
HEALTHCHECKПроверка здоровья контейнера

Multi-stage build

Уменьшает размер итогового image, поскольку сборочные инструменты не попадают в production:

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

WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/server

# --- Production image ---
FROM alpine:3.20

RUN apk add --no-cache ca-certificates
COPY --from=builder /app /app

USER nobody
EXPOSE 8080

ENTRYPOINT ["/app"]

Результат: вместо ~500MB (golang image) получается ~15MB (alpine + бинарник).

Оптимизация кеширования слоёв

Слои кешируются. Если слой не изменился, Docker использует кеш. Порядок инструкций критически важен:

# ПЛОХО — любое изменение кода сбрасывает кеш зависимостей
COPY . .
RUN npm ci

# ХОРОШО — зависимости кешируются отдельно от кода
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Правило: то, что меняется редко, размещай выше, а то, что часто, ниже.

.dockerignore

.git
node_modules
dist
*.log
.env
.env.*
docker-compose*.yml
Dockerfile
.dockerignore
README.md
.idea
.vscode
__pycache__
*.pyc

Уменьшает контекст сборки и предотвращает попадание лишнего в image.

Healthcheck

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://localhost:8080/health || exit 1

Непривилегированный пользователь

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

Не запускай контейнеры от root без необходимости.

Docker Compose

Описывает multi-container приложение в одном файле.

compose.yaml

services:
  api:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    volumes:
      - ./src:/app/src    # для dev — hot reload
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d myapp"]
      interval: 5s
      timeout: 3s
      retries: 5

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  adminer:
    image: adminer
    ports:
      - "8081:8080"
    profiles:
      - debug           # запускается только с --profile debug

volumes:
  pgdata:

Команды Compose

docker compose up                            # поднять всё
docker compose up -d                         # в фоне
docker compose up --build                    # пересобрать image перед запуском
docker compose down                          # остановить и удалить контейнеры, сети
docker compose down -v                       # + удалить volumes (данные!)
docker compose ps                            # статус сервисов
docker compose logs -f api                   # логи конкретного сервиса
docker compose exec api sh                   # зайти внутрь сервиса
docker compose run --rm api npm test         # запустить одноразовую команду
docker compose build                         # только собрать
docker compose pull                          # обновить image-и
docker compose restart api                   # перезапустить сервис
docker compose up --profile debug -d         # запустить с профилем
docker compose config                        # валидация и вывод итогового yaml

Dev vs Prod

compose.yaml (базовый), compose.override.yaml (автоматически подхватывается для dev):

# compose.override.yaml (dev)
services:
  api:
    build:
      target: dev
    volumes:
      - ./src:/app/src
    environment:
      - DEBUG=true
    command: npm run dev

Для prod нужно явно указать файлы:

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

Dev-флоу

Структура проекта

myapp/
├── compose.yaml              # сервисы: app, db, cache, ...
├── compose.override.yaml     # dev-overrides (volumes, debug)
├── compose.prod.yaml         # prod-overrides
├── Dockerfile                # production build
├── .dockerignore
├── src/
└── ...

Ежедневная работа

# Утро: поднять проект
docker compose up -d
docker compose logs -f api      # в отдельном терминале / tmux-панели

# Разработка: код меняется — hot reload через volume mount
# Ничего перезапускать не надо

# Зайти в контейнер (отладка, REPL, миграции)
docker compose exec api sh
docker compose exec db psql -U user myapp

# Запустить тесты
docker compose run --rm api npm test

# Пересобрать после изменения Dockerfile или зависимостей
docker compose up --build -d

# Вечер: остановить
docker compose down
# или оставить работать — ресурсы минимальны

Паттерн: одноразовые команды

# Миграции БД
docker compose run --rm api npx prisma migrate dev

# Генерация кода
docker compose run --rm api go generate ./...

# Lint
docker compose run --rm api npm run lint

--rm: контейнер удалится после завершения команды.

Паттерн: отладка зависимостей

# Проверить, доступна ли БД из контейнера api
docker compose exec api sh -c 'nc -zv db 5432'

# DNS-резолв
docker compose exec api nslookup db

# Посмотреть переменные окружения
docker compose exec api env

# Проверить сеть
docker network inspect myapp_default

Паттерн: чистый старт

# Всё пересобрать и пересоздать с нуля
docker compose down -v
docker compose build --no-cache
docker compose up -d

Dockerfile для разных стеков

Node.js

FROM node:22-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Python

FROM python:3.13-slim

WORKDIR /app

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

COPY . .

USER nobody
EXPOSE 8000
CMD ["gunicorn", "app:create_app()", "-b", "0.0.0.0:8000"]

Go

FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app /app
USER 65534
ENTRYPOINT ["/app"]

scratch: пустой image. Итоговый размер = размер бинарника (~10MB).

Безопасность

# 1. Минимальный базовый image
FROM node:22-alpine            # alpine вместо full image

# 2. Непривилегированный пользователь
USER node                      # не root

# 3. Не хранить секреты в image
# ПЛОХО:
ENV API_KEY=secret123
# ХОРОШО — передавать при запуске:
# docker run -e API_KEY=secret123 myapp
# или через secrets в compose

# 4. Read-only файловая система (в compose)
# services:
#   api:
#     read_only: true
#     tmpfs:
#       - /tmp

# 5. Ограничение ресурсов (в compose)
# services:
#   api:
#     deploy:
#       resources:
#         limits:
#           memory: 512M
#           cpus: "0.5"

Сканирование уязвимостей

docker scout cve <image>         # встроенный сканер Docker
trivy image <image>              # Trivy (популярный OSS-сканер)

Полезные алиасы

# ~/.bashrc или ~/.zshrc
alias dc='docker compose'
alias dcu='docker compose up -d'
alias dcd='docker compose down'
alias dcl='docker compose logs -f'
alias dce='docker compose exec'
alias dcr='docker compose run --rm'
alias dcb='docker compose up --build -d'
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dprune='docker system prune -af --volumes'

Частые проблемы

Контейнер сразу останавливается:

docker logs <container>          # смотреть логи — там будет ошибка
docker run -it <image> sh        # зайти и проверить руками

Частая причина: CMD/ENTRYPOINT завершается. Процесс должен работать на foreground (не демонизироваться).

Порт уже занят:

# Найти, кто занял порт
ss -tlnp | grep 8080
# или
lsof -i :8080
# Сменить порт в compose: "8081:80"

Нет доступа к файлам в volume (permission denied):

UID/GID внутри контейнера не совпадает с хостовым. Решения:

# 1. Указать UID при запуске
docker run -u $(id -u):$(id -g) myapp

# 2. В Dockerfile создать пользователя с нужным UID
RUN adduser -u 1000 -S app
USER app

Image раздулся:

docker history <image>           # какой слой сколько весит
# Объединять RUN-команды в один слой:
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Использовать multi-stage build
# Использовать alpine/slim базовые image

Compose: сервис не видит другой сервис:

Сервисы видят друг друга по имени сервиса в compose (не по имени контейнера). Убедись, что оба в одной сети (по умолчанию создаётся одна сеть на compose-проект).

Кеш сборки не работает:

docker compose build --no-cache  # сборка без кеша
# Или проверь порядок COPY в Dockerfile — файл изменился → все слои ниже пересобираются

Контейнер ест всю память:

# compose.yaml — ограничить ресурсы
services:
  api:
    deploy:
      resources:
        limits:
          memory: 512M