.htaccess и nginx: чем отличаются и как переносить правила

nginx не читает .htaccess вообще — это архитектурное решение. Вся конфигурация задаётся в блоках server {} и location {}, загружается один раз при старте и не требует чтения файлов при каждом запросе. Эта статья — таблица соответствий Apache↔nginx, ключевые различия (в том числе грабли с add_header) и ссылка на конвертер .htaccess → nginx.

Почему nginx не читает .htaccess

Это сознательное архитектурное решение. В Apache .htaccess — механизм per-directory конфигурации: файл лежит рядом с данными и читается Apache при каждом запросе ко всем файлам в этом каталоге. Это удобно для shared-хостинга (нет доступа к главному конфигу), но создаёт оверхед — дисковые операции на каждый запрос.

nginx спроектирован иначе: вся конфигурация описывается в файлах конфига (обычно /etc/nginx/sites-available/), загружается один раз при старте или перезагрузке (nginx -s reload) и хранится в памяти. Нет никаких .htaccess-файлов, нет дисковых операций при запросах. Следствие: если у вас Server: nginx и .htaccess лежит в каталоге — он просто не читается и не действует. Никаких ошибок — просто игнорируется.

Проверить, какой у вас сервер: curl -I https://example.com — смотрите заголовок Server:.

Таблица соответствий Apache↔nginx

  • RewriteEngine On + RewriteRule ^old$ /new [L]rewrite ^/old$ /new last; или return 301 /new;location)
  • Redirect 301 /old /newreturn 301 /new;
  • RedirectMatch 301 ^/old/(.*)$ /new/$1rewrite ^/old/(.*)$ /new/$1 permanent;
  • <FilesMatch "\.(sql|bak)$"> Require all deniedlocation ~* \.(sql|bak)$ { deny all; }
  • Header [always] set Name Valueadd_header Name Value [always]; (но см. грабли ниже)
  • Header unset X-Powered-Bymore_clear_headers X-Powered-By; (модуль headers_more)
  • ErrorDocument 404 /404.htmlerror_page 404 /404.html;
  • ExpiresByType text/css "access plus 1 month"location ~* \.css$ { expires 30d; } (по расширению, не по MIME)
  • Options -Indexesautoindex off;
  • DirectoryIndex index.php index.htmlindex index.php index.html;
  • Order Allow,Deny + Allow from allallow all;
  • Require all denieddeny all;
  • Require ip 192.168.1.0/24allow 192.168.1.0/24; deny all;
  • AuthType Basic + AuthUserFile /path/.htpasswd + Require valid-userauth_basic "Area"; + auth_basic_user_file /path/.htpasswd;
  • AddOutputFilterByType DEFLATE text/html text/cssgzip on; gzip_types text/html text/css;
  • RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule . index.php [L] (фронт-контроллер) → try_files $uri $uri/ /index.php$is_args$args;

Что в nginx делается иначе

  • Нет директивы Options — управление симлинками: disable_symlinks on;; нет аналога MultiViews (используйте try_files).
  • PHP через FastCGI — не модуль, как в Apache (mod_php), а отдельный процесс PHP-FPM. Настройка: location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; }.
  • Переменные окруженияSetEnvIf/[E=…] аналогов нет напрямую; используйте map { } или set $var value;.
  • RewriteMapmap { } в блоке http {}.
  • Apache <If>if () в nginx (но if в nginx — ограниченный, часто лучше map).
  • Нет php_value / php_flag — настройки PHP задаются в конфиге PHP-FPM (php.ini / пул-конфиг) или через fastcgi_param PHP_VALUE "…";.

Грабля: add_header не наследуется

В Apache директива Header в родительском <Directory> или <VirtualHost> наследуется во вложенные блоки <Location>. В nginx — нет: если в дочернем блоке location есть хотя бы один add_header, все add_header из родительского блока теряются.

nginx.confкопировать
server {
    # Этот заголовок НЕ будет отправляться из location /api/,
    # потому что там есть свой add_header:
    add_header X-Frame-Options "SAMEORIGIN" always;

    location /api/ {
        add_header Cache-Control "no-store" always;
        # X-Frame-Options здесь уже НЕ добавляется!
        proxy_pass http://backend;
    }
}
nginx.confкопировать
server {
    location /api/ {
        # Повторяем все нужные заголовки:
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header Cache-Control "no-store" always;
        proxy_pass http://backend;
    }
}

Схема nginx-фронт + Apache-бэк

Популярная схема на shared-хостингах с панелями управления (ISPmanager, Plesk в «proxy mode»): nginx принимает запросы, отдаёт статику (CSS, JS, картинки), а динамику проксирует на Apache, работающий на внутреннем порту (например, 8080). В этой схеме:

  • Статику (файлы, которые nginx отдаёт напрямую) .htaccess не обрабатывает — там нет Apache.
  • Динамику (PHP-запросы, которые nginx проксирует на Apache) .htaccess обрабатывает на стороне Apache.

Если хостинг переключил сайт в режим «только nginx» (nginx-only без Apache-бэка) — .htaccess полностью перестаёт действовать. Как это проверить — в хабе «.htaccess на хостингах» → раздел про чистый nginx.

Конвертер .htaccess → nginx

На сайте есть конвертер .htaccess → nginx: вставьте содержимое .htaccess — получите примерный nginx-конфиг. Конвертер переводит контроль доступа, заголовки, страницы ошибок, кэш, gzip, типовые RewriteRule. Что не переводится точно (произвольные цепочки RewriteCond, php_value, RewriteMap) — попадает в список «Нужно проверить вручную». Проверяйте каждую строку перед использованием в продакшене.

Когда выбрать Apache, когда nginx

  • Apache: shared-хостинг (там нет выбора), нужен .htaccess per-directory, простота конфигурации через файлы рядом с кодом, приложения, заточенные под Apache (старый WordPress, некоторые CMS).
  • nginx: высокая нагрузка, отдача статики, reverse-proxy, микросервисы, минимальное потребление памяти — всё в одном конфиге, без per-directory overhead.
  • Оба вместе: nginx-фронт (статика, SSL-терминация, rate-limiting) + Apache/PHP-FPM-бэк (динамика, .htaccess работает на Apache) — классика для нагруженных shared-хостингов.