HAProxy часто ставят на VDS как входную точку для сайта или API: он принимает клиентские соединения, завершает TLS, распределяет запросы между backend-узлами и быстро выводит из балансировки серверы, которые перестали отвечать. В такой схеме Nginx остаётся на backend-серверах и занимается привычной работой: отдаёт статику, проксирует PHP-FPM, Node.js, Python-приложение или другой локальный сервис.
Главная тонкость: если TLS завершается на HAProxy, то Nginx уже видит не реального посетителя, а IP балансировщика. Заголовок X-Forwarded-For полезен, но его можно подделать, если backend доступен извне. Для доверенного сегмента между балансировщиком и backend удобнее использовать PROXY protocol: HAProxy добавляет служебный префикс в начало TCP-соединения, а Nginx читает из него исходный IP и порт клиента.
Ниже — практическая инструкция: установка HAProxy и Nginx, базовая топология, TLS termination, health checks, включение PROXY protocol, ограничение доступа к backend-порту и диагностика частых ошибок.
Схема, которую будем настраивать
Для примера возьмём один публичный HAProxy и два backend-сервера с Nginx. IP-адреса условные, замените их на свои:
10.0.0.10— VDS с HAProxy, публичная точка входа на портах80и443;10.0.0.11— первый Nginx backend, слушает внутренний порт8080;10.0.0.12— второй Nginx backend, слушает внутренний порт8080;example.org— домен сайта;/healthz— простой endpoint для health checks.
Поток запроса выглядит так: браузер подключается к HAProxy по HTTPS, HAProxy выполняет TLS termination, добавляет служебные заголовки и PROXY protocol, выбирает живой backend, отправляет HTTP-запрос в Nginx, а Nginx передаёт запрос приложению или отдаёт контент сам.
Backend-порт с включённым
proxy_protocolнельзя оставлять открытым для интернета. Такой порт должен принимать соединения только от HAProxy.
Когда такая схема лучше прямого Nginx
Nginx тоже умеет работать как reverse proxy и load balancer, но HAProxy удобен, когда нужна отдельная точка входа с предсказуемым поведением под нагрузкой. Он силён в балансировке, health checks, управлении таймаутами, TCP/HTTP-режимах, сборе статистики и мягком выводе backend-узлов из ротации.
Типовые случаи для HAProxy на VDS:
- несколько одинаковых веб-узлов за одним публичным IP или доменным именем;
- blue-green или rolling deploy без остановки сайта;
- централизованная TLS termination, чтобы сертификаты не раскладывать на каждый backend;
- строгая передача реального IP клиента на Nginx backend через
PROXY protocol; - быстрое отключение проблемного backend по результатам health checks;
- единая точка для лимитов, логирования и первичной защиты от ошибочных запросов.
При этом HAProxy не отменяет Nginx. Хорошая практика — разделять роли: HAProxy как edge load balancer, Nginx как backend reverse proxy и веб-сервер приложения.

Установка HAProxy и Nginx
На балансировщике нужен HAProxy. На backend-серверах — Nginx. Команды зависят от семейства ОС, но сама конфигурация будет почти одинаковой.
Debian и Ubuntu
apt update
apt install haproxy nginx socat
systemctl enable --now haproxy
systemctl enable --now nginxЕсли на конкретном сервере устанавливается только HAProxy или только Nginx, ставьте только нужный пакет. socat пригодится для просмотра runtime-статистики HAProxy через unix socket.
AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux
dnf install haproxy nginx socat
systemctl enable --now haproxy
systemctl enable --now nginxНа системах с SELinux в enforcing-режиме HAProxy может быть ограничен при подключении к нестандартным backend-портам. Если в audit-логах видно, что именно SELinux блокирует исходящие соединения HAProxy, включите разрешение:
setsebool -P haproxy_connect_any 1Не используйте эту команду вслепую как замену нормальной диагностике: сначала проверьте, что backend доступен по сети и firewall не блокирует порт.
Fedora
dnf install haproxy nginx socat
systemctl enable --now haproxy
systemctl enable --now nginxВ Fedora версии пакетов обычно свежее, но для схемы из этой статьи достаточно обычных возможностей HAProxy и Nginx.
Подготовка сертификата для TLS termination
HAProxy ожидает сертификат и приватный ключ в одном PEM-файле: сначала цепочка сертификатов, затем ключ. Если сертификат уже выпущен и лежит в стандартном каталоге, соберите файл так:
mkdir -p /etc/haproxy/certs
cat /etc/letsencrypt/live/example.org/fullchain.pem /etc/letsencrypt/live/example.org/privkey.pem > /etc/haproxy/certs/example.org.pem
chmod 600 /etc/haproxy/certs/example.org.pem
chown root:root /etc/haproxy/certs/example.org.pemЕсли сертификатов несколько, HAProxy можно настроить через каталог или crt-list, но для начала лучше поднять один домен и убедиться, что TLS termination работает. Для коммерческих проектов, где важны гарантия и поддержка, можно использовать SSL-сертификаты GlobalSign. После обновления сертификата не забывайте пересобирать PEM-файл и делать reload HAProxy.
Проверить, что файл содержит и сертификат, и ключ, можно так:
openssl x509 -in /etc/haproxy/certs/example.org.pem -noout -subject -issuer -dates
openssl pkey -in /etc/haproxy/certs/example.org.pem -noout -checkКонфигурация HAProxy: HTTPS, health checks и PROXY protocol
Ниже пример /etc/haproxy/haproxy.cfg. Он принимает HTTP на 80, перенаправляет на HTTPS, завершает TLS на 443, проверяет backend по /healthz и передаёт соединения в Nginx с send-proxy-v2.
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
tune.ssl.default-dh-param 2048
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-bind-ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
timeout http-request 10s
timeout connect 5s
timeout client 60s
timeout server 60s
timeout http-keep-alive 10s
retries 2
frontend fe_http
bind :80
http-request redirect scheme https code 301
frontend fe_https
bind :443 ssl crt /etc/haproxy/certs/example.org.pem alpn h2,http/1.1
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
default_backend be_nginx
backend be_nginx
balance roundrobin
option httpchk
http-check send meth GET uri /healthz ver HTTP/1.1 hdr Host example.org
http-check expect status 200
default-server inter 3s fall 3 rise 2 maxconn 200
server web1 10.0.0.11:8080 check send-proxy-v2
server web2 10.0.0.12:8080 check send-proxy-v2bind :443 ssl crt включает TLS termination на HAProxy. Параметр alpn h2,http/1.1 позволяет клиентам использовать HTTP/2 на внешней стороне, при этом до backend в этом примере идёт обычный HTTP/1.1. Это нормальная схема: снаружи современный TLS и HTTP/2, внутри простая и контролируемая сеть.
option forwardfor добавляет или дополняет заголовок X-Forwarded-For. Он полезен для приложений и логов, но не должен быть единственным источником доверия. Поэтому дополнительно используется send-proxy-v2: HAProxy передаёт Nginx адрес клиента на уровне соединения, а Nginx принимает его только от доверенного IP балансировщика.
option httpchk, http-check send и http-check expect отвечают за health checks. HAProxy будет регулярно запрашивать /healthz и считать backend живым только при HTTP-статусе 200. Параметры fall 3 и rise 2 защищают от мгновенного дребезга: сервер выводится из ротации после трёх неудачных проверок и возвращается после двух успешных.
maxconn 200 на backend-сервере — пример локального предохранителя. Реальное значение зависит от ресурсов VDS, настроек Nginx, PHP-FPM или приложения. Начните с консервативного лимита, затем смотрите на очереди, время ответа и ошибки.
Конфигурация Nginx backend с proxy_protocol
На каждом backend-сервере Nginx должен слушать внутренний адрес и порт с параметром proxy_protocol. Если этот параметр включить, Nginx будет ожидать PROXY-заголовок в начале каждого соединения. Обычный HTTP-запрос без такого заголовка будет отвергнут.
Пример server block для 10.0.0.11. На втором backend замените IP в listen на 10.0.0.12 или используйте общий внутренний адрес интерфейса, если он совпадает с вашей схемой.
log_format proxy '$proxy_protocol_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_proto"';
server {
listen 10.0.0.11:8080 proxy_protocol;
server_name example.org;
access_log /var/log/nginx/example-access.log proxy;
error_log /var/log/nginx/example-error.log warn;
set_real_ip_from 10.0.0.10;
real_ip_header proxy_protocol;
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location / {
proxy_pass http://127.0.0.1:9000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
}
}set_real_ip_from 10.0.0.10 говорит Nginx доверять PROXY protocol только от HAProxy. real_ip_header proxy_protocol заменяет клиентский адрес на значение из PROXY-заголовка. В логах удобно явно писать $proxy_protocol_addr, чтобы быстро видеть реальный адрес посетителя и отличать его от адреса балансировщика.
Если Nginx сам отдаёт сайт без отдельного приложения, блок location / можно заменить на root, index и ваши стандартные правила. Суть не меняется: listen ... proxy_protocol, доверенный HAProxy в set_real_ip_from и отдельный /healthz для проверок.
Firewall: закрываем backend от внешнего мира
PROXY protocol безопасен только в доверенном сегменте. На backend-серверах порт 8080 должен быть доступен с IP HAProxy и недоступен всем остальным. Используйте тот firewall, который реально применён на вашем VDS, и не смешивайте правила без необходимости.
UFW на Debian или Ubuntu
ufw allow 22/tcp
ufw allow from 10.0.0.10 to any port 8080 proto tcp
ufw deny 8080/tcp
ufw enable
ufw status verboseЕсли Nginx backend не должен принимать публичный HTTP/HTTPS напрямую, не открывайте на нём 80 и 443. Публичные порты нужны на HAProxy.
firewalld на RHEL-based системах и Fedora
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.0.0.10" port protocol="tcp" port="8080" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" port protocol="tcp" port="8080" drop'
firewall-cmd --reload
firewall-cmd --list-allНа самом HAProxy откройте входящие 80/tcp и 443/tcp. Backend-порты на HAProxy открывать не нужно, если он не выполняет роль приложения.

Проверка конфигурации и безопасный reload
Сначала проверьте синтаксис Nginx на каждом backend-сервере:
nginx -t
systemctl reload nginxЗатем проверьте HAProxy на балансировщике:
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl reload haproxy
systemctl status haproxy --no-pagerЕсли HAProxy запущен с stats socket из примера, можно посмотреть состояние backend-узлов:
echo "show stat" | socat - /run/haproxy/admin.sock | headДля проверки TLS и маршрутизации используйте curl. Команда ниже должна вернуть ответ сайта через HAProxy:
curl -I https://example.orgHealth endpoint можно проверить через HAProxy:
curl -i https://example.org/healthzОбычный curl напрямую к backend-порту 8080 может завершиться ошибкой, потому что Nginx ждёт служебный PROXY-префикс. Это ожидаемо. Для прямой диагностики лучше смотреть состояние HAProxy, логи Nginx или временно поднимать отдельный диагностический порт без proxy_protocol только во внутренней сети.
Логи: как убедиться, что реальный IP не потерялся
После первого запроса откройте access log backend-сервера:
tail -f /var/log/nginx/example-access.logВ начале строки должен быть IP клиента, а не 10.0.0.10. Если видите IP HAProxy, проверьте три вещи: есть ли send-proxy-v2 в строке server на HAProxy, есть ли proxy_protocol в listen Nginx и совпадает ли IP в set_real_ip_from с реальным адресом балансировщика, с которого приходят соединения.
Для приложений важно не только видеть реальный IP, но и понимать исходную схему запроса. Так как TLS завершается на HAProxy, до Nginx запрос приходит по HTTP. Поэтому HAProxy задаёт X-Forwarded-Proto https, а Nginx передаёт его приложению. Без этого многие фреймворки строят неправильные абсолютные ссылки, зацикливают редиректы или считают соединение небезопасным.
Таймауты и лимиты: стартовые значения
Таймауты в примере подходят для обычного сайта или API без очень долгих запросов. Если у вас есть загрузка больших файлов, SSE, WebSocket, долгие отчёты или медленные внешние API, значения нужно пересмотреть. Главный принцип: таймауты HAProxy не должны быть короче нормального времени работы приложения, но и не должны бесконечно держать зависшие соединения.
timeout connect— время на установку соединения с backend. Обычно 3–5 секунд достаточно.timeout client— сколько HAProxy ждёт активность от клиента. Для обычного web можно начать с 60 секунд.timeout server— сколько HAProxy ждёт ответ от backend. Должно учитывать реальные длительные операции.timeout http-request— защита от клиентов, которые слишком медленно отправляют заголовки запроса.maxconn— ограничение конкурентных соединений, чтобы один backend не был перегружен сильнее, чем способен обработать.
Для WebSocket в HAProxy обычно добавляют отдельный backend или ACL по пути/заголовку и увеличивают timeout tunnel. Не смешивайте такие соединения с обычными HTTP-запросами без понимания профиля нагрузки. Если дополнительно нужны ограничения по частоте запросов, посмотрите материал про rate limiting в HAProxy через stick tables.
Как обновлять backend без простоя
Одна из причин ставить HAProxy как load balancer — возможность обслуживать узлы по очереди. Перед деплоем можно временно вывести backend из ротации через runtime socket, дождаться завершения активных соединений, обновить приложение, проверить /healthz и вернуть узел обратно.
Посмотреть список backend-серверов:
echo "show servers state" | socat - /run/haproxy/admin.sockМягко отключить сервер web1 в backend be_nginx:
echo "disable server be_nginx/web1" | socat - /run/haproxy/admin.sockВернуть сервер обратно:
echo "enable server be_nginx/web1" | socat - /run/haproxy/admin.sockЭто не заменяет полноценный процесс деплоя, но даёт удобный ручной рычаг при обслуживании VDS. Для автоматизации можно обернуть команды в Ansible, CI/CD job или systemd-скрипт, но сначала лучше отработать процедуру вручную и понять поведение приложения.
Типовые ошибки и быстрые проверки
Nginx пишет broken header while reading PROXY protocol
Обычно это значит, что на порт с proxy_protocol пришёл обычный HTTP-запрос. Возможные причины: вы проверяете backend прямым curl, порт открыт наружу, HAProxy не использует send-proxy или send-proxy-v2, либо запрос пришёл от другого промежуточного proxy. Проверьте firewall и строку backend-сервера в HAProxy.
HAProxy показывает backend DOWN
Сначала проверьте доступность порта с балансировщика:
nc -vz 10.0.0.11 8080
nc -vz 10.0.0.12 8080Если TCP доступен, смотрите Nginx error log. При включённом proxy_protocol health checks от HAProxy тоже должны идти с send-proxy-v2, как в примере. Если вы убрали send-proxy-v2, Nginx не сможет корректно прочитать проверку.
В приложении бесконечный редирект с HTTP на HTTPS
Почти всегда приложение не доверяет заголовку X-Forwarded-Proto или Nginx не передаёт его дальше. Проверьте http-request set-header X-Forwarded-Proto https в HAProxy и proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto в Nginx. Во многих фреймворках также нужно явно указать доверенные proxy.
В логах backend виден IP HAProxy вместо клиента
Проверьте соответствие версий протокола. Если HAProxy отправляет send-proxy-v2, Nginx не требует отдельного указания версии: параметр proxy_protocol понимает PROXY v1 и v2. Но set_real_ip_from должен содержать именно тот IP, с которого HAProxy подключается к backend. В сетях с несколькими интерфейсами это не всегда публичный адрес балансировщика.
Минимальный production-чеклист
- HAProxy слушает публичные
80и443, backend-порты наружу не открыты. - TLS-сертификат собран в PEM-файл, права на приватный ключ ограничены.
- В HAProxy настроены
timeout connect,timeout client,timeout serverи разумныйmaxconn. - Health checks проверяют не просто открытый порт, а реальную готовность приложения через
/healthz. - HAProxy отправляет
send-proxy-v2, а Nginx слушает backend-порт сproxy_protocol. - Nginx доверяет PROXY protocol только IP-адресу HAProxy через
set_real_ip_from. - В логи попадает реальный IP клиента, а приложение получает корректный
X-Forwarded-Proto. - Reload HAProxy и Nginx выполняется после проверки синтаксиса.
- Есть понятная процедура вывода backend из ротации перед деплоем.
Такую схему можно развивать дальше: добавить отдельные backend для API и статики, sticky sessions, Prometheus-метрики, отдельный stats frontend, dual RSA/ECDSA-сертификаты, mTLS до внутренних сервисов или автоматическую ротацию сертификатов. Если работаете с gRPC и нестандартной логикой маршрутизации, пригодится отдельный разбор HTTP/2, gRPC и Lua в HAProxy. Но фундамент остаётся тем же: HAProxy как надёжный edge reverse proxy, строгие health checks, закрытая внутренняя сеть и корректная передача клиентского IP до Nginx backend.


