前后端分离之后,前端构建产物通常只是一堆静态文件,谁来托管这些文件直接影响访问速度和运维成本。Nginx 凭借事件驱动的异步架构,处理静态资源的能力非常出色,而把它装进 Docker 容器之后,部署、回滚、扩容都变成了一条命令的事情。这篇文章就把整套流程拆开讲清楚,从镜像定制到配置优化,给出一套可以直接抄走用的方案。

为什么要容器化部署 Nginx
传统方式装 Nginx,需要在每台服务器上执行安装、改配置、开端口、设开机自启等一系列操作。服务器一多,环境差异就来了:有的机器是编译安装,有的是包管理器安装,配置目录一个在 /usr/local/nginx/conf,另一个在 /etc/nginx,排查问题的时候非常痛苦。容器化之后,Nginx 的版本、配置、目录结构全部固化在镜像里,开发、测试、生产三个环境跑的是同一个镜像,环境不一致导致的问题基本消失。
另一个好处是回滚方便。静态资源发布出问题时,传统做法是把旧文件拷回来,而容器化部署只需要切换回上一个镜像标签,几秒钟就能完成回滚。配合 CI/CD 流水线,前端构建完成后自动打镜像推送到仓库,服务器上执行一次 docker pull 加重启就完成了发布,整个过程不需要人工登录服务器改文件。
还有一个容易被忽略的点:资源隔离。容器可以限制 Nginx 的 CPU 和内存占用,避免某个服务把整台机器拖垮,这在同一台宿主机跑多个服务的场景下特别实用。
编写 Dockerfile 定制 Nginx 镜像
官方的 nginx 镜像开箱即用,但生产环境一般都要定制,比如替换默认配置、内置前端构建产物、调整时区和日志行为。下面是一份比较典型的 Dockerfile:
FROM nginx:1.25-alpine
# 设置时区,避免日志时间不对
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
# 删除默认配置,换成自己的
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/conf.d/site.conf
# 把前端构建产物直接打进镜像
COPY dist/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]选 alpine 版本作为基础镜像是主流做法,体积只有十几兆,拉取和分发都快。注意最后一行的 daemon off;,这是容器化 Nginx 的关键:容器的主进程必须在前台运行,如果 Nginx 以守护进程方式启动,主进程立刻退出,容器随之停止。
把构建产物打进镜像适合环境固定、发版频繁的项目,好处是镜像自带全部资源,跑起来就能服务。缺点是每次改一个文件都要重新构建镜像。如果静态资源更新非常频繁,也可以选择运行时挂载的方式,把宿主机目录映射进容器:
docker run -d --name web \ -p 80:80 \ -v /data/www:/usr/share/nginx/html:ro \ -v /data/nginx/site.conf:/etc/nginx/conf.d/site.conf:ro \ --restart=always \ nginx:1.25-alpine
这里加了 :ro 表示只读挂载,防止容器内的进程意外修改宿主机文件。两种方式各有适用场景,建议核心版本用镜像内置,临时活动页之类用挂载。
静态资源服务的核心配置
容器化只是部署形态,真正决定访问体验的还是 Nginx 配置。一份针对静态资源优化过的配置大概长这样:
server {
listen 80;
server_name example.ipipp.com;
root /usr/share/nginx/html;
index index.html;
# 开启 gzip 压缩,文本类资源体积可减少 60% 以上
gzip on;
gzip_comp_level 6;
gzip_min_length 1k;
gzip_types text/css application/javascript application/json image/svg+xml;
# 带 hash 的文件设置长缓存
location ~* \.(js|css|png|jpg|jpeg|gif|webp|woff2)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# index.html 禁止缓存,保证发版后用户能拿到最新入口
location = /index.html {
add_header Cache-Control "no-cache";
}
# 前端路由刷新 404 问题,全部回退到 index.html
location / {
try_files $uri $uri/ /index.html;
}
}这份配置里有几个细节值得展开。首先是缓存策略的分治:现代前端构建工具会给文件名加上内容 hash,比如 app.a3f9c2.js,文件内容一变名字就变,所以这类文件可以放心设置一个月的长缓存并标记 immutable,浏览器连协商请求都不会发。而 index.html 是入口文件,名字永远不变,必须设置 no-cache,否则发版后用户看到的还是旧页面。
其次是 try_files 那行,这是部署单页应用最容易踩的坑。React、Vue 的路由刷新后,浏览器会向服务器请求类似 /user/list 这样的路径,服务器上根本没有这个文件,直接返回 404。try_files $uri $uri/ /index.html 的意思是先找真实文件,找不到就回退到 index.html,交给前端路由处理。
最后是 gzip 的取舍。压缩级别不是越高越好,级别 6 以上 CPU 消耗明显增加而压缩率提升有限,一般设置 5 到 6 比较均衡。另外图片本身已经是压缩格式,再 gzip 基本没有收益,所以 gzip_types 里不用列图片类型,只压缩文本类资源。
容器化部署中常见的坑
第一个高频问题是改了配置不生效。很多人在宿主机改了挂载的配置文件,容器里的 Nginx 却毫无反应。原因很简单:Nginx 只在启动和收到 reload 信号时才读取配置。改完文件后需要执行 docker exec web nginx -s reload,或者干脆重启容器。如果希望配置变更全自动生效,可以配合 inotify 之类的工具监听文件变化触发 reload,但生产环境更推荐走 CI/CD 重新构建镜像,流程更可控。
第二个是文件权限问题。容器内 Nginx 的 worker 进程以 nginx 用户运行,如果宿主机挂载目录的权限不对,会出现 403 Forbidden。排查时先确认目录对其他用户有可读和可执行权限,文件本身有可读权限。用 docker logs 看错误日志,一般会有 Permission denied 的明确提示。
第三个是日志处理。容器默认把日志写到容器内部,容器一删日志就没了。建议把日志目录也挂载出来,或者干脆把标准输出交给 Docker 的日志驱动,由 ELK 或 Loki 统一收集:
# 在 nginx.conf 顶层把错误日志导向标准错误输出 error_log /dev/stderr warn; # 在 server 配置中把访问日志导向标准输出 access_log /dev/stdout main;
这样 docker logs 就能直接看到全部日志,对接日志采集平台也不需要额外挂载目录,是容器化场景下最干净的做法。
总的来说,Nginx 容器化部署静态资源的门槛不高,但要真正做到生产可用,需要在镜像定制、缓存策略、路由回退、日志收集这几个环节上都花心思。把上面的 Dockerfile 和配置模板落地之后,配合自动化流水线,就能得到一套发布快、回滚易、可观测性也不差的静态资源服务体系。