Десять сервисов на домашнем сервере — десять портов, которые не упомнишь. Nginx в Docker собирает всё под один адрес с нормальными доменами и SSL. Разбираем образы, конфиги, Certbot и грабли.

Nginx Proxy Manager (NPM) — полноценный веб-интерфейс для управления реверс-прокси. Не нужно править конфиги вообще: добавляешь хосты через браузер, настраиваешь SSL в пару кликов, управляешь перенаправлениями. Под капотом — тот же Nginx.
По статистике запросов — один из самых популярных вариантов для домашних серверов. И понятно почему: порог входа минимальный. Но гибкость ниже, чем у ручной конфигурации. Сложные правила проксирования, нестандартные заголовки, тонкая настройка кеширования — тут NPM начинает мешать.
Образ: nginx:alpine-slim
Ещё более урезанная версия Alpine-сборки. Убраны некоторые модули, минимизирован размер. Для реверс-прокси с базовой конфигурацией — достаточен.
Образ: bitnami/nginx
От Bitnami (VMware/Broadcom). Работает от непривилегированного пользователя из коробки (не root). Удобно в средах с жёсткими требованиями безопасности. Структура конфигов отличается от стандартной — /opt/bitnami/nginx/conf/ вместо /etc/nginx/.
Образ: nginxinc/nginx-unprivileged
Официальный образ от Nginx Inc., запускающийся без root-привилегий. Слушает порт 8080 вместо 80. Хороший выбор для Kubernetes (система оркестрации контейнеров) и сред, где root в контейнере нежелателен.
Если честно, для домашнего реверс-прокси в 80% случаев хватает двух вариантов:
nginx:alpine— если хочешь контроль и понимание, илиjc21/nginx-proxy-manager— если хочешь GUI и не лезть в конфиги. Остальные образы — для специфических задач.
Дальше — практика. Берём nginx:alpine и настраиваем руками. Это длиннее, чем NPM, зато ты понимаешь каждую строчку.
Структура проекта:
nginx-proxy/
├── docker-compose.yml
├── nginx.conf
├── conf.d/
│ ├── nextcloud.conf
│ ├── grafana.conf
│ └── jellyfin.conf
└── certs/yamlservices:
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
restart: unless-stopped
networks:
- proxy-network
networks:
proxy-network:
external: trueСеть proxy-network — внешняя. Создай её заранее:
bashdocker network create proxy-networkЗачем внешняя сеть? Потому что Nginx должен видеть контейнеры из других docker-compose.yml файлов. Если каждый сервис живёт в своём Compose-файле (а так обычно и бывает), они по умолчанию в разных сетях и не видят друг друга. Общая внешняя сеть решает проблему.
В каждый docker-compose.yml сервиса добавляешь:
yamlservices:
grafana:
image: grafana/grafana
# ... остальные настройки ...
networks:
- proxy-network
- default
networks:
proxy-network:
external: truenginxworker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Логирование
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
client_max_body_size 100m; # Для загрузки файлов в Nextcloud
# Подключаем конфиги сервисов
include /etc/nginx/conf.d/*.conf;
}Тут ничего необычного. Единственное — client_max_body_size. По умолчанию Nginx ограничивает тело запроса одним мегабайтом. Для Nextcloud (облачное хранилище) или Jellyfin (медиасервер) этого мало.
Файл conf.d/grafana.conf:
nginxserver {
listen 80;
server_name grafana.home.local;
location / {
proxy_pass http://grafana: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;
}
}Обрати внимание: proxy_pass http://grafana:3000 — здесь grafana — это имя контейнера (или имя сервиса в Docker Compose). Docker DNS (система доменных имён) внутри общей сети резолвит его в IP контейнера. Не нужно знать адрес — достаточно имени.
Заголовки X-Real-IP и X-Forwarded-For — чтобы сервис за прокси видел реальный IP клиента, а не IP контейнера Nginx. Без них в логах Grafana все запросы будут от 172.18.0.x — внутреннего адреса Docker-сети.
Некоторые сервисы (Home Assistant, Nextcloud Talk, Portainer) используют WebSocket (протокол двусторонней связи в реальном времени). Стандартный proxy_pass их не пробросит — нужны дополнительные заголовки:
nginxserver {
listen 80;
server_name ha.home.local;
location / {
proxy_pass http://homeassistant:8123;
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;
# WebSocket
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Без блока WebSocket Home Assistant будет открываться, но интерфейс начнёт терять соединение, виджеты перестанут обновляться в реальном времени. Классический симптом: «всё вроде работает, но что-то периодически отваливается».
HTTP — это, конечно, работает. Но браузеры ругаются, пароли летят открытым текстом, а если выставляешь сервис наружу через VPN (виртуальная частная сеть) или Cloudflare Tunnel — HTTPS обязателен.
Для локальной сети с внутренними доменами (.home.local) Let's Encrypt не подойдёт — нужен реальный домен. Но если у тебя есть свой домен и DNS-провайдер поддерживает API — всё решаемо.
Дополняем docker-compose.yml:
yamlservices:
nginx:
image: nginx:alpine
container_name: nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./conf.d:/etc/nginx/conf.d:ro
- ./certs/live:/etc/nginx/certs:ro
- ./certbot/www:/var/www/certbot:ro
restart: unless-stopped
networks:
- proxy-network
certbot:
image: certbot/certbot
volumes:
- ./certs:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
networks:
- proxy-network
networks:
proxy-network:
external: trueCertbot (утилита для автоматического получения SSL-сертификатов) будет проверять необходимость обновления каждые 12 часов. Сертификаты сохраняются в ./certs, откуда Nginx их читает.
В конфиге Nginx добавляем обработку challenge-запросов от Let's Encrypt:
nginxserver {
listen 80;
server_name grafana.example.com;
# Для Certbot
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Всё остальное — редирект на HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name grafana.example.com;
# ⚠️ Проверь путь к сертификатам! Зависит от домена
ssl_certificate /etc/nginx/certs/grafana.example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/grafana.example.com/privkey.pem;
location / {
proxy_pass http://grafana: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;
}
}Первичное получение сертификата:
bashdocker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
-d grafana.example.com \
--email YOUR_EMAIL_HERE \
--agree-tosПосле получения — перезагрузить Nginx, чтобы подхватил сертификаты:
bashdocker compose exec nginx nginx -s reloadДля внутренней сети без реального домена есть альтернатива — самоподписанные сертификаты или собственный CA (удостоверяющий центр). Инструмент mkcert упрощает этот процесс: генерирует корневой сертификат, устанавливает его в доверенные на твоих устройствах, и выпускает сертификаты для любых имён. Браузеры перестают ругаться. Но это тема для отдельной статьи.
Если предыдущие разделы показались слишком многословными — вот радикально другой подход.
yamlservices:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
ports:
- "80:80"
- "443:443"
- "81:81" # Веб-интерфейс управления
volumes:
- ./npm-data:/data
- ./npm-letsencrypt:/etc/letsencrypt
restart: unless-stopped
networks:
- proxy-network
networks:
proxy-network:
external: truebashdocker compose up -dОткрываешь http://192.168.1.x:81, логин по умолчанию — admin@example.com / changeme. NPM сразу попросит сменить.
Дальше — Proxy Hosts → Add Proxy Host. Заполняешь домен, указываешь IP или hostname контейнера, порт, включаешь SSL в один клик (NPM сам дёрнет Let's Encrypt). Готово.
Почему тогда не все используют NPM? Несколько причин:
Но для быстрого старта — работает отлично.
Самая частая ошибка при настройке реверс-прокси. Означает: Nginx не может достучаться до backend-сервиса.
Причины:
docker network inspect proxy-network — оба контейнера должны быть в спискеproxy_pass. Используй имя контейнера или имя сервиса из ComposeДиагностика:
bash# Логи Nginx
docker logs nginx-proxy
# Проверить, видит ли Nginx контейнер
docker exec nginx-proxy ping grafanaNginx не может зарезолвить имя из proxy_pass при старте и отказывается запускаться. Бывает, когда проксируемый контейнер ещё не создан.
Решение — использовать переменную и resolver:
nginxserver {
listen 80;
server_name grafana.home.local;
resolver 127.0.0.11 valid=30s; # Встроенный DNS Docker
location / {
set $upstream http://grafana:3000;
proxy_pass $upstream;
# ... заголовки ...
}
}С переменной $upstream Nginx не резолвит имя при старте — только при поступлении запроса. Если контейнер недоступен в момент запроса — будет 502, но Nginx хотя бы запустится.
Если Nginx отдаёт 403 при обращении к проксируемому сервису — проблема обычно не в Nginx, а в самом сервисе. Некоторые приложения (Nextcloud, Vaultwarden) требуют указания доверенных прокси.
Например, для Nextcloud в config.php:
php'trusted_proxies' => ['172.16.0.0/12'], // Подсеть Docker
'overwriteprotocol' => 'https',Nginx в Alpine-образе работает от пользователя nginx (UID 101). Если конфиг-файлы на хосте принадлежат root и имеют права 600 — Nginx не сможет их прочитать. Режим :ro в volumes не поможет с правами.
Решение: права 644 на файлы конфигурации.
bashchmod 644 nginx.conf
chmod -R 644 conf.d/Официальный образ Nginx по умолчанию пишет access.log и error.log в stdout/stderr контейнера. Это значит, что docker logs nginx-proxy покажет всё. Если хочешь хранить логи на диске — пробрось volume:
yamlvolumes:
- ./logs:/var/log/nginxНо для домашнего сервера stdout обычно достаточно. Логи можно собирать через Loki (система агрегации логов) или Promtail (агент сбора логов), если настроен стек мониторинга.
bash# Проверить синтаксис
docker exec nginx-proxy nginx -t
# Если OK — перезагрузить без даунтайма
docker exec nginx-proxy nginx -s reloadНикогда не делай docker restart nginx-proxy, если нужно просто применить новый конфиг. nginx -s reload подхватывает изменения без обрыва соединений.
Nginx отдаёт базовую статистику через stub_status:
nginxserver {
listen 8080;
server_name localhost;
location /nginx_status {
stub_status;
allow 172.16.0.0/12; # Только Docker-сеть
deny all;
}
}Для Prometheus (система мониторинга) есть экспортёр nginx/nginx-prometheus-exporter, который парсит stub_status и отдаёт метрики в формате Prometheus.
Схема для домашнего сервера:
Интернет / LAN
│
▼
Nginx (порты 80, 443)
│
├── grafana.home.local → контейнер Grafana:3000
├── nc.home.local → контейнер Nextcloud:8080
├── ha.home.local → контейнер HomeAssistant:8123
├── jelly.home.local → контейнер Jellyfin:8096
└── portainer.home.local → контейнер Portainer:9000Один контейнер Nginx, один docker-compose.yml, папка с конфигами — по файлу на сервис. Добавить новый сервис — скопировать конфиг, поменять имя и порт, nginx -s reload. Удалить — обратная операция.
Nginx в Docker-контейнере для реверс-прокси — решение, которое живёт годами. Обновления образа — docker compose pull && docker compose up -d. Бэкап — скопировать папку проекта. Перенос на другой сервер — rsync и docker compose up. Проще не бывает. Ну, почти.