Pull to refresh

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 какую библиотеку в Питоне любят чаще всего? Техстек не мой, для общего развития интересуюсь.

prometheus_client — официальный клиент.

Он простой, гибкий, без лишнего. Работает с любыми фреймворками, особенно популярен с FastAPI, Flask и Django.

Для него еще есть обертки формата fastapi-prometheus или django-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, конечно, намного компактнее, но там могут быть неожиданные проблемы с чем угодно: другой пакетный менеджер, свои версии системных зависимостей, нюансов много.

  1. Если сделать так, как рекомендуется (ENTRYPOINT + CMD), то docker run myapp bash, которым, видимо, пытаемся запустить bash в контейнере, запустит как раз python app.py bash.

  2. Для чего создавать нового пользователя, а не брать готового nobody:nogroup (65534:65534 в случае FROM scratch)?

  3. || exit 1 можно убрать. Нет, нельзя.

7.Возможно для того, чтобы прокинуть пользователя в хостовую ОС или получить оттуда.

Не рассказано, как можно посмотреть содержимое каждого слоя. И некоторые советы противоречат друг другу: то сказано собирать зависимости в отдельном образе, то опять ставится всё вместе и потом делается попытка вычистить лишнее (вслепую? содержимое слоя-то мы не видим).

Про podman ни слова опять же.

Про podman ни слова

В статье про докер, действительно.

еще можно взять uv, чтобы ускорить установку библиотек

Это настолько ускоряет что прям имеет смысл? (я не пробовал но тянуть замену pip стремновато)
Обычно (при наличии локальной прокси с нужными версиями, конечно) это и так не долго.

Вопрос, если что, без подвоха

в некоторых кейcах может быть до x100 ускорение, создание виртуального окружения тоже быстрее x10-15, в целом тулинг от astral.sh уже можно назвать проверенным

ну и с коробки есть поддержка pip интерфейса, достаточно перед командой pip install ...поставить uv

А ещё, покуда у нас в статье настолько внимательно считают мегабайты, 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гб образах. Удобство не всегда значит правильно. Идеальный контейнер это строгое наличие в нём только того что нужно процессу, и не бита больше. Пусть для каждого микросервиса придётся писать отдельный большой мультистейджовый докерфайл.

Каждый раз когда вижу один общий образ для всех проектов на всех языках компании аж кровь из глаз

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

И хотя ваша точка зрения является общепризнанным стандартом, дебаг такого контейнера это адов ад, без рута, без возможности что то поставить, еще и с проверками которые перезапускают контейнер. Да, еще и без шелла контейнеры бывают :)

В больших компаниях стандартная практика среду куба держать в закрытом контуре, запрещать запуск контейнеров от рута, и делать куб ексек в рут контейнера.

Если надо продиагностировать что-то есть масса других безопасных способов.

Кароче говоря, делай нормально чтоб было нормально

Было бы очень интересно послушать про эти способы, в частности когда проблема не воспроизводится в тестовом окружении.

Ps

Еще drop capabilities в копилку, это все хорошо и правильно но дебаг усложняет

Спасибо за статью, по делу и коротко.

У меня будет пара (субъективных) комментариев:

  1. Я бы не стал указывать версию образа с точностью до версии патча (то есть вместо python:3.12.1-slimиспользовал бы python:3.12-slim). Причина: обычно подтягивать патчи уязвимостей и мелких багов — это хорошая идея.

  2. Ценность .dockerignore сильно варьируется в зависимости от языка программирования. Если в случае с python он действительно позволяет не тянуть лишний мусор (поскольку мы обычно копируем все исходники), то в случае с той же Java как правило достаточно скопировать один только jar-файл. В этом случае можно обойтись без развесистого .dockerignore и использовать менее жадное копирование. Впрочем, @titan_pc выше уже отметил этот момент.

Удивительно, что ничего не сказано про сборку под конкретную платформу (хотя это была моя первая мысль, когда я увидел название статьи), но, возможно, это тут вне скоупа.

Ну всякое бывает в минорных релизах на самом деле, вот я оставлял версию без патч релиза, и напоролся на такое недавно https://github.com/python/cpython/issues/135171. Но в целом да, обычно такой подход работает (до тех пор, пока не происходит деградация, которая тебя затрагивает)

Насколько эффективно для проектов на FastAPI использовать вот этот образ?
tiangolo/uvicorn-gunicorn 

Не рекомендуется самим автором, см. страницу образа:

This Docker image is now deprecated. There's no need to use it, you can just use Uvicorn with --workers. ✨

По 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. Просто почитайте за него, он вам наверняка понравится.

Sign up to leave a comment.

Articles