Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~18 мин +50 XP

Деплой: chi за reverse-прокси

Деплой Go-сервиса на chi редко заканчивается go build: между интернетом и процессом встаёт reverse-прокси, и после выката за nginx журнал показывает один адрес прокси, а лимит по IP душит всех разом. Дальше по слоям: заголовки X-Forwarded-For и X-Real-IP, middleware.RealIP, X-Forwarded-Proto для редиректов, таймауты nginx и http.Server, multistage-образ и systemd-юнит — и живое место про подделанный заголовок.

Содержание урока · 10 разделов

Первый же выкат Go-сервиса на chi за nginx ломает привычную картину: в журнале на каждый запрос один и тот же адрес — 172.17.0.1 или адрес прокси, лимит запросов режет всех пользователей одной очередью, а редирект на страницу логина уводит с https:// на http://. Ничего в chi не сломалось: сервис честно видит то, что ему принёс прокси — TCP-соединение от nginx, а не от клиента. Деплой go сервиса — это два слоя, и настраивать их надо согласованно: прокси принимает TLS, сжимает ответы, отдаёт статику и переводит запрос на chi, а chi должен узнать настоящий адрес клиента и исходную схему из заголовков. Ниже — раскладка по слоям: что настраиваем в nginx, что в chi, где живут таймауты, куда ставить multistage-образ и systemd-юнит, и живое место про подделанный X-Forwarded-For, из-за которого лимиты по IP облетают стороной.

Деплой go сервиса за nginx: слои от TLS до chi#

Обязанности раскладываются так. Прокси держит TLS-терминацию: сертификаты, redirect с 80-го порта, http2 — всё здесь, chi работает по HTTP внутри доверенного периметра. Сжатие — gzip на прокси: жать в хендлере значит тратить CPU приложения на работу, которую прокси делает дешевле. Статика — images и собранные ассеты отдаёт nginx, chi занимается только API: файловый сервер chi через Mount остаётся для dev-режима и внутренних задач. Балансировка — upstream с несколькими инстансами chi, когда один перестал справляться. Плюс мелочи, которые в приложении делать дорого: ограничение соединений, чёрные списки, access-лог. chi за этим остаётся ровно тем, чем был весь курс: роутером и хендлерами на чистом net/http. Двусторонняя связь одна и простая: заголовки от прокси к chi и health-пробы от прокси к /healthz.

Dockerfile для Go: multistage и distroless#

Go-бинарник — редкий случай, когда контейнер может быть крошечным: сборке нужен тулчейн, запуску — нет. Multistage-сборка разделяет эти жизни: первая стадия FROM golang:1.22-alpine качает модули и компилирует, вторая FROM alpine (или пустой FROM scratch, или distroless) получает один файл через COPY --from=build. CGO_ENABLED=0 выключает cgo — бинарник становится статическим и запускается даже в scratch без libc. В финальном образе нет компилятора, модулей и shell — меньше поверхность атаки и мегабайты. EXPOSE 8080 — только документация порта, проброс делает -p при запуске. Health-check: /healthz из каркаса с graceful shutdown опрашивается прокси и оркестратором; HEALTHCHECK в Dockerfile требует исполняемый файл, а в distroless нет ни shell, ни curl — проверку переносят в docker-compose или Kubernetes либо копируют рядом крошечный check-бинарник.

Multistage-образ для сервиса на chi

GO
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /bin/notes ./cmd/notes

FROM alpine:3.20
COPY --from=build /bin/notes /notes
EXPOSE 8080
ENTRYPOINT ["/notes"]

Слои с go.mod и go.sum идут раньше кода: пока файлы модулей не менялись, docker build переиспользует кэш и не качает зависимости на каждый коммит. Финальная стадия забирает только бинарник — тулчейн, исходники и кэш модулей остаются в build-стадии. Docker stop посылает SIGTERM, который ловит каркас из урока про graceful shutdown. Smoke-тест бинарника удобно прогнать локальным Go до сборки образа: go build && ./notes и curl на /healthz.

Проверка: что попадает в финальный образ

Что именно копирует в финальную стадию строка COPY --from=build /bin/notes /notes?

X-Forwarded-For и middleware.RealIP в chi#

Адрес клиента живёт в заголовках, которые выставляет прокси. X-Real-IP — один адрес, тот, что прокси видел на своём входе. X-Forwarded-For — цепочка через запятую, где каждый прокси дописывает свой прыжок. X-Forwarded-Proto — исходная схема, http или https. Важная деталь nginx: по умолчанию он передаёт на бэкенд только Host и Connection — все три заголовка надо выставить proxy_set_header вручную. В chi адрес чинит middleware.RealIP. По исходникам v5.0.8 он проверяет заголовки в порядке True-Client-IP, X-Real-IP, X-Forwarded-For, у цепочки берёт первое значение до запятой, валидирует строку через net.ParseIP — мусор молча игнорируется — и переписывает r.RemoteAddr. Ставить слой надо рано, до Logger: адрес в строке лога читается из RemoteAddr в момент сборки, и порядок слоёв из урока про RequestID и Logger работает и здесь.

nginx перед chi: location с заголовками

GO
server {
    listen 443 ssl;
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 30s;
    }
}

proxy_pass переводит запрос на chi, слушающий 127.0.0.1:8080 — снаружи порт не открыт, до RealIP добирается только nginx. X-Forwarded-For здесь затирается адресом клиента, а не дополняется: почему — в красной врезке ниже. X-Forwarded-Proto отдаёт исходную схему, и редиректы перестают уводить с https. Проверка руками: chi локальным Go на :8080, nginx на :8081, curl -H «X-Forwarded-For: 203.0.113.9» — хендлер увидит честный RemoteAddr.

Проверка: что окажется в RemoteAddr

За nginx без proxy_set_header идёт запрос с клиентским заголовком «X-Forwarded-For: 1.2.3.4». На chi включён middleware.RealIP. Что в r.RemoteAddr?

Какой адрес выберет RealIP?

Прочитайте код и выберите, что он выведет:

// заголовки запроса, дошедшего до chi:
X-Real-IP: 203.0.113.7
X-Forwarded-For: 198.51.100.9, 10.0.0.5

// стек: r.Use(middleware.RealIP)
// в хендлере:
fmt.Print(r.RemoteAddr)

X-Forwarded-Proto и таймауты прокси и http.Server#

Схема нужна каждому, кто строит абсолютные URL или решает, редиректить ли на https: за прокси r.TLS всегда nil — соединение к chi идёт по HTTP, и проверка «r.TLS == nil» врёт. Отсюда конвенция: прокси выставляет X-Forwarded-Proto $scheme, приложение читает заголовок — и redirect на логин остаётся на https://. Второй слой согласования — таймауты. У nginx свой бюджет: proxy_connect_timeout, proxy_send_timeout и proxy_read_timeout, по умолчанию 60 секунд каждый; в http.Server — ReadTimeout, WriteTimeout и IdleTimeout из каркаса. Правило: дедлайн chi должен срабатывать раньше бюджета прокси. Timeout-хендлер на 5 секунд при proxy_read_timeout 60s — нормально; отчёт на 90 секунд при прокси 60s — прокси отрежет соединение, клиент увидит 502, а хендлер доработает впустую. Дедлайны и троттлинг выставляются с учётом прокси-бюджета, а не в отрыве от него.

systemd без Docker: unit-файл#

Docker — не единственный способ жить на сервере, и для одиночного VPS systemd честно проще: один бинарник, unit-файл, никакого реестра образов. ExecStart указывает на собранный бинарник, User — на непривилегированного пользователя (root не нужен: chi слушает порт выше 1024, а 80/443 держит nginx), Restart=on-failure поднимает процесс после падения, EnvironmentFile подкладывает переменные окружения. systemctl enable --now notes — сервис в автозапуске; journalctl -u notes — логи. Docker stop и systemctl stop делают одно и то же: шлют SIGTERM, который ловит graceful shutdown из каркаса — долетающие запросы заканчиваются, новые не принимаются. Чего в systemd-варианте нет — изоляции и воспроизводимости: версии libc и окружение зависят от машины, и собирать бинарник нужно на совместимой системе.

Unit-файл для сервиса на chi

GO
[Unit]
Description=notes-api (chi)
After=network-online.target

[Service]
User=notes
ExecStart=/opt/notes/notes
EnvironmentFile=/etc/notes/env
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

After=network-online.target откладывает старт до появления сети — слушателю нужен интерфейс. User=notes: сервис работает от непривилегированного пользователя с правами только на бинарник и env-файл. Restart=on-failure с RestartSec=2 — пауза между попытками, чтобы не крутить бесконечный рестарт-цикл при битой конфигурации. После правки юнита — systemctl daemon-reload, иначе systemd продолжит держать старую версию файла в памяти.

Слой → что настраиваем#

СлойГде настраиваемЧто делаем
TLSnginx / Caddyсертификаты, redirect с 80 на 443, http2
Сжатиеnginx / Caddygzip для text/*: JSON и HTML, без уже сжатых картинок
Статикаnginx / Caddylocation на каталог ассетов и cache-заголовки
Заголовки клиентаnginx → chiX-Real-IP, X-Forwarded-For, X-Forwarded-Proto через proxy_set_header
Адрес клиентаchimiddleware.RealIP в начале стека, до Logger
Таймаутыnginx + http.Serverproxy_read_timeout больше дедлайнов хендлеров chi
Перезапускsystemd / DockerRestart=on-failure или restart-политика контейнера

Чек-лист выката#

  • proxy_set_header четвёркой: Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto — nginx по умолчанию передаёт только Host и Connection.
  • X-Forwarded-For затирается $remote_addr, а не дополняется $proxy_add_x_forwarded_for — иначе клиент подделает адрес.
  • middleware.RealIP стоит в начале стека, до Logger; порт chi снаружи закрыт.
  • Таймауты согласованы: proxy_read_timeout больше дедлайнов chi, иначе долгие запросы умрут 502-ми на прокси.
  • /healthz отвечает и в момент graceful shutdown — прокси снимает инстанс с балансировки до остановки.
  • Restart=on-failure в systemd или restart-политика в Docker — падение не оставляет сервис мёртвым до утра.

Восстановите location перед chi

Три строки nginx-конфига: порт бэкенда, адрес клиента и исходная схема.

location / { proxy_pass http://127.0.0.1:; proxy_set_header Host $host; proxy_set_header X-Real-IP $; proxy_set_header X-Forwarded-Proto $; }

Соберите multistage-файл

Расставьте строки Dockerfile: от базового образа сборки до переноса бинарника в финальную стадию.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: chi за прокси

Термин

X-Forwarded-For

нажмите, чтобы перевернуть
Определение

Цепочка адресов через запятую: каждый прокси дописывает свой прыжок. chi берёт первое значение до запятой. Подделывается клиентом, если прокси не затирает заголовок.

1 / 6

Сервис за прокси перестаёт быть игрушкой на ноутбуке: TLS и сжатие съел nginx, адрес клиента доехал через X-Real-IP и RealIP до RemoteAddr, редиректы держат схему из X-Forwarded-Proto, таймауты согласованы, а падение поднимает Restart. Остаётся выключаться вежливо: graceful shutdown из отдельного урока уже ловит SIGTERM от systemd и Docker stop, а /healthz держит прокси в курсе живости инстанса. Бюджеты по времени выставляются вместе с Timeout и Throttle: сначала дедлайн chi, потом прокси-запас. Полная карта курса — на странице chi.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Схема «nginx/Caddy → chi»: TLS, сжатие и статика на прокси, chi занимается логикой на чистом net/http.
  • nginx по умолчанию передаёт только Host и Connection — X-Real-IP, X-Forwarded-For и X-Forwarded-Proto выставляются proxy_set_header вручную.
  • middleware.RealIP переписывает RemoteAddr по True-Client-IP → X-Real-IP → X-Forwarded-For и безопасен только за доверенным прокси.
  • Подделанный X-Forwarded-For при $proxy_add_x_forwarded_for обходит лимиты по IP: заголовок затирается на границе, порт chi закрыт снаружи.
  • Таймауты согласованы: дедлайны chi срабатывают раньше proxy_read_timeout — иначе прокси отрезает долгие запросы 502-ми.
  • Multistage-образ: сборка в полной стадии, в финал копируется один статический бинарник; health-check — /healthz плюс оркестратор.
  • Без Docker — systemd: ExecStart, User, Restart=on-failure, EnvironmentFile и SIGTERM для graceful shutdown.

Домашнее задание

Соберите multistage-образ сервиса: build-стадия с CGO_ENABLED=0, финальная на alpine, EXPOSE и ENTRYPOINT. Рядом напишите nginx-location с четвёркой proxy_set_header — X-Forwarded-For затирается $remote_addr — и unit-файл systemd с Restart=on-failure. Проверьте curl-ом с подделанным X-Forwarded-For, что в RemoteAddr оказался честный адрес клиента.

Личные заметкиПусто
Хранятся только в вашем браузере (Chi · Деплой: chi за reverse-прокси)

Вопросы и ответы по уроку

Нужен ли reverse-прокси, если chi сам умеет таймауты и статику?

Умеет — но каждый слой, который вы тащите в приложение, съедает его CPU и появляется в коде. Прокси снимает TLS, сжатие и лимиты соединений до chi и даёт точку, где сеть настраивается без пересборки бинарника. Для pet-проекта чистый Go с autocert допустим; прод обычно всё равно заканчивается прокси.

Почему после middleware.RealIP в RemoteAddr нет порта?

chi v5 подставляет в RemoteAddr значение из заголовка как есть — голый IP без порта, в отличие от обычного «IP:port» от TCP-соединения. Код, который разбирает адрес через net.SplitHostPort, начнёт получать ошибку «missing port». Валидация у chi есть — net.ParseIP отсекает мусор, — но порт слой не дописывает.

X-Real-IP или X-Forwarded-For — что безопаснее?

Одиночный X-Real-IP от собственного прокси проще: в нём одно значение, и его легко затереть на границе. X-Forwarded-For рассчитан на цепочки прокси, а chi берёт из него первый элемент — тот, что ближе к клиенту. Пока за вами один nginx, безопаснее X-Real-IP или затёртый XFF; цепочке нужен доверенный каждый прыжок.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Redis25 уроков~12 чпрактика25 уроков уже доступны

Redis в Go: кэш, сессии и очереди на go-redis

Курс Redis для Go-разработчиков: кэширование запросов, сессии, счётчики, Pub/Sub, Streams и rate limiter. 25 уроков с примерами кода go-redis v9.

  • Подключаться к Redis из Go и настраивать пул соединений
  • Кэшировать результаты запросов и придумывать TTL
  • Работать с хэшами, списками, множествами и ZSET
  • Строить сессии, счётчики, лидерборды и rate limiter