Загрузка файлов и медиа в PHP кажется простой задачей, пока проект не вырастет и не появятся десятки мегабайт на входе, нестабильная сеть и пара уровней прокси перед бэкендом. Тогда в логах начинаются таинственные UPLOAD_ERR_INI_SIZE, 413 от Nginx и пустые $_FILES.
В этой статье разберём, как устроен процесс uploads в PHP, какие лимиты влияют на загрузку, чем могут помешать Nginx и Apache, и как настроить всё так, чтобы медиа загружались предсказуемо и безопасно — и на обычном виртуальном хостинге, и на собственном VDS.
Как PHP обрабатывает uploads: путь файла от клиента до диска
Чтобы понимать, что ломается, важно понимать, как вообще работает загрузка файла в PHP:
- Браузер отправляет запрос с телом
multipart/form-data, где содержимое файла идёт в теле HTTP. - Web-сервер (Nginx или Apache) принимает тело запроса, складывает его во временный файл или в буферы, одновременно следит за своими лимитами.
- PHP получает поток данных (через mod_php, FastCGI, FPM и т.д.), применяет свои собственные лимиты и складывает файл во временную директорию.
- PHP заполняет массив
$_FILESи, если вы явно не сохраните файл черезmove_uploaded_file()или аналог, временный файл будет удалён в конце запроса.
Любой из этих этапов может обрезать или отбросить загрузку, а ошибка при этом будет не всегда очевидна. Поэтому полезно смотреть на конфигурацию сразу трёх уровней:
- лимиты PHP (
upload_max_filesize,post_max_size,max_file_uploads,max_input_time); - лимиты Nginx или Apache (максимальный размер тела запроса, таймауты);
- ограничения балансировщиков и прокси (если они есть, но это уже отдельная история).
Хорошая новость — почти все проблемы с uploads воспроизводимы и решаемы, если подходить к ним системно и не забывать смотреть в логи веб-сервера и PHP.
Ключевые настройки PHP для uploads
Начнём с php.ini. Это базовый уровень, без которого дальше нет смысла копать: если PHP режет запрос по размеру или времени, до вашего кода он просто не дойдёт.
Размер файла и тела запроса: upload_max_filesize и post_max_size
Две главные директивы для uploads:
upload_max_filesize— максимальный размер одного загружаемого файла;post_max_size— максимальный размер всего тела POST-запроса (включая все файлы и остальные поля формы).
Частая ошибка — увеличить только upload_max_filesize и оставить post_max_size меньше. В этом случае PHP даже не начнёт разбирать форму и вы получите пустой $_FILES.
Пример настроек для загрузки до 50 МБ:
; php.ini или отдельный .ini для пула FPM
upload_max_filesize = 50M
post_max_size = 60M
Рекомендуется делать post_max_size немного больше upload_max_filesize, чтобы учесть накладные расходы формата multipart/form-data и несколько файлов в одной форме.
Количество файлов: max_file_uploads
По умолчанию PHP ограничивает число одновременно загружаемых файлов (директива max_file_uploads, обычно 20). Для SPA/админок с множественной загрузкой через drag & drop это ограничение часто выстреливает неожиданно.
Если пользователь выбирает 100 файлов в диалоге, а max_file_uploads = 20, лишние просто отбрасываются. В $_FILES вы увидите только часть файлов, без явной ошибки.
max_file_uploads = 100
Не забудьте при этом добавить валидацию на стороне приложения, чтобы не позволить пользователю загружать тысячи файлов за один запрос и не положить диск.
Таймауты: max_input_time и max_execution_time
Для больших uploads важны не только размеры, но и время. На медленном соединении пользователь может отправлять файл десятки секунд.
max_input_time— максимум секунд на чтение тела запроса (parsing данных формы);max_execution_time— максимум секунд на выполнение скрипта после того, как входные данные прочитаны.
Если max_input_time слишком мал, PHP может оборвать чтение тела до конца. В логах ошибок вы можете увидеть что-то вроде «Maximum execution time of X seconds exceeded in Unknown on line 0» на очень ранней стадии.
Для загрузки больших медиа разумно увеличить max_input_time, но не делать его бесконечным. Например:
max_input_time = 120
max_execution_time = 30
Обратите внимание: max_execution_time = 0 означает бесконечное время выполнения скрипта. Это удобно для CLI, но опасно в веб-контексте.
Временная директория: upload_tmp_dir
PHP складывает загружаемые файлы во временную директорию, указанную в upload_tmp_dir (или в системную TMP, если директива не задана).
Типичные проблемы:
- директория не существует или у PHP нет прав на запись;
- недостаточно места на разделе, где лежит TMP (часто это небольшой /tmp на tmpfs);
- ограничения по inode при огромном количестве временных мелких файлов.
Если у вас свой VDS и много загрузок, полезно вынести временные файлы в отдельный каталог на большом разделе и убедиться, что он монтируется с нужными опциями.
upload_tmp_dir = /var/www/php-uploads-tmp
После этого не забудьте создать директорию и дать права на запись пользователю, от которого работает PHP-FPM или Apache.
Проверка ошибок upload в PHP
Каждый файл в $_FILES содержит поле error с одним из константных кодов:
UPLOAD_ERR_OK(0) — всё хорошо;UPLOAD_ERR_INI_SIZE(1) — превысилиupload_max_filesize;UPLOAD_ERR_FORM_SIZE(2) — превысилиMAX_FILE_SIZEиз HTML-формы;UPLOAD_ERR_PARTIAL(3) — файл загружен частично;UPLOAD_ERR_NO_FILE(4) — файл не был загружен;- другие коды — для более редких сценариев.
Обязательно логируйте эти коды, иначе будете долго гадать, почему пользователь «ничего не может загрузить».
if (!isset($_FILES['file'])) {
throw new RuntimeException('Файл не найден в запросе');
}
$error = $_FILES['file']['error'];
if ($error !== UPLOAD_ERR_OK) {
throw new RuntimeException('Ошибка загрузки файла, код: ' . $error);
}
Полезно дополнительно логировать размер загружаемого файла, IP клиента и user-agent — это сильно экономит время при разборе инцидентов.

Nginx и uploads: client_max_body_size, буферы и таймауты
Если вы используете Nginx + PHP-FPM, то первым «вратарём» для загружаемых файлов будет именно Nginx. Даже если в php.ini вы поставили лимит 100 МБ, Nginx по умолчанию может отрезать тело запроса раньше.
Размер тела запроса: client_max_body_size
Главная директива Nginx, влияющая на uploads, — client_max_body_size. Она задаёт максимальный размер тела одного HTTP-запроса.
По умолчанию она равна 1M (или не задана в некоторых сборках, тогда берётся внутренний дефолт). Если пользователь пытается загрузить файл больше лимита, Nginx вернёт 413 Request Entity Too Large, а PHP о запросе даже не узнает.
Настраивать лучше всего в контексте server или отдельного location для uploads:
server {
listen 80;
server_name example.com;
client_max_body_size 50m;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Важно: client_max_body_size должен быть не меньше post_max_size в PHP, иначе пользователь будет упираться в 413 от Nginx, и ваши проверки в коде не сработают.
Буферы и временные файлы Nginx
Nginx может буферизовать тело запроса в память и во временные файлы. Для uploads это значит, что часть нагрузки ложится на дисковую подсистему до того, как запрос дойдёт до PHP.
Ключевые директивы:
client_body_buffer_size— размер буфера в памяти;client_body_temp_path— путь к временным файлам тела запроса;client_body_in_file_only— принудительно сохранять тело только в файл (обычно не нужно).
Если у вас много больших uploads на VDS, имеет смысл:
- вынести
client_body_temp_pathна быстрый диск с достаточным объёмом; - контролировать количество одновременно загружаемых больших файлов, чтобы не уткнуться в I/O.
Таймауты на приём тела запроса
Помимо размера, важно время, за которое клиент передаёт файл. За это отвечают:
client_body_timeout— время ожидания следующего блока данных от клиента;client_header_timeout— ожидание заголовков;keepalive_timeout— общее время жизни keep-alive соединения.
Если пользователь сидит на плохом канале, а client_body_timeout слишком мал, Nginx может закрыть соединение посреди загрузки файла. В логах вы увидите что-то вроде client timed out.
Для сценариев с большими медиа, но без фанатизма, можно выставить что-то вроде:
client_body_timeout 60s;
Не стоит завышать таймауты до сотен секунд, если у вас публичный сервис: это увеличивает окно для slowloris-подобных атак.

Apache и uploads: LimitRequestBody и таймауты
Если PHP работает через Apache (mod_php или proxy_fcgi), картинка другая, но принципы те же: сначала запрос «фильтрует» Apache, потом он попадает в PHP.
Размер тела запроса: LimitRequestBody
За максимальный размер тела запроса в Apache отвечает директива LimitRequestBody. Её можно задавать в контексте сервера, виртуального хоста или директории.
<VirtualHost *:80>
ServerName example.com
LimitRequestBody 52428800
<Directory /var/www/html>
AllowOverride All
</Directory>
</VirtualHost>
Размер указывается в байтах, в примере это 50 МБ. Если лимит меньше, чем post_max_size в PHP, то запрос будет отброшен Apache, и до PHP он не дойдёт.
Также стоит помнить, что некоторые модули (например, mod_security) могут накладывать свои ограничения на тело запроса.
Таймауты Apache
Для uploads в Apache важны:
Timeout— общее время ожидания завершения запроса;RequestReadTimeout(mod_reqtimeout) — тонкая настройка времени чтения заголовков и тела запроса;KeepAliveTimeout— время ожидания повторных запросов по соединению.
Если RequestReadTimeout выставлен слишком агрессивно, запросы с большими файлами по медленному каналу могут обрываться, не доходя до PHP. Проверьте этот модуль, если у вас начинаются «рандомные» обрывы загрузки.
Согласование лимитов PHP, Nginx/Apache и приложения
Самые неприятные баги с uploads появляются, когда лимиты на разных уровнях не синхронизированы. В проде это проявляется странно: у кого-то всё работает, а у кого-то 413, пустые $_POST или файлы размером 0 байт.
Простая схема согласования лимитов
Допустим, вы хотите позволить загружать до 50 МБ за один файл и до 200 МБ на запрос. Разумная конфигурация может выглядеть так:
- PHP:
upload_max_filesize = 50M; - PHP:
post_max_size = 220M(с запасом на накладные расходы); - Nginx:
client_max_body_size 220m; - Apache:
LimitRequestBody 230686720(220 МБ); - приложение: собственная валидация размера файла и количества файлов.
При этом UI (JS, мобильные клиенты) должен знать ваши лимиты и проверять размер до отправки файла, чтобы пользователь не ждал напрасно. Для SPA и мобильных приложений это практически must-have.
Где именно задавать лимиты
Систему ограничений удобно строить по слоям:
- UI (JavaScript, мобильное приложение): мягкий лимит, дружелюбные ошибки для пользователя.
- Приложение (PHP): жёсткая бизнес-валидация (типы файлов, размеры, количество).
- PHP-конфиг: технический предел (защита от ошибок и атак).
- Web-сервер (Nginx/Apache): верхний предел на весь бэкенд, чуть выше PHP.
Так проще диагностировать проблемы и избегать ситуаций, когда Nginx возвращает 413, а пользователь в интерфейсе видит бессмысленную ошибку без пояснений.
Организация директорий и прав для загруженных медиа
Даже если uploads доходят до PHP, дальше можно легко сломаться на записи файла в файловую систему. Это отдельный слой, который часто забывают протестировать на стейдже.
Куда складывать медиа
Базовые требования к каталогу для загруженных файлов:
- на том разделе, где достаточно места (учитывайте рост за годы и бэкапы);
- с правами на запись пользователю PHP (или группе веб-сервера);
- с понятной иерархией: по пользователям, дате, типу контента.
Простейший подход: структура вида uploads/YYYY/MM/DD/. Это позволяет избежать десятков тысяч файлов в одной директории и облегчает бэкапы.
$baseDir = __DIR__ . '/uploads';
$subdir = date('Y/m/d');
$targetDir = $baseDir . '/' . $subdir;
if (!is_dir($targetDir) && !mkdir($targetDir, 0755, true) && !is_dir($targetDir)) {
throw new RuntimeException('Не удалось создать директорию для загрузок');
}
Дополнительно имеет смысл закладываться на перенос каталога с медиа на отдельный диск или сеть: не жёстко хардкодьте пути и используйте конфиги или переменные окружения.
Права доступа и безопасность
Самый неприятный класс уязвимостей — загрузка исполняемых файлов (PHP, CGI, скриптов) в директорию, доступную на исполнение через веб-сервер. На продакшене это обычно заканчивается взломом.
Рекомендуемые меры:
- запретить исполнение PHP в каталогах с медиа (через Nginx или Apache конфигурацию);
- хранить оригинальные загрузки вне веб-директории и отдавать их через контроллер или скрипт при необходимости авторизации;
- строгая проверка mime-типов и расширений, не доверять только
$_FILES['type']; - генерировать имена файлов самостоятельно, не использовать оригинальное имя в качестве base name без фильтрации.
Для Nginx часто делают отдельный location с раздачей только статических медиа (изолированный от PHP), а PHP-скрипты — в другом location.
Практическая диагностика проблем с uploads
Когда «ничего не работает», полезно иметь чек-лист, что именно проверить. Ниже — базовые сценарии, с которых стоит начинать.
Сценарий: пользователь не может загрузить файл > 2 МБ
- Проверяем логи Nginx или Apache: есть ли 413 или большие 4xx при попытке загрузки.
- Проверяем
phpinfo()или CLIphp -iна предметupload_max_filesizeиpost_max_size. - Сравниваем значения с
client_max_body_size(Nginx) илиLimitRequestBody(Apache). - Логируем
$_FILES['file']['error']и фактический размер тела запроса.
В подавляющем большинстве случаев дело оказывается в неправильно настроенном client_max_body_size или post_max_size.
Сценарий: файл загружается, но в каталоге пусто
- Проверяем, вызывается ли
move_uploaded_file()и нет ли ошибок при его выполнении. - Проверяем права на каталог для сохранения (права пользователя PHP и владельца директории).
- Убеждаемся, что диск не заполнен (в том числе по inode).
- Проверяем
open_basedir(если включён) — нет ли ограничений на путь.
Частая ситуация на VDS: отдельный диск для /var почти забит логами, при этом /home ещё свободен, а upload_tmp_dir лежит в /var/tmp и отваливается первым.
Сценарий: случайные обрывы загрузки на медленном интернете
- Проверяем
client_body_timeout(Nginx) илиRequestReadTimeout(Apache). - Проверяем
max_input_timeв PHP. - Смотрим access и error-логи на предмет timeouts и обрывов соединения.
- Тестируем загрузку через curl или HTTP-клиент с ограничением скорости, чтобы воспроизвести поведение.
Если обрывы проявляются только под нагрузкой, не забывайте смотреть на состояние диска (iowait) и сетевого канала.
Расширенные сценарии: большие медиа и асинхронная обработка
Для обычных форм на несколько мегабайт стандартные настройки PHP и Nginx или Apache обычно достаточно один раз подкрутить и забыть. Но если вы работаете с видео, большими архивами или пользовательскими бэкапами, картина меняется.
Не блокировать PHP долгими upload-запросами
Классический подход — принимать файл в PHP, сразу сохранять в файловую систему или объектное хранилище, а тяжёлую обработку (конвертация видео, создание превью, анализ содержимого) отдавать в очередь фоновых задач.
Преимущества:
- HTTP-запрос занимает умеренное время, не упираясь в большие
max_execution_time; - PHP-FPM-пулы не забиты долгими рабочими процессами;
- обработку можно масштабировать отдельно (несколько воркеров, отдельный сервер или VDS для обработки медиа).
По возможности не делайте тяжёлую конвертацию «в онлайне» в том же запросе, где идёт загрузка.
Контроль нагрузки на диск и сеть
На VDS с ограниченными ресурсами легко получить ситуацию, когда пара десятков одновременных пользователей, загружающих по 100 МБ, приводит к резкой деградации дисковой подсистемы и сети.
Что можно сделать:
- поставить разумный лимит на одновременные большие uploads через приложение (очередь загрузок, пределы по пользователю);
- реализовать пошаговую загрузку (chunked upload) и контроль прогресса, чтобы не держать один гигантский запрос;
- разносить логи, временные файлы и медиа по разным дискам или как минимум по разным разделам.
Дополнительно помогает мониторинг: следите за iowait, latency диска и показателями сети, чтобы заранее заметить, что ресурсы заканчиваются.
Резюме
Надёжная работа uploads в PHP — это не только два параметра в php.ini. На итоговое поведение влияют:
- лимиты PHP —
upload_max_filesize,post_max_size,max_file_uploads,max_input_time,upload_tmp_dir; - лимиты и таймауты Nginx или Apache —
client_max_body_sizeиLimitRequestBody,client_body_timeout,RequestReadTimeout; - права и структура каталогов для медиа;
- валидация и обработка ошибок на уровне приложения и UI.
Если вы изначально согласуете лимиты на всех уровнях и заложите безопасную архитектуру хранения медиа, большинство проблем с загрузкой файлов исчезнет, а оставшиеся будут легко диагностироваться по логам и понятным кодам ошибок.


