Не удается обслужить статические файлы в развернутом приложении Django (Docker, gunicorn, whitenoise, Heroku).

Я создал Django REST API, который обслуживает только JSON ответы. Моя проблема заключается в том, что в продакшене (развернутом на Heroku с debug=False) приложение, похоже, не обслуживает соответствующие статические файлы, необходимые для правильной стилизации интерфейса администратора (единственный случай использования статических файлов). Обратите внимание, что в разработке (localhost с debug=True) интерфейс администратора правильно оформлен.

При переходе к маршруту администратора по развернутому адресу (Heroku) содержимое доставляется, но без стилизации. Инструменты разработчика браузера показывают, что таблицы стилей не могут быть загружены из-за кода ошибки 500. Вывод логов Django показывает следующую деталь.

django.request ERROR    Internal Server Error: /static/admin/css/base.1f418065fc2c.css
Traceback (most recent call last):
File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 47, in inner
response = get_response(request)
File "/app/secure_my_spot/custom_middleware/request_logging.py", line 30, in __call__
print(f"Content: {response.content}")
File "/usr/local/lib/python3.8/site-packages/django/http/response.py", line 407, in content
raise AttributeError(
AttributeError: This WhiteNoiseFileResponse instance has no `content` attribute. Use `streaming_content` instead.

Я зашел в Heroku dyno и убедился, что статические файлы, которые вызывают ошибку 500, действительно находятся в static_root в соответствии с настройками Django settings.py. Я потратил значительное количество времени, рыская по интернету в поисках подсказок, что может быть причиной того, что файлы не обслуживаются в продакшене, но что бы я ни пробовал, это не сработало.

Ниже приведен краткий обзор соответствующих файлов и настроек.

Dockerfile

FROM python:3.8-alpine

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

RUN apk update && apk add \
    gcc \
    libc-dev \
    python3-dev \
    musl-dev \
    postgresql-dev \
    vim \
    bash --no-cache --upgrade

RUN pip install \
    pipenv \
    psycopg2

COPY Pipfile Pipfile.lock ./

RUN pipenv install --system --deploy --pre

COPY . .

RUN python manage.py collectstatic --noinput

CMD gunicorn secure_my_spot.wsgi:application --bind 0.0.0.0:$PORT

heroku.yml

build:
  docker:
    web: Dockerfile
release:
  image: web
  command:
    - chmod u+x heroku_entrypoint.sh
run:
  web: ./heroku_entrypoint.sh

heroku_entrypoint.sh

#!/bin/bash

python manage.py collectstatic --noinput
python manage.py migrate --noinput
gunicorn secure_my_spot.wsgi:application --bind 0.0.0.0:$PORT

settings.py

BASE_DIR = Path(__file__).resolve().parent.parent

INSTALLED_APPS = [
    "django_extensions",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
    "rest_framework.authtoken",
    "app",
    "corsheaders",
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "secure_my_spot.custom_middleware.request_logging.RequestLogging",
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

STATIC_URL = "/static/"

STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles/")

STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)

STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

wsgi.py

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "secure_my_spot.settings")

if settings.DEBUG:
    application = StaticFilesHandler(get_wsgi_application())
else:
    application = get_wsgi_application()

После прочтения this я переместил Django SecurityMiddleware на вершину массива промежуточного ПО, сразу за ним следует WhiteNoiseMiddleware (см. ниже). Это небольшое изменение сделало свое дело, и теперь все статические активы обслуживаются, как и ожидалось.

settings.py

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    "corsheaders.middleware.CorsMiddleware",
    "secure_my_spot.custom_middleware.request_logging.RequestLogging",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

К сожалению, у меня нет хорошего объяснения, почему это простое изменение порядка элементов массива промежуточного ПО вызывает эту проблему. Возможно, перемещение WhiteNoiseMiddleware перед CorsMiddleware важно, даже если сообщения об ошибках не указывают на какие-либо ошибки CORS.

Вернуться на верх