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。直接暴露这些端口有两个问题:

  1. 不安全:没有 HTTPS,数据明文传输
  2. 不方便:多个服务要记不同的端口号

Nginx 反向代理可以统一入口:一个域名、一个端口(443),通过 URL 路径分发到不同的后端容器。同时提供 HTTPS、gzip 压缩、静态文件缓存等能力。

基础反向代理配置

先从最简单的开始。假设有一个 Nuxt 博客跑在 localhost:3000

NGINX
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 网桥的 IP
  • X-Forwarded-Proto:告诉后端客户端用的是 http 还是 https(影响 Nuxt SSR 的链接生成)
  • X-Forwarded-For:传递完整的代理链 IP

多服务分发

如果同时有前端(3000)、后端 API(8080)、管理面板(4000),可以用路径分发:

NGINX
# 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

BASH
apt update && apt install -y certbot python3-certbot-nginx

自动申请证书

BASH
# --nginx 会自动修改 nginx 配置并重载
certbot --nginx -d example.com -d www.example.com

这条命令做了三件事:

  1. 向 Let's Encrypt 验证域名所有权(HTTP-01 challenge)
  2. 下载证书到 /etc/letsencrypt/live/example.com/
  3. 修改 nginx 配置,添加 SSL 相关参数

自动续期

Let's Encrypt 证书有效期 90 天,需要定期续期:

BASH
# 测试续期
certbot renew --dry-run

# 添加 crontab 定时任务(每天凌晨 3 点)
crontab -e
# 添加:
0 3 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

--deploy-hook 很重要——证书更新后 nginx 需要重新加载才能使用新证书。

手动 HTTPS 配置参考

如果不用 certbot 自动配置,手动写 SSL 配置:

NGINX
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。

排查

BASH
# 确认端口是否在监听
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,验证就会失败。

解决

NGINX
# 确保 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;
    }
}

然后申请:

BASH
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(无斜杠)会原样传递。

NGINX
# /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:

NGINX
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;
}

性能优化建议

静态文件缓存

NGINX
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 块中启用:

NGINX
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
gzip_min_length 1024;

文件上传大小限制

Docker 化的应用经常遇到上传文件失败,但 nginx 默认只允许 1MB:

NGINX
client_max_body_size 20m;

基于当前环境,实际以业务压测为准。

总结

Nginx 反向代理是 VPS 上跑 Docker 服务的标准姿势。核心要点:

  1. proxy_set_header 一定要配全,否则后端拿不到真实 IP 和协议
  2. HTTPS 用 Let's Encrypt 免费证书 + crontab 自动续期
  3. proxy_pass 末尾的 / 决定路径是否被截断,写之前想清楚
  4. Docker 端口映射要绑定 127.0.0.1,别直接暴露到公网
  5. 遇到 502 先查 ss -tlnp 确认端口在监听,再查 nginx error log

配置 Nginx 的过程不复杂,但每个细节都可能坑你一下。把这些坑踩过一遍,基本就能写出靠谱的反向代理配置了。

评论