Пошук уроків, статей та іншого контенту
Навчитеся використовувати зворотний проксі для маршрутизації, кешування, TLS-термінації та захисту сервісів.
Зворотний проксі — це сервер, який приймає запити клієнтів і передає їх внутрішнім сервісам. Клієнт взаємодіє із проксі та зазвичай не знає адрес і топології внутрішньої мережі.
Типовий потік запиту:
Клієнт
│
▼
Зворотний проксі
├── /api/* → API-сервіс
├── /admin/* → Сервіс адміністративної панелі
└── інші шляхи → ВебзастосунокЗворотний проксі відрізняється від прямого проксі:
прямий проксі діє від імені клієнта та приховує клієнта від зовнішнього сервісу;
зворотний проксі діє від імені сервера та приховує внутрішні сервіси від клієнта.
Популярні реалізації зворотного проксі:
Nginx;
HAProxy;
Apache HTTP Server;
Traefik;
хмарні балансувальники навантаження.
Зворотний проксі часто виконує кілька функцій одночасно:
маршрутизує запити до потрібного сервісу;
завершує TLS-з'єднання;
кешує відповіді;
балансує навантаження між екземплярами сервісу;
обмежує частоту запитів;
приховує внутрішні адреси;
централізовано додає або змінює HTTP-заголовки;
обробляє стиснення та статичні файли.
Важливо розділяти ці функції концептуально. Наприклад, маршрутизація та TLS-термінація можуть виконуватися одним проксі, але кешування потрібно вмикати лише для відповідей, які безпечно повторно використовувати.
Маршрутизація визначає, до якого upstream-сервісу передати запит.
Наприклад:
/api/ — API-сервіс;
/static/ — статичні файли;
/ — вебінтерфейс.
Під час проксування потрібно передати важливі заголовки:
Host — початкове ім'я хоста;
X-Real-IP — IP-адреса клієнта;
X-Forwarded-For — ланцюжок проксі, через які пройшов запит;
X-Forwarded-Proto — початковий протокол, наприклад http або https.
Без цих заголовків застосунок може неправильно визначати адресу клієнта, протокол або домен.
http {
upstream api_service {
server api:3000;
}
upstream web_service {
server web:3000;
}
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://api_service;
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;
}
location / {
proxy_pass http://web_service;
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;
}
}
}proxy_pass http://api_service; передає upstream повний URI запиту. Наприклад, запит /api/users залишиться /api/users.
Якщо вказати URI із завершальним слешем, поведінка зміниться:
location /api/ {
proxy_pass http://api_service/;
}У цьому випадку префікс /api/ буде замінено на /. Запит /api/users стане /users на стороні upstream-сервісу. Це важлива відмінність, оскільки неправильний слеш часто спричиняє помилки маршрутизації.
Нижче наведено мінімальний застосунок із двома Node.js-сервісами та Nginx:
web обробляє вебмаршрути;
api повертає дані API;
proxy маршрутизує запити та кешує GET-відповіді API.
Структура файлів:
reverse-proxy-example/
├── docker-compose.yml
├── nginx/
│ └── default.conf
├── api/
│ ├── Dockerfile
│ └── server.js
└── web/
├── Dockerfile
└── server.jsdocker-compose.ymlservices:
proxy:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
- web
api:
build: ./api
web:
build: ./webapi/DockerfileFROM node:22-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]api/server.jsconst http = require("node:http");
const server = http.createServer((request, response) => {
if (request.url === "/api/health") {
response.writeHead(200, {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=10"
});
response.end(JSON.stringify({
service: "api",
status: "ok"
}));
return;
}
response.writeHead(404, {
"Content-Type": "application/json"
});
response.end(JSON.stringify({
error: "Not found"
}));
});
server.listen(3000, "0.0.0.0", () => {
console.log("API service is listening on port 3000");
});web/DockerfileFROM node:22-alpine
WORKDIR /app
COPY server.js .
EXPOSE 3000
CMD ["node", "server.js"]web/server.jsconst http = require("node:http");
const server = http.createServer((request, response) => {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8"
});
response.end(`
<!doctype html>
<html lang="uk">
<head>
<meta charset="utf-8">
<title>Reverse Proxy Demo</title>
</head>
<body>
<h1>Вебсервіс працює</h1>
<p>Цю відповідь передав зворотний проксі.</p>
</body>
</html>
`);
});
server.listen(3000, "0.0.0.0", () => {
console.log("Web service is listening on port 3000");
});nginx/default.confproxy_cache_path /var/cache/nginx/api
keys_zone=api_cache:10m
max_size=100m
inactive=60s
use_temp_path=off;
upstream api_service {
server api:3000;
}
upstream web_service {
server web:3000;
}
server {
listen 80;
server_name _;
location /api/ {
proxy_cache api_cache;
proxy_cache_methods GET HEAD;
proxy_cache_valid 200 10s;
proxy_cache_lock on;
# Не використовуємо кеш для запитів із обліковими даними.
proxy_cache_bypass $http_authorization;
proxy_no_cache $http_authorization;
proxy_pass http://api_service;
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;
}
location / {
proxy_pass http://web_service;
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;
}
}Запуск:
docker compose up --buildПісля запуску можна перевірити маршрутизацію:
curl http://localhost:8080/
curl http://localhost:8080/api/healthПерший запит передається до web, а другий — до api.
Кешований результат можна побачити за заголовком Age або за поведінкою запитів у журналі Nginx. У реальному середовищі до відповіді також часто додають діагностичний заголовок:
add_header X-Cache-Status $upstream_cache_status always;Можливі значення:
MISS — відповіді не було в кеші;
HIT — відповідь взято з кешу;
BYPASS — кеш обійдено;
EXPIRED — кешований запис протермінований.
Кешування зменшує кількість запитів до застосунку та скорочує час відповіді. Проксі зберігає відповідь і повертає її для наступних однакових запитів протягом заданого часу.
Кешування добре підходить для:
публічних GET-запитів;
статичних ресурсів;
даних, які змінюються нечасто;
відповідей, для яких сервер явно задає коректний Cache-Control.
Кешування небезпечне для:
персональних відповідей;
відповідей, що залежать від cookie;
сторінок після автентифікації;
запитів, результат яких залежить від заголовка Authorization;
операцій зі зміною стану.
Методи POST, PUT, PATCH і DELETE зазвичай не кешують.
Навіть для GET-запиту кеш може бути неправильним, якщо URL однаковий, але відповідь залежить від користувача. У такому випадку кеш потрібно вимкнути або правильно включити відповідні параметри в ключ кешу.
TLS-термінація означає, що HTTPS-з'єднання завершується на зворотному проксі. Проксі розшифровує запит і передає його внутрішньому сервісу.
Схема:
Клієнт -- HTTPS --> Зворотний проксі -- HTTP або HTTPS --> Внутрішній сервісЦе спрощує керування сертифікатами: сертифікат встановлюється в одному місці, а не на кожному сервісі.
Приклад конфігурації:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://web_service;
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;
}
}Застосунок має використовувати X-Forwarded-Proto, щоб розуміти, що початковий запит був HTTPS. Це важливо, зокрема, для:
генерації HTTPS-посилань;
безпечних cookie;
перенаправлень;
захисту від циклічних перенаправлень.
Заголовку X-Forwarded-Proto не можна безумовно довіряти, якщо клієнт може напряму звернутися до застосунку в обхід проксі. Внутрішні сервіси мають бути доступні лише через дозволені мережеві правила або приватну мережу.
Зворотний проксі створює єдину точку, у якій можна застосувати базові обмеження.
У Nginx зону для обмеження запитів оголошують у контексті http, а правило застосовують у server або location:
http {
limit_req_zone $binary_remote_addr
zone=api_limit:10m
rate=10r/s;
server {
listen 80;
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://api_service;
}
}
}У цьому прикладі для кожної IP-адреси дозволено в середньому 10 запитів за секунду. burst=20 дозволяє короткочасний сплеск до 20 додаткових запитів.
Обмеження частоти не замінює автентифікацію та авторизацію. Воно лише зменшує вплив надмірної кількості запитів.
Внутрішні сервіси не повинні публікувати свої порти назовні без потреби. У Docker Compose зовнішнім портом можна відкрити лише проксі:
services:
proxy:
ports:
- "8080:80"
api:
build: ./apiСервіс api доступний проксі за ім'ям api, але його порт не опубліковано на хості.
Проксі може додавати заголовки безпеки:
server {
listen 80;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://web_service;
}
}Такі заголовки не усувають помилки в застосунку, але допомагають задати безпечніші правила для браузера.
Під час налагодження потрібно перевіряти кожен рівень окремо:
Чи доступний проксі.
Чи розв'язується ім'я upstream-сервісу.
Чи слухає сервіс потрібний порт.
Чи правильно передається URI.
Чи не блокує запит кеш або обмеження частоти.
Чи передаються необхідні заголовки.
Корисні HTTP-коди:
200 — запит успішно оброблено;
301 або 302 — перенаправлення;
400 — некоректний запит;
401 — потрібна автентифікація;
403 — доступ заборонено;
404 — маршрут не знайдено;
429 — перевищено ліміт запитів;
502 — проксі не отримав коректну відповідь від upstream;
504 — upstream не відповів вчасно.
Код 502 зазвичай означає проблему між проксі та сервісом:
сервіс зупинено;
неправильний порт;
неправильне ім'я хоста;
сервіс слухає лише 127.0.0.1, а не адресу контейнера;
з'єднання відхилено.
Код 504 частіше вказує на тайм-аут або завислий upstream.
Не слід кешувати відповідь лише тому, що метод запиту — GET. Якщо відповідь залежить від користувача, кеш може повернути дані одного користувача іншому.
Потрібно перевіряти:
Authorization;
cookie;
заголовки та параметри, що впливають на відповідь;
директиви Cache-Control.
proxy_passВідсутність або наявність завершального слеша змінює URI, який отримає upstream. Це потрібно перевіряти на конкретному маршруті, а не припускати.
Якщо не передати X-Forwarded-For, застосунок часто бачитиме IP-адресу проксі. Це впливає на журналювання, аудит і обмеження частоти.
X-Forwarded-ForКлієнт може сам надіслати такий заголовок. Якщо проксі доступний напряму з недовіреної мережі, не можна вважати перше значення цього заголовка справжньою адресою клієнта.
Якщо API доступний напряму в обхід проксі, користувач може обійти TLS-термінацію, обмеження частоти та централізовані правила доступу.
Надто довгий тайм-аут утримує з'єднання та може виснажити ресурси проксі. Надто короткий тайм-аут призводить до помилкових 504 для повільних, але коректних операцій.
Зворотний проксі приймає зовнішні запити та передає їх внутрішнім сервісам. Його основні можливості:
маршрутизація за шляхом, доменом або іншими властивостями запиту;
кешування публічних відповідей;
завершення HTTPS-з'єднання;
передача інформації про початковий запит через X-Forwarded-*;
обмеження частоти запитів;
приховування внутрішніх сервісів.
Під час налаштування потрібно особливо уважно перевіряти URI в proxy_pass, правила кешування, довіру до проксі-заголовків і доступність upstream-сервісів лише через потрібні мережеві межі.