"Коротко:
<br />
Nginx Proxy Manager (NPM) — это веб-интерфейс поверх Nginx для управления reverse proxy. Настраиваешь через браузер: указываешь домен, IP и порт бэкенда, SSL получается автоматически через Let's Encrypt. В статье — готовые конфиги для 10 популярных self-hosted сервисов с объяснением, почему каждая строка там нужна.<br />
<h2>Диагноз: почему это вообще нужно</h2>
<p><a class="wpil_keyword_link" title="Виртуализация" href="https://it-apteka.com/category/virtualise/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2287">Proxmox</a> висит на <code>192.168.1.10:8006</code>. Nextcloud — на <code>192.168.1.15:8080</code>. Pi-hole — на <code>192.168.1.20:80</code>. Vaultwarden — на <code>192.168.1.25:8080</code>. Grafana — на <code>192.168.1.30:3000</code>.</p>
<p>Полгода так жить можно. Через год начинаешь вести блокнотик с портами. Ещё через год блокнотик теряется.</p>
<p>NPM решает это один раз. После настройки у тебя: <code>https://pve.domain.com</code>, <code>https://cloud.domain.com</code>, <code>https://vault.domain.com</code> — всё с валидным SSL, все порты спрятаны внутрь, единая точка входа.</p>
<p>Что сделаем в этой статье:</p>
<ul>
<li>Поднимем NPM через <a class="wpil_keyword_link" title="Docker" href="https://it-apteka.com/tag/docker/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2282">Docker</a> Compose</li>
<li>Разберём, что значит каждый параметр в настройках</li>
<li>Настроим 10 сервисов: Proxmox VE, Proxmox <a class="wpil_keyword_link" title="Резервное копирование" href="https://it-apteka.com/category/rezervnoe-kopirovanie/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2286">Backup</a> Server, Zimbra, MailCow, Pi-hole, Nextcloud, OpenMediaVault, Vaultwarden, Grafana</li>
<li>Разберём типичные ошибки и как их чинить</li>
</ul>
<p>Потребуется: сервер с Docker, домен с <a class="wpil_keyword_link" title="DNS" href="https://it-apteka.com/tag/dns/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2285">DNS</a>-записями на твой IP, 30-60 минут.</p>
<h2>Архитектура: как это работает</h2>
<pre class="mermaid">%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Браузер / клиент
https://pve.domain.com"] --> B["NPM
порты 80 / 443"]
B --> C["Proxmox VE
192.168.1.10:8006 https"]
B --> D["Nextcloud
nextcloud-app:80 http"]
B --> E["Vaultwarden
vaultwarden:80 http"]
B --> F["Grafana
grafana:3000 http"]
G["Let's Encrypt"] --> B
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#92400e
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
</pre>
<p>NPM принимает входящий HTTPS-трафик, терминирует SSL и пересылает запрос на бэкенд. Бэкенд может работать по HTTP внутри Docker-сети — снаружи всё равно будет HTTPS.</p>
<h2>Базовые понятия: что нужно понять до настройки</h2>
<h3>Scheme: http или https к бэкенду</h3>
<p>Это не про то, как клиент подключается к NPM. Это про то, как NPM подключается к твоему сервису сзади. Proxmox всегда слушает на HTTPS с самоподписанным сертом — ставишь <code>https</code> и добавляешь <code>proxy_ssl_verify off</code>. Контейнеры в одной Docker-сети — обычно <code>http</code>, и это нормально.</p>
<h3>WebSocket Support</h3>
<p>Включай для всего, что показывает данные в реальном времени: консоли VM в Proxmox, live-графики в Grafana, уведомления в Nextcloud Talk, статистика Pi-hole. Без этой галочки эти функции просто не работают — без ошибок, просто молча.</p>
<h3>Заголовки X-Forwarded-*</h3>
<p>Когда NPM проксирует запрос, бэкенд видит IP адрес NPM, а не реального пользователя. Заголовки <code>X-Forwarded-For</code>, <code>X-Real-IP</code> и <code>X-Forwarded-Proto</code> передают реальный IP и протокол. Без них: некорректные логи, сломанный CSRF в MailCow и Zimbra, неправильные редиректы в Nextcloud.</p>
<h3>Docker-сеть как транспорт</h3>
<p>Если NPM и сервис в одной Docker-сети, обращайся к контейнеру по имени, не по IP. IP контейнера меняется при каждом перезапуске. Имя — нет.</p>
<h2>Установка Nginx Proxy Manager</h2>
<p>Рекомендуемый способ — Docker Compose. Никакой магии, просто удобнее обновлять и бэкапить.</p>
<p>Создай директорию и файл конфига:</p>
<pre><code class="language-bash">
mkdir -p /opt/nginx-proxy-manager && cd /opt/nginx-proxy-manager
</code></pre>
<p>Создай <code>docker-compose.yml</code>:</p>
<pre><code class="language-text">
version: '3.8'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- proxy-network
networks:
proxy-network:
driver: bridge
name: proxy-network
</code></pre>
<p>Запусти:</p>
<pre><code class="language-bash">
docker compose up -d
</code></pre>
<p>Открой <code>http://твой-сервер:81</code>. Логин по умолчанию: <code>admin@example.com</code> / <code>changeme</code>.</p>
"Первое,
<br />
Сразу меняй email и пароль администратора. Логин admin@example.com / changeme — публично известен и прекрасно гуглится ботами.<br />
<p>Если сервер смотрит в интернет — порт 81 закрой через firewall или Access List в самом NPM. Интерфейс управления не должен быть доступен снаружи.</p>
<h3>Системные требования</h3>
<table>
<thead>
<tr>
<th>Компонент</th>
<th>Минимум</th>
<th>Рекомендуется</th>
</tr>
</thead>
<tbody>
<tr>
<td>Docker</td>
<td>20.x+</td>
<td>последняя стабильная</td>
</tr>
<tr>
<td>Docker Compose</td>
<td>v2.x (плагин)</td>
<td>v2.20+</td>
</tr>
<tr>
<td>RAM</td>
<td>256 МБ</td>
<td>512 МБ+</td>
</tr>
<tr>
<td>Порты</td>
<td>80, 443, 81</td>
<td>свободны на хосте</td>
</tr>
<tr>
<td>ОС</td>
<td>любой <a class="wpil_keyword_link" title="Linux" href="https://it-apteka.com/category/linux/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2288">Linux</a> с Docker</td>
<td>Ubuntu 22.04 / <a class="wpil_keyword_link" title="Debian" href="https://it-apteka.com/tag/debian/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2292">Debian</a> 12</td>
</tr>
</tbody>
</table>
<p>На момент публикации актуальна версия NPM 2.12.x. Перед установкой проверь свежие релизы на <a href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases" target="_blank" rel="nofollow noopener">GitHub</a>.</p>
<h3>Таблица портов</h3>
<table>
<thead>
<tr>
<th>Порт</th>
<th>Протокол</th>
<th>Назначение</th>
<th>Доступен снаружи?</th>
</tr>
</thead>
<tbody>
<tr>
<td>80</td>
<td>TCP</td>
<td>HTTP трафик, HTTP-challenge Let’s Encrypt</td>
<td>Да</td>
</tr>
<tr>
<td>443</td>
<td>TCP</td>
<td>HTTPS трафик</td>
<td>Да</td>
</tr>
<tr>
<td>81</td>
<td>TCP</td>
<td>Веб-интерфейс управления NPM</td>
<td>Нет (только LAN / VPN)</td>
</tr>
</tbody>
</table>
<h2>Proxmox VE: настройка reverse proxy (порт 8006)</h2>
<p>Proxmox — один из самых капризных в плане проксирования. Самоподписанный сертификат, WebSocket для консолей VM и лимит на загрузку ISO. Всё это нужно учесть.</p>
<p>В NPM создаёшь новый Proxy Host:</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Domain Names</td>
<td>pve.yourdomain.com</td>
</tr>
<tr>
<td>Scheme</td>
<td>https</td>
</tr>
<tr>
<td>Forward Hostname/IP</td>
<td>IP твоего Proxmox сервера</td>
</tr>
<tr>
<td>Forward Port</td>
<td>8006</td>
</tr>
<tr>
<td>WebSocket Support</td>
<td>включить</td>
</tr>
<tr>
<td>Block Common Exploits</td>
<td>включить</td>
</tr>
<tr>
<td>SSL — Let’s Encrypt</td>
<td>включить, Force SSL</td>
</tr>
</tbody>
</table>
<p>Вкладка Advanced — вставляй конфиг:</p>
<pre><code class="language-text">
# Proxmox использует самоподписанный сертификат - проверку отключаем
proxy_ssl_verify off;
# Стандартные заголовки прокси
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_set_header X-Forwarded-Port 443;
# WebSocket - без этого консоли VM и LXC не откроются
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 60 минут - консоль держит соединение, дефолтные 60 секунд не хватит
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Отключаем буферизацию - консоль должна работать в реальном времени
proxy_buffering off;
# Для загрузки ISO и дисков через интерфейс
client_max_body_size 50G;
</code></pre>
<p>Проверка: открой <code>https://pve.yourdomain.com</code>, зайди, запусти консоль любой VM. Если консоль не открывается — первым делом смотри, включён ли WebSocket Support в настройках Proxy Host, не в Advanced конфиге.</p>
<h2>Proxmox Backup Server: настройка reverse proxy (порт 8007)</h2>
<p>Всё то же самое, что PVE, плюс бесконечный таймаут для длительных задач резервного копирования.</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Domain Names</td>
<td>pbs.yourdomain.com</td>
</tr>
<tr>
<td>Scheme</td>
<td>https</td>
</tr>
<tr>
<td>Forward Port</td>
<td>8007</td>
</tr>
<tr>
<td>WebSocket Support</td>
<td>включить</td>
</tr>
</tbody>
</table>
<pre><code class="language-text">
proxy_ssl_verify off;
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_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Без ограничений - бэкапы бывают очень большими
client_max_body_size 0;
# 24 часа - длительные задачи резервного копирования
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# Отключаем перекодирование для бинарных данных
proxy_set_header Accept-Encoding "";
</code></pre>
<h2>Zimbra Collaboration: настройка reverse proxy (порты 8443 и 7071)</h2>
<p>Zimbra — два отдельных веб-интерфейса. Веб-клиент на порту 8443 и административная панель на 7071. Каждый получает свой Proxy Host.</p>
<h3>Веб-клиент (mail.yourdomain.com -> порт 8443)</h3>
<pre><code class="language-text">
proxy_ssl_verify off;
# Zimbra чувствителен к этим заголовкам - без них ломается авторизация
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_set_header X-Forwarded-Port 443;
# Для загрузки вложений
client_max_body_size 1024M;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
# Zimbra сам управляет сжатием
proxy_set_header Accept-Encoding "";
</code></pre>
<h3>Административная панель (zimbra-admin.yourdomain.com -> порт 7071)</h3>
<p>Конфиг аналогичен веб-клиенту. Но вот тут важно: ограничь доступ через Access Lists только для администраторских IP. Не выставляй admin-панель в открытый интернет.</p>
<h2>MailCow Dockerized: настройка reverse proxy</h2>
<p>Тут чуть сложнее. MailCow по умолчанию слушает на <code>0.0.0.0:80</code> и <code>0.0.0.0:443</code> — напрямую конфликтует с NPM. Нужно перенести MailCow на другие порты и объединить их в одну Docker-сеть.</p>
<h3>Шаг 1: перенастраиваем порты MailCow</h3>
<p>В файле <code>mailcow.conf</code> меняй:</p>
<pre><code class="language-text">
HTTP_PORT=8081
HTTPS_PORT=8442
HTTP_BIND=127.0.0.1
HTTPS_BIND=127.0.0.1
</code></pre>
<h3>Шаг 2: подключаем контейнер nginx-mailcow к общей сети</h3>
<p>Создай файл <code>docker-compose.override.yml</code> в директории MailCow:</p>
<pre><code class="language-text">
version: '3.8'
services:
nginx-mailcow:
networks:
- proxy-network
networks:
proxy-network:
external: true
</code></pre>
<h3>Шаг 3: перезапускаем MailCow</h3>
<pre><code class="language-bash">
docker compose down && docker compose up -d
</code></pre>
<h3>Настройка Proxy Host в NPM</h3>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Hostname/IP</td>
<td>nginx-mailcow (имя контейнера)</td>
</tr>
<tr>
<td>Forward Port</td>
<td>80</td>
</tr>
<tr>
<td>WebSocket Support</td>
<td>включить</td>
</tr>
</tbody>
</table>
<pre><code class="language-text">
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;
# Критически важно для CSRF-защиты MailCow - без этого получишь "Invalid CSRF token"
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
</code></pre>
<h2>Pi-hole: настройка reverse proxy (порт 80)</h2>
<p>Pi-hole прост в проксировании, но у него отдельный WebSocket endpoint для live-статистики дашборда. Начиная с версии 6 интерфейс переработан — проверяй пути в актуальной документации.</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Port</td>
<td>80</td>
</tr>
<tr>
<td>WebSocket Support</td>
<td>включить</td>
</tr>
</tbody>
</table>
<pre><code class="language-text">
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 для WebSocket Pi-hole v5 (для v6 проверяй документацию)
location /ws {
proxy_pass http://pihole-ip:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Security заголовки
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
</code></pre>
<h2>Nextcloud: настройка reverse proxy (Docker)</h2>
<p>Nextcloud — самый требовательный к конфигурации из всех. Он проверяет заголовки, требует явного указания trusted_proxies и ломает ссылки если не указать overwriteprotocol. Зато после правильной настройки работает без нареканий.</p>
<h3>Настройка Proxy Host в NPM</h3>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Hostname/IP</td>
<td>nextcloud-app (имя контейнера)</td>
</tr>
<tr>
<td>Forward Port</td>
<td>80</td>
</tr>
<tr>
<td>WebSocket Support</td>
<td>включить</td>
</tr>
<tr>
<td>SSL</td>
<td>Let’s Encrypt, Force SSL, HTTP/2</td>
</tr>
</tbody>
</table>
<pre><code class="language-text">
# Базовые заголовки
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
# Security заголовки - убирают предупреждения в /settings/admin/overview
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Для загрузки больших файлов
client_max_body_size 10G;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# WebSocket для Nextcloud Talk
location /push/ {
proxy_pass http://nextcloud-app:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
# WebDAV для клиентов синхронизации
location /remote.php/dav/ {
proxy_pass http://nextcloud-app:80;
client_max_body_size 10G;
proxy_read_timeout 3600s;
}
# Редиректы для CalDAV и CardDAV
location /.well-known/carddav {
return 301 /remote.php/dav/;
}
location /.well-known/caldav {
return 301 /remote.php/dav/;
}
</code></pre>
<h3>Обязательные изменения в config/config.php Nextcloud</h3>
"Без
<br />
Открой config/config.php и добавь следующие параметры. Подсеть 172.20.0.0/16 — стандартная для Docker. Если у тебя другая — посмотри через docker <a class="wpil_keyword_link" title="Сети" href="https://it-apteka.com/category/networks/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2283">network</a> inspect proxy-network.<br />
<pre><code class="language-text">
'overwriteprotocol' => 'https',
'overwritehost' => 'cloud.yourdomain.com',
'overwritewebroot' => '/',
// Доверенные прокси - подсеть Docker или конкретный IP NPM
'trusted_proxies' => ['172.20.0.0/16'],
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
'trusted_domains' => [
'cloud.yourdomain.com',
],
</code></pre>
<p>Проверка: открой <code>https://cloud.yourdomain.com/settings/admin/overview</code>. Раздел <a title="Безопасность MikroTik CHR: базовая настройка SSH, логов и защиты от брутфорса" href="https://it-apteka.com/bezopasnost-mikrotik-chr-bazovaja-nastrojka-ssh-logov-i-zashhity-ot-brutforsa/" target="_blank" rel="noopener" data-wpil-monitor-id="2298">«Безопасность и настройка»</a> не должен содержать предупреждений о заголовках, протоколе или доверенных прокси.</p>
<h2>OpenMediaVault: настройка reverse proxy (порт 80 или 8080)</h2>
<p>OMV по умолчанию занимает порт 80. Если на том же хосте уже что-то висит — меняй порт через omv-env.</p>
<h3>Если нужно освободить порт 80 на хосте OMV</h3>
<pre><code class="language-bash">
sudo omv-env set OMV_WEBGUI_HTTP_PORT 8080
sudo omv-salt deploy run webgui
</code></pre>
<p>Вот тут важно: OMV нужен именно <code>$http_host</code> в заголовке Host, а не <code>$host</code>. Разница небольшая, но OMV без этого ломает сессии.</p>
<pre><code class="language-text">
# OMV требует $http_host, не $host - с $host ломаются сессии
proxy_set_header Host $http_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 /ws {
proxy_pass http://omv-server:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
</code></pre>
<h2>Vaultwarden: настройка reverse proxy (Bitwarden self-hosted)</h2>
<p>Vaultwarden требует HTTPS — без валидного сертификата клиентские приложения Bitwarden просто не подключатся. Не «ругаются», а именно не подключаются. Поэтому Force SSL и HSTS обязательны.</p>
<p>У Vaultwarden два порта для WebSocket: основной HTTP на 80 и отдельный WebSocket на 3012. Нужно проксировать оба.</p>
<table>
<thead>
<tr>
<th>Параметр</th>
<th>Значение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Scheme</td>
<td>http</td>
</tr>
<tr>
<td>Forward Port</td>
<td>80</td>
</tr>
<tr>
<td>SSL</td>
<td>Let’s Encrypt, Force SSL, HSTS</td>
</tr>
</tbody>
</table>
<pre><code class="language-text">
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 для синхронизации между устройствами - отдельный порт 3012
location /notifications/hub {
proxy_pass http://vaultwarden:3012;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location /notifications/hub/negotiate {
proxy_pass http://vaultwarden:80;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "same-origin" always;
</code></pre>
<p>В docker-compose.yml Vaultwarden добавь переменные среды:</p>
<pre><code class="language-text">
environment:
- WEBSOCKET_ENABLED=true
- DOMAIN=https://vault.yourdomain.com
</code></pre>
<h2>Grafana: настройка reverse proxy (порт 3000)</h2>
<p>Grafana проксируется просто, но без правильного <code>root_url</code> в конфиге ломаются ссылки в дашбордах и уведомлениях алертов. Ссылки будут вести на <code>localhost:3000</code> вместо твоего домена.</p>
<pre><code class="language-text">
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 для live-обновления графиков
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
</code></pre>
<p>В <code>grafana.ini</code> или через переменные среды в docker-compose.yml:</p>
<pre><code class="language-text">
# grafana.ini
[server]
domain = grafana.yourdomain.com
root_url = https://grafana.yourdomain.com
serve_from_sub_path = false
</code></pre>
<pre><code class="language-text">
# docker-compose.yml - через переменные среды
environment:
- GF_SERVER_DOMAIN=grafana.yourdomain.com
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
</code></pre>
<h2>Docker Compose для всей инфраструктуры</h2>
<p>Если поднимаешь всё сразу — вот шаблон. Все контейнеры в одной сети, NPM обращается к ним по именам.</p>
<pre><code class="language-text">
version: '3.8'
networks:
proxy-network:
name: proxy-network
driver: bridge
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
networks:
- proxy-network
nextcloud:
image: nextcloud:latest
container_name: nextcloud-app
restart: unless-stopped
volumes:
- ./nextcloud/html:/var/www/html
- ./nextcloud/data:/var/www/html/data
networks:
- proxy-network
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- WEBSOCKET_ENABLED=true
- DOMAIN=https://vault.yourdomain.com
volumes:
- ./vaultwarden/data:/data
networks:
- proxy-network
pihole:
image: pihole/pihole:latest
container_name: pihole
restart: unless-stopped
environment:
- TZ=Europe/Moscow
- WEBPASSWORD=твой_пароль
volumes:
- ./pihole/etc-pihole:/etc/pihole
- ./pihole/etc-dnsmasq.d:/etc/dnsmasq.d
networks:
- proxy-network
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SERVER_DOMAIN=grafana.yourdomain.com
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
volumes:
- ./grafana/data:/var/lib/grafana
networks:
- proxy-network
</code></pre>
<h2>Продвинутая настройка: SSO с Authelia</h2>
<p>Если хочешь единый вход для всех сервисов с двухфакторной аутентификацией — смотри в сторону <a href="https://www.authelia.com" target="_blank" rel="noopener">Authelia</a>. Интегрируется с NPM через <code>auth_request</code>. Подробно разберём в отдельной статье, но вот скелет конфига для Proxy Host:</p>
<pre><code class="language-text">
location / {
auth_request /authelia;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
# Редирект на страницу входа при 401
error_page 401 =302 https://auth.yourdomain.com/?rd=$scheme://$host$request_uri;
proxy_pass http://твой-сервис:порт;
}
location = /authelia {
internal;
proxy_pass http://authelia:9091/api/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
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;
}
</code></pre>
<h2>Ограничение доступа по IP</h2>
<p>Для административных интерфейсов — обязательно. Zimbra Admin, Proxmox — не должны быть открыты для всего интернета.</p>
<pre><code class="language-text">
# Разрешаем только из локальной сети и VPN
allow 192.168.1.0/24;
allow 10.8.0.0/24;
deny all;
</code></pre>
<h2>Проверка работы</h2>
<p>После <a title="Docker Compose — установка, команды и настройка контейнеров" href="https://it-apteka.com/docker-compose-ustanovka-komandy-i-nastrojka-kontejnerov/" target="_blank" rel="noopener" data-wpil-monitor-id="2293">настройки каждого сервиса — проверяй командами</a>, а не только «открыл в браузере и посмотрел.</p>
<pre><code class="language-bash">
# Проверить синтаксис конфигурации Nginx внутри NPM
docker exec nginx-proxy-manager nginx -t
# Логи NPM - последние 100 строк
docker logs --tail 100 nginx-proxy-manager
# Проверить SSL сертификат
openssl s_client -connect cloud.yourdomain.com:443 -servername cloud.yourdomain.com 2>/dev/null | openssl x509 -noout -dates
# Проверить заголовки ответа
curl -I https://cloud.yourdomain.com
# Access-лог конкретного proxy host (1 = номер хоста из URL в интерфейсе NPM)
docker exec nginx-proxy-manager tail -f /data/logs/proxy-host-1_access.log
# Проверить WebSocket
curl -i \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://pve.yourdomain.com
# Проверить реальный IP в заголовках (должен видеть твой IP, не IP NPM)
curl -s https://cloud.yourdomain.com/ocs/v2.php/cloud/user | grep -i forwarded
</code></pre>
<h2>Осложнения: типичные ошибки и их решения</h2>
<h3>Ошибка: Invalid CSRF token в MailCow или Zimbra</h3>
"Причина:
<br />
Приложение генерирует CSRF-токен привязанный к хосту. Если NPM не передаёт X-Forwarded-Host и X-Forwarded-Port — приложение думает что запрос пришёл с другого хоста и отклоняет его.<br />
<pre><code class="language-text">
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;
</code></pre>
<h3>Ошибка: консоль VM в Proxmox не открывается</h3>
<p>Либо не включён WebSocket Support в настройках Proxy Host (это галочка на вкладке Details, не в Advanced), либо маленький таймаут.</p>
<pre><code class="language-text">
proxy_read_timeout 3600s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
</code></pre>
<h3>Ошибка 413: Request Entity Too Large</h3>
<p>Nginx блокирует загрузку файлов больше 1 МБ по умолчанию. Добавляй в Advanced конфиг нужного хоста:</p>
<pre><code class="language-text">
# Для Nextcloud и Zimbra - конкретный лимит
client_max_body_size 10G;
# Для PBS - без ограничений
client_max_body_size 0;
</code></pre>
<h3>Ошибка: Nextcloud пишет «Untrusted domain»</h3>
<p>Домен не добавлен в <code>trusted_domains</code> или NPM не в <code>trusted_proxies</code> в config.php.</p>
<pre><code class="language-text">
'trusted_domains' => ['cloud.yourdomain.com'],
'trusted_proxies' => ['172.20.0.0/16'],
'overwriteprotocol' => 'https',
</code></pre>
<h3>Ошибка: Let’s Encrypt сертификат не получается</h3>
<p>Три частых причины:</p>
<ul>
<li>Порт 80 закрыт на файерволле или роутере — HTTP challenge не проходит</li>
<li>DNS запись ещё не распространилась — жди до 30 минут</li>
<li>Rate limit Let’s Encrypt — не больше 5 сертификатов на домен в неделю</li>
</ul>
<pre><code class="language-bash">
# Смотреть ошибки получения сертификата
docker logs nginx-proxy-manager | grep -i "cert\|renew\|error\|acme"
# Проверить что порт 80 доступен снаружи
curl -v http://pve.yourdomain.com
# Проверить DNS запись
nslookup pve.yourdomain.com
</code></pre>
<h3>Ошибка: реальные IP не отображаются в логах бэкенда</h3>
<pre><code class="language-text">
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
</code></pre>
<p>Для Nextcloud — дополнительно в config.php:</p>
<pre><code class="language-text">
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
</code></pre>
<h3>Ошибка: 502 Bad Gateway</h3>
<p>NPM не может достучаться до бэкенда. Первым делом — проверить что сервис вообще запущен и слушает на нужном порту:</p>
<pre><code class="language-bash">
# Проверить с хоста NPM
docker exec nginx-proxy-manager curl -k https://192.168.1.10:8006
# Если сервис в Docker - проверить что в одной сети
docker network inspect proxy-network | grep -A 4 "nginx-proxy-manager\|nextcloud"
</code></pre>
<h2>Безопасность</h2>
<p>NPM — единая точка входа в твою инфраструктуру. Если NPM падает или взламывают NPM — всё падает или взламывается всё. Минимум который нужно сделать:</p>
<ul>
<li>Смени пароль по умолчанию — логин <code>admin@example.com / changeme</code> сканируется ботами</li>
<li>Закрой порт 81 от внешнего мира через firewall или Access List в NPM</li>
<li>Для административных сервисов — ограничь доступ по IP через Access Lists</li>
<li>Включай HSTS для сервисов с чувствительными данными — особенно Vaultwarden</li>
<li>Регулярно обновляй NPM</li>
</ul>
<pre><code class="language-bash">
# Обновление NPM без потери конфигурации
cd /opt/nginx-proxy-manager
docker compose pull
docker compose up -d
# Проверить что обновилось
docker inspect nginx-proxy-manager | grep -i "image\|version"
</code></pre>
<h2>Резервное копирование NPM</h2>
"Что
<br />
Вся конфигурация NPM хранится в примонтированных директориях. Сертификаты, настройки прокси-хостов, access lists — всё там. Бэкапируй раз в день, храни хотя бы 7 копий.<br />
<pre><code class="language-bash">
# Бэкап конфигурации и сертификатов
cd /opt/nginx-proxy-manager
tar -czf /backup/npm-backup-$(date +%Y%m%d-%H%M%S).tar.gz ./data ./letsencrypt
# Восстановление - остановить NPM, распаковать, запустить
docker compose down
tar -xzf /backup/npm-backup-20260101-120000.tar.gz
docker compose up -d
</code></pre>
<p>Добавь в <a title="Бэкап Docker Compose: автоматизация через Bash и Crontab" href="https://it-apteka.com/311/" target="_blank" rel="noopener" data-wpil-monitor-id="2294">crontab для ежедневного бэкапа</a>:</p>
<pre><code class="language-bash">
# Ежедневный бэкап в 2:00
0 2 * * * cd /opt/nginx-proxy-manager && tar -czf /backup/npm-backup-$(date +\%Y\%m\%d).tar.gz ./data ./letsencrypt
</code></pre>
<h2>Сравнительная таблица всех сервисов</h2>
<table>
<thead>
<tr>
<th>Сервис</th>
<th>Порт</th>
<th>Scheme</th>
<th>WebSocket</th>
<th>Особенности</th>
</tr>
</thead>
<tbody>
<tr>
<td>Proxmox VE</td>
<td>8006</td>
<td>https</td>
<td>Да</td>
<td>proxy_ssl_verify off, client_max_body_size 50G, timeout 3600s</td>
</tr>
<tr>
<td><a title="Proxmox Backup Server: установка, настройка и восстановление VM — полный гайд" href="https://it-apteka.com/proxmox-backup-server-stavim-nastraivaem-ne-terjaem-vm/" target="_blank" rel="noopener" data-wpil-monitor-id="2295">Proxmox Backup Server</a></td>
<td>8007</td>
<td>https</td>
<td>Да</td>
<td>proxy_ssl_verify off, client_max_body_size 0, timeout 86400s</td>
</tr>
<tr>
<td>Zimbra (webmail)</td>
<td>8443</td>
<td>https</td>
<td>Нет</td>
<td>proxy_ssl_verify off, client_max_body_size 1G</td>
</tr>
<tr>
<td>Zimbra (admin)</td>
<td>7071</td>
<td>https</td>
<td>Нет</td>
<td>Ограничить доступ по IP</td>
</tr>
<tr>
<td>MailCow</td>
<td>80 (контейнер)</td>
<td>http</td>
<td>Да</td>
<td>X-Forwarded-Host, общая Docker-сеть, override compose</td>
</tr>
<tr>
<td>Pi-hole</td>
<td>80</td>
<td>http</td>
<td>Да (/ws)</td>
<td>Отдельный location для /ws</td>
</tr>
<tr>
<td>Nextcloud</td>
<td>80 (контейнер)</td>
<td>http</td>
<td>Да (/push/)</td>
<td>trusted_proxies в config.php, client_max_body_size 10G</td>
</tr>
<tr>
<td>OpenMediaVault</td>
<td>80 или 8080</td>
<td>http</td>
<td>Да (/ws)</td>
<td>Host $http_host вместо $host</td>
</tr>
<tr>
<td>Vaultwarden</td>
<td>80 + 3012 (WS)</td>
<td>http</td>
<td>Да (порт 3012)</td>
<td>HTTPS обязателен, DOMAIN в env, HSTS</td>
</tr>
<tr>
<td>Grafana</td>
<td>3000</td>
<td>http</td>
<td>Да</td>
<td>root_url в grafana.ini или через env</td>
</tr>
</tbody>
</table>
<h2>Rate Limiting: защита от перебора паролей</h2>
<p>NPM не делает rate limiting из коробки, но Nginx умеет. Добавляется в Custom <a title="ACME + Nginx: настройка Let’s Encrypt вручную и через Nginx Proxy Manager" href="https://it-apteka.com/acme-nginx-nastrojka-let-s-encrypt-vruchnuju-i-cherez-nginx-proxy-manager/" target="_blank" rel="noopener" data-wpil-monitor-id="2296">Nginx Configuration нужного Proxy</a> Host.</p>
<p>Для Vaultwarden и любых форм авторизации:</p>
<pre><code class="language-text">
limit_req_zone $binary_remote_addr zone=vault_auth:10m rate=5r/m;
location /api/accounts/login {
limit_req zone=vault_auth burst=3 nodelay;
limit_req_status 429;
proxy_pass http://vaultwarden:80;
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;
}
</code></pre>
<p>Для Nextcloud ограничиваем login endpoint:</p>
<pre><code class="language-text">
limit_req_zone $binary_remote_addr zone=nc_login:10m rate=10r/m;
location /login {
limit_req zone=nc_login burst=5 nodelay;
proxy_pass http://nextcloud-app:80;
}
</code></pre>
<h2>Security Headers: укрепляем HTTP-ответы</h2>
<p>Заголовки безопасности — не только для Nextcloud. Хорошая практика добавлять их для всех публично доступных сервисов.</p>
<pre><code class="language-text">
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# HSTS - только HTTPS. Добавляй только если уверен что SSL работает стабильно.
# После включения браузер откажется открывать сайт по HTTP на год вперёд.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
</code></pre>
<p>HSTS добавляй последним — после того как убедился что Let’s Encrypt обновляет сертификат нормально.</p>
<h2>Профилактика: как не сломать снова</h2>
<p>Три вещи, которые ломают NPM после того как всё настроено:</p>
<ul>
<li>Обновление MailCow без пересоздания override-файла для Docker-сети — контейнер nginx-mailcow отваливается от proxy-network</li>
<li>Смена IP бэкенд-сервиса без обновления Forward Hostname в NPM — получаешь 502</li>
<li>Истечение сертификата из-за закрытого порта 80 на файерволле — NPM не может пройти HTTP-challenge</li>
</ul>
<p>Простая профилактика:</p>
<ol>
<li>Раз в месяц проверяй сроки: SSL Certificates в интерфейсе NPM показывает дату истечения для каждого сертификата</li>
<li>Docker Compose файлы держи в <a class="wpil_keyword_link" title="Git" href="https://it-apteka.com/tag/git/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2284">Git</a> — при любом изменении видно что именно изменилось</li>
<li>После любых правок файерволла проверяй что порт 80 доступен снаружи</li>
<li>Бэкап перед любым обновлением NPM — это 30 секунд одной командой</li>
</ol>
<p>И ещё одно: если что-то перестало работать после плановых обновлений сервисов — первым делом смотри логи NPM, потом логи бэкенда. Порядок диагностики: NPM -> сеть -> бэкенд. Не наоборот.</p>
<h2>Обновление NPM</h2>
<p>Смотри на <a href="https://github.com/NginxProxyManager/nginx-proxy-manager/releases" target="_blank" rel="nofollow noopener">страницу релизов</a> перед обновлением — иногда бывают breaking changes. Процедура простая:</p>
<pre><code class="language-bash">
# 1. Сделай бэкап перед обновлением
tar -czf /backup/npm-before-update-$(date +%Y%m%d).tar.gz ./data ./letsencrypt
# 2. Обновляй
docker compose pull && docker compose up -d
# 3. Проверь что всё работает
docker compose ps
docker logs nginx-proxy-manager --tail 50
</code></pre>
<h2>Получение wildcard-сертификата через DNS-challenge</h2>
<p>HTTP-challenge подходит для публичных серверов. Но что если сервер не смотрит в интернет, или хочешь один сертификат на все поддомены? Тогда DNS-challenge.</p>
<p>Wildcard <code>*.domain.com</code> покрывает все поддомены одним сертификатом. Настройка раз — работает для pve, cloud, vault, grafana и всего остального.</p>
<p>В NPM: SSL Certificates -> Add SSL Certificate -> Let’s Encrypt. Включи «Use a DNS Challenge». Выбери своего провайдера.</p>
<p>Для Cloudflare нужен API-токен:</p>
<ol>
<li>Зайди в Cloudflare Dashboard -> My Profile -> API Tokens</li>
<li>Create Token -> Edit zone DNS</li>
<li>Zone Resources: Include -> Specific zone -> твой домен</li>
<li>Скопируй токен</li>
</ol>
<p>В NPM вставляешь токен в поле Cloudflare API Token. Домен указываешь как <code>*.domain.com</code>. Нажимаешь Save — сертификат получится через 30-60 секунд.</p>
"DNS-challenge
<br />
DNS-challenge не требует доступа к серверу снаружи. Let's Encrypt проверяет запись TXT в DNS, которую NPM создаёт автоматически через API твоего DNS-провайдера. Работает за NAT, на серверах без публичного IP, во внутренних сетях.<br />
<p>Поддерживаемые провайдеры в NPM: Cloudflare, Namecheap, Route53, DigitalOcean, Linode, Godaddy и другие. Полный список — в документации <a href="https://letsencrypt.org/docs/client-options/" target="_blank" rel="noopener">Let’s Encrypt</a>.</p>
<h2>Access Lists: ограничение доступа</h2>
<p>Access Lists в NPM — мощный инструмент. Можно ограничить доступ по IP, добавить Basic Auth или комбинировать оба способа.</p>
<p>Создаёшь Access List: Access Lists -> Add Access List.</p>
<p>Пример для Proxmox: только из локальной <a href="https://it-apteka.com/kakoj-vpn-server-ustanovit-na-ubuntu-dlja-nativnogo-podkljuchenija/" title="Настройка IKEv2 VPN сервера на Ubuntu 24.04: StrongSwan без лишних клиентов" target="_blank" rel="noopener" data-wpil-monitor-id="2747">сети и VPN:</a></p>
<table>
<thead>
<tr>
<th>Тип</th>
<th>Значение</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
<tr>
<td>IP</td>
<td>192.168.1.0/24</td>
<td>allow</td>
</tr>
<tr>
<td>IP</td>
<td>10.8.0.0/24</td>
<td>allow</td>
</tr>
<tr>
<td>IP</td>
<td>all</td>
<td>deny</td>
</tr>
</tbody>
</table>
<p>Потом привязываешь Access List к нужному Proxy Host на вкладке Details. Снаружи вместо сервиса увидят 403 Forbidden.</p>
<p>Если нужен Basic Auth поверх IP-фильтрации — добавляй учётные данные на вкладке Authorization в Access List. Два фактора защиты одновременно.</p>
<h2>Настройка локального DNS для self-hosted стека</h2>
<p>Всё что сделано выше работает для публичных доменов. Но можно и без интернета — только для внутренней сети.</p>
<p>Схема: используешь Pi-hole или AdGuard Home как локальный DNS. Добавляешь записи вида <code>pve.home.lan -> 192.168.1.5</code>. NPM принимает запросы на этом IP и проксирует на бэкенды. Сертификаты получаешь через DNS-challenge.</p>
<pre class="mermaid">%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Браузер
pve.home.lan"] --> B["Pi-hole / AdGuard
локальный DNS"]
B --> C["Отвечает IP сервера с NPM
192.168.1.5"]
C --> D["NPM
443/80"]
D --> E["Proxmox VE
192.168.1.10:8006"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#92400e
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
</pre>
<p>В Pi-hole добавляешь Local DNS Records: <code>pve.home.lan -> 192.168.1.5</code>. В AdGuard Home: Settings -> DNS rewrites.</p>
<p>Устройства в сети должны использовать Pi-hole/AdGuard как DNS-сервер. Обычно настраивается в DHCP на роутере.</p>
<h2>Мониторинг и алерты</h2>
<p>NPM не имеет встроенного мониторинга. Но логи есть — и это уже что-то.</p>
<pre><code class="language-bash">
# Следить за access-логами в реальном времени
docker exec nginx-proxy-manager tail -f /data/logs/proxy-host-1_access.log
# Смотреть ошибки 4xx и 5xx
docker exec nginx-proxy-manager grep -E " (4|5)[0-9][0-9] " /data/logs/proxy-host-1_access.log | tail -50
# Логи получения сертификатов
docker exec nginx-proxy-manager cat /data/logs/letsencrypt.log
# Статус сертификатов - когда истекают
docker exec nginx-proxy-manager find /etc/letsencrypt/live -name "fullchain.pem" -exec openssl x509 -in {} -noout -subject -dates \;
</code></pre>
<p>Если используешь Uptime Kuma или Grafana + Prometheus — подключай туда. Uptime Kuma умеет мониторить HTTP-эндпоинты и отправлять уведомления в <a class="wpil_keyword_link" title="Telegram" href="https://t.me/it_apteka_com/34" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2290">Telegram</a>.</p>
<p>Минимальный <a class="wpil_keyword_link" title="Мониторинг" href="https://it-apteka.com/category/monitoring/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2289">мониторинг</a>: проверка что сертификаты не истекают и что каждый проксируемый сервис отвечает 200. Это можно сделать простым <a class="wpil_keyword_link" title="Bash" href="https://it-apteka.com/tag/bash/" target="_blank" rel="noopener" data-wpil-keyword-link="linked" data-wpil-monitor-id="2291">bash</a>-скриптом в cron:</p>
<pre><code class="language-bash">
#!/bin/bash
# Проверка сроков SSL-сертификатов
DOMAINS="pve.yourdomain.com cloud.yourdomain.com vault.yourdomain.com"
for DOMAIN in $DOMAINS; do
EXPIRY=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 14 ]; then
echo "WARN: $DOMAIN - сертификат истекает через $DAYS_LEFT дней"
else
echo "OK: $DOMAIN - $DAYS_LEFT дней до истечения"
fi
done
</code></pre>
<h2>Итоговый чеклист после настройки</h2>
<ul>
<li>Все сервисы открываются по HTTPS по своим доменам</li>
<li>SSL-сертификаты действительны, Let’s Encrypt не показывает ошибок</li>
<li>Консоли VM в Proxmox открываются без ошибок</li>
<li>Загрузка файлов работает в Nextcloud и Zimbra</li>
<li>Нет ошибок CSRF в MailCow и Zimbra</li>
<li>Реальные IP пользователей отображаются в логах</li>
<li>Nextcloud не показывает предупреждений в /settings/admin/overview</li>
<li>Vaultwarden синхронизирует данные на мобильном клиенте</li>
<li>Порт 81 (интерфейс NPM) закрыт от внешнего мира</li>
<li>Административные интерфейсы защищены Access Lists</li>
<li>Пароль администратора NPM сменён</li>
<li>Бэкап ./data и ./letsencrypt настроен и проверен</li>
</ul>
<h2>NPM или Traefik: что выбрать</h2>
<p>Вопрос регулярный — вот честный ответ.</p>
<p>NPM лучше если: у тебя домашняя лаборатория или небольшой self-hosted стек, нужен UI без погружения в YAML-конфиги, не пишешь код и не управляешь Kubernetes.</p>
<p>Traefik лучше если: управляешь кластером контейнеров, нужна <a title="Автоматическое обновление Docker контейнеров: полное руководство и примеры" href="https://it-apteka.com/avtomaticheskoe-obnovlenie-docker-kontejnerov-polnoe-rukovodstvo-i-primery/" target="_blank" rel="noopener" data-wpil-monitor-id="2297">автоматическая конфигурация через Docker</a> Labels, важна интеграция с CI/CD.</p>
<p>Разница не в качестве, а в подходе. NPM — кликаешь мышкой. Traefik — пишешь конфиги. Оба делают одно и то же.</p>
<h2>FAQ</h2>
<h3>Нужен ли белый IP для SSL-сертификата Let’s Encrypt?</h3>
<p>Для HTTP-challenge — да, порт 80 должен быть доступен из интернета. Для DNS-challenge — нет. DNS-challenge поддерживает Cloudflare, Namecheap, Route53 и другие. Позволяет получать wildcard-сертификаты (<code>*.domain.com</code>) и работать на полностью изолированных серверах без публичного IP.</p>
<h3>Как настроить NPM для сервисов без публичного доступа?</h3>
<p>Используй локальный DNS через Pi-hole или AdGuard Home — пусть твои домены резолвятся только внутри сети. Сертификаты получай через DNS-challenge. Снаружи ничего не открываешь.</p>
<h3>Почему Nextcloud продолжает генерировать http-ссылки после настройки NPM?</h3>
<p>Не добавлен параметр <code>overwriteprotocol => 'https'</code> в config.php. Или NPM не в списке <code>trusted_proxies</code> — Nextcloud игнорирует заголовки X-Forwarded-Proto от недоверенных прокси.</p>
<h3>Что делать если сертификат Let’s Encrypt не обновляется?</h3>
<pre><code class="language-bash">
# Смотри ошибки
docker logs nginx-proxy-manager | grep -i "renew\|error\|acme"
# Порт 80 должен быть открыт
curl -v http://твой-домен.com
# NPM пытается обновить за 30 дней до истечения
# Можно принудить через UI: SSL Certificates -> три точки -> Renew
</code></pre>
<h3>Как ограничить доступ к интерфейсу NPM (порт 81)?</h3>
<p>Firewall на хосте — самый надёжный способ:</p>
<pre><code class="language-bash">
# UFW - разрешить только из локальной сети
ufw allow from 192.168.1.0/24 to any port 81
ufw deny 81
</code></pre>
<p>Или через Access List в самом NPM: Settings -> Default Site -> Require Auth.</p>
<h3>Как проверить что WebSocket работает?</h3>
<pre><code class="language-bash">
# Должен вернуть 101 Switching Protocols
curl -i \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://твой-домен.com
# Если вернул 200 или 400 - WebSocket не работает
</code></pre>
<h2>Прогноз</h2>
<p>После настройки по этой статье у тебя: 10 сервисов за одним HTTPS-прокси, SSL получается и обновляется автоматически, все нужные заголовки передаются, консоли Proxmox открываются, Nextcloud не ругается на trusted_domains, Vaultwarden синхронизирует пароли между устройствами.</p>
<p>Следующий шаг — Authelia поверх NPM: единый вход с двухфактором для всего self-hosted стека. Один логин для Proxmox, Nextcloud, Grafana и всего остального. Разберём в отдельной статье.</p>
"Не
<br />
Описывай точно: какой сервис, какая ошибка, что уже пробовал. Разберёмся.<br />
Коротко: что получишь
Nginx Proxy Manager (NPM) — это веб-интерфейс поверх Nginx для управления reverse proxy. Настраиваешь через браузер: указываешь домен, IP и порт бэкенда, SSL получается автоматически через Let’s Encrypt. В статье — готовые конфиги для 10 популярных self-hosted сервисов с объяснением, почему каждая строка там нужна.
Диагноз: почему это вообще нужно
Proxmox висит на 192.168.1.10:8006. Nextcloud — на 192.168.1.15:8080. Pi-hole — на 192.168.1.20:80. Vaultwarden — на 192.168.1.25:8080. Grafana — на 192.168.1.30:3000.
Полгода так жить можно. Через год начинаешь вести блокнотик с портами. Ещё через год блокнотик теряется.
NPM решает это один раз. После настройки у тебя: https://pve.domain.com, https://cloud.domain.com, https://vault.domain.com — всё с валидным SSL, все порты спрятаны внутрь, единая точка входа.
Что сделаем в этой статье:
- Поднимем NPM через Docker Compose
- Разберём, что значит каждый параметр в настройках
- Настроим 10 сервисов: Proxmox VE, Proxmox Backup Server, Zimbra, MailCow, Pi-hole, Nextcloud, OpenMediaVault, Vaultwarden, Grafana
- Разберём типичные ошибки и как их чинить
Потребуется: сервер с Docker, домен с DNS-записями на твой IP, 30-60 минут.
Архитектура: как это работает
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Браузер / клиент
https://pve.domain.com"] --> B["NPM
порты 80 / 443"]
B --> C["Proxmox VE
192.168.1.10:8006 https"]
B --> D["Nextcloud
nextcloud-app:80 http"]
B --> E["Vaultwarden
vaultwarden:80 http"]
B --> F["Grafana
grafana:3000 http"]
G["Let's Encrypt"] --> B
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#92400e
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style F fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style G fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
NPM принимает входящий HTTPS-трафик, терминирует SSL и пересылает запрос на бэкенд. Бэкенд может работать по HTTP внутри Docker-сети — снаружи всё равно будет HTTPS.
Базовые понятия: что нужно понять до настройки
Scheme: http или https к бэкенду
Это не про то, как клиент подключается к NPM. Это про то, как NPM подключается к твоему сервису сзади. Proxmox всегда слушает на HTTPS с самоподписанным сертом — ставишь https и добавляешь proxy_ssl_verify off. Контейнеры в одной Docker-сети — обычно http, и это нормально.
WebSocket Support
Включай для всего, что показывает данные в реальном времени: консоли VM в Proxmox, live-графики в Grafana, уведомления в Nextcloud Talk, статистика Pi-hole. Без этой галочки эти функции просто не работают — без ошибок, просто молча.
Заголовки X-Forwarded-*
Когда NPM проксирует запрос, бэкенд видит IP адрес NPM, а не реального пользователя. Заголовки X-Forwarded-For, X-Real-IP и X-Forwarded-Proto передают реальный IP и протокол. Без них: некорректные логи, сломанный CSRF в MailCow и Zimbra, неправильные редиректы в Nextcloud.
Docker-сеть как транспорт
Если NPM и сервис в одной Docker-сети, обращайся к контейнеру по имени, не по IP. IP контейнера меняется при каждом перезапуске. Имя — нет.
Установка Nginx Proxy Manager
Рекомендуемый способ — Docker Compose. Никакой магии, просто удобнее обновлять и бэкапить.
Создай директорию и файл конфига:
mkdir -p /opt/nginx-proxy-manager && cd /opt/nginx-proxy-manager
Создай docker-compose.yml:
version: '3.8'
services:
app:
image: 'jc21/nginx-proxy-manager:latest'
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- '80:80'
- '81:81'
- '443:443'
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
- proxy-network
networks:
proxy-network:
driver: bridge
name: proxy-network
Запусти:
docker compose up -d
Открой http://твой-сервер:81. Логин по умолчанию: admin@example.com / changeme.
Первое, что делаешь после входа
Сразу меняй email и пароль администратора. Логин admin@example.com / changeme — публично известен и прекрасно гуглится ботами.
Если сервер смотрит в интернет — порт 81 закрой через firewall или Access List в самом NPM. Интерфейс управления не должен быть доступен снаружи.
Системные требования
| Компонент |
Минимум |
Рекомендуется |
| Docker |
20.x+ |
последняя стабильная |
| Docker Compose |
v2.x (плагин) |
v2.20+ |
| RAM |
256 МБ |
512 МБ+ |
| Порты |
80, 443, 81 |
свободны на хосте |
| ОС |
любой Linux с Docker |
Ubuntu 22.04 / Debian 12 |
На момент публикации актуальна версия NPM 2.12.x. Перед установкой проверь свежие релизы на GitHub.
Таблица портов
| Порт |
Протокол |
Назначение |
Доступен снаружи? |
| 80 |
TCP |
HTTP трафик, HTTP-challenge Let’s Encrypt |
Да |
| 443 |
TCP |
HTTPS трафик |
Да |
| 81 |
TCP |
Веб-интерфейс управления NPM |
Нет (только LAN / VPN) |
Proxmox VE: настройка reverse proxy (порт 8006)
Proxmox — один из самых капризных в плане проксирования. Самоподписанный сертификат, WebSocket для консолей VM и лимит на загрузку ISO. Всё это нужно учесть.
В NPM создаёшь новый Proxy Host:
| Параметр |
Значение |
| Domain Names |
pve.yourdomain.com |
| Scheme |
https |
| Forward Hostname/IP |
IP твоего Proxmox сервера |
| Forward Port |
8006 |
| WebSocket Support |
включить |
| Block Common Exploits |
включить |
| SSL — Let’s Encrypt |
включить, Force SSL |
Вкладка Advanced — вставляй конфиг:
# Proxmox использует самоподписанный сертификат - проверку отключаем
proxy_ssl_verify off;
# Стандартные заголовки прокси
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_set_header X-Forwarded-Port 443;
# WebSocket - без этого консоли VM и LXC не откроются
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 60 минут - консоль держит соединение, дефолтные 60 секунд не хватит
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# Отключаем буферизацию - консоль должна работать в реальном времени
proxy_buffering off;
# Для загрузки ISO и дисков через интерфейс
client_max_body_size 50G;
Проверка: открой https://pve.yourdomain.com, зайди, запусти консоль любой VM. Если консоль не открывается — первым делом смотри, включён ли WebSocket Support в настройках Proxy Host, не в Advanced конфиге.
Proxmox Backup Server: настройка reverse proxy (порт 8007)
Всё то же самое, что PVE, плюс бесконечный таймаут для длительных задач резервного копирования.
| Параметр |
Значение |
| Domain Names |
pbs.yourdomain.com |
| Scheme |
https |
| Forward Port |
8007 |
| WebSocket Support |
включить |
proxy_ssl_verify off;
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_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Без ограничений - бэкапы бывают очень большими
client_max_body_size 0;
# 24 часа - длительные задачи резервного копирования
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# Отключаем перекодирование для бинарных данных
proxy_set_header Accept-Encoding "";
Zimbra Collaboration: настройка reverse proxy (порты 8443 и 7071)
Zimbra — два отдельных веб-интерфейса. Веб-клиент на порту 8443 и административная панель на 7071. Каждый получает свой Proxy Host.
Веб-клиент (mail.yourdomain.com -> порт 8443)
proxy_ssl_verify off;
# Zimbra чувствителен к этим заголовкам - без них ломается авторизация
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_set_header X-Forwarded-Port 443;
# Для загрузки вложений
client_max_body_size 1024M;
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
# Zimbra сам управляет сжатием
proxy_set_header Accept-Encoding "";
Административная панель (zimbra-admin.yourdomain.com -> порт 7071)
Конфиг аналогичен веб-клиенту. Но вот тут важно: ограничь доступ через Access Lists только для администраторских IP. Не выставляй admin-панель в открытый интернет.
MailCow Dockerized: настройка reverse proxy
Тут чуть сложнее. MailCow по умолчанию слушает на 0.0.0.0:80 и 0.0.0.0:443 — напрямую конфликтует с NPM. Нужно перенести MailCow на другие порты и объединить их в одну Docker-сеть.
Шаг 1: перенастраиваем порты MailCow
В файле mailcow.conf меняй:
HTTP_PORT=8081
HTTPS_PORT=8442
HTTP_BIND=127.0.0.1
HTTPS_BIND=127.0.0.1
Шаг 2: подключаем контейнер nginx-mailcow к общей сети
Создай файл docker-compose.override.yml в директории MailCow:
version: '3.8'
services:
nginx-mailcow:
networks:
- proxy-network
networks:
proxy-network:
external: true
Шаг 3: перезапускаем MailCow
docker compose down && docker compose up -d
Настройка Proxy Host в NPM
| Параметр |
Значение |
| Scheme |
http |
| Forward Hostname/IP |
nginx-mailcow (имя контейнера) |
| Forward Port |
80 |
| WebSocket Support |
включить |
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;
# Критически важно для CSRF-защиты MailCow - без этого получишь "Invalid CSRF token"
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
Pi-hole: настройка reverse proxy (порт 80)
Pi-hole прост в проксировании, но у него отдельный WebSocket endpoint для live-статистики дашборда. Начиная с версии 6 интерфейс переработан — проверяй пути в актуальной документации.
| Параметр |
Значение |
| Scheme |
http |
| Forward Port |
80 |
| WebSocket Support |
включить |
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 для WebSocket Pi-hole v5 (для v6 проверяй документацию)
location /ws {
proxy_pass http://pihole-ip:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# Security заголовки
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
Nextcloud: настройка reverse proxy (Docker)
Nextcloud — самый требовательный к конфигурации из всех. Он проверяет заголовки, требует явного указания trusted_proxies и ломает ссылки если не указать overwriteprotocol. Зато после правильной настройки работает без нареканий.
Настройка Proxy Host в NPM
| Параметр |
Значение |
| Scheme |
http |
| Forward Hostname/IP |
nextcloud-app (имя контейнера) |
| Forward Port |
80 |
| WebSocket Support |
включить |
| SSL |
Let’s Encrypt, Force SSL, HTTP/2 |
# Базовые заголовки
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_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
# Security заголовки - убирают предупреждения в /settings/admin/overview
add_header Referrer-Policy "no-referrer" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Download-Options "noopen" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Permitted-Cross-Domain-Policies "none" always;
add_header X-Robots-Tag "none" always;
add_header X-XSS-Protection "1; mode=block" always;
# Для загрузки больших файлов
client_max_body_size 10G;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# WebSocket для Nextcloud Talk
location /push/ {
proxy_pass http://nextcloud-app:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
# WebDAV для клиентов синхронизации
location /remote.php/dav/ {
proxy_pass http://nextcloud-app:80;
client_max_body_size 10G;
proxy_read_timeout 3600s;
}
# Редиректы для CalDAV и CardDAV
location /.well-known/carddav {
return 301 /remote.php/dav/;
}
location /.well-known/caldav {
return 301 /remote.php/dav/;
}
Обязательные изменения в config/config.php Nextcloud
Без этих изменений Nextcloud будет генерировать http-ссылки и ругаться на домен
Открой config/config.php и добавь следующие параметры. Подсеть 172.20.0.0/16 — стандартная для Docker. Если у тебя другая — посмотри через docker
network inspect proxy-network.
'overwriteprotocol' => 'https',
'overwritehost' => 'cloud.yourdomain.com',
'overwritewebroot' => '/',
// Доверенные прокси - подсеть Docker или конкретный IP NPM
'trusted_proxies' => ['172.20.0.0/16'],
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
'trusted_domains' => [
'cloud.yourdomain.com',
],
Проверка: открой https://cloud.yourdomain.com/settings/admin/overview. Раздел «Безопасность и настройка» не должен содержать предупреждений о заголовках, протоколе или доверенных прокси.
OpenMediaVault: настройка reverse proxy (порт 80 или 8080)
OMV по умолчанию занимает порт 80. Если на том же хосте уже что-то висит — меняй порт через omv-env.
Если нужно освободить порт 80 на хосте OMV
sudo omv-env set OMV_WEBGUI_HTTP_PORT 8080
sudo omv-salt deploy run webgui
Вот тут важно: OMV нужен именно $http_host в заголовке Host, а не $host. Разница небольшая, но OMV без этого ломает сессии.
# OMV требует $http_host, не $host - с $host ломаются сессии
proxy_set_header Host $http_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 /ws {
proxy_pass http://omv-server:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
Vaultwarden: настройка reverse proxy (Bitwarden self-hosted)
Vaultwarden требует HTTPS — без валидного сертификата клиентские приложения Bitwarden просто не подключатся. Не «ругаются», а именно не подключаются. Поэтому Force SSL и HSTS обязательны.
У Vaultwarden два порта для WebSocket: основной HTTP на 80 и отдельный WebSocket на 3012. Нужно проксировать оба.
| Параметр |
Значение |
| Scheme |
http |
| Forward Port |
80 |
| SSL |
Let’s Encrypt, Force SSL, HSTS |
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 для синхронизации между устройствами - отдельный порт 3012
location /notifications/hub {
proxy_pass http://vaultwarden:3012;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
location /notifications/hub/negotiate {
proxy_pass http://vaultwarden:80;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "same-origin" always;
В docker-compose.yml Vaultwarden добавь переменные среды:
environment:
- WEBSOCKET_ENABLED=true
- DOMAIN=https://vault.yourdomain.com
Grafana: настройка reverse proxy (порт 3000)
Grafana проксируется просто, но без правильного root_url в конфиге ломаются ссылки в дашбордах и уведомлениях алертов. Ссылки будут вести на localhost: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;
# WebSocket для live-обновления графиков
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
В grafana.ini или через переменные среды в docker-compose.yml:
# grafana.ini
[server]
domain = grafana.yourdomain.com
root_url = https://grafana.yourdomain.com
serve_from_sub_path = false
# docker-compose.yml - через переменные среды
environment:
- GF_SERVER_DOMAIN=grafana.yourdomain.com
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
Docker Compose для всей инфраструктуры
Если поднимаешь всё сразу — вот шаблон. Все контейнеры в одной сети, NPM обращается к ним по именам.
version: '3.8'
networks:
proxy-network:
name: proxy-network
driver: bridge
services:
npm:
image: jc21/nginx-proxy-manager:latest
container_name: nginx-proxy-manager
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./npm/data:/data
- ./npm/letsencrypt:/etc/letsencrypt
networks:
- proxy-network
nextcloud:
image: nextcloud:latest
container_name: nextcloud-app
restart: unless-stopped
volumes:
- ./nextcloud/html:/var/www/html
- ./nextcloud/data:/var/www/html/data
networks:
- proxy-network
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
- WEBSOCKET_ENABLED=true
- DOMAIN=https://vault.yourdomain.com
volumes:
- ./vaultwarden/data:/data
networks:
- proxy-network
pihole:
image: pihole/pihole:latest
container_name: pihole
restart: unless-stopped
environment:
- TZ=Europe/Moscow
- WEBPASSWORD=твой_пароль
volumes:
- ./pihole/etc-pihole:/etc/pihole
- ./pihole/etc-dnsmasq.d:/etc/dnsmasq.d
networks:
- proxy-network
grafana:
image: grafana/grafana:latest
container_name: grafana
restart: unless-stopped
environment:
- GF_SERVER_DOMAIN=grafana.yourdomain.com
- GF_SERVER_ROOT_URL=https://grafana.yourdomain.com
volumes:
- ./grafana/data:/var/lib/grafana
networks:
- proxy-network
Продвинутая настройка: SSO с Authelia
Если хочешь единый вход для всех сервисов с двухфакторной аутентификацией — смотри в сторону Authelia. Интегрируется с NPM через auth_request. Подробно разберём в отдельной статье, но вот скелет конфига для Proxy Host:
location / {
auth_request /authelia;
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
# Редирект на страницу входа при 401
error_page 401 =302 https://auth.yourdomain.com/?rd=$scheme://$host$request_uri;
proxy_pass http://твой-сервис:порт;
}
location = /authelia {
internal;
proxy_pass http://authelia:9091/api/verify;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
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;
}
Ограничение доступа по IP
Для административных интерфейсов — обязательно. Zimbra Admin, Proxmox — не должны быть открыты для всего интернета.
# Разрешаем только из локальной сети и VPN
allow 192.168.1.0/24;
allow 10.8.0.0/24;
deny all;
Проверка работы
После настройки каждого сервиса — проверяй командами, а не только «открыл в браузере и посмотрел.
# Проверить синтаксис конфигурации Nginx внутри NPM
docker exec nginx-proxy-manager nginx -t
# Логи NPM - последние 100 строк
docker logs --tail 100 nginx-proxy-manager
# Проверить SSL сертификат
openssl s_client -connect cloud.yourdomain.com:443 -servername cloud.yourdomain.com 2>/dev/null | openssl x509 -noout -dates
# Проверить заголовки ответа
curl -I https://cloud.yourdomain.com
# Access-лог конкретного proxy host (1 = номер хоста из URL в интерфейсе NPM)
docker exec nginx-proxy-manager tail -f /data/logs/proxy-host-1_access.log
# Проверить WebSocket
curl -i \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://pve.yourdomain.com
# Проверить реальный IP в заголовках (должен видеть твой IP, не IP NPM)
curl -s https://cloud.yourdomain.com/ocs/v2.php/cloud/user | grep -i forwarded
Осложнения: типичные ошибки и их решения
Ошибка: Invalid CSRF token в MailCow или Zimbra
Причина: приложение не видит реальный хост и протокол
Приложение генерирует CSRF-токен привязанный к хосту. Если NPM не передаёт X-Forwarded-Host и X-Forwarded-Port — приложение думает что запрос пришёл с другого хоста и отклоняет его.
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto https;
Ошибка: консоль VM в Proxmox не открывается
Либо не включён WebSocket Support в настройках Proxy Host (это галочка на вкладке Details, не в Advanced), либо маленький таймаут.
proxy_read_timeout 3600s;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
Ошибка 413: Request Entity Too Large
Nginx блокирует загрузку файлов больше 1 МБ по умолчанию. Добавляй в Advanced конфиг нужного хоста:
# Для Nextcloud и Zimbra - конкретный лимит
client_max_body_size 10G;
# Для PBS - без ограничений
client_max_body_size 0;
Ошибка: Nextcloud пишет «Untrusted domain»
Домен не добавлен в trusted_domains или NPM не в trusted_proxies в config.php.
'trusted_domains' => ['cloud.yourdomain.com'],
'trusted_proxies' => ['172.20.0.0/16'],
'overwriteprotocol' => 'https',
Ошибка: Let’s Encrypt сертификат не получается
Три частых причины:
- Порт 80 закрыт на файерволле или роутере — HTTP challenge не проходит
- DNS запись ещё не распространилась — жди до 30 минут
- Rate limit Let’s Encrypt — не больше 5 сертификатов на домен в неделю
# Смотреть ошибки получения сертификата
docker logs nginx-proxy-manager | grep -i "cert\|renew\|error\|acme"
# Проверить что порт 80 доступен снаружи
curl -v http://pve.yourdomain.com
# Проверить DNS запись
nslookup pve.yourdomain.com
Ошибка: реальные IP не отображаются в логах бэкенда
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Для Nextcloud — дополнительно в config.php:
'forwarded_for_headers' => ['HTTP_X_FORWARDED_FOR'],
Ошибка: 502 Bad Gateway
NPM не может достучаться до бэкенда. Первым делом — проверить что сервис вообще запущен и слушает на нужном порту:
# Проверить с хоста NPM
docker exec nginx-proxy-manager curl -k https://192.168.1.10:8006
# Если сервис в Docker - проверить что в одной сети
docker network inspect proxy-network | grep -A 4 "nginx-proxy-manager\|nextcloud"
Безопасность
NPM — единая точка входа в твою инфраструктуру. Если NPM падает или взламывают NPM — всё падает или взламывается всё. Минимум который нужно сделать:
- Смени пароль по умолчанию — логин
admin@example.com / changeme сканируется ботами
- Закрой порт 81 от внешнего мира через firewall или Access List в NPM
- Для административных сервисов — ограничь доступ по IP через Access Lists
- Включай HSTS для сервисов с чувствительными данными — особенно Vaultwarden
- Регулярно обновляй NPM
# Обновление NPM без потери конфигурации
cd /opt/nginx-proxy-manager
docker compose pull
docker compose up -d
# Проверить что обновилось
docker inspect nginx-proxy-manager | grep -i "image\|version"
Резервное копирование NPM
Что бэкапить: директории ./data и ./letsencrypt
Вся конфигурация NPM хранится в примонтированных директориях. Сертификаты, настройки прокси-хостов, access lists — всё там. Бэкапируй раз в день, храни хотя бы 7 копий.
# Бэкап конфигурации и сертификатов
cd /opt/nginx-proxy-manager
tar -czf /backup/npm-backup-$(date +%Y%m%d-%H%M%S).tar.gz ./data ./letsencrypt
# Восстановление - остановить NPM, распаковать, запустить
docker compose down
tar -xzf /backup/npm-backup-20260101-120000.tar.gz
docker compose up -d
Добавь в crontab для ежедневного бэкапа:
# Ежедневный бэкап в 2:00
0 2 * * * cd /opt/nginx-proxy-manager && tar -czf /backup/npm-backup-$(date +\%Y\%m\%d).tar.gz ./data ./letsencrypt
Сравнительная таблица всех сервисов
| Сервис |
Порт |
Scheme |
WebSocket |
Особенности |
| Proxmox VE |
8006 |
https |
Да |
proxy_ssl_verify off, client_max_body_size 50G, timeout 3600s |
| Proxmox Backup Server |
8007 |
https |
Да |
proxy_ssl_verify off, client_max_body_size 0, timeout 86400s |
| Zimbra (webmail) |
8443 |
https |
Нет |
proxy_ssl_verify off, client_max_body_size 1G |
| Zimbra (admin) |
7071 |
https |
Нет |
Ограничить доступ по IP |
| MailCow |
80 (контейнер) |
http |
Да |
X-Forwarded-Host, общая Docker-сеть, override compose |
| Pi-hole |
80 |
http |
Да (/ws) |
Отдельный location для /ws |
| Nextcloud |
80 (контейнер) |
http |
Да (/push/) |
trusted_proxies в config.php, client_max_body_size 10G |
| OpenMediaVault |
80 или 8080 |
http |
Да (/ws) |
Host $http_host вместо $host |
| Vaultwarden |
80 + 3012 (WS) |
http |
Да (порт 3012) |
HTTPS обязателен, DOMAIN в env, HSTS |
| Grafana |
3000 |
http |
Да |
root_url в grafana.ini или через env |
Rate Limiting: защита от перебора паролей
NPM не делает rate limiting из коробки, но Nginx умеет. Добавляется в Custom Nginx Configuration нужного Proxy Host.
Для Vaultwarden и любых форм авторизации:
limit_req_zone $binary_remote_addr zone=vault_auth:10m rate=5r/m;
location /api/accounts/login {
limit_req zone=vault_auth burst=3 nodelay;
limit_req_status 429;
proxy_pass http://vaultwarden:80;
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;
}
Для Nextcloud ограничиваем login endpoint:
limit_req_zone $binary_remote_addr zone=nc_login:10m rate=10r/m;
location /login {
limit_req zone=nc_login burst=5 nodelay;
proxy_pass http://nextcloud-app:80;
}
Заголовки безопасности — не только для Nextcloud. Хорошая практика добавлять их для всех публично доступных сервисов.
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# HSTS - только HTTPS. Добавляй только если уверен что SSL работает стабильно.
# После включения браузер откажется открывать сайт по HTTP на год вперёд.
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
HSTS добавляй последним — после того как убедился что Let’s Encrypt обновляет сертификат нормально.
Профилактика: как не сломать снова
Три вещи, которые ломают NPM после того как всё настроено:
- Обновление MailCow без пересоздания override-файла для Docker-сети — контейнер nginx-mailcow отваливается от proxy-network
- Смена IP бэкенд-сервиса без обновления Forward Hostname в NPM — получаешь 502
- Истечение сертификата из-за закрытого порта 80 на файерволле — NPM не может пройти HTTP-challenge
Простая профилактика:
- Раз в месяц проверяй сроки: SSL Certificates в интерфейсе NPM показывает дату истечения для каждого сертификата
- Docker Compose файлы держи в Git — при любом изменении видно что именно изменилось
- После любых правок файерволла проверяй что порт 80 доступен снаружи
- Бэкап перед любым обновлением NPM — это 30 секунд одной командой
И ещё одно: если что-то перестало работать после плановых обновлений сервисов — первым делом смотри логи NPM, потом логи бэкенда. Порядок диагностики: NPM -> сеть -> бэкенд. Не наоборот.
Обновление NPM
Смотри на страницу релизов перед обновлением — иногда бывают breaking changes. Процедура простая:
# 1. Сделай бэкап перед обновлением
tar -czf /backup/npm-before-update-$(date +%Y%m%d).tar.gz ./data ./letsencrypt
# 2. Обновляй
docker compose pull && docker compose up -d
# 3. Проверь что всё работает
docker compose ps
docker logs nginx-proxy-manager --tail 50
Получение wildcard-сертификата через DNS-challenge
HTTP-challenge подходит для публичных серверов. Но что если сервер не смотрит в интернет, или хочешь один сертификат на все поддомены? Тогда DNS-challenge.
Wildcard *.domain.com покрывает все поддомены одним сертификатом. Настройка раз — работает для pve, cloud, vault, grafana и всего остального.
В NPM: SSL Certificates -> Add SSL Certificate -> Let’s Encrypt. Включи «Use a DNS Challenge». Выбери своего провайдера.
Для Cloudflare нужен API-токен:
- Зайди в Cloudflare Dashboard -> My Profile -> API Tokens
- Create Token -> Edit zone DNS
- Zone Resources: Include -> Specific zone -> твой домен
- Скопируй токен
В NPM вставляешь токен в поле Cloudflare API Token. Домен указываешь как *.domain.com. Нажимаешь Save — сертификат получится через 30-60 секунд.
DNS-challenge и изолированные серверы
DNS-challenge не требует доступа к серверу снаружи. Let’s Encrypt проверяет запись TXT в DNS, которую NPM создаёт автоматически через API твоего DNS-провайдера. Работает за NAT, на серверах без публичного IP, во внутренних сетях.
Поддерживаемые провайдеры в NPM: Cloudflare, Namecheap, Route53, DigitalOcean, Linode, Godaddy и другие. Полный список — в документации Let’s Encrypt.
Access Lists: ограничение доступа
Access Lists в NPM — мощный инструмент. Можно ограничить доступ по IP, добавить Basic Auth или комбинировать оба способа.
Создаёшь Access List: Access Lists -> Add Access List.
Пример для Proxmox: только из локальной сети и VPN:
| Тип |
Значение |
Действие |
| IP |
192.168.1.0/24 |
allow |
| IP |
10.8.0.0/24 |
allow |
| IP |
all |
deny |
Потом привязываешь Access List к нужному Proxy Host на вкладке Details. Снаружи вместо сервиса увидят 403 Forbidden.
Если нужен Basic Auth поверх IP-фильтрации — добавляй учётные данные на вкладке Authorization в Access List. Два фактора защиты одновременно.
Настройка локального DNS для self-hosted стека
Всё что сделано выше работает для публичных доменов. Но можно и без интернета — только для внутренней сети.
Схема: используешь Pi-hole или AdGuard Home как локальный DNS. Добавляешь записи вида pve.home.lan -> 192.168.1.5. NPM принимает запросы на этом IP и проксирует на бэкенды. Сертификаты получаешь через DNS-challenge.
%%{init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#ffffff',
'primaryTextColor': '#1e293b',
'primaryBorderColor': '#94a3b8',
'lineColor': '#64748b',
'fontSize': '15px',
'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
},
'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
A["Браузер
pve.home.lan"] --> B["Pi-hole / AdGuard
локальный DNS"]
B --> C["Отвечает IP сервера с NPM
192.168.1.5"]
C --> D["NPM
443/80"]
D --> E["Proxmox VE
192.168.1.10:8006"]
style A fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
style B fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
style C fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
style D fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#92400e
style E fill:#f8fafc,stroke:#94a3b8,stroke-width:1px,color:#1e293b
В Pi-hole добавляешь Local DNS Records: pve.home.lan -> 192.168.1.5. В AdGuard Home: Settings -> DNS rewrites.
Устройства в сети должны использовать Pi-hole/AdGuard как DNS-сервер. Обычно настраивается в DHCP на роутере.
Мониторинг и алерты
NPM не имеет встроенного мониторинга. Но логи есть — и это уже что-то.
# Следить за access-логами в реальном времени
docker exec nginx-proxy-manager tail -f /data/logs/proxy-host-1_access.log
# Смотреть ошибки 4xx и 5xx
docker exec nginx-proxy-manager grep -E " (4|5)[0-9][0-9] " /data/logs/proxy-host-1_access.log | tail -50
# Логи получения сертификатов
docker exec nginx-proxy-manager cat /data/logs/letsencrypt.log
# Статус сертификатов - когда истекают
docker exec nginx-proxy-manager find /etc/letsencrypt/live -name "fullchain.pem" -exec openssl x509 -in {} -noout -subject -dates \;
Если используешь Uptime Kuma или Grafana + Prometheus — подключай туда. Uptime Kuma умеет мониторить HTTP-эндпоинты и отправлять уведомления в Telegram.
Минимальный мониторинг: проверка что сертификаты не истекают и что каждый проксируемый сервис отвечает 200. Это можно сделать простым bash-скриптом в cron:
#!/bin/bash
# Проверка сроков SSL-сертификатов
DOMAINS="pve.yourdomain.com cloud.yourdomain.com vault.yourdomain.com"
for DOMAIN in $DOMAINS; do
EXPIRY=$(echo | openssl s_client -servername $DOMAIN -connect $DOMAIN:443 2>/dev/null | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 14 ]; then
echo "WARN: $DOMAIN - сертификат истекает через $DAYS_LEFT дней"
else
echo "OK: $DOMAIN - $DAYS_LEFT дней до истечения"
fi
done
Итоговый чеклист после настройки
- Все сервисы открываются по HTTPS по своим доменам
- SSL-сертификаты действительны, Let’s Encrypt не показывает ошибок
- Консоли VM в Proxmox открываются без ошибок
- Загрузка файлов работает в Nextcloud и Zimbra
- Нет ошибок CSRF в MailCow и Zimbra
- Реальные IP пользователей отображаются в логах
- Nextcloud не показывает предупреждений в /settings/admin/overview
- Vaultwarden синхронизирует данные на мобильном клиенте
- Порт 81 (интерфейс NPM) закрыт от внешнего мира
- Административные интерфейсы защищены Access Lists
- Пароль администратора NPM сменён
- Бэкап ./data и ./letsencrypt настроен и проверен
NPM или Traefik: что выбрать
Вопрос регулярный — вот честный ответ.
NPM лучше если: у тебя домашняя лаборатория или небольшой self-hosted стек, нужен UI без погружения в YAML-конфиги, не пишешь код и не управляешь Kubernetes.
Traefik лучше если: управляешь кластером контейнеров, нужна автоматическая конфигурация через Docker Labels, важна интеграция с CI/CD.
Разница не в качестве, а в подходе. NPM — кликаешь мышкой. Traefik — пишешь конфиги. Оба делают одно и то же.
FAQ
Нужен ли белый IP для SSL-сертификата Let’s Encrypt?
Для HTTP-challenge — да, порт 80 должен быть доступен из интернета. Для DNS-challenge — нет. DNS-challenge поддерживает Cloudflare, Namecheap, Route53 и другие. Позволяет получать wildcard-сертификаты (*.domain.com) и работать на полностью изолированных серверах без публичного IP.
Как настроить NPM для сервисов без публичного доступа?
Используй локальный DNS через Pi-hole или AdGuard Home — пусть твои домены резолвятся только внутри сети. Сертификаты получай через DNS-challenge. Снаружи ничего не открываешь.
Почему Nextcloud продолжает генерировать http-ссылки после настройки NPM?
Не добавлен параметр overwriteprotocol => 'https' в config.php. Или NPM не в списке trusted_proxies — Nextcloud игнорирует заголовки X-Forwarded-Proto от недоверенных прокси.
Что делать если сертификат Let’s Encrypt не обновляется?
# Смотри ошибки
docker logs nginx-proxy-manager | grep -i "renew\|error\|acme"
# Порт 80 должен быть открыт
curl -v http://твой-домен.com
# NPM пытается обновить за 30 дней до истечения
# Можно принудить через UI: SSL Certificates -> три точки -> Renew
Как ограничить доступ к интерфейсу NPM (порт 81)?
Firewall на хосте — самый надёжный способ:
# UFW - разрешить только из локальной сети
ufw allow from 192.168.1.0/24 to any port 81
ufw deny 81
Или через Access List в самом NPM: Settings -> Default Site -> Require Auth.
Как проверить что WebSocket работает?
# Должен вернуть 101 Switching Protocols
curl -i \
-H "Connection: Upgrade" \
-H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" \
-H "Sec-WebSocket-Version: 13" \
https://твой-домен.com
# Если вернул 200 или 400 - WebSocket не работает
Прогноз
После настройки по этой статье у тебя: 10 сервисов за одним HTTPS-прокси, SSL получается и обновляется автоматически, все нужные заголовки передаются, консоли Proxmox открываются, Nextcloud не ругается на trusted_domains, Vaultwarden синхронизирует пароли между устройствами.
Следующий шаг — Authelia поверх NPM: единый вход с двухфактором для всего self-hosted стека. Один логин для Proxmox, Nextcloud, Grafana и всего остального. Разберём в отдельной статье.
Не получилось - пиши в комментарии
Описывай точно: какой сервис, какая ошибка, что уже пробовал. Разберёмся.