Основы Docker: Dockerfile и docker-compose.yml

Edit...
basics
Illustrated by Igan Pol

Привет, %username%! Поскольку Docker мы уже установили, теперь надо что-то в нем запустить. И как только мы захотели что-то запустить, то первое с чем мы сталкиваемся это Dockerfile и compose.yml (исторически — docker-compose.yml). О них и будет речь далее.

🔄 Обновлено 2026-05-29: поправил отступы в compose.yml (был сломан YAML) и опечатки в командах сборки. Ранее, 2026-05-15, подтянул примеры под современные версии — образ python:3.12-slim вместо 3.8, postgres:16-alpine вместо 12, убрал version: из compose-файла (deprecated с Compose v2). Команда docker-compose (Python, v1) End-of-life с июня 2023 — пиши docker compose (плагин, v2, без дефиса). В конце поста добавил блоки про BuildKit/multi-stage и альтернативы (Podman, nerdctl).

Образы Docker#

Для начала заглянув в документацию вспомним, что же такое контейнер Docker и поймем, что Docker-контейнер - это Docker image (образ) который оживили. Собственно говоря Docker image - это то, из чего запускается любой Docker-container.

Каждому Docker image соответствует свой набор инструкций и файл с такими инструкция называется Dockerfile – без расширений и каких-либо точек. Из Dockerfile в дальнейшем собираются Docker images (образы). Сброка нового образа выполняется запуском команды:

docker build -f ./Dockerfile .

Точка в конце — это контекст сборки (текущая директория), её Docker отправляет демону. Учитывая, что наш Dockerfile расположен в той же директории где и мы запускаем команду сборки, команду можно упростить до такого вида:

docker build .

Каждый контейнер состоит из слоев, а каждый слой контейнера (за исключением последнего) предназначен исключительно для чтения. Собственно основная задача Dockerfile состоит в том, чтобы описать последовательность данных слоев – в каком порядке они идут.

Учитывая, что в Unix все есть файл мы можем сказать, что отдельный слой это всего лишь файл, который описывает изменение состояния по сравнению с предыдущим слоем.

Базовый образ является лишь исходным слоем (слоями) создаваемого образа (иногда называют родительским образом).

Файл Dockerfile#

В Dockerfile написаны инструкции по соданию образа. Каждая инструкция пишется с начала строки заглавными буквами, а после инструкции идут ее аргументы. Обработка инструкций происходит сверху вниз согласно тому порядку, как они написаны в файле. Простейший пример Dockerfile выглядит следующим образом:

FROM ubuntu:24.04
COPY . /var/www/html

Новые слои в итоговом образе создаются только инструкциями FROM, RUN, COPY, ADD. Остальные инструкции что-то описывают, настраивают или общаются с Docker’ом говоря, например – открыть такой-то порт.

Инструкции Dockerfile#

  1. FROM — задаёт базовый (родительский) образ.
  2. LABEL — описывает метаданные. Например — сведения о том, кто создал и поддерживает образ.
  3. ENV — устанавливает постоянные переменные среды.
  4. RUN — выполняет команду и создаёт слой образа. Используется для установки в контейнер пакетов.
  5. COPY — копирует в контейнер файлы и папки которые лежат локально.
  6. ADD — копирует файлы и папки в контейнер, может распаковывать локальные .tar-файлы, а так же получать на вход URL и скачивать файл внутрь image.
  7. CMD — описывает команду с аргументами, которую нужно выполнить когда контейнер будет запущен. Аргументы могут быть переопределены при запуске контейнера. В файле может присутствовать лишь одна инструкция CMD.
  8. WORKDIR — задаёт рабочую директорию для следующей инструкции.
  9. ARG — задаёт переменные для передачи Docker во время сборки образа.
  10. ENTRYPOINT — предоставляет команду с аргументами для вызова во время выполнения контейнера. Аргументы не переопределяются.
  11. EXPOSE — указывает на необходимость открыть порт. Также можно открыть socket, но это тема для отдельной заметки.
  12. VOLUME — создаёт точку монтирования для работы с постоянным хранилищем.

Подробности и нюансы можно почерпнуть из официальной документации.

Живой пример Dockerfile#

Вот живой, но довольно простой пример Dockerfile:

# В качестве родителя используем slim-образ с Python 3.12
FROM python:3.12-slim

# Просим Python не писать .pyc файлы
ENV PYTHONDONTWRITEBYTECODE=1

# Просим Python не буферизовать stdin/stdout
ENV PYTHONUNBUFFERED=1

# Задаем рабочую директорию
WORKDIR /opt/app

# Копируем файл с зависимостями
COPY ./req.txt /opt/app/requirements.txt

# Устанавливаем зависимости
RUN pip install -r /opt/app/requirements.txt

# Копируем исходный код приложения
COPY ./src /opt/app

# Говорим что надо открыть снаружи порт 8000
EXPOSE 8000

# Команда которая должна быть выполнена при запуске контейнера
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Думаю ничего сложного тут нет, учитывая все комментарии и вышеизложенное.

Сборка/публикация в registry#

Вот мы обзавелись простым Dockerfile, а что дальше? Дальше нам необходимо выполнить сборку:

docker build -t jtprog/django_movie:0.1 .

По порядку: мы говорим докеру сбилдить образ, дать ему имя django_movie и тэг (-t – сокращение от —tag) 0.1, ну а Dockerfile взять из текущей директории (точка в конце). jtprog – говорит о том, что данный образ будет принадлежать мне и будет привязан к моему аккаунту на hub.docker.com

Теперь у нас есть собраный образ и он хранится локально. Посмотреть его можно вот так:

docker images
REPOSITORY            TAG                 IMAGE ID            CREATED             SIZE
jtprog/django_movie   0.1                 e4d1fb505769        24 minutes ago      230MB
python                3.12-slim           4e2d08f34f6d        3 days ago          155MB

Данная команда покажет все images которые у нас есть и локально доступны – скачаны. Собственно после сборки образа его можно отправить в реестр (docker registry), дабы им могли воспользоваться другие или вы сами с другого компьютера/сервера. Самым простым вариантом в таком случае является собственно Docker Hub и к тому же он бесплатный. Регистрация там простая, так что справитесь.

После регистрации нам необходимо выполнить в консоли вот эту команду:

docker login

У вас будет спрошен адрес удаленного реестра, логин и пароль для доступа к нему. Указываете свои логин и пароль – готово!

Теперь отправим свой образ в реестр – на Docker Hub. Делается это так же довольно просто:

docker image push jtprog/django_movie:0.1

Теперь наш Docker image доступен всем и каждому вот тут – django_movie .

Запуск#

Научились простенько собирать образы и публиковать их на Docker Hub, а теперь попробуем воспользоваться нашим образом. Попробуем запустить из него контейнер. Для этого выполним вот такую команду:

docker run -p 8000:8000 --detach --name movie jtprog/django_movie:0.1

Тут по порядку: docker run – просим запустить, -p 8000:8000 – пробросить порт 8000 с нашего сервера (компуктера) на порт 8000 нашего контейнера, --detach – отключиться (не интерактивная работа с контейнером), --name movie – называем наш контейнер movie, jtprog/django_movie:0.1 – имя пользователя в реестре/имя образа/тэг. Вроде внятно и понятно.

Теперь в списке запущенных контейнеров наш свеженький появится с именем movie. А посмотреть список запущенных контейнеров можно следующей командой:

docker ps

И вроде бы всё хорошо, мы видим наш контейнер, но что-то мы забыли – не кажется? Правильно – у нас же ж простое приложение на Django, которое использует в качестве БД PostgreSQL. А мы ничего для ее – БД – запуска не сделали, соответственно наше приложение не может подключиться для инициализации к базе данных.

Используем docker-compose#

Согласно одной из легенд, docker-compose появился после того, как к разработчикам Docker пришли и сказали: “Docker – отличная вещь! Но сделайте удобно!” Будем считать что он уже поставлен у вас. Так что сразу перейдем к делу – содаем локально рядом с Dockerfile еще и compose.yml (или docker-compose.yml — старое имя файла, его до сих пор подхватывают все версии compose).

С 2023 года официально docker compose (без дефиса, плагин на Go). Старая команда docker-compose (Python, v1) — End-of-Life. В большинстве дистрибутивов docker-compose-plugin ставится автоматически вместе с docker-ce. Если у тебя docker compose version отвечает осмысленно — всё ок.

И приводим его к такому виду:

# В Compose v2 поле `version:` больше не нужно — формат файла определяется
# самим compose-движком автоматически. Если оно у тебя ещё есть — можно смело удалять.

# Сервисы которые мы будем запускать
services:
  # Первый сервис - db
  db:
    # Образ на основе которого он будет запускаться
    image: postgres:16-alpine
    # volumes - магическая вещь, которая создает некоторое устройство в
    # рамках Docker и монтирует его в директорию /var/lib/postgresql/data
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    # Переменные окружения
    environment:
      POSTGRES_USER: movie
      POSTGRES_PASSWORD: 123456
      POSTGRES_DB: movie
    # Говорим открыть снаружи порт 5432
    expose:
      - 5432

  # Второй сервис - app
  app:
    # Говорим что его надо будет собрать - в качестве контекста
    # передаем текущую директорию - в ней лежит Dockerfile
    build: .
    # Монтируем локальную директорию ./src в директорию
    # внутри контейнера /opt/app
    volumes:
      - ./src:/opt/app
    # Говорим пробросить порт 8000 хоста в порт 8000 контейнера
    ports:
      - 8000:8000
    # Зависит от сервиса db - запускать после него
    depends_on:
      - db

# Просто говорим создать volume с именем postgres_data
volumes:
  postgres_data:

После этого в консоли выполним вот эту команду:

docker compose up --build -d

Тут мы говорим: up – поднять, --build – собрать, -d – пусть робит в фоне. После чего мы можем посмотреть список запущенных сервисов:

docker compose ps
Name                       Command               State           Ports
----------------------------------------------------------------------------------------
django_movie_drf_app_1   python /opt/app/manage.py  ...   Up      0.0.0.0:8000->8000/tcp
django_movie_drf_db_1    docker-entrypoint.sh postgres    Up      5432/tcp

Важно: Все команды docker compose должны выполняться в той же директории где расположен файл compose.yml (или docker-compose.yml). В противном случае необходимо указывать его явно через флаг -f и путь до файла.

К слову у вас может быть несколько файлов docker-compose.yml и их можно включать все например вот такой конструкцией:

docker compose -f compose.yml -f compose.admin.yml run -it backup_db

Кейс: в файле docker-compose.admin.yml может быть больше доступа. Или для среды разработки можно поднимать моки вместо реальных сервисов.

Так же замечу, что все сервисы описанные в рамках одного docker-compose.yml файла (в нашем примере это сервисы db и app), будут сразу “из коробки” видеть друг друга по указанным именам.

Ну а теперь откройте в браузере http://127.0.0.1:8000/swagger/ и убедитесь, что приложение поднялось и заработало – откроется Swagger-документация по API нашего приложения. По крайней мере так может казаться на первый взгляд.

BuildKit и multi-stage#

Пост написан в 2020-ом, и тогда docker build был «классический»: один Dockerfile = одна цепочка слоёв, никакого кеша зависимостей между сборками, никаких параллельных стадий. С Docker 23.x (январь 2023) BuildKit — дефолтный сборщик. Это даёт сразу несколько полезных штук.

Включить новый синтаксис. Первой строкой в Dockerfile:

# syntax=docker/dockerfile:1.7

Это говорит BuildKit’у использовать актуальный фронтенд с поддержкой RUN --mount, heredocs и прочих современных директив.

Кеш пакетного менеджера. Без кеша каждый pip install / apt-get install качает зависимости заново. С BuildKit можно подмонтировать кеш-том:

# syntax=docker/dockerfile:1.7
FROM python:3.12-slim
WORKDIR /opt/app
COPY req.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r req.txt
COPY ./src /opt/app
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

Кеш сохраняется между сборками — повторный docker build ставит зависимости из кеша за секунды.

Multi-stage. Финальный образ часто получается жирным: компилятор, dev-зависимости, build-tooling в нём не нужны. Multi-stage позволяет собрать в одном слое, а в финальный взять только результат:

# syntax=docker/dockerfile:1.7

# --- build stage ---
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

# --- runtime stage ---
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/app /app
USER nonroot
ENTRYPOINT ["/app"]

Результат — 10-20 MB вместо 800+ MB исходного Go-образа.

Альтернативы Docker#

Docker — не единственный игрок на поле контейнеров. Полезно знать про альтернативы — особенно когда упираешься в demon-only режим, root-привилегии или хочешь чего-то более OCI-нативного.

  • Podman — daemonless, rootless из коробки, CLI почти 1-в-1 с docker (alias docker=podman работает в 95% случаев). Стандарт в RHEL/Rocky/Alma 8+. Умеет в pod’ы (k8s-style группировки контейнеров) и в systemd-юниты через podman generate systemd / quadlet.
  • nerdctl + containerd — docker-совместимый CLI поверх containerd (того самого, что под капотом у Kubernetes). Удобно, когда уже есть containerd и не хочется держать второй runtime.
  • Rootless Docker — если уходить от Docker не хочется, но root-демона стрёмно, можно запустить сам Docker в rootless-режиме . Чуть медленнее по I/O, но безопасности заметно больше.
  • BuildKit standalone (buildctl) — если нужен только build-этап без runtime’а. Часто используется в CI.

Для большинства повседневных задач Docker всё ещё де-факто стандарт, но если ты пишешь что-то для прода в 2026-ом — стоит хотя бы раз попробовать Podman или nerdctl, чтобы понимать что есть выбор.

Работа напильником#

Действительно, на первый взгляд может показаться, что все работает, но нет. Мы же в консоли через manage.py создаем миграции, накатываем их, собираем статику, создаем суперпользователя. Так что давай-ка подумаем: как нам выполнить всё то же самое в контейнере? И готов поспорить, что первое что пришло тебе в голову:

RUN python manage.py makemigrations
RUN python manage.py migrate

Как только тебе это пришло в голову – иди попей чаю, покушай, отдохни, поспи! Короче гони из головы эту дичь! Я какбэ не запрещаю тебе страдать хернёй, но тем не менее делать так не советую.

Как быть дальше и с какой стороны/силы приложить напильник, будет описано в следующей статье.

На этом всё! Profit!


Если у тебя есть вопросы, комментарии и/или замечания – заходи в чат , а так же подписывайся на канал .

О способах отблагодарить автора можно почитать на странице “Донаты ”.