Файловые поля и хранилища в Django: чек-лист перед продом
FileField и ImageField работают локально, но на проде отдают чужие файлы, теряют загрузки при рестарте и валятся под нагрузкой. Разбираем, что проверить до релиза.

Локально всё работает: пользователь загружает аватар, картинка сохраняется в media/, отдаётся по ссылке. На проде выясняется, что при деплое в новый контейнер старые загрузки исчезли, два инстанса приложения видят разные файлы, а прямая ссылка на приватный документ открывается у кого угодно. Django не решает эти вопросы за вас — FileField хранит только путь, а где и как лежит сам файл, определяет бэкенд хранилища и ваша конфигурация.
Ниже — чек-лист для Django 4.2 и 5.x: что проверить в файловых полях и хранилищах перед выкатом. Разбираем именно продовые грабли, а не то, как загрузить файл в форме. Обработку картинок (ресайз, миниатюры) и антивирусную проверку загрузок оставляем за скобками — это отдельные темы.
Что именно проверяем
Файловая подсистема Django состоит из трёх слоёв, и ошибка возможна на каждом:
- Поле модели —
FileFieldилиImageField: как формируется путь, что попадает в базу. - Хранилище (storage backend) — куда физически кладётся файл: локальный диск, S3-совместимое объектное хранилище, что-то своё.
- Раздача — кто и как отдаёт файл клиенту: nginx, сам Django, presigned-ссылки объектного хранилища.
1. FileField хранит путь, а не файл
В колонке базы лежит строка вида uploads/2024/invoice.pdf — относительный путь внутри хранилища. Отсюда два следствия. Если вы переезжаете на другой бэкенд, пути в базе остаются прежними, а файлы физически должны оказаться там же относительно нового корня. И если два файла загружены с одинаковым именем, Django по умолчанию добавит суффикс, но только если хранилище сообщит, что имя занято — на объектных хранилищах эта проверка стоит лишнего запроса.
2. upload_to без пользовательского ввода в пути
Частая ошибка — собирать путь из данных, которые контролирует пользователь: имени файла, поля формы, ника. Это открывает подмену пути и коллизии. Формируйте имя сами.
import uuid
from pathlib import Path
from django.db import models
def document_upload_path(instance, filename):
# Генерируем имя сами, сохраняя исходное расширение.
# Пользовательское имя файла в путь не попадает.
ext = Path(filename).suffix.lower()
return f"documents/{instance.owner_id}/{uuid.uuid4().hex}{ext}"
class Document(models.Model):
owner = models.ForeignKey("auth.User", on_delete=models.CASCADE)
file = models.FileField(upload_to=document_upload_path)
original_name = models.CharField(max_length=255) # исходное имя — в отдельное полеИсходное имя, если оно нужно для скачивания, храните отдельным полем и отдавайте через заголовок Content-Disposition, а не через путь на диске.
3. MEDIA_ROOT в контейнере — это ловушка
Если приложение живёт в контейнере или на нескольких инстансах, а MEDIA_ROOT указывает на локальный путь, вы получите один из двух сценариев: загрузки исчезнут при пересоздании контейнера, либо разные реплики будут видеть разные наборы файлов. Локальный диск подходит, только когда инстанс один и данные лежат на примонтированном постоянном томе. Во всех остальных случаях — объектное хранилище.
Правило простое: если вы не можете назвать конкретный постоянный том, на котором лежит MEDIA_ROOT, значит, загрузки вы уже теряете — просто ещё не заметили.
4. django-storages и S3-совместимое хранилище
Для S3, MinIO, Yandex Object Storage и подобных используется пакет django-storages с бэкендом boto3. В Django 4.2+ хранилища настраиваются через словарь STORAGES.
# settings.py, Django 4.2+, django-storages с boto3
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
"bucket_name": "my-app-media",
"region_name": "ru-central1",
"endpoint_url": "https://storage.yandexcloud.net",
# По умолчанию объекты приватные — ссылки подписываются
"default_acl": None,
"querystring_auth": True,
"file_overwrite": False, # не затирать одноимённые файлы
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
# Ключи доступа — только из окружения, не в коде
# AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY читаются boto3 из envТочные имена ключей внутри OPTIONS зависят от версии django-storages — сверяйтесь с документацией пакета для вашей версии, набор параметров между релизами менялся. Ключи доступа держите в переменных окружения или секрет-хранилище, в коде — только заглушки вида YOUR_ACCESS_KEY.
5. Приватные файлы не должны быть публичными по ссылке
Если бакет настроен на публичное чтение, любой, кто угадал или получил ссылку, скачает файл — включая договоры и паспорта. Для приватных данных объекты должны быть приватными (default_acl=None или явный private), а ссылки — подписанными и короткоживущими (presigned URL). Тогда ссылка действует, скажем, 15 минут и не переиспользуется.
Если раздаёте приватные файлы через сам Django, проверяйте права в view, а физическую отдачу перекладывайте на nginx через X-Accel-Redirect, чтобы не гонять байты через Python-процесс.
6. Валидация: тип и размер
Расширение файла ничего не гарантирует — .jpg может оказаться исполняемым файлом. Ограничивайте размер на уровне приложения и проверяйте содержимое, а не только имя.
DATA_UPLOAD_MAX_MEMORY_SIZEиFILE_UPLOAD_MAX_MEMORY_SIZE— сколько Django держит в памяти до сброса на диск. Крупные файлы должны идти во временный файл, а не в память.- Ограничение размера тела запроса дублируйте на nginx (
client_max_body_size) — иначе гигабайтная загрузка дойдёт до Python и там упадёт. - MIME-тип проверяйте по сигнатуре содержимого, а не по заголовку от клиента.
7. Удаление файла из хранилища
Удаление объекта модели не удаляет связанный файл — с версий, где убрали автоматическую очистку, это ответственность разработчика. Осиротевшие файлы копятся и занимают место. Вешайте удаление на сигнал post_delete или используйте фоновую задачу, которая сверяет хранилище с базой.
Сравнение вариантов хранения
| Критерий | Локальный диск | Сетевой том (NFS) | Объектное хранилище (S3) |
|---|---|---|---|
| Несколько инстансов | Не работает | Работает, но медленно | Работает |
| Переживает пересоздание контейнера | Нет | Да | Да |
| Раздача под нагрузкой | nginx напрямую | Зависит от NFS | Из хранилища / CDN |
| Приватные ссылки | Своими силами | Своими силами | Presigned из коробки |
| Стоимость на старте | Ноль | Средняя | Оплата за объём и запросы |
Когда что выбирать
Один инстанс на VPS с примонтированным диском и нечастыми загрузками — локальный диск, не усложняйте. Несколько реплик, контейнеры, автоскейл или приватные документы — объектное хранилище. NFS оправдан редко: чаще это компромисс от нежелания переписывать код под S3, и он привносит собственные проблемы с блокировками и задержками.
Что ломается первым
- Потеря загрузок после деплоя. Симптом: старые аватары стали битыми ссылками. Причина:
MEDIA_ROOTвнутри эфемерного контейнера. - 502 или таймаут на больших файлах. nginx режет тело запроса или Django пытается уложить файл в память. Проверяйте
client_max_body_sizeи лимиты памяти. - Утечка приватных файлов. Публичный бакет плюс предсказуемые пути. Ссылка на чужой документ открывается без авторизации.
- Рост счёта за хранилище. Осиротевшие файлы после удаления записей. Раз в период сверяйте объекты в бакете с путями в базе.
Что сделать дальше
Пройдитесь по пунктам чек-листа на текущем окружении: где физически лежит MEDIA_ROOT, приватны ли объекты, есть ли лимит размера на nginx, чистятся ли файлы при удалении записей. Заведите метрику на объём хранилища и число объектов — резкий рост подскажет об осиротевших файлах раньше, чем придёт счёт. Перед переездом на S3 напишите management-команду, которая проверит, что все пути из базы реально существуют в хранилище: расхождение здесь означает битые ссылки в продакшене.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: Django documentation — Managing files и File storage API, django-storages documentation
Частые вопросы
У меня Django 3.2, там нет настройки STORAGES — как быть?
Файлы всё равно теряются после деплоя, хотя MEDIA_ROOT задан. Что не так?
Нужно ли ограничивать размер загрузки и в Django, и в nginx?
Как отдавать приватные файлы, чтобы их не скачал посторонний?
Почему после удаления записи файл остаётся в хранилище?
django-storages поддерживает Yandex Object Storage и MinIO?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.