#nginx #frp #HTTPS #反向代理 #Docker #服务器运维
背景:N100 上跑了若干 Docker 服务,一直通过 VPS:裸端口 访问。本文记录将它们改造为子域名 HTTPS 访问的完整过程,以及途中遇到的两个坑(certbot 依赖冲突、chicken-and-egg 证书问题)。
一、现状与目标
改造前:每个服务对外暴露一个裸端口,访问方式不统一,无法用标准 443 HTTPS。
| 服务 |
原访问方式 |
| MT Photos |
https://x.x.x.x:18063 |
| WebDAV |
https://x.x.x.x:4455 |
| qBittorrent |
https://x.x.x.x:19090 |
改造后:每个服务对应一个子域名,统一走 443 HTTPS。
| 服务 |
新访问方式 |
| MT Photos |
https://mtphotos.your.domain.example |
| WebDAV |
https://dav.your.domain.example |
| qBittorrent |
https://qbt.your.domain.example |
二、架构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 用户浏览器 │ HTTPS 443 ▼ Cloudflare(DNS 代理) │ ▼ VPS nginx(x.x.x.x) ├── mtphotos.your.domain.example → 127.0.0.1:18064 ├── dav.your.domain.example → 127.0.0.1:4456 └── qbt.your.domain.example → 127.0.0.1:19091 │ │(frp 隧道,各服务独立端口) ▼ N100(192.168.x.14) ├── mt-photos 容器 :8063 ├── webdav 容器 :4455 └── qbittorrent 容器 :9090
|
关键前提:frp 隧道已存在,无需新增。每条隧道独立监听一个 VPS 端口,VPS nginx 直接 proxy_pass 过去,不需要 Host header 路由。
三、确认各服务端口
1 2 3 4 5
| docker ps --format 'table {{.Names}}\t{{.Ports}}'
cat /home/user/frp/frpc.ini
|
整理结果:
| 服务 |
N100 本地端口 |
frp 远程端口(VPS 侧) |
| mt-photos |
8063 |
18064 |
| webdav |
4455 |
4456 |
| qbittorrent WebUI |
9090 |
19091 |
其中 openclaw 已绑定 127.0.0.1:18789(仅本机),已有独立子域名配置,无需处理。
四、VPS nginx 配置
新建 /etc/nginx/sites-available/home-services.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| server { listen 80; server_name mtphotos.your.domain.example dav.your.domain.example qbt.your.domain.example; return 301 https://$host$request_uri; }
server { listen 443 ssl; server_name mtphotos.your.domain.example;
ssl_certificate /etc/letsencrypt/live/mtphotos.your.domain.example/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/mtphotos.your.domain.example/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 1000m;
location / { proxy_pass http://127.0.0.1:18064; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; 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 https; proxy_read_timeout 300s; } }
server { listen 443 ssl; server_name dav.your.domain.example;
ssl_certificate /etc/letsencrypt/live/dav.your.domain.example/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dav.your.domain.example/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; client_max_body_size 0;
location / { proxy_pass http://127.0.0.1:4456/milin/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header Depth $http_depth; proxy_set_header Destination $http_destination; proxy_set_header Overwrite $http_overwrite; proxy_read_timeout 300s; proxy_send_timeout 300s; }
location /milin/ { proxy_pass http://127.0.0.1:4456/milin/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header Depth $http_depth; proxy_set_header Destination $http_destination; proxy_set_header Overwrite $http_overwrite; proxy_read_timeout 300s; proxy_send_timeout 300s; }
location /joplin/ { proxy_pass http://127.0.0.1:4456/joplin/; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header Depth $http_depth; proxy_set_header Destination $http_destination; proxy_set_header Overwrite $http_overwrite; proxy_read_timeout 300s; proxy_send_timeout 300s; } }
server { listen 443 ssl; server_name qbt.your.domain.example;
ssl_certificate /etc/letsencrypt/live/qbt.your.domain.example/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/qbt.your.domain.example/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3;
location / { proxy_pass http://127.0.0.1:19091; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host "127.0.0.1:9090"; proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Proto https; proxy_read_timeout 300s; } }
|
启用:
1
| sudo ln -sf /etc/nginx/sites-available/home-services.conf /etc/nginx/sites-enabled/
|
五、申请 SSL 证书(两个坑)
坑一:系统 certbot 依赖冲突
Ubuntu 22.04 apt 源的 certbot 1.21.0 与新版 urllib3 不兼容,运行时报:
1
| ImportError: cannot import name 'appengine' from 'urllib3.contrib'
|
修复:卸载 apt 版,改用 snap 版:
1 2
| sudo snap install --classic certbot sudo ln -sf /snap/bin/certbot /usr/bin/certbot
|
坑二:chicken-and-egg 证书问题
nginx 配置里引用了证书路径,但证书还没签发 → nginx -t 失败 → certbot --nginx 插件先跑 nginx -t → 也失败,拒绝继续。
解法:用 --standalone 模式,certbot 自己临时监听 80 端口,绕开 nginx 插件。需要先停 nginx:
1 2 3 4 5 6 7 8 9 10 11 12 13
| sudo rm /etc/nginx/sites-enabled/home-services.conf sudo systemctl stop nginx
DOMAIN="your.domain.example" sudo certbot certonly --standalone -d mtphotos.${DOMAIN} --non-interactive --agree-tos --email admin@${DOMAIN} sudo certbot certonly --standalone -d dav.${DOMAIN} --non-interactive --agree-tos --email admin@${DOMAIN} sudo certbot certonly --standalone -d qbt.${DOMAIN} --non-interactive --agree-tos --email admin@${DOMAIN}
sudo ln -sf /etc/nginx/sites-available/home-services.conf /etc/nginx/sites-enabled/ sudo systemctl start nginx sudo nginx -t && sudo systemctl reload nginx
|
nginx 停机约 30 秒。Cloudflare 会透传 /.well-known/acme-challenge/,无需关闭代理。
六、N100 局域网 nginx(可选)
局域网内直接用 IP:端口访问即可,若想用子域名(需配合路由器/Pi-hole DNS),可在 N100 部署 nginx 容器:
/home/user/dockerfile/nginx/docker-compose.yml:
1 2 3 4 5 6 7 8
| services: nginx-proxy: image: nginx:alpine container_name: nginx-proxy network_mode: host volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro restart: unless-stopped
|
nginx.conf 核心:server_name 对应子域名,proxy_pass 指向各容器本地端口(8063、4455、9090)。使用 network_mode: host 直接访问宿主机端口,无需额外网络配置。
七、Cloudflare DNS 记录
所有记录一律灰云(DNS only)。
| 类型 |
名称 |
内容 |
Proxy |
| A |
@ / www |
VPS IP |
❌ 灰云 |
| A |
mtphotos |
VPS IP |
❌ 灰云 |
| A |
dav |
VPS IP |
❌ 灰云 |
| A |
qbt |
VPS IP |
❌ 灰云 |
为何全部灰云:VPS 在阿里云国内(中国大陆),Cloudflare 免费版橙云对国内用户连接质量极差,经常出现 ERR_CONNECTION_CLOSED(CF 节点在境外,国内访问不稳定)。境外爬虫(ClaudeBot 等)能走通橙云,但真实国内用户浏览器被直接断连。此外服务类子域名还有带宽限制和 CSRF 等问题。灰云下 Let’s Encrypt 证书照常提供 HTTPS,直连 VPS 对国内用户反而更快。
八、验证
1 2 3 4 5 6 7 8 9 10 11
| DOMAIN="your.domain.example" for sub in mtphotos dav qbt; do echo -n "${sub}: " curl -s -o /dev/null -w "%{http_code}" \ --resolve "${sub}.${DOMAIN}:443:VPS_IP" \ "https://${sub}.${DOMAIN}/" echo done
|
九、后续 TODO
延伸阅读