Новинка Виртуальный VDS сервер в Нидерландах от 390р
Выберите продукт

HAProxy на VDS: TLS termination, health checks, PROXY protocol и Nginx backend

Разберём схему, где HAProxy принимает HTTPS, завершает TLS, проверяет живость узлов и передаёт трафик на Nginx backend по PROXY protocol. Будут команды установки, конфиги, проверки и типовые ошибки.
HAProxy на VDS: TLS termination, health checks, PROXY protocol и Nginx backend

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 и веб-сервер приложения.

Схема передачи реального IP клиента через PROXY protocol от HAProxy к Nginx

Установка 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
FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Конфигурация 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-v2

bind :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 открывать не нужно, если он не выполняет роль приложения.

Health checks HAProxy и закрытый backend-порт Nginx во внутренней сети

Проверка конфигурации и безопасный 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.org

Health 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.

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

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

MySQL Community vs MariaDB vs Percona Server в 2026: что выбрать для VDS OpenAI Статья написана AI (GPT 5)

MySQL Community vs MariaDB vs Percona Server в 2026: что выбрать для VDS

В 2026 году выбор MySQL-совместимой СУБД для VDS уже не сводится к вопросу «что быстрее». Важнее совместимость с приложением, обно ...
Security updates на Linux VDS: unattended-upgrades, dnf-automatic и безопасная reboot policy OpenAI Статья написана AI (GPT 5)

Security updates на Linux VDS: unattended-upgrades, dnf-automatic и безопасная reboot policy

Автообновления закрывают уязвимости быстрее, чем ручной патчинг раз в месяц. Разбираем unattended-upgrades и dnf-automatic, логи, ...
Etckeeper на Linux VDS: версионируем /etc в Git и безопасно откатываем конфиги OpenAI Статья написана AI (GPT 5)

Etckeeper на Linux VDS: версионируем /etc в Git и безопасно откатываем конфиги

Etckeeper превращает каталог /etc в аккуратный Git-репозиторий: видно, кто и когда поменял конфиг, что принесло обновление пакетов ...