Не удается обслужить статические файлы в развернутом приложении 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.