传统部署方式里,我们通常把Nginx直接装在服务器上,网站文件扔进/var/www,改完配置执行nginx -s reload就算上线。这种方式在单机、单个项目时没什么问题,但一旦服务器上要跑多个项目,或者需要频繁在测试环境和生产环境之间迁移,环境不一致带来的麻烦就会接踵而至。Docker的出现让部署这件事变得标准化:把网站和它依赖的一切打包成镜像,任何一台装了Docker的机器都能原样运行。这篇文章就来完整梳理Nginx与Docker结合部署网站的思路、配置和容易踩的坑。

一、整体架构思路:Nginx放在容器内还是容器外
动手之前先要回答一个架构问题:Nginx自己要不要也容器化?两种方案各有适用场景。第一种是把Nginx跑在宿主机上,作为所有流量的统一入口,通过端口映射把请求转发到各个业务容器。这种方式的好处是Nginx配置修改起来直接,SSL证书管理集中,排查问题时不用进容器,适合服务器上项目较多、需要灵活调整路由规则的场景。
第二种方案是Nginx也打包成容器,和业务容器一起由docker-compose编排,整站可以做到一键启动、一键销毁。这种方式环境一致性最好,换一台服务器只需要docker compose up -d就能把整套服务拉起来,特别适合需要频繁迁移或者做CI/CD流水线的项目。下面我们以第二种方案为主线展开,因为它更能体现容器化部署的完整价值。
典型的架构链路是:外部请求进入宿主机80或443端口,先到达Nginx容器,Nginx根据域名或路径规则把请求转发给前端静态资源目录、或者后端应用容器对应的内部网络地址。容器之间通过自定义的Docker网络通信,用服务名代替IP,避免了硬编码地址带来的维护问题。
二、编写Dockerfile打包网站
假设我们要部署一个前后端分离的项目:前端是Vue编译出的静态文件,后端是一个Node.js的API服务。前端镜像的Dockerfile可以这样写:
# 前端静态站镜像 FROM nginx:1.25-alpine # 删除默认配置,换上自己的 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;"]
这里有几个细节值得注意。基础镜像选择nginx:1.25-alpine而不是完整版,体积从一百多MB缩小到二十几MB,拉取和启动都更快。最后一行的daemon off;是关键,它让Nginx在前台运行,因为容器的生命周期取决于主进程,如果Nginx以守护进程方式运行,主进程立即退出,容器也会跟着停止。
后端服务的Dockerfile思路类似,核心是把运行环境和代码一起固化进镜像:
# 后端API镜像 FROM node:20-alpine WORKDIR /app COPY package*.json ./ RUN npm install --only=production COPY . . EXPOSE 3000 CMD ["node", "server.js"]
注意COPY package*.json和npm install要放在复制源码之前,这是利用Docker的层缓存机制:只要依赖清单没变,重新构建时就不会重新安装依赖,构建速度能快好几倍。这是新手最容易忽略的优化点。
三、用docker-compose编排整套服务
单靠Dockerfile只能启动一个容器,多个容器之间的网络、依赖关系、启动顺序要靠docker-compose来管理。下面是一份可直接使用的编排文件:
version: "3.8"
services:
web:
build: ./frontend
container_name: mysite-web
ports:
- "80:80"
- "443:443"
volumes:
- ./certs:/etc/nginx/certs:ro
- weblogs:/var/log/nginx
depends_on:
- api
networks:
- appnet
api:
build: ./backend
container_name: mysite-api
expose:
- "3000"
volumes:
- apilogs:/app/logs
restart: unless-stopped
networks:
- appnet
networks:
appnet:
driver: bridge
volumes:
weblogs:
apilogs:这份配置里有几个设计决策需要解释。API容器用的是expose而不是ports,意味着3000端口只在容器网络内部可见,不映射到宿主机,外部流量必须经过Nginx这一层,安全性更好。Nginx容器挂载了证书目录用来支持HTTPS,日志则通过命名卷持久化,即使容器被删除,日志数据依然保留,方便事后排查。
两个容器处于同一个自定义网络appnet,Docker内置的DNS会把这个网络里的服务名解析成对应容器IP,所以Nginx配置里可以直接写proxy_pass http://api:3000,不需要关心容器的实际IP是什么,容器重建后IP变了也不影响。
四、Nginx反向代理与HTTPS配置
容器内Nginx的核心工作是两件事:对外提供静态文件服务,对内把API请求代理给后端容器。下面这份配置覆盖了常见需求:
server {
listen 80;
server_name www.ipipp.com;
# 全部跳转HTTPS
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name www.ipipp.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# 静态资源
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API请求转发给后端容器
location /api/ {
proxy_pass http://api: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;
}
# 开启gzip压缩
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1024;
}try_files $uri $uri/ /index.html这一行对前端单页应用至关重要。Vue、React这类框架使用前端路由,用户直接访问/user/profile这样的深层路径时,服务器上并不存在对应文件,没有这条回退规则就会返回404。把它回退到index.html后,由前端路由接管页面渲染。
代理API时别忘了那几行proxy_set_header。不设置X-Real-IP和X-Forwarded-For,后端拿到的客户端IP永远是Nginx容器的内网地址,做访问统计、限流或者风控时数据就全错了。如果后端使用WebSocket,还需要额外加上Upgrade和Connection两个头,并把超时时间调大,否则连接会频繁断开。
五、常见踩坑点与排查思路
容器化部署Nginx最常见的报错是502 Bad Gateway,多半是代理的目标地址写错了。排查时先在Nginx容器里执行ping api确认DNS能否解析,再用wget -qO- http://api:3000验证后端服务是否存活。如果服务名解析失败,检查两个容器是否在同一个网络里;如果解析正常但连不上,看后端容器日志确认应用是否真的监听了0.0.0.0而不是127.0.0.1,后者会导致容器内服务只接受本机连接。
第二个高频问题是挂载配置不生效。很多人把宿主机的nginx.conf挂载进容器后忘记执行docker exec mysite-web nginx -s reload,或者挂载的是单个文件而主配置里没有include对应目录,导致修改被静默忽略。建议挂载整个配置目录而不是单个文件,并养成改完配置用nginx -t先做语法检查的习惯。
第三个坑是端口冲突。宿主机的80端口如果已经被其他程序占用,容器启动时会直接报port is already allocated。用ss -lntp | grep 80查一下占用者,要么停掉它,要么把容器映射到其他端口。另外时区问题也容易被忽视:alpine镜像默认是UTC时区,日志时间戳会差八小时,在Dockerfile里加一句RUN apk add --no-cache tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime即可解决。
最后补充一点运维层面的建议:镜像构建完成后记得打上版本标签,比如mysite-web:20240601,不要长期依赖latest标签。有了明确的版本号,发布出问题时一条docker compose down加回退旧镜像就能快速回滚,这也是容器化部署相比传统方式最实在的收益之一。