Начиная работать с Podman, я задал себе вопрос — функционирует ли healthcheck в Podman так же, как в Docker? Да и нет. В этой статье разберем, зачем он вообще нужен, какие бывают типичные ошибки при запуске контейнера с healthcheck, выясним детали о systemd и Quadlet.

Статью написал Роман Шубин, CTO и автор Telegram-канала Bash Days.

Различия между Podman и Docker

Ключевая разница между Podman и Docker кроется не в самих «хелфчеках», а в архитектуре. Docker использует централизированный демон, который управляет контейнерами и следит за их состоянием.

Работает это так. В файле Dockerfile/Compose задается проверка:

HEALTHCHECK CMD curl --fail http://localhost:8080 || exit 1

Демон Docker сам запускает проверки, хранит статус healthy/unhealthy, может перезапускать контейнер в связке с restart policy. А в Podman все происходит чуть менее магически. Да, в нем есть такие же healthcheck, как и в Docker, но нет демона, который будет все это крутить. Все проверки происходят на уровне яsystemd или podman healthcheck run.

Podman не будет автоматически проверять контейнер каждые N секунд, если вы это явно не настроете. Это выглядит уже более гибко и по-современному.

И получаем в итоге такую картину:

  • Docker → «убери руки, я сам все проверю и прослежу».

  • Podman → «я дам тебе инструменты, а ты решай, как их использовать».

Хорошо, с этим разобрались, стало намного яснее. Едем дальше и переходим к практическим действиям.

Нам важно мнение тех, кто в проде

Поделитесь опытом работы с железом и облаками. Мы учтем его, чтобы развивать Bare Metal Cloud дальше.

Ответить на вопросы →

Создаем и запускаем контейнер с healthcheck и разбором ошибок

Расчехляем лабораторию с развернутым Podman. Создаем файл app.py:

from flask import Flask, jsonify
import os

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello from Podman!"

@app.route("/health")
def health():
    if os.getenv("FAIL", "false") == "true":
        return jsonify({"status": "unhealthy"}), 500
    return jsonify({"status": "ok"}), 200

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

Готовим Dockerfile:

FROM python:3.11-slim

WORKDIR /app
COPY app.py .
RUN pip install flask
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl --fail http://localhost:8080/health || exit 1
CMD ["python", "app.py"]

Собираем и запускаем:

podman build -t healthcheck-demo

Сборка образа прошла успешно, но мы получаем сообщение:

HEALTHCHECK is not supported for OCI image format and will be ignored Must use docker format

Всплыло очень важное отличие Podman от Docker. По умолчанию Podman собирает образы в формате OCI, а в нем healthcheck не поддерживается.

Да, тут важно знать, что есть два формата образов:

  • OCI (Open Container Initiative) — стандарт, который использует Podman.

  • Docker format — старый формат Docker.

Поэтому указываем дополнительный ключ при сборке:

podman build --format docker -t healthcheck-demo

Все супер: собралось, ошибок и предупреждений не вылезло. Но это старый формат образа. И нам такое не совсем подходит, у нас OCI и Podman. Давайте ковырять.

Удаляем строчку с HEALTHCHECK из Dockerfile и пересобираем образ без ключа --format. Запускаем контейнер:

podman run -d -p 8080:8080 --name health-demo healthcheck-demo

Смотрим:

Хм, видим, колонка STATUS есть, но healthcheck не видно. Останавливаем контейнер:

podman stop d384c0c67528

И запускаем в таком формате:

podman run -d \
  --health-cmd="curl --fail http://localhost:8080/health || exit 1" \
  --health-interval=10s \
  --health-timeout=3s \
  --health-retries=3 \
  --name demo \
  healthcheck-demo

Отлично, в STATUS появился unhealthy. Это уже что-то! Давайте отдебажим, почему контейнер не поднял. Запускаем:

podman inspect demo

И видим:

Вот и причина — в собранном образе отсутствует утилита curl. Исправим это, добавив в Dockerfile строчку:

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

Пересобираем OCI образ и запускаем:

podman run -d \
  --replace \
  -p 8080:8080 \
  --health-cmd="curl --fail http://localhost:8080/health || exit 1" \
  --health-interval=10s \
  --health-timeout=3s \
  --health-retries=3 \
  --name demo \
  healthcheck-demo

Я добавил в команду ключ --replace, теперь Podman сам удалит старый контейнер и создаст новый. Если ключ не указать, то получим такую ошибку:

You have to remove that container to be able to reuse that name: that name is already in use, or use –replace to instruct Podman to do so.

Запустилось, смотрим:

Класс, контейнер жив, STATUS = healthy. Смотрим podman inspect demo:

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

Тоже все в порядке, мы видим {“status”:”ok”}.

Бесплатный курс «Системный администратор Linux с нуля»

Освойте администрирование Linux на SelectOS и станьте востребованным специалистом.

Зарегистрироваться →

Ключи запуска

Теперь давайте разберем ключи запуска:

  • health-interval=10s — каждые 10 секунд выполняется health-cmd.

  • health-timeout=3s — если curl не успел выполниться за три секунды → ошибка.

  • health-retries=3 — три неудачных проверки подряд → контейнер становится unhealthy (именно подряд, а не суммарно).

Dev

Для dev-окружений обычно выставляют:

  • --health-interval=5s,

  • --health-retries=1.

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

Prod

Для production-окружений:

  • --health-interval=30s,

  • --health-retries=3-5.

Получаем устойчивость к кратким лагам.

Вообще эти параметры докручиваются индивидуально под потребности проекта, у каждого будет своя «золотая середина». В большинстве случаев на небольших пет-проектах healthcheck вообще не используют, а зря.

Systemd

Итого получается, что Podman healthcheck — это не часть образа, а часть конфигурации контейнера. Чтобы Podman вел себя как «production-ready», обычно используют systemd.

Генерируем unit:

podman generate systemd --name demo --files --new

И получаем предупреждение: DEPRECATED command: It is recommended to use Quadlets for running containers and pods under systemd.

Ладно, как скажете, будем использовать Quadlets.

Quadlet

Quadlet — это способ описать контейнер сразу как systemd unit. Удаляем результат прошлой команды:

rm container-demo.service && systemctl --user daemon-reload

Создаем Quadlets-файл:

mkdir -p ~/.config/containers/systemd

Создаем файл: /etc/containers/systemd/demo.container с содержимым:

[Unit]
Description=Demo container with healthcheck

[Container]
Image=localhost/healthcheck-demo:latest

PublishPort=8080:8080

HealthCmd=curl --fail http://localhost:8080/health || exit 1
HealthInterval=10s
HealthTimeout=3s
HealthRetries=3

[Install]
WantedBy=multi-user.target

Перезагружаем systemd:

systemctl daemon-reexec
systemctl daemon-reload
Запускаем контейнер через systemd:
systemctl start demo.service
systemctl status demo.service

Отлично! На красное внимание не обращайте, это «варнинги». Сообщается, что мы запустили контейнер в developer-режиме, классическое предупреждение от Flask.

Смотрим, что говорит Podman:

Ага, мы только что подняли контейнер с помощью systemd, а healthcheck у нас явно прописан в unit-файле.

Quadlet-сервисы являются сгенерированными unit’ами, поэтому их нельзя напрямую включить через systemctl enable.

Как это работает

  1. systemd стартует,

  2. quadlet-generator читает /etc/containers/systemd/,

  3. генерирует demo.service,

  4. смотрит [Install] → WantedBy=multi-user.target,

  5. автоматически включает unit.

Перезагружаем машину:

Видим, что unit автоматически стартанул наш контейнер. Даже без команды systemctl enable demo.service.

Эта тема довольно неочевидная, но очень интересная. О ней мало подробностей, потому что обычно все довольствуются подходом «Работает, не трогаем. То, что под капотом — это уже не наша забота». Но мне всегда было интересно, как оно устроено внутри, поэтому и вы теперь это знаете.

Насчет compose-файлов сильно погружаться не буду, но скажу так: главное отличие от юнитов в следующем:

  • docker-compose up → отдельный процесс,

  • systemd → управляет контейнером как сервисом.

Почему systemd — это круто? Потому что там есть автозапуск на более низком уровне, логи через journalctl, restart policy, зависимости After / Requires, единый контроль сервисов. В общем, Quadlet — это декларативный способ описания контейнеров для systemd, который частично заменяет Docker-compose в systemd-ориентированных окружениях.

Вернемся к healthcheck

Ключевая идея — healthcheck выполняется внутри контейнера, а не снаружи.

Команда запускается как отдельный процесс, внутри того же неймспейса с тем же filesystem. Healthcheck проверяет внутреннее состояние контейнера, а не доступность сервиса извне.

У healthcheck есть три состояния (STATUS):

  • starting,

  • healthy,

  • unhealthy.

Тут все очевидно: стартуем, живем, упало.

Важно! Healthcheck ≠ restart, Podman не перезапускает контейнер сам, этим он отличается от того же Kubernetes. Чтобы добавить перезапуск, можно подшаманить unit-файл и добавить Restart=always.

Вывод

А для чего вообще нужен healthcheck? Если кратко — чтобы понять, реально ли приложение работает или просто имеется статус «контейнер запущен». Только не тешьте себя напрасными надеждами, что healthcheck добавляет какую-то «магическую» самовосстановляемость (или самовосстанавливаемость?). Нет, это просто честная и прозрачная проверка состояния контейнера. Инструмент подскажет, если что-то пойдет не так, но исправлять ситуацию за вас не станет.

В связке с systemd или Quadlet так вообще красота получается. Можно построить полноценную, управляемую и предсказуемую систему. Без лишней магии, но с полным контролем.