Режим обслуживания кажется простой задачей: положить красивую страницу «мы скоро вернёмся» и на время работ закрыть сайт. На практике у администраторов быстро появляются нюансы: поисковикам нужно отдать именно 503, браузерам и CDN нельзя закешировать заглушку надолго, выпуск и продление сертификатов через ACME challenge не должны сломаться, а мониторинг и балансировщик должны понимать, что происходит с узлом.
В этой инструкции соберём рабочий вариант nginx maintenance mode для VDS: управляемый флаг-файлом, с кодом 503 Service Unavailable, заголовком Retry-After, whitelist для администраторов и отдельными health checks. Конфигурация рассчитана на обычный Nginx как фронтенд перед PHP-FPM, Node.js, Python-приложением или другим upstream.
Главная идея: maintenance mode не должен выглядеть как авария. Для клиентов это понятное временное окно, для поисковиков — временная недоступность, для мониторинга — контролируемое состояние, для админов — возможность проверить сайт до снятия заглушки.
Что должен делать правильный maintenance mode
Если просто заменить корень сайта статической страницей или поставить редирект на /maintenance.html, получится не совсем корректно. Редирект с кодом 302 или 200-страница обслуживания может быть воспринята роботами как нормальный контент. В худшем случае поисковик временно увидит вместо главной страницы текст заглушки и обновит сниппет.
Правильная схема для плановых работ обычно выглядит так:
- обычным посетителям Nginx возвращает
503, а не200; - в ответе есть
Retry-After, чтобы клиенты и роботы понимали ожидаемую паузу; - заглушка не кешируется браузерами, CDN и промежуточными прокси;
- администраторы из
nginx whitelistобходят заглушку и видят реальное приложение; - пути
/.well-known/acme-challenge/доступны всегда, иначе автоматическое продление SSL может упасть; health checksразделены по смыслу: один проверяет, жив ли Nginx, другой — готов ли узел принимать пользовательский трафик.
Разделение проверок особенно важно на VDS, где на одном сервере часто живут веб-сервер, приложение, база данных, cron-задачи и мониторинг. Если заглушка включается на время миграции базы, не всегда нужно будить дежурного из-за «падения сайта»: достаточно показать, что maintenance включён осознанно.
Подготовка файлов и каталогов
Команды ниже подходят для Debian/Ubuntu и RHEL-based систем, потому что мы создаём обычные каталоги и файлы. Отличия в пакетном менеджере нужны только если Nginx ещё не установлен.
Установка Nginx, если он ещё не установлен
Debian/Ubuntu:
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux:
sudo dnf install nginx
sudo systemctl enable --now nginx
Теперь создадим каталог для флагов обслуживания и простую страницу. В реальном проекте вместо текстовой строки обычно кладут аккуратный HTML-файл с логотипом, временем окончания работ и понятным сообщением для пользователей.
sudo mkdir -p /etc/nginx/maintenance /var/www/maintenance
echo 'Сайт временно на обслуживании. Пожалуйста, повторите запрос позже.' | sudo tee /var/www/maintenance/503.html
Флаг будем включать командой touch, а выключать удалением файла. Для домена example.com используем имя /etc/nginx/maintenance/example.com.enabled. В бою замените домен на свой.
sudo touch /etc/nginx/maintenance/example.com.enabled
sudo rm -f /etc/nginx/maintenance/example.com.enabled

Базовая конфигурация: 503 и Retry-After
Блоки geo и map должны находиться в контексте http, то есть не внутри server и не внутри location. На Debian/Ubuntu их удобно вынести в отдельный файл, который подключается из /etc/nginx/nginx.conf через каталог /etc/nginx/conf.d/. В RHEL-based системах подход такой же: главное, чтобы файл попал в http-контекст.
geo $maintenance_whitelist {
default 0;
127.0.0.1 1;
203.0.113.10 1;
203.0.113.0/24 1;
}
map $uri $maintenance_uri_allowed {
default 0;
~^/\.well-known/acme-challenge/ 1;
=/healthz 1;
=/readyz 1;
}
map "$maintenance_flag:$maintenance_whitelist:$maintenance_uri_allowed" $maintenance_return {
default 0;
"1:0:0" 1;
}
Ниже — фрагмент server-блока. Его можно адаптировать под существующий виртуальный хост. Важно не копировать вслепую поверх всей конфигурации: сохраните свои listen, server_name, SSL-настройки, proxy/FastCGI location и логи.
server {
listen 80;
listen 443 ssl http2;
server_name example.com www.example.com;
set $maintenance_flag 0;
if (-f /etc/nginx/maintenance/example.com.enabled) {
set $maintenance_flag 1;
}
if ($maintenance_return) {
return 503;
}
error_page 503 @maintenance;
location @maintenance {
internal;
root /var/www/maintenance;
add_header Retry-After 600 always;
add_header Cache-Control "no-store, no-cache, must-revalidate, max-age=0" always;
add_header Pragma "no-cache" always;
add_header Expires "0" always;
try_files /503.html =503;
}
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
try_files $uri =404;
}
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok";
}
location = /readyz {
access_log off;
default_type text/plain;
if (-f /etc/nginx/maintenance/example.com.enabled) {
return 503;
}
return 200 "ready";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
В этой схеме переменная $maintenance_flag становится равной 1, если существует флаг-файл. Переменная $maintenance_whitelist равна 1 для разрешённых IP. Переменная $maintenance_uri_allowed равна 1 для служебных путей, которые должны работать даже во время обслуживания. Итоговая переменная $maintenance_return включает 503 только при сочетании «maintenance включён, IP не в whitelist, URI не служебный».
Директивы if в Nginx часто ругают, и не без причины: внутри location они могут вести себя неожиданно, если использовать сложную перепись URI. Но в таком варианте мы делаем только set и return, это безопасный и распространённый шаблон.
Retry-After: секунды или дата
Заголовок Retry-After можно задать числом секунд или HTTP-датой. Для коротких работ удобнее число:
add_header Retry-After 600 always;
Это означает «попробуйте снова через 600 секунд». Для заранее объявленного окна можно указать дату, но тут легко ошибиться с часовым поясом и форматом. Поэтому на практике для maintenance mode чаще используют секунды: 300, 600, 1800 или 3600.
Ключевое слово always важно: без него Nginx добавляет некоторые заголовки не ко всем кодам ответа. Нам нужно, чтобы Retry-After и запрет кеширования присутствовали именно на 503.
Не ставьте слишком большой Retry-After «на всякий случай». Если работы занимают 10 минут, значение на сутки будет мешать роботам и некоторым клиентам вернуться вовремя. Лучше продлить окно при необходимости и обновить конфигурацию или использовать переменную через include, если у вас строгие регламенты.
Whitelist: как не закрыть доступ самим себе
В блоке geo мы перечислили IP-адреса, которым можно видеть реальный сайт во время обслуживания. Это удобно для проверки миграции, прогрева кеша, ручного smoke-теста и финального контроля перед открытием доступа для всех.
Для сопровождения удобнее держать whitelist в отдельном файле:
geo $maintenance_whitelist {
default 0;
include /etc/nginx/maintenance/whitelist.conf;
}
А содержимое /etc/nginx/maintenance/whitelist.conf может быть таким:
127.0.0.1 1;
203.0.113.10 1;
203.0.113.0/24 1;
После изменения whitelist проверьте и перезагрузите Nginx:
sudo nginx -t
sudo systemctl reload nginx
Если сервер стоит за CDN, внешним балансировщиком или reverse proxy, $remote_addr может быть адресом прокси, а не клиента. Тогда whitelist внезапно начнёт пропускать всех пользователей через доверенный прокси или, наоборот, не пропустит никого. В такой архитектуре сначала корректно настройте получение реального IP.
set_real_ip_from 198.51.100.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
Здесь set_real_ip_from должен содержать только адреса ваших доверенных прокси или балансировщиков. Нельзя доверять заголовку X-Forwarded-For от всего интернета: клиент может подставить туда любой IP и обойти whitelist.
ACME challenge: почему его нужно пропускать
Если сертификаты выпускаются через HTTP-01, клиент размещает проверочный файл в /.well-known/acme-challenge/, а центр сертификации обращается к вашему домену по HTTP. Если maintenance mode перехватит этот путь и вернёт 503, продление сертификата может завершиться ошибкой. Это особенно неприятно, когда обслуживание затянулось и совпало с автоматическим renewal.
Именно поэтому в map $uri $maintenance_uri_allowed путь /.well-known/acme-challenge/ помечен как разрешённый. Даже при включённой заглушке Nginx будет искать файл проверки в каталоге /var/www/letsencrypt.
Проверьте, что challenge действительно не блокируется:
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
echo 'test' | sudo tee /var/www/letsencrypt/.well-known/acme-challenge/ping
curl -i http://example.com/.well-known/acme-challenge/ping
Ожидаемый результат — 200 или хотя бы не 503, если файла нет и вы получили 404. Для проверки нам важно, что запрос не попал в maintenance-заглушку.
Если вы используете коммерческие SSL-сертификаты или смешанную схему с разными центрами сертификации, всё равно полезно держать служебные пути отдельно от пользовательской заглушки: это упрощает продление, диагностику и миграции доменов.
Health checks: /healthz и /readyz — не одно и то же
Частая ошибка — использовать один URL для всех проверок. Например, внешний мониторинг, балансировщик и Kubernetes-подобная система опрашивают /health, а администратор не понимает, должен ли он возвращать 200 во время maintenance. Лучше разделить смысл.
/healthz— жив ли сам Nginx и отвечает ли виртуальный хост. Во время обслуживания может возвращать200./readyz— готов ли узел принимать пользовательский трафик. Во время обслуживания обычно возвращает503.
Такой подход удобен для балансировщика: если /readyz вернул 503, узел можно временно исключить из ротации, но при этом /healthz покажет, что веб-сервер не умер. Для одиночного VDS внешний мониторинг можно настроить по-разному: либо проверять главную страницу и учитывать maintenance-окна, либо проверять /healthz, а факт включённой заглушки контролировать отдельным алертом.
Если приложение умеет отдавать собственный readiness, можно проксировать /readyz в upstream, но добавить проверку флаг-файла перед проксированием. Тогда maintenance гарантированно выводит узел из пользовательского трафика даже если приложение технически живо.
location = /readyz {
access_log off;
if (-f /etc/nginx/maintenance/example.com.enabled) {
return 503;
}
proxy_pass http://127.0.0.1:3000/readyz;
}

Включение, выключение и проверка
После добавления конфигурации всегда начинайте с проверки синтаксиса. Nginx не применит битую конфигурацию при reload, но лучше увидеть ошибку до начала окна работ.
sudo nginx -t
sudo systemctl reload nginx
Включить maintenance:
sudo touch /etc/nginx/maintenance/example.com.enabled
Выключить maintenance:
sudo rm -f /etc/nginx/maintenance/example.com.enabled
Проверка обычного ответа:
curl -i https://example.com/
В ответе для не-whitelist IP должны быть HTTP/2 503 или HTTP/1.1 503 Service Temporarily Unavailable, Retry-After и заголовки запрета кеширования. Проверка служебных URL:
curl -i https://example.com/healthz
curl -i https://example.com/readyz
curl -i https://example.com/.well-known/acme-challenge/ping
Если вы тестируете с самого сервера, запрос может попадать в whitelist через 127.0.0.1. Это нормально, но для проверки поведения обычного посетителя используйте внешний IP, временно уберите свой адрес из whitelist или проверьте через отдельную сеть.
Логи и наблюдаемость во время обслуживания
Во время плановых работ полезно видеть, сколько пользователей упёрлось в заглушку, какие боты продолжают активно ходить по сайту и не заблокировали ли вы важные служебные пути. Можно завести отдельный access log для maintenance-location, но у named location @maintenance есть нюансы: логирование чаще удобнее оставить на уровне server, добавив в формат переменные состояния.
log_format main_ext '$remote_addr $host $request $status maintenance=$maintenance_return whitelist=$maintenance_whitelist request_time=$request_time';
access_log /var/log/nginx/example.com.access.log main_ext;
После этого в логах будет видно, какие ответы прошли через maintenance-логику. Для быстрой проверки:
sudo tail -f /var/log/nginx/example.com.access.log
Если у вас есть Prometheus-экспортёр, blackbox monitoring или внешняя система статусов, заранее договоритесь, как она интерпретирует 503 на главной странице во время плановых работ. Самый плохой вариант — включить обслуживание, получить шквал ложных алертов, отключить мониторинг вручную и забыть включить его обратно.
Типовые ошибки
Заглушка отдаётся с кодом 200
Это удобно визуально, но плохо для SEO и автоматических клиентов. Страница обслуживания должна быть телом ответа 503, а не отдельной успешной страницей. Используйте error_page 503 @maintenance и return 503.
Забыли always у add_header
Без always заголовок Retry-After может не появиться там, где вы его ждёте. Проверяйте реальный ответ через curl -i, а не только глазами в браузере.
CDN закешировал maintenance-страницу
Добавьте Cache-Control: no-store на 503 и отдельно проверьте правила CDN. Некоторые панели имеют собственные настройки кеширования ошибок. Если CDN стоит перед VDS, убедитесь, что он не превращает временную заглушку в долгоживущий объект.
Whitelist не работает за прокси
Проверьте, какой IP видит Nginx. Временно добавьте в лог $remote_addr, $realip_remote_addr и $http_x_forwarded_for. Настраивайте real_ip_header только для доверенных прокси.
ACME HTTP-01 начал падать
Убедитесь, что /.well-known/acme-challenge/ исключён из maintenance-режима и что location для challenge находится в нужном server-блоке. Если у вас несколько доменов и редирект с HTTP на HTTPS, проверьте оба сценария. При переносе сайта между серверами пригодится отдельный план: например, чеклист из статьи про миграцию сайта без простоя.
Мини-чеклист перед плановыми работами
- Заранее добавьте и протестируйте конфигурацию на staging или в коротком тестовом окне.
- Проверьте
nginx -tи reload без ошибок. - Убедитесь, что ваш IP находится в whitelist и не меняется из-за VPN или провайдера.
- Проверьте
503иRetry-Afterчерезcurl -i. - Проверьте доступность
ACME challenge. - Разделите
/healthzи/readyz, особенно если есть балансировщик. - После завершения работ удалите флаг-файл и проверьте главные пользовательские сценарии.
Такой maintenance mode хорош тем, что он прост: нет отдельного демона, нет зависимости от приложения, включение и выключение делаются атомарным созданием или удалением файла. При этом схема достаточно гибкая для VDS с несколькими сайтами, reverse proxy и автоматическим SSL. Если однажды подготовить её аккуратно, плановые обновления, миграции базы и релизы перестают быть нервным «закроем всё firewall-ом» и превращаются в управляемую эксплуатационную процедуру.


