Как заставить gunicorn передавать SIGINT в uvicorn при запуске внутри Docker?

У меня есть скрипт, запущенный в Docker (с помощью wsl2) с помощью CMD, который странно ведет себя в отношении сигналов SIGINT. Вот этот скрипт:

#!/usr/bin/env bash

python manage.py init_db

exec gunicorn foobar.asgi:application \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --graceful-timeout 5 \
  --log-level debug \
  -w 4

Проблема в том, что когда я нажимаю Ctrl+C, gunicorn в итоге вынужден принудительно убить работающих uvicorn. Через 5 секунд я вижу следующие ошибки:

^C[2024-10-29 21:15:35 +0000] [1] [INFO] Handling signal: int
[2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:8) was sent SIGKILL! Perhaps out of memory?
[2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:9) was sent SIGKILL! Perhaps out of memory?
[2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:10) was sent SIGKILL! Perhaps out of memory?
[2024-10-29 21:15:40 +0000] [1] [ERROR] Worker (pid:7) was sent SIGKILL! Perhaps out of memory?

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

  1. Зайдите в контейнер и запустите скрипт изнутри. Теперь Ctrl+C, кажется, работает лучше, потому что теперь рабочие uvicorn выходят вовремя, но gunicorn все еще печатает некоторые ошибки:
^C[2024-10-29 21:21:56 +0000] [1] [INFO] Handling signal: int
... worker shutdown cleanup output omitted
[2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:15) was sent SIGINT!
[2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:14) was sent SIGINT!
[2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:13) was sent SIGINT!
[2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:10) was sent SIGINT!
[2024-10-29 21:21:56 +0000] [7] [ERROR] Worker (pid:11) was sent SIGINT!
  1. Добавьте -it в команду docker run:
docker run -it -p 8000:8000 -v $(pwd):/app foobar:latest

Это приводит к такому же поведению, как и в случае с 1.

  1. Замените exec на ручную переадресацию сигнала:
#!/usr/bin/env bash

python manage.py init_db

# Forward SIGINT signal
trap 'kill -INT $PID' INT

# Start Gunicorn
gunicorn foobar.asgi:application \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --graceful-timeout 5 \
  --log-level debug \
  -w 4 & \
PID=$!

# Wait for Gunicorn process
wait $PID

Теперь, нажав Ctrl+C, я вижу только следующее, но я замечаю, что рабочие, похоже, больше не выполняют свой код выключения:

^C[2024-10-29 21:26:37 +0000] [7] [INFO] Handling signal: int

Что здесь происходит, и каков хороший способ убедиться, что uvicorn workers правильно отключается на Ctrl+C?

EDIT: Вот мой Dockerfile, поскольку один из комментаторов спросил меня об этом. Обратите внимание, что я получаю такое же поведение Ctrl+C, если запускаю gunicorn непосредственно из Dockerfile, а не запускаю start-django bash-скрипт:

CMD ["gunicorn", "foobar.asgi:application", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", \
     "--graceful-timeout", "5", \
     "--log-level", "debug", \
     "-w", "4"]

Dockerfile:

# >>> Build stage <<<
FROM python:3.11-slim AS build

WORKDIR /app

# Install C toolchain and C build-time dependencies.
RUN apt-get update && \
  DEBIAN_FRONTEND=noninteractive \
  apt-get install --no-install-recommends --assume-yes \
    # gcc, make, etc.
    build-essential \
    # psycopg2 client libs and header files for building psycopg2
    libpq-dev && \
  rm -rf /var/lib/apt/lists/*

# Create virtual environment and add it to PATH.
RUN python -m venv /venv
ENV PATH="/venv/bin:$PATH"

# Copy requirements and install.
COPY requirements.txt .
RUN pip install --upgrade pip && \
  pip install --no-cache-dir --no-warn-script-location -r requirements.txt


# >>> Run stage <<<
FROM python:3.11-slim

WORKDIR /app

# Install C runtime dependencies.
RUN apt-get update && \
  DEBIAN_FRONTEND=noninteractive \
  apt-get install --no-install-recommends --assume-yes \
    # psycopg2 runtime lib
    libpq5 && \
  rm -rf /var/lib/apt/lists/*

# Create and switch to appuser.
RUN groupadd -r appuser && \
  useradd --no-log-init -r -g appuser appuser && \
  chown -R appuser:appuser /app
USER appuser

# Copy virtual environment from the build stage and add it to PATH.
COPY --chown=appuser:appuser --from=build /venv /venv
ENV PATH=/venv/bin:$PATH

# Copy the rest of the application code.
COPY --chown=appuser:appuser . .

# Set Python environment variables.
# Prevent Python from writing .pyc files
ENV PYTHONDONTWRITEBYTECODE=1
# Ensure output is sent to stdout/stderr immediately
ENV PYTHONUNBUFFERED=1

# Start server.
CMD ["/app/deployment/start-django"]

EDIT2: Я наблюдаю точно такое же поведение на совершенно новом проекте Django только с этими тремя зависимостями в моем requirements.txt, используя тот же Dockerfile и bash-скрипт, что и выше:

django
gunicorn
uvicorn[standard]
Вернуться на верх