Featured image of post Nginx 自建镜像实战:前端静态、反向代理、WebSocket 与四层转发

Nginx 自建镜像实战:前端静态、反向代理、WebSocket 与四层转发

把前端 dist、nginx.conf、ssl 证书打包成一个 nginx 镜像,覆盖 80/443 反代、gzip、autoindex、stream 四层转发、WebSocket 升级等高频配置

Nginx 容器化部署看似简单——docker run -d -p 80:80 nginx 就能跑——但生产里要解决的是:怎么把前端 dist、nginx.conf、SSL 证书打包成可复用的镜像,而不是每次新环境都 ssh 进去改配置。这篇把多年自建 Nginx 镜像的套路整理清楚。

阅读对象:前端工程师、运维、需要部署 Web 服务的全栈工程师 覆盖范围:前端 dist 打包 + nginx.conf 模板 + 反向代理 / WebSocket / TCP UDP 四层 + SSL + bitnami/nginx 优势对比

一、为什么需要自建 Nginx 镜像

docker run nginx 看似省事,但生产场景里很快撞墙:

  • 前端 dist 在外面:每次新构建都要 docker cpbind mount配置不统一
  • nginx.conf 在外面:集群里 5 台机器 5 份配置,改一行同步半天
  • SSL 证书在容器内:Let’s Encrypt 三个月续期,每台机器都跑一遍 certbot
  • 个性化配置散落:gzip、autoindex、限流、灰度发布,每个项目都要从头写

自建 Nginx 镜像的本质:把"配置 + 静态资源 + 证书"做成不可变的镜像。新机器拉新镜像即用,配置漂移被堵在镜像层之外

When to use

  • 同一份前端代码需要部署到多台机器(灰度 / 多机房)
  • 业务方只关心"上传代码 + 重启容器",不想碰 nginx.conf
  • 团队有统一的反代规范(统一超时、统一 gzip、统一 WebSocket 头)

二、最小化 Dockerfile

1
2
3
FROM dockerhub.example.com/base/nginx:latest
ADD html /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

文件结构:

1
2
3
4
5
.
├── Dockerfile
├── html
│   └── index.html
└── nginx.conf

构建:

1
2
3
docker build -t dockerhub.example.com/library/nginx:20240730 .
docker run -d --restart=always --name apph5 --net=host \
  dockerhub.example.com/library/nginx:20240730

关键点

  • ADD html 会把整个目录解压到镜像里,COPY html 更适合静态文件(虽然官方推荐 COPY,但 ADD 对本地目录行为一致)
  • --net=host 让 Nginx 直接监听宿主机端口,省一层 NAT——H5 活动页常用
  • library/nginx:20240730日期做 tag,避免覆盖旧版本

三、生产级 nginx.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
user  root;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    keepalive_timeout  65;

    # ============ 核心优化 ============
    autoindex  on;                          # 目录浏览(文件服务器场景)

    gzip on;                                # 开启 gzip
    gzip_buffers 32 4K;                     # 压缩缓冲
    gzip_comp_level 6;                      # 压缩级别 1-9,6 是性价比甜蜜点
    gzip_min_length 1k;                     # 1K 以下不压缩
    gzip_types application/javascript text/css text/xml;  # 压缩类型
    gzip_disable "MSIE [1-6]\.";            # 兼容老 IE
    gzip_http_version 1.1;                  # 最低 HTTP 版本
    gzip_vary on;                           # 传输压缩标志

    # ============ 大文件上传 ============
    client_max_body_size 2000M;             # 上传文件大小限制
    client_header_buffer_size    128k;
    large_client_header_buffers  4  128k;

    # ============ 业务 server ============
    server {
        listen  10082;
        server_name location;
        add_header 'Access-Control-Allow-Origin' '*';   # 跨域
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;   # SPA 路由

        # 反向代理后端 API
        location ^~ /api/ {
            proxy_pass http://localhost:9527/;
        }
    }
}

实战要点

  • try_files $uri $uri/ /index.htmlSPA 路由必备——直接访问 /user/profile 不 404
  • gzip_types 务必加 application/javascript(不是 text/javascript)——新版 MIME 标准
  • client_max_body_size 2000M配合后端 Spring Boot 的 spring.servlet.multipart.max-file-size 一起调
  • add_header 'Access-Control-Allow-Origin' '*'只用于开发,生产应该用 https://www.example.com 白名单

四、WebSocket 反向代理

Nginx 反代 WebSocket 关键是保留 Upgrade 头

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# http block 顶部加映射
map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

# server block 内 location
location /ws/ {
    proxy_pass http://localhost:8080/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $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_read_timeout 300s;
    proxy_send_timeout 300s;
}

没加 Upgrade 头时浏览器控制台会报 WebSocket connection to 'ws://...' failed: Invalid frame header

五、四层转发:TCP / UDP 代理

Nginx 从 1.9 开始支持 stream 模块(四层),可以代理 MySQL、Redis、MQTT、SIP 等非 HTTP 协议。

 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
# nginx.conf 顶层加 stream block(与 http 同级)
stream {
    log_format proxy '[$time_local] $protocol status:$status, '
                     'bytes client:$bytes_sent/$bytes_received $session_time, '
                     'client: $remote_addr, upstream:"$upstream_addr", '
                     'bytes upstream:$upstream_bytes_sent $upstream_bytes_received';
    access_log /var/log/nginx/stream.log proxy;

    # MySQL 反代
    upstream mysql {
        server 10.0.0.1:3306 max_fails=2 fail_timeout=10s;
        server 10.0.0.2:3306 max_fails=2 fail_timeout=10s;
    }
    server {
        listen 3306;
        proxy_pass mysql;
        proxy_connect_timeout 5s;
    }

    # 端口段转发(PASV FTP 必备)
    server {
        listen 21100-21110;
        proxy_pass 10.0.0.1:$server_port;   # $server_port 是动态端口
    }
}

实战案例:FTP 服务主动模式要在客户端出口网络(防火墙 / NAT)做端口段转发,stream 模块就是干这个的。

六、SSL / HTTPS 完整配置

 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
server {
    server_name api.example.com;
    listen 443 ssl;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # 协议版本(生产只开 TLS 1.2+)
    ssl_protocols TLSv1.2 TLSv1.3;

    # 加密套件
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
    ssl_prefer_server_ciphers on;

    # session 复用
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;

    # HTTP 强转 HTTPS
    error_page 497 https://$host$request_uri;

    location / {
        proxy_pass http://localhost:9000;
        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;
    }
}

# 80 强转 443
server {
    listen 80;
    server_name api.example.com;
    return 301 https://$server_name$request_uri;
}

证书挂载(推荐用 bind mount 而非 ADD 进镜像):

1
2
3
docker run -d \
  -v /etc/letsencrypt/live/api.example.com:/etc/nginx/ssl:ro \
  ...

证书 3 个月续期时 nginx -s reload 一下,不用重启容器

七、bitnami/nginx 镜像优势

bitnami/nginx 相对官方 nginx 镜像有几个明显优势:

特性官方 nginxbitnami/nginx
非 root 用户运行默认 root安全风险默认 uid 1001
默认 HSTS / 安全头
健康检查HEALTHCHECK 内置
日志切分接入 stdout/stderr
镜像体积~ 140MB~ 90MB

生产强烈推荐 bitnami 镜像root 运行的 nginx 容器一旦被攻破就拿到宿主 root——bitnami 默认 nginx 用户运行,减少 90% 的攻击面

八、生产部署清单

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 1. 准备前端 dist
npm run build  # 生成 dist/

# 2. 准备 nginx.conf(用上面的模板)

# 3. 准备 SSL 证书
mkdir -p ssl && cp /etc/letsencrypt/live/example.com/* ssl/

# 4. 构建镜像
docker build -t dockerhub.example.com/library/web:20240730 .

# 5. 推到 Harbor
docker push dockerhub.example.com/library/web:20240730

# 6. 生产部署
docker run -d --name web --restart=always \
  --network=web \
  -p 443:443 -p 80:80 \
  -v /data/web/ssl:/etc/nginx/ssl:ro \
  dockerhub.example.com/library/web:20240730

九、监控与日志

日志接入

1
2
3
4
5
6
7
# 用 Loki / ELK 收集,docker-compose 里加 logging driver
logging:
  driver: loki
  options:
    loki-url: "http://internal.example.com:3100/loki/api/v1/push"
    max-size: "50m"
    max-file: "10"

健康检查

1
2
3
4
5
6
location /nginx_status {
    stub_status on;
    access_log off;
    allow 127.0.0.1;       # 只允许内网
    deny all;
}

容器外查:

1
2
3
4
5
curl http://container-ip/nginx_status
# Active connections: 2
# server accepts handled requests
#  100  100  200
# Reading: 0 Writing: 1 Waiting: 1

十、常见坑位

10.1 反代后端丢失 HTTPS 信息

后端 Spring Boot 拿到 request.getScheme()http 不是 https——因为 nginx 用 HTTP 协议连后端

修复:后端配 server.tomcat.remoteip.protocol-header=X-Forwarded-Proto Nginx 显式传:

1
proxy_set_header X-Forwarded-Proto $scheme;

10.2 SPA 路由 404

直接访问 https://app.example.com/user/profile 返回 404。

修复try_files $uri $uri/ /index.html;——所有未匹配路径都返回 index.html,由前端路由处理。

10.3 WebSocket 反复断连

症状:WSS 连接 60 秒断一次。

原因:默认 proxy_read_timeout 60s,WebSocket 是长连接,超过 60 秒没数据就被切断。

修复proxy_read_timeout 300s; 或更长。

10.4 gzip 对小文件无效果

gzip_min_length 1k; 控制 1KB 以下的文件不压缩——gzip_types 只决定"压缩什么类型",文件大小阈值由 min_length 控制

2024+ 视角补充

本文写于 2023-09,2024-2026 期间 Nginx 自建镜像关键演进:

  • Nginx 1.27 LTS(2024-11):HTTP/3(QUIC)GA;OpenSSL 3.x;动态 SSL 证书加载
  • Nginx 1.28 LTS(2025-Q4 预期):ACMEv2 自动证书(不用 certbot);BoringSSL 可选
  • Nginx Unit 1.34+(2024-2026):应用服务器模式(Go / Node / Python / Java / PHP)——Web + App Server 一体
  • Nginx Ingress 1.12+(K8s):Gateway API GAIPV6 双栈WAF 集成
  • OpenResty 1.25+:Lua 生态成熟,API Gateway / WAF 场景仍是首选
  • bitnami/nginx 镜像:2024+ 仍维护,非 root 用户 / 健康检查仍是优势
  • 替代品(2024+ 视角):
    • Caddy 2.x自动 HTTPS / Caddyfile 简洁——新项目首选
    • Traefik 3.x:K8s 原生 / 自动服务发现
    • HAProxy 3.x四层代理性能优于 Nginx——数据库 / TCP 代理首选

实战建议(2025-2026 视角)

  • 传统 Web 反代 / 静态站Nginx 1.27 LTS 仍是生产首选
  • 想要最少配置 / 自动 HTTPSCaddy 2.x
  • K8s 入口Nginx Ingress 1.12+ / Traefik 3.x / Gateway API
  • API Gateway / WAFOpenResty 1.25+ 仍是稳态
  • 数据库 / TCP 代理HAProxy 3.x

下一步

使用 Hugo 构建
主题 StackJimmy 设计