ЯдроКодаподготовка к экзаменам
Учебная платформа

Загружаем материалы

Подготавливаем материалы и навигацию по разделу.

Python-сервис в контейнере: конфигурация, stdout и корректная остановка

Автор: · Обновлено

Чтобы Python-программа стала удобным контейнерным сервисом, ей нужен понятный контракт: адрес и порт, конфигурация из окружения, логи в стандартные потоки и корректная реакция на…

Чтобы Python-программа стала удобным контейнерным сервисом, ей нужен понятный контракт: адрес и порт, конфигурация из окружения, логи в стандартные потоки и корректная реакция на остановку. Dockerfile из [урока Python и Docker](/lessons/without-university/docker-containerization/docker-developer-08) упаковывает код; здесь улучшим поведение самого процесса.

Самостоятельное приложение

Создайте app.py. Пример использует только стандартную библиотеку Python:

import json
import logging
import os
import signal
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

port = int(os.getenv("PORT", "8000"))
if not 1 <= port <= 65535:
    raise ValueError("PORT must be between 1 and 65535")
greeting = os.getenv("GREETING", "hello-container")
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("service")
stopping = False


def request_stop(signum, frame):
    global stopping
    stopping = True


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path not in ("/", "/health"):
            self.send_error(404)
            return
        payload = {"status": "ok"} if self.path == "/health" else {"message": greeting}
        body = json.dumps(payload).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, template, *args):
        log.info(template, *args)


signal.signal(signal.SIGTERM, request_stop)
signal.signal(signal.SIGINT, request_stop)
server = ThreadingHTTPServer(("0.0.0.0", port), Handler)
worker = threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.1})
worker.start()
log.info("started port=%s", port)
try:
    while not stopping:
        time.sleep(0.1)
finally:
    server.shutdown()
    server.server_close()
    worker.join(timeout=5)
    log.info("stopped cleanly")

Почему shutdown не находится в signal handler

Python исполняет обработчики сигналов в главном потоке. Внутри handler мы меняем только простой флаг: без логирования, блокировок и Event.set. Эти операции могут захватить lock, который был прерван в том же потоке. Ограничения signal.

server.shutdown() должен выполняться из другого потока, чем serve_forever(), иначе возникает deadlock. Здесь серверный цикл работает в worker, а shutdown вызывает главный поток после проверки флага. Это конкретное условие API, а не рекомендация «добавить threading на всякий случай». Правила socketserver.

ThreadingHTTPServer в этом учебном примере не гарантирует завершение всех длинных запросов до выхода. Для production нужны ограниченные таймауты, контроль активных запросов и выбранный сервер приложения. Но приведённый код позволяет корректно остановить принимающий цикл и завершиться с кодом 0.

Упаковка и наблюдение

Создайте Dockerfile:

FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY app.py ./app.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "app.py"]
docker build -t dk-python-service:1 .
docker run -d --name dk-python-service -p 127.0.0.1:18092:8000 -e GREETING=service-ready dk-python-service:1
curl http://127.0.0.1:18092/
curl http://127.0.0.1:18092/health
docker logs --tail 20 dk-python-service
docker stop --timeout 5 dk-python-service
docker inspect --format '{{.State.ExitCode}}' dk-python-service
docker logs --tail 5 dk-python-service

Ожидайте service-ready, статус ok, запись started, затем stopped cleanly и код 0. При раннем запросе повторите curl после запуска процесса. Exec-форма CMD запускает Python напрямую; сигнал Docker не застревает в ненужной shell-обёртке. Stop сначала посылает сигнал завершения и только после таймаута может принудительно остановить процесс. Семантика Docker stop.

Настройка и ошибки

Если изменить PORT на 9000, измените и внутреннюю сторону публикации: -p 127.0.0.1:18092:9000. EXPOSE не перенастраивает Python. Неверное значение PORT должно дать понятную ошибку до начала обслуживания, а не тихо включить случайный порт. Не записывайте в логи всё окружение: в нём могут быть пароли и токены.

Добавление /health даёт endpoint, но не создаёт Docker healthcheck автоматически. Настройка проверки с таймаутом и интервалом рассматривается в [уроке Compose healthcheck](/lessons/without-university/docker-containerization/docker-developer-24). Связь PID 1 и restart policy разобрана в [уроке об остановке контейнера](/lessons/without-university/docker-containerization/docker-developer-26).

Упражнение

Запустите сервис с другим GREETING и убедитесь, что образ не пересобирался. Затем проверьте ошибочный PORT, сохранив контейнер для чтения logs. Наконец повторите корректный запуск и stop с таймаутом 5 секунд: критерий успеха — запись завершения и exit code 0, а не просто исчезновение из docker ps. Удалите только созданные контейнеры после чтения их состояния.

Источники