1. Сертификат Let's Encrypt
mkdir -p /opt/certbot
cd /opt/certbot
nano docker-compose.yml
services:
certbot:
container_name: certbot
image: certbot/certbot
network_mode: host
volumes:
- ./certs:/etc/letsencrypt
- ./var-lib-letsencrypt:/var/lib/letsencrypt
docker run --rm \
-v /opt/certbot/certs:/etc/letsencrypt \
-v /opt/certbot/var-lib-letsencrypt:/var/lib/letsencrypt \
--network host \
certbot/certbot certonly --standalone \
--non-interactive --agree-tos \
--email [почта] \
-d [домен]
На время выдачи сертификата порт 80 должен быть свободен — если certbot ругается, проверьте, кто его занял: ss -lntp | grep ':80'.
2. docker-compose ноды с nginx
services:
remnawave-nginx:
image: nginx:1.28
container_name: remnawave-nginx
network_mode: host
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /opt/certbot/certs/live/[домен]/fullchain.pem:/etc/nginx/ssl/[домен]/fullchain.pem:ro
- /opt/certbot/certs/live/[домен]/privkey.pem:/etc/nginx/ssl/[домен]/privkey.pem:ro
- /var/www/html:/var/www/html:ro
command: sh -c 'exec nginx -g "daemon off;"'
Сертификаты монтируются только в nginx — Xray они не нужны. Порт, на котором слушает сам Xray (в примере 8443), наружу не открывается вообще.
3. nginx.conf — ключевой location
upstream xray_trojan_grpc {
server 127.0.0.1:8443;
keepalive 64;
}
server {
listen 443 ssl;
http2 on;
server_name [домен];
ssl_certificate /etc/nginx/ssl/[домен]/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/[домен]/privkey.pem;
root /var/www/html;
location ^~ /trgrpc {
grpc_pass grpc://xray_trojan_grpc;
grpc_set_header Host $host;
grpc_set_header X-Real-IP $remote_addr;
grpc_read_timeout 1h;
grpc_send_timeout 1h;
access_log off;
error_page 502 = @grpc_error;
}
location @grpc_error { return 404; }
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
location / {
try_files $uri $uri/ /index.html;
}
}
location /trgrpc должен буквально совпадать с serviceName в inbound-е Xray — если serviceName называется trojan-grpc, location тоже должен быть /trojan-grpc.
4. Inbound на стороне Xray
{
"tag": "TROJAN_GRPC_NGINX",
"listen": "127.0.0.1",
"port": 8443,
"protocol": "trojan",
"settings": { "clients": [] },
"streamSettings": {
"network": "grpc",
"security": "none",
"grpcSettings": { "serviceName": "trgrpc", "multiMode": false }
},
"sniffing": { "enabled": true, "destOverride": ["http", "tls", "quic"] }
}
security: "none" — потому что TLS уже принимает nginx на 443. Если включить TLS ещё и в Xray, схема сломается: nginx будет слать обычный gRPC, а Xray — ждать TLS поверх него. listen: 127.0.0.1 — Xray должен принимать трафик только от nginx, порт 8443 наружу открывать не нужно.
5. Настройки на клиентской стороне
| Поле | Значение |
| Address | [домен] |
| Port | 443 (не 8443!) |
| Network | gRPC |
| Security | TLS |
| gRPC serviceName | trgrpc |
| ALPN | h2 |
| Fingerprint | firefox или random |
Клиентам всегда указывается порт 443 — 8443 существует только между nginx и Xray на самой ноде.
6. Firewall и запуск
ufw allow 80/tcp
ufw allow 443/tcp
ufw status
cd /opt/remnanode
docker compose down
docker compose up -d
docker exec -it remnawave-nginx nginx -t
curl -vk https://[домен]/healthz
Ожидаемый ответ на healthz — ok. Если увидели его — схема работает.
Частые ошибки
- nginx не видит сертификат — пути в docker-compose и в nginx.conf должны совпадать буквально, вплоть до имени домена в пути.
- 400 PRI * HTTP/2.0 — location не совпадает с serviceName, или трафик идёт не на тот location.
- В Xray стоит security: tls — при схеме через nginx должно быть строго "none", иначе двойной TLS ломает соединение.
- Порт 8443 открыт наружу — проверить:
ss -lntp | grep 8443, должно быть только 127.0.0.1:8443.