VPS 上 Nginx 反向代理:HTTPS 配置与 Docker 容器代理
说明: 本文为配置思路与示例整理,不代表作者已在自己的服务器上逐项验证全部命令。执行涉及公网暴露、账户权限、数据删除或服务重启的操作前,请先备份,并结合官方文档与实际环境核验。
本文与《Docker 容器化部署实践》(侧重容器编排与多服务管理)不同,专注于 Nginx 反向代理层的配置与常见问题排查。如果你已经部署好了 Docker 容器,需要统一管理域名和 HTTPS,本文更适合作为参考。
环境信息
- 操作系统:Debian 11 (bullseye)
- Nginx:1.18.0
- Certbot:1.12.0
- OpenSSL:1.1.1w
- Docker:24.x
- 服务器:VPS(公网 IP)
本文示例基于 Debian 11 + Nginx 1.18 环境整理,实际配置请根据自身服务器环境调整。请勿直接照搬到生产环境,建议先在测试环境验证。
为什么需要反向代理
在 VPS 上跑 Docker 服务时,容器内的应用(比如 Nuxt 前端跑在 3000 端口、Go 后端跑在 8080 端口)通常只监听容器内部或 localhost。直接暴露这些端口有两个问题:
- 不安全:没有 HTTPS,数据明文传输
- 不方便:多个服务要记不同的端口号
Nginx 反向代理可以统一入口:一个域名、一个端口(443),通过 URL 路径分发到不同的后端容器。同时提供 HTTPS、gzip 压缩、静态文件缓存等能力。
基础反向代理配置
先从最简单的开始。假设有一个 Nuxt 博客跑在 localhost:3000:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1: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_set_header:
X-Real-IP:让后端拿到真实客户端 IP,而不是 Docker 网桥的 IPX-Forwarded-Proto:告诉后端客户端用的是 http 还是 https(影响 Nuxt SSR 的链接生成)X-Forwarded-For:传递完整的代理链 IP
多服务分发
如果同时有前端(3000)、后端 API(8080)、管理面板(4000),可以用路径分发:
# API 请求 → Go 后端
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
}
# 管理面板 → Vue 应用
location /admin/ {
proxy_pass http://127.0.0.1:4000/;
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;
}
# 其他所有请求 → Nuxt 前端
location / {
proxy_pass http://127.0.0.1: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;
}注意 location /admin/ 的 proxy_pass 末尾加了 /,这会把 /admin/foo 转发为 /foo 到后端。不加斜杠的话,路径会原样传递。
HTTPS 配置:Let's Encrypt 免费证书
安装 Certbot
apt update && apt install -y certbot python3-certbot-nginx自动申请证书
# --nginx 会自动修改 nginx 配置并重载
certbot --nginx -d example.com -d www.example.com这条命令做了三件事:
- 向 Let's Encrypt 验证域名所有权(HTTP-01 challenge)
- 下载证书到
/etc/letsencrypt/live/example.com/ - 修改 nginx 配置,添加 SSL 相关参数
自动续期
Let's Encrypt 证书有效期 90 天,需要定期续期:
# 测试续期
certbot renew --dry-run
# 添加 crontab 定时任务(每天凌晨 3 点)
crontab -e
# 添加:
0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"--deploy-hook 很重要——证书更新后 nginx 需要重新加载才能使用新证书。
手动 HTTPS 配置参考
如果不用 certbot 自动配置,手动写 SSL 配置:
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 安全参数
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# HSTS(启用后浏览器会强制 HTTPS,谨慎开启)
# add_header Strict-Transport-Security "max-age=63072000" always;
location / {
proxy_pass http://127.0.0.1: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;
}
}常见问题排查
问题 1:Docker 容器端口映射后 Nginx 连不上
现象:proxy_pass http://127.0.0.1:3000 返回 502 Bad Gateway。
原因:Docker 容器如果用了 -p 3000:3000 端口映射,Nginx 可以通过 127.0.0.1:3000 访问。但如果容器只在 Docker 网络内部通信(没有 -p),Nginx 就连不上。
解决:
- 方案 A:给容器加端口映射
-p 127.0.0.1:3000:3000(只绑定 localhost,不暴露到公网) - 方案 B:Nginx 容器化,和应用容器共享 Docker 网络
- 方案 C:用 Docker 的
host.docker.internal或宿主机 IP
问题 2:Nginx 502 但端口没被占用
现象:明明服务在跑,Nginx 也配了正确的端口,但 502。
排查:
# 确认端口是否在监听
ss -tlnp | grep 3000
# 确认 nginx 有权限连接
# www-data 用户需要能连接到目标端口
# 如果服务绑定在 127.0.0.1,确认不是绑定在 ::1 (IPv6 only)
curl -4 http://127.0.0.1:3000常见原因:服务绑定在 0.0.0.0 但防火墙/iptables 限制了端口访问,或者 Nginx 的 worker_connections 不够。
问题 3:SSL 证书申请失败(HTTP-01 Challenge)
现象:certbot --nginx 报错 urn:ietf:params:acme:error:unauthorized。
原因:Let's Encrypt 通过访问 http://example.com/.well-known/acme-challenge/xxx 来验证域名,如果 80 端口不通或被重定向到 HTTPS,验证就会失败。
解决:
# 确保 80 端口有处理 .well-known 的 location
server {
listen 80;
server_name example.com;
# 先放行 acme challenge
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# 其他请求重定向到 HTTPS
location / {
return 301 https://$server_name$request_uri;
}
}然后申请:
certbot certonly --webroot -w /var/www/certbot -d example.com问题 4:proxy_pass 末尾斜杠的坑
现象:请求 GET /api/v1/articles,后端收到的是 /api/v1/articles 而不是 /v1/articles。
原因:proxy_pass http://127.0.0.1:8080/api/(末尾有 /)会把匹配的 location 前缀去掉。而 proxy_pass http://127.0.0.1:8080/api(无斜杠)会原样传递。
# /api/foo → 后端收到 /api/foo(原样传递)
location /api/ {
proxy_pass http://127.0.0.1:8080;
}
# /api/foo → 后端收到 /foo(去掉了 /api 前缀)
location /api/ {
proxy_pass http://127.0.0.1:8080/;
}问题 5:WebSocket 连接中断
现象:前端 WebSocket 连接几秒就断开。
解决:添加 WebSocket 支持的 header:
location /ws/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}性能优化建议
静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff2)$ {
proxy_pass http://127.0.0.1:3000;
expires 7d;
add_header Cache-Control "public, immutable";
}Gzip 压缩
在 http 块中启用:
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1024;文件上传大小限制
Docker 化的应用经常遇到上传文件失败,但 nginx 默认只允许 1MB:
client_max_body_size 20m;基于当前环境,实际以业务压测为准。
总结
Nginx 反向代理是 VPS 上跑 Docker 服务的标准姿势。核心要点:
proxy_set_header一定要配全,否则后端拿不到真实 IP 和协议- HTTPS 用 Let's Encrypt 免费证书 + crontab 自动续期
proxy_pass末尾的/决定路径是否被截断,写之前想清楚- Docker 端口映射要绑定
127.0.0.1,别直接暴露到公网 - 遇到 502 先查
ss -tlnp确认端口在监听,再查 nginx error log
配置 Nginx 的过程不复杂,但每个细节都可能坑你一下。把这些坑踩过一遍,基本就能写出靠谱的反向代理配置了。
评论
游客无需注册即可评论。
你提交的昵称、邮箱、网址和评论内容会保存在服务端,用于展示评论身份、接收回复及必要的安全审计。
浏览器会本地保存已填游客信息和评论草稿,方便下次免填。
回复提醒会通过站内消息和邮件通知。