ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

PHP uploads: настройка загрузки файлов и медиа на Nginx и Apache

Разбираем, как в PHP устроена загрузка файлов: временные файлы, лимиты размера и времени, ключевые параметры в php.ini, влияние настроек Nginx и Apache. Пошагово пройдём типичные ошибки при загрузке медиа и разберём практики для продакшена на хостинге и VDS.
PHP uploads: настройка загрузки файлов и медиа на Nginx и Apache

Загрузка файлов и медиа в PHP кажется простой задачей, пока проект не вырастет и не появятся десятки мегабайт на входе, нестабильная сеть и пара уровней прокси перед бэкендом. Тогда в логах начинаются таинственные UPLOAD_ERR_INI_SIZE, 413 от Nginx и пустые $_FILES.

В этой статье разберём, как устроен процесс uploads в PHP, какие лимиты влияют на загрузку, чем могут помешать Nginx и Apache, и как настроить всё так, чтобы медиа загружались предсказуемо и безопасно — и на обычном виртуальном хостинге, и на собственном VDS.

Как PHP обрабатывает uploads: путь файла от клиента до диска

Чтобы понимать, что ломается, важно понимать, как вообще работает загрузка файла в PHP:

  1. Браузер отправляет запрос с телом multipart/form-data, где содержимое файла идёт в теле HTTP.
  2. Web-сервер (Nginx или Apache) принимает тело запроса, складывает его во временный файл или в буферы, одновременно следит за своими лимитами.
  3. PHP получает поток данных (через mod_php, FastCGI, FPM и т.д.), применяет свои собственные лимиты и складывает файл во временную директорию.
  4. 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 — это сильно экономит время при разборе инцидентов.

Схема прохождения загрузки файла от браузера через веб-сервер и PHP до диска

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-подобных атак.

DevOps проверяет структуру директорий и права доступа для загруженных медиа на сервере

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.

Где именно задавать лимиты

Систему ограничений удобно строить по слоям:

  1. UI (JavaScript, мобильное приложение): мягкий лимит, дружелюбные ошибки для пользователя.
  2. Приложение (PHP): жёсткая бизнес-валидация (типы файлов, размеры, количество).
  3. PHP-конфиг: технический предел (защита от ошибок и атак).
  4. 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 МБ

  1. Проверяем логи Nginx или Apache: есть ли 413 или большие 4xx при попытке загрузки.
  2. Проверяем phpinfo() или CLI php -i на предмет upload_max_filesize и post_max_size.
  3. Сравниваем значения с client_max_body_size (Nginx) или LimitRequestBody (Apache).
  4. Логируем $_FILES['file']['error'] и фактический размер тела запроса.

В подавляющем большинстве случаев дело оказывается в неправильно настроенном client_max_body_size или post_max_size.

Сценарий: файл загружается, но в каталоге пусто

  1. Проверяем, вызывается ли move_uploaded_file() и нет ли ошибок при его выполнении.
  2. Проверяем права на каталог для сохранения (права пользователя PHP и владельца директории).
  3. Убеждаемся, что диск не заполнен (в том числе по inode).
  4. Проверяем open_basedir (если включён) — нет ли ограничений на путь.

Частая ситуация на VDS: отдельный диск для /var почти забит логами, при этом /home ещё свободен, а upload_tmp_dir лежит в /var/tmp и отваливается первым.

Сценарий: случайные обрывы загрузки на медленном интернете

  1. Проверяем client_body_timeout (Nginx) или RequestReadTimeout (Apache).
  2. Проверяем max_input_time в PHP.
  3. Смотрим access и error-логи на предмет timeouts и обрывов соединения.
  4. Тестируем загрузку через 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.

Если вы изначально согласуете лимиты на всех уровнях и заложите безопасную архитектуру хранения медиа, большинство проблем с загрузкой файлов исчезнет, а оставшиеся будут легко диагностироваться по логам и понятным кодам ошибок.

Поделиться статьей

Вам будет интересно

HTTP‑лимиты в Nginx: client_max_body_size и тело запроса для PHP и API OpenAI Статья написана AI (GPT 5)

HTTP‑лимиты в Nginx: client_max_body_size и тело запроса для PHP и API

Почему Nginx внезапно отвечает 413, 400 или зависает на загрузке файлов? Часто причина в лимитах тела HTTP‑запроса: client_max_bod ...
Лимиты ресурсов на VDS: ulimit, sysctl и настройка для веб‑проектов OpenAI Статья написана AI (GPT 5)

Лимиты ресурсов на VDS: ulimit, sysctl и настройка для веб‑проектов

На новом VDS всё обычно летает, пока сайт не вырастет и не упрётся в скрытые лимиты: открытые файлы, процессы, сокеты, память. Раз ...
cron на виртуальном хостинге: как настроить и не сломать сайт OpenAI Статья написана AI (GPT 5)

cron на виртуальном хостинге: как настроить и не сломать сайт

Разбираемся, как грамотно настроить cron на виртуальном хостинге: от формата расписаний и особенностей запуска PHP-скриптов до воп ...