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

Загрузка файлов в Django выглядит обманчиво простой: добавили FileField, сделали миграцию, форму с enctype="multipart/form-data" — и файлы вроде бы сохраняются. Проблемы начинаются на проде: файлы недоступны по ссылке, папка media разрастается мусором, при удалении объекта физический файл остаётся на диске, а имена вида photo.jpg перезаписывают друг друга. Разбираем ошибки, которые повторяются из проекта в проект, и что с ними делать. Актуально для Django 5.x, но большинство пунктов работает начиная с 3.x.
Ошибка 1. Путать MEDIA и STATIC
Самая частая путаница у тех, кто только освоил статику. STATIC — это файлы, которые кладёте вы: CSS, JS, картинки шаблона. Они собираются командой collectstatic и версионируются в репозитории. MEDIA — файлы, которые загружают пользователи через FileField и ImageField. Их в репозитории нет, они появляются в рантайме.
Если пытаться раздавать медиа через механизм статики, в продакшене вы получите 404: collectstatic про пользовательские файлы ничего не знает, а runserver раздаёт их только при DEBUG = True.
# settings.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Куда сохраняются загруженные файлы на диске
MEDIA_ROOT = BASE_DIR / "media"
# Префикс URL, по которому они отдаются
MEDIA_URL = "/media/"
В режиме разработки медиа нужно подключить в корневом urls.py:
# urls.py
from django.conf import settings
from django.conf.urls.static import static
from django.urls import path
urlpatterns = [
# ваши маршруты
]
# Работает ТОЛЬКО при DEBUG = True, для локальной разработки
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
В продакшене медиа раздаёт не Django, а Nginx или объектное хранилище. Строка соstatic()внутриif settings.DEBUG— это заглушка для локалки, не для боевого сервера.
Ошибка 2. Отдавать медиа через Django в проде
Раздача файлов Python-процессом — это лишняя нагрузка и медленно. На каждый запрос картинки поднимается воркер, который мог бы обрабатывать бизнес-логику. Отдача статики и медиа — задача веб-сервера или CDN.
Пример блока для Nginx: файлы из MEDIA_ROOT раздаются напрямую, минуя приложение.
location /media/ {
alias /var/www/myproject/media/;
expires 30d;
}
Путь в alias должен совпадать с MEDIA_ROOT. Если файлы лежат в одном месте, а Nginx смотрит в другое — снова 404, только теперь молча, без ошибки в логах Django.
Ошибка 3. Хранить всё в одной папке с исходным именем
Если задать upload_to="uploads", все файлы валятся в одну директорию. Два пользователя загрузили document.pdf — второй файл не перезапишет первый: Django добавит к имени случайный суффикс. Но папка на десятки тысяч файлов тормозит файловую систему, а разбираться в ней вручную невозможно.
Решение — раскладывать файлы по датам или по идентификатору владельца. upload_to принимает не только строку, но и функцию.
import uuid
from pathlib import Path
from django.db import models
def user_avatar_path(instance, filename):
# Сохраняем расширение, но генерируем безопасное имя
ext = Path(filename).suffix.lower()
new_name = f"{uuid.uuid4().hex}{ext}"
# Раскладываем по id пользователя: media/avatars/42/abc123.jpg
return f"avatars/{instance.user_id}/{new_name}"
class Profile(models.Model):
user = models.OneToOneField("auth.User", on_delete=models.CASCADE)
avatar = models.ImageField(upload_to=user_avatar_path, blank=True)
Дата тоже поддерживается строкой с шаблоном strftime: upload_to="reports/%Y/%m/" разложит файлы по годам и месяцам без своей функции.
Генерация имени через uuid4 заодно закрывает вопрос с небезопасными именами: пользователь может прислать файл ../../etc/passwd.jpg, и полагаться на то, что Django всё вычистит, не стоит.
Ошибка 4. Забывать, что файл переживает объект
Удаление записи из базы не удаляет связанный файл с диска. Это поведение изменилось давно: до Django 1.3 файл удалялся автоматически, но такой механизм ронял данные при откате транзакции, и его убрали. Теперь при obj.delete() строка исчезает из БД, а файл остаётся навсегда. Через год media набита файлами, на которые нет ни одной ссылки.
Чистить нужно явно. Самый прямой способ — сигнал post_delete:
from django.db.models.signals import post_delete
from django.dispatch import receiver
@receiver(post_delete, sender=Profile)
def delete_avatar_file(sender, instance, **kwargs):
if instance.avatar:
# delete(save=False) убирает файл из хранилища,
# не трогая уже удалённую запись в БД
instance.avatar.delete(save=False)
Отдельная проблема — замена файла. Когда пользователь загружает новый аватар вместо старого, старый файл тоже остаётся сиротой. Ловить это через pre_save сложнее: нужно достать прежнее значение из БД и сравнить пути. Для проектов, где такое важно, проще взять готовый пакет вроде django-cleanup, который вешает сигналы за вас.
Ошибка 5. Доверять расширению и типу файла
ImageField проверяет, что загруженный файл — валидное изображение (для этого нужен установленный Pillow), но не ограничивает размер и формат. Пользователь может загрузить 200-мегабайтный TIFF, и Django его примет. А обычный FileField не проверяет вообще ничего.
Ограничения задаются валидаторами и проверкой в форме. Расширение можно ограничить встроенным валидатором:
from django.core.validators import FileExtensionValidator
from django.db import models
class Document(models.Model):
file = models.FileField(
upload_to="documents/%Y/%m/",
validators=[FileExtensionValidator(allowed_extensions=["pdf", "docx"])],
)
Важно понимать границу: FileExtensionValidator смотрит только на расширение в имени, а не на реальное содержимое. Файл с вредоносным содержимым и расширением .pdf он пропустит. Для проверки настоящего типа нужен анализ содержимого (например, по сигнатуре файла отдельной библиотекой) — это отдельная тема, здесь фиксируем только сам факт, что расширению верить нельзя.
Размер ограничивают в методе clean формы:
from django import forms
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 МБ
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
fields = ["file"]
def clean_file(self):
uploaded = self.cleaned_data["file"]
if uploaded.size > MAX_UPLOAD_SIZE:
raise forms.ValidationError("Файл больше 10 МБ.")
return uploaded
Ошибка 6. Хранить файлы на диске, когда серверов несколько
Пока приложение крутится на одной машине, локальный диск работает. Как только вы разворачиваете два инстанса за балансировщиком, файл, загруженный на первый сервер, недоступен со второго. Пользователь загрузил аватар, обновил страницу — попал на другой инстанс, аватара нет.
Решение — вынести файлы в объектное хранилище (S3-совместимое: AWS S3, Yandex Object Storage, MinIO). В Django 5.x за это отвечает механизм STORAGES в настройках, а для S3 обычно ставят пакет django-storages. Точные имена бэкендов и параметры зависят от версии пакета — сверяйте их с актуальной документацией django-storages, а не подставляйте по памяти.
| Критерий | Локальный диск (FileSystemStorage) | Объектное хранилище (S3-совместимое) |
|---|---|---|
| Число серверов приложения | Только один | Любое |
| Настройка | Из коробки | Доп. пакет + доступы |
| Резервное копирование | Своими силами | Часто на стороне провайдера |
| Раздача | Через Nginx | Напрямую или через CDN |
| Стоимость на старте | Ноль | Плата за объём и трафик |
Для одиночного сервера или пет-проекта локальный диск с раздачей через Nginx — разумный выбор. Переходить на S3 стоит, когда появляется второй инстанс или объём файлов делает бэкап диска неудобным.
Ошибка 7. Обращаться к .url и .path без проверки
Если поле пустое (blank=True), обращение к obj.avatar.url в шаблоне выбросит ValueError. А .path у файла в объектном хранилище вызовет NotImplementedError, потому что физического пути на локальной ФС нет. Всегда проверяйте, что файл есть, и не используйте .path, если код должен работать и с S3.
<!-- В шаблоне -->
{% if profile.avatar %}
<img src="{{ profile.avatar.url }}" alt="Аватар">
{% else %}
<img src="{% static 'img/default_avatar.png' %}" alt="Нет аватара">
{% endif %}
Что проверить в своём проекте прямо сейчас
- Открывается ли загруженный файл по прямой ссылке на проде (не в DEBUG).
- Кто раздаёт медиа — Nginx/CDN или Python-воркер.
- Раскладываются ли файлы по подпапкам или всё валится в одну.
- Удаляются ли файлы с диска при удалении и замене записи.
- Ограничены ли размер и допустимые форматы загрузки.
- Что будет с файлами, если добавить второй инстанс приложения.
Начните с первых двух пунктов — они дают самые частые баги на боевом сервере. Затем настройте upload_to и уборку сирот. Перед выкладкой на несколько серверов заранее решите вопрос с хранилищем: миграция уже загруженных файлов в S3 постфактум болезненнее, чем настройка с нуля.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: Django documentation: Managing files, django-storages documentation
Частые вопросы
У меня файлы отдаются локально, но на проде 404. Что смотреть первым?
Я на Django 3.x, а не 5.x — приёмы подойдут?
Удалил объект, а файлы остались на диске — это баг?
Достаточно ли ImageField, чтобы обезопаситься от вредоносных загрузок?
Настроил всё локально, а на нескольких серверах картинки то есть, то нет. Почему?
Обращение к avatar.url падает с ошибкой. Что не так?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.