Подсказки по Docker
Ссылка на документацию https://docs.docker.com/build/concepts/dockerfile
Базовое создание Dockerfile для Python-приложения и запуск контейнера
1. Сердце контейнеризации: Создание Dockerfile
Итак, у нас есть "стройматериалы" (наше python-приложение) и "завод" (установленный Docker). Теперь нам нужен "чертеж", по которому завод будет собирать наш продукт. В мире Docker этот чертеж называется Dockerfile.
Что такое Dockerfile?
Это простой текстовый файл. В нем мы по шагам, команда за командой, описываем, как собрать образ нашего приложения. Думайте об этом как о рецепте: возьмите то, добавьте это, запустите вот так.
Прямо в корне нашей папки my-python-app, рядом с app.py и requirements.txt, создайте файл с именем Dockerfile.
| У файла не должно быть расширения. Просто Dockerfile. |
А теперь давайте напишем наш "рецепт" шаг за шагом. Откройте этот файл и начнем добавлять в него строки.
Шаг 1: Выбираем фундамент (FROM)
Используем официальный образ Python как основу
FROM python:3.11-slim
Каждый образ Docker строится на основе другого, "родительского" образа. Команда FROM — это всегда первая строчка в Dockerfile. Здесь мы говорим: "Взять за основу официальный образ с уже установленным Python версии 3.11".
Почему slim? Это "облегченная" версия образа. В ней нет лишних пакетов и документации, которые нам не нужны для запуска приложения. В результате наш итоговый образ будет весить значительно меньше. Золотое правило: всегда старайтесь делать образы как можно меньше.
Шаг 2 : Указываем рабочую папку (WORKDIR)
Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
Команда WORKDIR делает две вещи: создает директорию /app внутри нашего будущего контейнера (если ее нет) и делает ее текущей. Это значит, что все последующие команды (COPY, RUN и т.д.) будут выполняться из этой папки. Это хорошая практика, чтобы не разбрасывать файлы нашего приложения по всей файловой системе контейнера.
Шаг 3: Устанавливаем зависимости (COPY, RUN)
Копируем файл с зависимостями и устанавливаем их
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
А вот и первый хитрый момент, который отличает хороший Dockerfile от плохого. Казалось бы, почему не скопировать сразу все файлы проекта?
Дело в том, как Docker кэширует слои. Каждый RUN, COPY, ADD создает новый слой. Когда вы пересобираете образ, Docker проверяет, изменилась ли команда или файлы, которые она использует. Если нет — он берет готовый слой из кэша.
Зависимости в requirements.txt меняются редко, а вот наш код в app.py — постоянно. Разделяя эти шаги, мы добиваемся того, что Docker будет заново устанавливать все библиотеки (долгий процесс) только в том случае, если мы изменим файл requirements.txt. Если же мы поменяем только код в app.py, Docker возьмет уже готовый слой с установленными библиотеками из кэша, и сборка пройдет за секунды.
--no-cache-dir — полезный флаг, который говорит pip не сохранять кэш скачанных пакетов. Это еще один способ уменьшить итоговый размер образа.
Шаг 4: Копируем код нашего приложения (COPY)
Копируем весь остальной код приложения
COPY . .
Теперь, когда зависимости установлены, мы копируем все остальные файлы из текущей директории (первая точка) в рабочую директорию /app внутри контейнера (вторая точка).
Шаг 5: Сообщаем порт (EXPOSE)
"Сообщаем" Docker, что наше приложение будет работать на порту 5000
EXPOSE 5000
Команда EXPOSE — это, по сути, документация. Она не открывает порт по-настоящему, но сообщает Docker (и человеку, который будет этот образ использовать), что приложение внутри контейнера ожидает подключения на порту 5000.
Шаг 6: Указываем команду для запуска (CMD)
Указываем команду, которая запустится при старте контейнера
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
Это финальный аккорд. Команда CMD определяет, что будет выполнено при запуске контейнера. Мы запускаем наш gunicorn, приказываем ему слушать на всех интерфейсах (0.0.0.0) на порту 5000 и указываем, что запускать нужно объект app из модуля app (app.py).
Итоговый рецепт
Вот как должен выглядеть ваш Dockerfile целиком:
# 1. Используем официальный образ Python как основу FROM python:3.11-slim # 2. Устанавливаем рабочую директорию внутри контейнера WORKDIR /app # 3. Копируем файл с зависимостями и устанавливаем их (используем кэш) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # 4. Копируем весь остальной код приложения COPY . . # 5. "Сообщаем" Docker, что наше приложение будет работать на порту 5000 EXPOSE 5000 # 6. Указываем команду, которая запустится при старте контейнера CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
2. Сборка и запуск: Оживляем наш контейнер
Все подготовительные работы завершены. Сейчас мы выполним всего две команды в терминале, чтобы собрать и запустить наше приложение.
Убедитесь, что вы находитесь в терминале (командной строке) внутри вашей папки my-python-app, там же, где лежат все наши файлы.
Шаг 1: Собираем образ с помощью docker build
Эта команда читает наш Dockerfile и выполняет все инструкции по очереди, создавая в итоге готовый образ.
Выполните в терминале:
docker build -t my-python-app .
Давайте разберем, что здесь происходит:
-
docker build: Это основная команда для сборки образа.
-
-t my-python-app: Флаг -t (от "tag") позволяет нам присвоить образу имя и, опционально, тег. Мы назвали наш образ my-python-app. Это гораздо удобнее, чем работать со случайным ID, который Docker присвоил бы ему по умолчанию.
-
. (точка в конце): Это очень важная часть. Точка указывает на контекст сборки. Она говорит Docker: "Ищи Dockerfile в текущей директории (.) и отсюда же бери все файлы для копирования в образ (например, app.py и requirements.txt)".
После запуска команды вы увидите, как Docker поочередно выполняет каждый шаг из нашего Dockerfile: FROM, WORKDIR, COPY, RUN… Если все прошло успешно, в конце вы увидите сообщение о том, что образ собран.
Чтобы убедиться, что образ действительно создался, можно выполнить команду:
docker images
Вы должны увидеть в списке свежесозданный образ my-python-app.
Шаг 2: Запускаем контейнер из образа с помощью docker run
Образ — это, по сути, шаблон, выключенный "слепок" нашего приложения. Чтобы он ожил, из него нужно запустить контейнер. Контейнер — это работающий экземпляр образа.
Выполните эту команду:
docker run -d -p 8080:5000 --name my-running-app my-python-app
Эта команда выглядит сложнее, но тут все логично:
-
docker run: Основная команда для запуска контейнера.
-
-d: От "detached". Этот флаг запускает контейнер в фоновом режиме. Ваш терминал сразу освободится, а контейнер продолжит работать "за сценой".
-
-p 8080:5000: Флаг -p (от "publish") — ключевой момент. Он "пробрасывает" порт изнутри контейнера наружу, на нашу машину. Мы связываем порт 8080 на нашем компьютере (хост-машине) с портом 5000 внутри контейнера. Именно на 5000-м порту слушает наше приложение (gunicorn).
-
--name my-running-app: Мы даем нашему работающему контейнеру человеческое имя my-running-app. Это поможет нам в дальнейшем им управлять (останавливать, смотреть логи и т.д.).
-
my-python-app: В конце мы указываем имя образа, из которого мы хотим создать контейнер.
Шаг 3: Проверяем результат!
Самый приятный момент. Откройте ваш любимый браузер и перейдите по адресу:
Если вы все сделали правильно — ваше приложение заработает.
Поздравляю! Вы только что упаковали Python-приложение в контейнер и запустили его. Ваш код теперь работает в полностью изолированной среде, и вы можете быть уверены, что он точно так же запустится на любой другой машине, где есть Docker.
3. Управление контейнерами: Основные команды
Наше приложение работает в фоновом режиме, но оно не превратилось в неуправляемый черный ящик. У нас есть полный контроль над его жизненным циклом. Вот базовый "джентльменский набор" команд, который вы будете использовать постоянно.
1. Посмотреть, что запущено: docker ps
Эта команда — ваш главный инструмент мониторинга. Она показывает список всех активных контейнеров.
Выполните в терминале:
docker ps
Вы увидите аккуратную табличку с информацией о вашем контейнере: * его ID, из какого образа он создан (my-python-app), * статус (Up for…), * информацию о портах (0.0.0.0:8080→5000/tcp), * его имя (my-running-app).
Лайфхак: Если хотите увидеть вообще все контейнеры, включая те, что были остановлены, добавьте флаг -a: docker ps -a.
2. Заглянуть внутрь: docker logs
Что, если в приложении произошла ошибка? Или вы просто хотите посмотреть логи gunicorn, чтобы увидеть входящие запросы? Для этого есть команда docker logs.
docker logs my-running-app
Она выведет в терминал все, что ваше приложение напечатало с момента запуска. Это невероятно полезно для отладки.
Лайфхак: Чтобы следить за логами в реальном времени (как команда tail -f в Linux), добавьте флаг -f: docker logs -f my-running-app. Нажмите Ctrl+C, чтобы выйти из этого режима.
3. Остановить контейнер: docker stop
Когда вам нужно выключить ваше приложение, используйте команду docker stop. Она аккуратно посылает контейнеру сигнал о завершении работы, давая ему время корректно остановиться.
docker stop my-running-app
Терминал вернет вам имя остановленного контейнера. Если вы теперь снова выполните docker ps, список будет пуст. А вот docker ps -a покажет ваш контейнер со статусом Exited.
4. Запустить остановленный контейнер: docker start
Остановленный контейнер никуда не удаляется. Его можно запустить снова той же командой, что и заводится автомобиль:
docker start my-running-app
Он запустится с теми же параметрами, с которыми был создан изначально (с тем же пробросом портов и т.д.).
5. Удалить контейнер: docker rm
Когда контейнер вам больше не нужен и вы просто хотите освободить место, его можно удалить.
| Удалить можно только остановленный контейнер. |
# Сначала останавливаем, если он еще работает docker stop my-running-app # Затем удаляем docker rm my-running-app
После этого контейнер исчезнет навсегда. Обратите внимание: удаляется только контейнер, а не образ my-python-app. Вы в любой момент можете создать новый контейнер из этого образа с помощью команды docker run.
Вот и все! Этих пяти команд — ps, logs, stop, start, rm — вам хватит для 90% повседневных задач по управлению контейнерами. Теперь вы не просто создали контейнер, но и умеете им управлять.
6. Лучшие практики и что дальше?
Поздравляю! Вы прошли путь от простого Python-скрипта до работающего, изолированного контейнера. Это уже огромный шаг. Но, как и в любом ремесле, здесь есть свои хитрости, которые отличают новичка от профессионала.
Немного о лучших практиках Вот пара простых советов, которые сразу сделают ваши Docker-образы лучше, меньше и безопаснее.
1. Используйте .dockerignore
Когда вы выполняете команду docker build ., Docker сначала запаковывает весь "контекст" (все файлы и папки в текущей директории) и отправляет его своему движку. В этот архив попадает всё: виртуальное окружение .venv, кэш Python pycache, папки .git и другие служебные файлы, которые вашему приложению в контейнере абсолютно не нужны.
Чтобы этого избежать, создайте в корне проекта файл .dockerignore (по аналогии с .gitignore). Все, что вы в нем перечислите, будет проигнорировано при сборке.
Пример хорошего .dockerignore для Python-проекта:
# Исключаем виртуальное окружение .venv venv env # Исключаем кэш Python __pycache__/ *.pyc *.pyo # Исключаем служебные файлы IDE и ОС .idea/ .vscode/ .DS_Store # Исключаем Git .git .gitignore
Результат: Контекст сборки становится меньше (сборка быстрее), а итоговый образ не содержит мусора.
2. Не работайте под root
По умолчанию все процессы внутри контейнера запускаются от имени суперпользователя (root). С точки зрения безопасности — это плохая идея. Если злоумышленник найдет уязвимость в вашем приложении, он получит права root внутри контейнера, что может привести к серьезным последствиям.
Правильный подход — создать отдельного, непривилегированного пользователя и запускать приложение от его имени.
Добавьте эти строчки в ваш Dockerfile перед командой CMD:
# ... после COPY . . # Создаем пользователя appuser RUN addgroup -S appgroup && adduser -S appuser -G appgroup # Переключаемся на этого пользователя USER appuser # Указываем команду для запуска (она выполнится от имени appuser) CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
Это простой шаг, который значительно повышает безопасность вашего контейнера.
Что дальше? Ваш путь в мире Docker
Docker — это целая вселенная. Вы сделали первый и самый важный шаг, но впереди еще много интересного. Вот что я настоятельно рекомендую изучить дальше:
Docker Compose: дирижер для вашего оркестра
Наше приложение пока одиноко. Но что, если ему понадобится база данных (например, PostgreSQL) или кэш (Redis)? Запускать каждый контейнер отдельной docker run командой с кучей параметров — путь в никуда.
Здесь на сцену выходит Docker Compose. Это инструмент, который позволяет в простом YAML-файле описать всю связку сервисов вашего приложения: веб-сервер, базу данных, фоновые обработчики и т.д. Одной командой docker-compose up вы поднимаете все это хозяйство, и сервисы могут общаться друг с другом по сети, которую Docker Compose создает автоматически.
Это логичный следующий шаг в вашем обучении. Умение работать с Docker Compose — это уже стандарт де-факто для локальной разработки в большинстве компаний.
Container Registry: ваш личный склад образов
Сейчас ваши образы хранятся только на вашем компьютере. Чтобы использовать их на сервере или поделиться с коллегой, их нужно куда-то загрузить. Для этого существуют реестры контейнеров. Самый известный — Docker Hub. Вы можете бесплатно хранить там свои публичные образы (и несколько приватных) и скачивать их где угодно с помощью команды docker pull.
Полезная статья об оптимизации Dockerfile
Ссылка на оригинал статьи
Конспект статьи: это разбор эволюции Dockerfile для Go-приложения от «лишь бы запустилось» до production-ready multi-stage образа.
1. Плохой стартовый Dockerfile
Пример начинается с простого варианта:
FROM golang:latest COPY . . RUN go mod tidy && go build . CMD ["./myapp"]
Проблемы:
-
golang:latestделает сборку невоспроизводимой: завтраlatestможет указывать на другую версию Go. -
COPY. . тащит в образ всё:.git, README, тесты, временные файлы, потенциальные секреты. -
go mod tidyвнутри Dockerfile плохо, потому что может менятьgo.modиgo.sum. -
Образ получается тяжёлый: в статье указан пример около
1.3 ГБ. -
Сборка медленная: кеш слоёв часто инвалидируется.
-
Нет
LABEL, поэтому непонятно, кто поддерживает образ и что это за версия. -
Контейнер запускается от
root, что плохо для безопасности.
2. Первые улучшения
Что добавляют:
FROM golang:1.25.7-alpine LABEL ... WORKDIR /opt/app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build . CMD ["./myapp"]
Ключевые идеи:
-
фиксируем версию базового образа вместо
latest; -
используем
alpine, чтобы уменьшить размер; -
добавляем
LABEL maintainer,version,description; -
задаём
WORKDIR, чтобы не работать в корне файловой системы; -
сначала копируем только
go.modиgo.sum, затем делаемgo mod download— так зависимости лучше кешируются; -
go mod downloadпредпочтительнееgo mod tidy, потому что не меняет манифесты зависимостей; -
добавляем
.dockerignore, минимум:
.git README.md
Но этого ещё мало: в финальном образе всё ещё остаются исходники, кеш, Go-компилятор, тесты и возможные секреты; контейнер всё ещё работает не от отдельного пользователя.
3. Multi-stage build
Дальше сборку и запуск разделяют на две стадии:
-
builderна базеgolang:1.25.7-alpine; -
финальный runtime-образ на базе
alpine:3.21.
# Stage 1: сборка зависимостей.
FROM golang:1.25.7-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /build/myapp .
# Stage 2: финальный образ.
FROM alpine:3.21
LABEL maintainer="andrey@express42.com" \
version="0.2" \
description="Backend API"
RUN apk add --no-cache postgresql17-client
WORKDIR /opt/app
ENV API_TOKEN="super-secret-token-123"
RUN addgroup -S appuser --gid 2100 && \
adduser -S appuser --uid 2101 -G appuser
COPY --chown=appuser:appuser --from=builder /build/myapp .
USER appuser:appuser
ENTRYPOINT ["./myapp"]
На стадии сборки:
-
WORKDIR /build; -
копируются
go.modиgo.sum; -
выполняется
go mod download; -
копируется код;
-
бинарник собирается командой с параметрами:
CGO_ENABLED=0 GOOS=linux go build -o /build/myapp .
Смысл параметров:
-
GOOS=linuxявно собирает бинарник под Linux; -
CGO_ENABLED=0делает Go-бинарник независимым от системных C-библиотек; -
-o myappфиксирует имя бинарника, чтобы оно не зависело от имени модуля.
В финальном образе:
-
нет Go-компилятора;
-
копируется только готовый бинарник;
-
добавляется runtime-зависимость
postgresql17-client, потому что приложению нуженpsqlдля миграций; -
используется
apk add --no-cache, чтобы не раздувать образ кешем пакетного менеджера; -
создаётся отдельный пользователь и группа:
addgroup -S appuser --gid 2100 adduser -S appuser --uid 2101 -G appuser
-
бинарник копируется с владельцем:
COPY --chown=appuser:appuser --from=builder /build/myapp .
-
запуск переключается на:
USER appuser:appuser
-
вместо
CMDиспользуетсяENTRYPOINT ["./myapp"].
Разница: ENTRYPOINT фиксирует основную команду запуска, а аргументы из docker run добавляются к ней, а не полностью заменяют её.
4. Что ещё исправляют для production
В промежуточном варианте был плохой момент:
ENV API_TOKEN="super-secret-token-123"
Так делать нельзя: секреты не должны храниться в Dockerfile и попадать в образ.
# Stage 1: сборка зависимостей.
FROM golang:1.25.7-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# Stage 2: финальный образ.
FROM alpine:3.21
ARG BUILD_TIME
ARG VERSION
ARG PORT=8080
LABEL com.express42.compiler="Golang" \
org.opencontainers.image.version=${VERSION} \
org.opencontainers.image.authors="andrey@express42.com" \
org.opencontainers.image.description="Backend API" \
org.opencontainers.image.created=${BUILD_TIME}
WORKDIR /opt/app
RUN apk add --no-cache postgresql17-client curl && \
addgroup -S appuser --gid 2100 && \
adduser -S appuser --uid 2101 -G appuser
COPY --chown=appuser:appuser --from=builder /build/myapp .
USER appuser:appuser
EXPOSE ${PORT}
ENV PORT=${PORT}
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl --fail http://localhost:${PORT}/health || exit 1
ENTRYPOINT ["./myapp"]
CMD ["--help"]
Смысл:
-
ARGпозволяет передавать версию и время сборки из CI/CD; -
OCI labels удобны для registry, аудита и корпоративных стандартов;
-
HEALTHCHECKпозволяет Docker понять, живо приложение или зависло; -
EXPOSEдокументирует порт; -
CMDзадаёт аргументы по умолчанию дляENTRYPOINT.
5. Финальный вывод статьи
Автор делает важную оговорку: не все best practices надо слепо засовывать в Dockerfile. В реальной инфраструктуре часть настроек может жить в docker-compose.yaml, Kubernetes Deployment или другом оркестраторе.
Поэтому из итогового Dockerfile можно убрать:
-
EXPOSE; -
HEALTHCHECK; -
CMD;
если это уже описано на уровне инфраструктуры.
Вот наш итоговый оптимизированный, стабильный, multi-stage Dockerfile:
# Stage 1: сборка зависимостей.
FROM golang:1.25.7-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .
# Stage 2: финальный образ.
FROM alpine:3.21
ARG BUILD_TIME
ARG VERSION
LABEL com.express42.compiler="Golang" \
org.opencontainers.image.version=${VERSION} \
org.opencontainers.image.authors="andrey@express42.com" \
org.opencontainers.image.description="Backend API" \
org.opencontainers.image.created=${BUILD_TIME}
WORKDIR /opt/app
RUN apk add --no-cache postgresql17-client && \
addgroup -S appuser --gid 2100 && \
adduser -S appuser --uid 2101 -G appuser
COPY --chown=appuser:appuser --from=builder /build/myapp .
USER appuser:appuser
# ENTRYPOINT запускает приложение.
ENTRYPOINT ["./myapp"]
Итоговый production-подход
Финальная идея такая:
-
фиксированный базовый образ, не
latest; -
multi-stage build;
-
зависимости кешируются отдельно от исходников;
-
go mod downloadвместоgo mod tidy; -
.dockerignore; -
финальный образ без исходников и компилятора;
-
только нужные runtime-зависимости;
-
запуск не от
root; -
COPY --chown; -
OCI labels;
-
параметры сборки через
ARG; -
секреты не хранить в образе;
-
healthcheck, port и CMD добавлять только если это не дублируется инфраструктурой.
Дополнительные улучшения из статьи: фиксировать базовый образ не только по версии, но и по digest/hash, подключить hadolint, добавить README по сборке/запуску, вынести секреты в secret storage, добавить vulnerability scanning.