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

Привет, %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#
FROM— задаёт базовый (родительский) образ.LABEL— описывает метаданные. Например — сведения о том, кто создал и поддерживает образ.ENV— устанавливает постоянные переменные среды.RUN— выполняет команду и создаёт слой образа. Используется для установки в контейнер пакетов.COPY— копирует в контейнер файлы и папки которые лежат локально.ADD— копирует файлы и папки в контейнер, может распаковывать локальные .tar-файлы, а так же получать на вход URL и скачивать файл внутрь image.CMD— описывает команду с аргументами, которую нужно выполнить когда контейнер будет запущен. Аргументы могут быть переопределены при запуске контейнера. В файле может присутствовать лишь одна инструкцияCMD.WORKDIR— задаёт рабочую директорию для следующей инструкции.ARG— задаёт переменные для передачи Docker во время сборки образа.ENTRYPOINT— предоставляет команду с аргументами для вызова во время выполнения контейнера. Аргументы не переопределяются.EXPOSE— указывает на необходимость открыть порт. Также можно открыть socket, но это тема для отдельной заметки.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!
Если у тебя есть вопросы, комментарии и/или замечания – заходи в чат , а так же подписывайся на канал .
О способах отблагодарить автора можно почитать на странице “Донаты ”.