Docker для начинающих: образ, контейнер, Dockerfile и Compose

Docker для начинающих: образ, контейнер, Dockerfile и Compose Полезное

Docker — это платформа контейнеризации: она упаковывает приложение вместе с зависимостями в образ и запускает его в изолированном процессе-контейнере, который одинаково ведет себя на ноутбуке разработчика, в CI и на сервере. Ниже — словарь терминов, первые команды, свой образ через Dockerfile, данные в volume, несколько сервисов через docker compose и минимум безопасности.

Пять терминов, которые путают

Термин Что это Аналогия
Образ (image) Неизменяемый шаблон: файловая система из слоев + метаданные (команда запуска, порты) Чертеж
Контейнер (container) Запущенный экземпляр образа со своим тонким записываемым слоем Дом по чертежу
Dockerfile Текстовый рецепт сборки образа Техзадание на чертеж
Volume Хранилище данных вне контейнера, переживает его удаление Внешний диск
Registry Хранилище образов (Docker Hub, приватный реестр) Библиотека чертежей

Цепочка на одном примере: Dockerfile -> docker build -> образ hello-app:1.0 -> docker run -> контейнер. Из одного образа можно запустить сколько угодно контейнеров. Удаление контейнера стирает его записываемый слой, но не образ и не volume.

Как это работает и чем контейнер отличается от виртуальной машины

Контейнер — это обычный процесс хоста, изолированный механизмами ядра Linux: namespaces ограничивают, что процесс видит (процессы, сеть, файловую систему), cgroups — сколько ресурсов он может взять. Команду docker обрабатывает демон Docker Engine, который запускает контейнеры через containerd и runc.

Контейнер Виртуальная машина
Ядро ОС Общее с хостом Свое у каждой VM
Старт Обычно секунды и быстрее Загрузка гостевой ОС
Размер Десятки-сотни МБ Обычно гигабайты
Изоляция Слабее: общее ядро Сильнее: граница гипервизора

Упрощение и его граница: «контейнер легче VM» верно для типичных случаев, но из-за общего ядра Linux-контейнер требует ядра Linux. На macOS и Windows Docker Desktop поднимает для этого легкую Linux-VM, поэтому контейнеры там все равно работают внутри виртуальной машины.

Первые команды

Установка: на Linux — Docker Engine из официального репозитория Docker для своего дистрибутива, на macOS и Windows — Docker Desktop. Проверка:

docker version
docker run --rm hello-world

Если все в порядке, вторая команда скачает маленький образ и напечатает Hello from Docker!. Флаг --rm удалит контейнер после выхода.

Запустим веб-сервер в фоне с пробросом порта:

docker run -d --name web -p 8080:80 nginx:1.30-alpine
docker ps
docker logs web
docker stop web && docker rm web

-d — фоновый режим, -p 8080:80 — порт 8080 хоста ведет на порт 80 контейнера. Пока контейнер работает, http://localhost:8080 в браузере покажет приветственную страницу nginx. Тег 1.30-alpine (текущая стабильная ветка nginx) зафиксирован сознательно: latest через месяц может означать другую версию. Ветку при этом нужно время от времени обновлять: старые ветки, например 1.28, перестают получать исправления.

Свой образ: минимальный полный пример

Три файла в пустой папке. Приложение на стандартной библиотеке Python, app.py:

import os
from http.server import BaseHTTPRequestHandler, HTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"Hello from container, uid={os.getuid()}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain; charset=utf-8")
        self.end_headers()
        self.wfile.write(body)


HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

Dockerfile:

FROM python:3.14-slim

RUN useradd --create-home --uid 10001 appuser
WORKDIR /app
COPY app.py .

USER appuser
EXPOSE 8000
CMD ["python", "app.py"]

.dockerignore — чтобы в контекст сборки не попали лишние и секретные файлы:

.git
.env
db_password.txt
__pycache__/

Сборка и запуск:

docker build -t hello-app:1.0 .
docker run -d --name hello -p 8080:8000 hello-app:1.0
curl http://localhost:8080

Ожидаемый ответ:

Hello from container, uid=10001

Разбор. FROM задает базовый образ с конкретной версией Python. useradd и USER appuser запускают процесс не от root — uid в ответе это подтверждает. CMD в exec-форме (JSON-массив) делает Python главным процессом контейнера, и docker stop корректно доставляет ему сигнал. EXPOSE только документирует порт, публикует его флаг -p.

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

Данные: volume и bind mount

Все, что контейнер записал в свой слой, исчезнет вместе с docker rm. Для данных есть два основных способа:

Named volume Bind mount
Где лежит В области Docker, управляется им Любая папка хоста
Синтаксис -v pgdata:/var/lib/postgresql -v "$(pwd)/src:/app"
Когда брать Данные БД, состояние сервисов Код при разработке, конфиги

Несколько сервисов: docker compose

Когда контейнеров больше одного, их описывают в compose.yaml. Актуальная команда — docker compose (плагин Compose, линейка v2 и новее; docker compose version сейчас показывает 5.x). Старый отдельный docker-compose (v1) больше не поддерживается, а поле version: в начале файла устарело: Compose v2 его игнорирует и выводит предупреждение.

services:
  web:
    build: .
    ports:
      - "8080:8000"
    depends_on:
      - db
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    volumes:
      - pgdata:/var/lib/postgresql

secrets:
  db_password:
    file: ./db_password.txt

volumes:
  pgdata:

Перед запуском создайте файл с паролем рядом с compose.yaml: без него up остановится с ошибкой bind source path does not exist. Файл не должен попасть в git — добавьте его в .gitignore.

printf '%s' 'замените-на-длинный-пароль' > db_password.txt
chmod 600 db_password.txt
docker compose up -d --build
docker compose ps
docker compose logs db
docker compose down

up -d --build соберет web, скачает postgres:18-alpine и запустит оба сервиса в общей сети, где web видит базу по имени db. Пароль лежит в файле, который монтируется в /run/secrets/, а не в тексте compose.yaml. down удалит контейнеры и сеть, но volume pgdata сохранится; удалить его вместе с данными можно только явно, через down -v.

Граница: depends_on в такой форме ждет запуска контейнера db, а не готовности PostgreSQL принимать соединения. Для этого добавляют healthcheck и condition: service_healthy.

Безопасность: минимум, который нужен сразу

Типичная ошибка — секрет в образе:

ENV DB_PASSWORD=supersecret

Результат: пароль виден любому, у кого есть образ, через docker inspect hello-app:1.0 и docker history. С файлами то же самое: если секрет скопирован через COPY, а следующей инструкцией удален RUN rm, он остается в предыдущем слое образа.

Исправление: секрет не попадает в сборку, а передается при запуске — файлом-секретом, как в compose-примере выше, или через секрет-хранилище платформы. Если секрет нужен именно при сборке, используют BuildKit: RUN --mount=type=secret,id=token ... с docker build --secret id=token,src=token.txt — так он не сохраняется в слоях.

Остальные базовые правила:

  • Не root в контейнере. Инструкция USER с непривилегированным uid, как в примере.
  • Pin версий. Конкретный тег (python:3.14-slim), а для воспроизводимости в production — digest @sha256:.... Обновление версии — осознанный коммит, а не сюрприз.
  • Минимальная база. slim/alpine/distroless-образы — меньше пакетов, меньше уязвимостей. Образы стоит проверять сканером уязвимостей в CI.
  • Сокет Docker — это root. Членство в группе docker и проброс /var/run/docker.sock в контейнер дают фактически полный доступ к хосту.

Это базовый уровень, а не «полностью защищенная» конфигурация: в production еще нужны ограничение ресурсов, read-only файловая система где возможно, сетевые политики и регулярное обновление базовых образов.

Если не получилось

Симптом Вероятная причина Что делать
permission denied ... docker.sock Пользователь без прав на демон Linux: sudo или группа docker (помня, что это root-доступ)
Cannot connect to the Docker daemon Демон не запущен Linux: sudo systemctl start docker; Desktop: запустить приложение
port is already allocated Порт хоста занят Сменить левую часть: -p 8081:8000
Контейнер сразу в статусе Exited Главный процесс завершился или упал docker logs <имя> покажет ошибку
curl не отвечает, контейнер жив Приложение слушает 127.0.0.1 внутри контейнера Слушать 0.0.0.0, как в app.py

Выводы

  • Образ — неизменяемый шаблон, контейнер — запущенный процесс из него; данные, которые должны пережить docker rm, хранят в volume.
  • Контейнер делит ядро с хостом, поэтому он легче VM, но изолирован слабее; на macOS и Windows работает внутри Linux-VM.
  • Свой образ описывают в Dockerfile, порядок инструкций влияет на кеш сборки.
  • Несколько сервисов запускают через docker compose (v2 и новее), поле version в compose.yaml не нужно.
  • Базовая безопасность: не root, секреты не в образе, зафиксированные версии образов.

Где применяется / связь с практикой

Освойте тему на практике

Docker — основа современной поставки приложений: из того же образа, что собран в CI, запускают тесты, стенды и production, а оркестраторы вроде Kubernetes управляют уже контейнерами. Сборку образов, пайплайны CI/CD, инфраструктуру как код и мониторинг системно разбирают на курсе «DevOps практики и инструменты». Попробовать формат и задать вопросы практикующим инженерам можно на бесплатных открытых уроках.

FAQ

Docker бесплатный?
Docker Engine и CLI — открытое ПО. Docker Desktop бесплатен для личного использования, обучения и небольших компаний, а для крупных организаций и госструктур требует платной подписки; актуальные условия — в лицензии на сайте Docker.

Можно ли запустить Windows-приложение в Docker на Linux?
Нет: Windows-контейнеры требуют ядра Windows и запускаются на Windows-хосте в соответствующем режиме.

Чем Docker отличается от Podman?
Podman — совместимый по командам инструмент без постоянного демона, изначально ориентированный на запуск без root. Образы формата OCI работают в обоих.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)