Comments 43
Спасибо за статью, заберу себе, даже если для своих проектов - всë равно полезно
Шестой пункт неправильно написан.
Что конкретно неверно и как на ваш взгляд должно быть? :)
Например то, что entrypoint при создании контейнера из образа (да-да, тот самый шмокер run
из примера в самой статье) можно переопределить?
Also я лично обожаю заменять вот такие конструкции вида
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-deps --no-index --find-links=/wheels -r requirements.txt
на один RUN с --mount bind
. Ибо во-первых это сложится в один слой, а во-вторых можно requirements.txt вообще внутрь не копировать, а прямо на смонтированный натравить pip uv
Какой пакет выставляет health эндпойнт?
Никакой пакет сам по себе не делает /health
. Это не “фича из коробки”.
HEALTHCHECK
— это инструкция Docker'а, а эндпойнт ты должен сделать сам в коде.
Например, для FastAPI:
@app.get("/health")
def health():
return {"status": "ok"}
Потом в Dockerfile
:
HEALTHCHECK CMD curl -f http://localhost:8000/health || exit 1
Если ты не напишешь такой роут — Docker будет проверять воздух.
Спасибо вам за ответ, хотя фамильярность тут не всем заходит. Полагал, что есть аналог actuator.
Спасибо за замечание. :) Постараюсь держать тон точнее, все же еще считаюсь здесь "новичком"
Увы, налогов актуатора в мире Python/Node по умолчанию нет.
Всё чаще делают руками — отдельный /health
или /ping
, который сам отвечает "200 OK", если всё норм.
Но есть библиотеки, если хочется автоматизации:
> для Python: healthcheck, fastapi-health, starlette-exporter
> для Node: express-healthcheck, terminus
Лишь наблюдение. Замечания тут НЛО делает. А для метрик Prometheus какую библиотеку в Питоне любят чаще всего? Техстек не мой, для общего развития интересуюсь.
Спасибо! А что с /tmp в образе? Её нужно монтировать на хост?
Нет, монтировать /tmp
из контейнера на хост не нужно.
По умолчанию /tmp
— это временная папка внутри контейнера, и это ок. Она очищается при удалении.
Монтируй только если:
– нужен доступ к этим файлам снаружи
– tmp‑кеш должен пережить перезапуск
– отлаживаешь что-то и хочешь глянуть содержимое
Но просто так — не трогай. Всё работает без этого.
tmp‑кеш должен пережить перезапуск
Стандарты ожидают, что /tmp является volatile. То есть не сохраняет содержимое при перезапуске и вообще всегда имеет под собой tmpfs или zram. В качестве non-volatile директории для временных файлов выступает /var/tmp.
в 6 пункте вы говорите что желательно использовать ENTRYPOINT
хотя в изначальном примере все же используете CMD
Можете рассказать почему вы все же используете вариант с CMD
В самом первом примере - вариант именно плохого Dockerfile, в том числе по причине cmd.
В третьем пункте - подразумевается запуск docker run myapp
без каких-либо дополнительных тегов, но если планируется как-то распространять или с проектом работают несколько человек, то разумеется такой вариант тоже будет не самым лучшим решением и правильнее будет использовать ENTRYPOINT["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Entrypoint нужен ровно для одного: чтобы сунуть туда что-то, исполняющее три базовые обязанности PID 1: подхватывать orphaned процессы, делать teardown zombie процессов и спускать полученные сигналы своим дочерним процессам. Например подойдет условный полулярный tini. И никогда это не менять. Поэтому, собственно, и появилось разделение на ENTRYPOINT и CMD. ENTRYPOINT изначально подразумевался как нечто намертво вшитое в образ, без возможности переопределить это в момент создания контейнера из образа (что позже радостно починили сломали и разрешили entrypoint тоже менять). А в CMD вы уже пихаете то, что хотите запускать по дефолту при создании контейнера, причем это уже легко переопределяется ежели вы хотите запустить что-то иное.
Короче говоря, вы практически всегда хотите видеть что-то типа такого (раз уж в статье часто упоминается python):
ENTRYPOINT [ "/usr/bin/tini", "--" ]
CMD [ "/path/to/python", "/path/to/hello-world.py*, "arguments", "more_arguments", "even_more_arguments" ]
И пихать что-то концептуально другое в entrypoint можно только когда вы очень хорошо понимаете что именно пихаете, зачем вы это пихаете и как это будет (или не будет) пинать потенциальные дочерние процессы. Иначе легко можно нарваться на ситуацию, когда родительский процесс в контейнере завершился раньше дочернего, а подхватить этот дочерний процесс и забрать его код завершения некому. Ура, вы только что начали зомби-апокалипсис в отдельно взятом компукторе!
Спасибо за статью, забрал в закладки. Если можете, распишите CI часть подробнее с примерами.
Брать
python:3.12-slim
илиpython:3.12-alpine
, но только если понимаешь, как с ним работать и для чего ты его выбираешь.
Отличная статья, спасибо. Хочу внести небольшое дополнение. По опыту для большинства python приложений достаточно slim образа + pyproject.toml. Если надо поставить какую-нибудь сложную библиотеку, типа pytorch, cupy или подобное - ищем образ от вендора, проиграешь по объёму, выиграешь по скорости развертывания и сэкономишь много нервных клеток. Alpine, конечно, намного компактнее, но там могут быть неожиданные проблемы с чем угодно: другой пакетный менеджер, свои версии системных зависимостей, нюансов много.
Если сделать так, как рекомендуется (
ENTRYPOINT
+CMD
), тоdocker run myapp bash
, которым, видимо, пытаемся запуститьbash
в контейнере, запустит как разpython app.py bash
.Для чего создавать нового пользователя, а не брать готового nobody:nogroup (65534:65534 в случае
FROM scratch
)?Нет, нельзя.|| exit 1
можно убрать.
Не рассказано, как можно посмотреть содержимое каждого слоя. И некоторые советы противоречат друг другу: то сказано собирать зависимости в отдельном образе, то опять ставится всё вместе и потом делается попытка вычистить лишнее (вслепую? содержимое слоя-то мы не видим).
Про podman ни слова опять же.
Это настолько ускоряет что прям имеет смысл? (я не пробовал но тянуть замену pip стремновато)
Обычно (при наличии локальной прокси с нужными версиями, конечно) это и так не долго.
Вопрос, если что, без подвоха
А ещё, покуда у нас в статье настолько внимательно считают мегабайты, uv шикарно запускается при сборке и будучи примонтированным из временного образа, который, в результате, не попадёт в итоговый слой (как и .lock-файл с pyproject-конфигом). Ссылка на пример использования фичи: https://docs.astral.sh/uv/guides/integration/docker/#using-uv-temporarily
Да и, вообще, по этой ссылке весь раздел руководства я нахожу весьма полезным. Может быть, даже чуточку полезнее некоторых статей.
6.Entrypoint есть всегда. По умолчанию /bin/sh -с
т.о. запускается /bin/sh -c python app.py
В случае с командой docker run myapp bash
выполнится /bin/sh -c bash
Добавлю что команда HEALTHCHECK исключительно для Docker демона. Она не поддерживается другими контейнерными движками.
Podman будет игнорить, открытая issue https://github.com/containers/podman/issues/18904
Kubernetes тоже будет игнорить, там в коде Deployments нужно свои пробы задавать (Liveness, Readiness, Startup)
Годная статья.
Спасибо!
утащил в закладки.
COPY . .
И потом огромный пухлый .dockerignore.
Лучше просто копировать нужные папки и файлы. Так Вы полностью контроллируете что будет внутри.
Установку библиотек следует сбилдить в 1 докер img, который положить в свой docker registry. Когда речь про микросервисную разработку - так удобнее. А от него уже наследоваться. И плодить микросервисы.
Потому что микросервисы на 1м проекте как правило используют одни и те же библиотеки.
Лучше просто копировать нужные папки и файлы. Так Вы полностью контроллируете что будет внутри.
Там есть нюансы. При запуске сборки, docker сначала формирует контекст, исключая указанное в .dockerignore. Затем он отправляет его докер демону. Все команды в Dockerfile взаимодействуют с копией рабочей директории из контекста.
Поэтому наличие .dockerignore может существенно сокращать время сборки за счёт уменьшения размера передаваемых докеру данных. Чтобы не писать огромные .dockerignore можно заранее копировать нужное в отдельную директорию или .tar архив, которые потом использовать в качестве контекста.
Подробнее здесь https://docs.docker.com/build/concepts/context/
Ага, и потом мы видим крошечные микросервисы в 5гб образах. Удобство не всегда значит правильно. Идеальный контейнер это строгое наличие в нём только того что нужно процессу, и не бита больше. Пусть для каждого микросервиса придётся писать отдельный большой мультистейджовый докерфайл.
Каждый раз когда вижу один общий образ для всех проектов на всех языках компании аж кровь из глаз
Думаю имелось ввиду отделить установку пакетов (как долгую часть сборки) и заливку кода или бинарников собственно приложения, а не общий базовый образ на все.
И хотя ваша точка зрения является общепризнанным стандартом, дебаг такого контейнера это адов ад, без рута, без возможности что то поставить, еще и с проверками которые перезапускают контейнер. Да, еще и без шелла контейнеры бывают :)
В больших компаниях стандартная практика среду куба держать в закрытом контуре, запрещать запуск контейнеров от рута, и делать куб ексек в рут контейнера.
Если надо продиагностировать что-то есть масса других безопасных способов.
Кароче говоря, делай нормально чтоб было нормально
Спасибо за статью, по делу и коротко.
У меня будет пара (субъективных) комментариев:
Я бы не стал указывать версию образа с точностью до версии патча (то есть вместо
python:3.12.1-slim
использовал быpython:3.12-slim
). Причина: обычно подтягивать патчи уязвимостей и мелких багов — это хорошая идея.Ценность
.dockerignore
сильно варьируется в зависимости от языка программирования. Если в случае с python он действительно позволяет не тянуть лишний мусор (поскольку мы обычно копируем все исходники), то в случае с той же Java как правило достаточно скопировать один только jar-файл. В этом случае можно обойтись без развесистого.dockerignore
и использовать менее жадное копирование. Впрочем, @titan_pc выше уже отметил этот момент.
Удивительно, что ничего не сказано про сборку под конкретную платформу (хотя это была моя первая мысль, когда я увидел название статьи), но, возможно, это тут вне скоупа.
Ну всякое бывает в минорных релизах на самом деле, вот я оставлял версию без патч релиза, и напоролся на такое недавно https://github.com/python/cpython/issues/135171. Но в целом да, обычно такой подход работает (до тех пор, пока не происходит деградация, которая тебя затрагивает)
Насколько эффективно для проектов на FastAPI использовать вот этот образ?tiangolo/uvicorn-gunicorn
По python - нет никакого смысла брать образ slim для builder и потом ставить туда build-essentials, это просто расход энергии впустую. Лучше всего взять обычный образ для билдера, там установлены специально большинство нужных зависимостей для сборки, а для результирующего образа взять slim.
Ну и как опция - можно целиком стянуть готовый venv из билдера вместо установки из wheel, но это уже на любителя.
Разрешите добавить свои 50 центов:
.dockerignore
must-have, и, более того, примерно такого вида:
## Ignore everything
*
## Except the following files and directories
!/cmd
!/internal
!/go.*
Таким образом в контексте демона будет только то что надо, он не будет "пухнуть", и кэширование слоев (а демон инвалидирует кэш слоев если контект был изменен) вернется в чат.
podman
вместоdocker
есть бро, с его киллер-фичей--cache-from
и--cache-to
. Просто почитайте за него, он вам наверняка понравится.
Как собрать Docker-образ, который можно запускать в проде (а не только у себя на ноуте)