Docker 和 Nginx 这对组合,很多人都是分开学的:先装 Docker,再改 Nginx 配置,等真正把项目部署上去,才发现容器端口、配置文件挂载、反向代理路径、负载均衡上游,每个环节都能卡一次。这篇文章直接按“容器部署、反向代理、负载均衡”这条主线拆开讲,适合刚学完 Docker 基础、准备把自研 Web 项目往服务器上部署的读者,也适合本地开发环境需要多站点代理的场景。先给结论:单机场景下,Docker 负责把 Nginx 跑起来,Nginx 负责把流量转到对应服务;多机或高并发场景下,再叠加 upstream 负载均衡。把这三层搞清楚,比背一百条命令管用。
1. 先想清楚:这一套到底要解决什么问题
1.1 三个关键词各有各的职责
容器部署解决的是“运行环境一致”的问题。你在一台机器上手动装 Nginx,系统版本、依赖库、编译参数、配置文件位置都可能和另一台机器不同。放到容器里,镜像本身把运行环境打包好了,Nginx 版本固定、依赖固定,配置通过挂载目录从宿主机注入,换一台机器只需要同样挂载一份配置,行为基本一致。
反向代理解决的是“入口统一”的问题。你本机或者服务器上跑了好几个服务,比如 8080 是前端开发服务器,8081 是后端接口,8082 是某个内部工具页面。默认情况下,用户访问哪个端口就去哪个服务,服务地址一多就会乱。Nginx 作为反向代理,统一监听 80 或 443,根据域名、路径、Header 把请求转发到对应后端服务。对外暴露的地址收敛成一个,域名也不用带端口。
负载均衡解决的是“单点扛不住”的问题。一个后端实例处理不过来,就需要把请求分散到多个实例。Nginx 在配置文件里声明一个 upstream 组,把多个后端地址放进去,再通过权重、健康检查、失败重试等参数控制流量分配。
这里最容易混淆的是反向代理和负载均衡。反向代理是“转发到谁”;负载均衡是“转发到多个里的哪一个”。实际项目里通常是先配反向代理,等某个服务压力上来了,再把那个服务节点从单个实例扩展成 upstream 组。
1.2 容器里的 Nginx 和物理机上的 Nginx 有什么不同
在物理机上改 Nginx,你改的是 /etc/nginx/nginx.conf、/etc/nginx/conf.d/ 这些固定路径。在 Docker 里,镜像内部也有这些路径,但你不能每次改配置都进容器里改,否则容器一删,配置全没了。
正确的做法是:宿主机上建一个目录放配置,启动容器时用 -v 参数挂载进去。宿主机目录是持久化的,容器是随时可以删掉重建的。这个思路看起来多了一步,实际上带来的好处很大——迁移环境时,只需要把宿主机上的配置目录拷贝走,在新机器上重新 docker run 一把,服务就回来了。
判断配置是否生效,也不要再用“改完文件就完事”的思路。容器里跑 Nginx 时,改完宿主机挂载的配置,先执行一次语法检查,再 reload。后面我会专门讲这个顺序。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:Docker 跑起来之前,最容易卡在哪
2.1 Docker Desktop 启动失败,多半是虚拟化没开
Windows 上安装 Docker Desktop,最常见的报错是 Docker Desktop failed to start because virtualization support wasn't detected。这个提示的意思很明确:你的 CPU 虚拟化功能没有开启,或者开启后没有被系统识别。
排查顺序一般是:
- 重启进 BIOS/UEFI,找到 Intel VT-x 或者 AMD-V 相关的选项,确认处于 Enabled。
- Windows 功能里确认“适用于 Linux 的 Windows 子系统”和“虚拟机平台”两个选项已打开,然后重启。
- 确认你的系统是 Windows 10/11 的 64 位版本,且满足 Docker Desktop 对版本的要求。
- 如果电脑装的是 Windows 家庭版,部分虚拟化功能入口可能显示不全,需要先确认系统版本满足条件。
这里有个容易误判的情况:BIOS 里已经开启了虚拟化,但系统里之前装过别的虚拟化软件,驱动冲突导致 Docker 识别不到。可以先停掉其他虚拟机软件,再重新启动 Docker Desktop。
macOS 上一般直接用 Docker Desktop 的 Apple Silicon 或 Intel 版本,先看芯片类型再下载对应安装包。Linux 上不需要 Docker Desktop,直接安装 docker-ce 或 docker.io 即可。判断安装是否成功,不需要看花哨的界面,直接打开终端执行:
bash复制docker version
docker info
能正常输出版本号和系统信息,说明 Docker 服务本身没问题。后面再遇到容器问题,就不要第一时间怀疑 Docker 没装好,而是去看镜像、端口、网络和配置。
2.2 镜像选型:nginx 还是 nginx:alpine
Nginx 官方镜像有两个常见选择:带完整工具链的常规版本,以及基于 Alpine Linux 的精简版本。对大多数普通部署场景,我更推荐先用 alpine 版本。
原因有几点:
- 镜像体积小,拉取快,占磁盘少。
- 基础系统精简,暴露的攻击面更小。
- 功能上满足常规的静态文件服务、反向代理、负载均衡需求。
但有个坑要注意:alpine 镜像里用的包管理工具是 apk,不是 Ubuntu 里的 apt。如果你习惯了进容器执行 apt-get,在 alpine 容器里会直接提示命令不存在。这是正常现象,不是容器坏了。
另外,热词里常见“nginx alpine挂载conf.d报错”。这个报错通常不是镜像本身的问题,而是挂载方式不对。常见原因有两个:
- 宿主机挂载目录的权限不对,容器内 nginx 工作进程读不到配置。
- 把单个文件挂载到了目录上,或者目录挂载后没有更新配置,reload 后仍然用旧配置。
建议在宿主机建立独立目录,例如 /opt/nginx/conf.d,所有站点配置都放在这个目录里,然后整体挂载到容器内的 /etc/nginx/conf.d。不要只挂载一个空文件,也不要挂载到 nginx.conf 更上层的路径,除非你已经完全理解主配置里的 include 关系。
注意:第一次测试时,不要一上来就挂一堆参数。先用一条
docker run命令确认 Nginx 能启动、能访问默认页,再逐步加入配置挂载、日志挂载和端口映射。
3. 容器部署 Nginx:从最小可运行实例开始
3.1 第一次启动:跑通默认页
先跑一个最简单的容器,把 Nginx 默认欢迎页跑出来。执行:
bash复制docker run -d --name nginx-test -p 80:80 nginx:alpine
然后访问本机 http://localhost。看到一个 Nginx 欢迎页,说明容器、镜像、端口映射这条链路已经通了。
这时再做几个检查:
bash复制docker ps
docker logs nginx-test
docker ps 看容器状态是否是 Up,docker logs 看有没有报错。如果容器反复重启,优先看日志,不要急着改 Nginx 配置,因为这时候配置都还是默认的,大概率是端口冲突或者资源问题。
80 端口被占用是最常见的启动失败原因。可以先执行:
bash复制netstat -ano | grep 80
或者用系统自带的端口查询工具。如果 80 被占用,两个选择:关掉占用程序,或者把容器映射到别的端口,例如 -p 8080:80。注意这里左边是宿主机端口,右边是容器内端口,我见过不少人把这两个写反,导致访问失败。
3.2 挂载配置:让 Nginx 真正变成你自己的
默认欢迎页只能证明服务能跑,实际使用必须引入宿主机配置目录。推荐按这个结构准备:
text复制/opt/nginx/
├── conf.d/
│ └── default.conf
├── html/
└── logs/
启动命令调整成:
bash复制docker run -d --name nginx \
-p 80:80 \
-v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \
-v /opt/nginx/html:/usr/share/nginx/html:ro \
-v /opt/nginx/logs:/var/log/nginx \
nginx:alpine
这里每个参数都有明确含义:
-v /opt/nginx/conf.d:/etc/nginx/conf.d:ro:把宿主机配置目录挂进容器,:ro表示容器内部只读,防止容器内误改配置。-v /opt/nginx/html:/usr/share/nginx/html:ro:挂载静态文件目录。-v /opt/nginx/logs:/var/log/nginx:挂载日志目录,方便直接在宿主机查看访问日志和错误日志。
第一次这样启动后,因为宿主机 conf.d 里还没有配置文件,容器内的默认站点可能失效,访问会 403 或 404。这不是错误,而是挂载目录覆盖了镜像默认配置造成的。在 conf.d/default.conf 里写一个最简单的站点:
nginx复制server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
}
然后在 /opt/nginx/html/index.html 里放一个测试页面,执行:
bash复制docker exec nginx nginx -t
docker exec nginx nginx -s reload
先 nginx -t 做语法检查,再 reload。这个顺序必须养成习惯。直接跳过语法检查,配置写错一个分号,reload 不会帮你判断,等到服务全部 502 或 404 再去翻日志,浪费的时间更多。
实际测试时,我不会一开始就写完整站点配置。先在
html目录放一个带明显标记的index.html,能访问到自己的内容,说明挂载没问题,再进入反向代理环节。
4. 反向代理:单站点、多站点、HTTPS 和常见转发头
4.1 单站点反向代理:后端服务不需要直接暴露端口
假设你的后端服务跑在宿主机 127.0.0.1:8080,现在不想让用户直接访问 8080,而是通过 80 端口进入,Nginx 配置可以这样写:
nginx复制server {
listen 80;
server_name app.example.com;
location / {
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;
}
}
这段配置里,最关键的是 proxy_pass。它决定请求往后转发到哪里。后面的几个 proxy_set_header 也不是随便写的:
Host $host:让后端能拿到用户访问的原始域名。很多 Web 框架根据 Host 判断站点,不传这个头会导致链接生成错误。X-Real-IP:记录真实客户端 IP。X-Forwarded-For:记录完整转发链。X-Forwarded-Proto:让后端知道用户是通过 HTTP 还是 HTTPS 进来的。
写完之后还是要走一遍语法检查和 reload。如果访问返回 502 Bad Gateway,先确认后端服务真的在 127.0.0.1:8080 上监听。可以在服务器本机执行 curl http://127.0.0.1:8080 验证,不要直接怀疑 Nginx。
这里还要注意容器网络和宿主机的区别。如果 Nginx 也在容器里,那么 Nginx 容器里的 127.0.0.1 是 Nginx 容器自己的回环地址,不是宿主机的。此时要访问宿主机上的服务,通常要改成宿主机在 Docker 网络中的网关地址,或者把后端服务也放进同一个 Docker 网络里,用容器名访问。这个细节非常容易踩,热词里的“docker网络不通”,有相当一部分就是这种情况。
4.2 多站点配置:一个 Nginx 管理多个域名
开发环境常见需求是:本地虚拟机里跑了好几个项目,希望用不同域名访问不同端口,例如 a.test 访问项目 A,b.test 访问项目 B。
做法是在 conf.d 下建两个文件,每个文件一个 server 块:
nginx复制# a.conf
server {
listen 80;
server_name a.test;
location / {
proxy_pass http://127.0.0.1:8081;
}
}
nginx复制# b.conf
server {
listen 80;
server_name b.test;
location / {
proxy_pass http://127.0.0.1:8082;
}
}
然后本机 hosts 文件里加上域名解析:
text复制127.0.0.1 a.test
127.0.0.1 b.test
这样访问 a.test 进到 8081,访问 b.test 进到 8082。生产环境则是把域名 DNS 解析到服务器 IP,不需要改 hosts。
多站点最容易出的问题是:访问哪个域名都返回同一个站点。这个现象通常是因为只写了一个 server 块监听 80,并且 server_name 用了 _ 或者默认值。Nginx 匹配站点时,优先按 server_name 精确匹配,没有匹配项才走默认。所以每个站点都要写明确的 server_name,不要让多个站点共用同一个默认 server。
如果配合面板工具做多站点反向代理,思路也是一样的。面板只是帮你生成这段 server 配置,但域名、证书路径、后端地址、proxy_pass 是否带路径前缀,这些信息还是需要自己理解。出了问题从生成后的配置文件入手看,比反复点界面更有效。
4.3 HTTPS 和证书不生效的排查
配置 HTTPS 时,常规写法是在 443 端口监听:
nginx复制server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/certs/app.example.com.pem;
ssl_certificate_key /etc/nginx/certs/app.example.com.key;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
在 Docker 场景下,证书文件也要通过挂载方式放进容器,路径以容器内的路径为准。很多人把宿主机路径当成容器内路径写,导致 Nginx 启动时报找不到证书文件。
热词里有个很典型的报错:net::ERR_CERT_COMMON_NAME_INVALID。这个报错的意思是证书里的域名和你访问的域名对不上。使用免费证书服务签发的证书时,要确保证书绑定的域名和浏览器地址栏里的域名完全一致,包括二级域名。example.com 和 www.example.com 一般要分开申请或使用通配证书。
“替换 SSL 证书不生效”也是一个高频问题。大部分情况下不是配置没改,而是这几个原因:
- 浏览器缓存了旧证书。换一个无痕窗口或用 curl 验证,比反复清缓存更高效。
- 证书文件路径没变,但挂载的宿主机文件没有替换干净。
- 只替换了
ssl_certificate,没有替换ssl_certificate_key,证书和密钥不匹配。
还有个容易忽略的问题:如果你同时保留 80 端口的 HTTP 服务,用户访问旧链接可能仍然走 HTTP。生产环境建议把 80 端口统一做重定向,把 HTTP 请求 301 跳转到 HTTPS。
5. 负载均衡:upstream、权重和健康检查
5.1 基础配置:把请求分到多个后端
当某个后端服务按照常规思路拆成多个实例后,Nginx 需要维护一份后端列表,这就是 upstream 块:
nginx复制upstream backend_servers {
server 172.16.0.11:8080 weight=2 max_fails=2 fail_timeout=30s;
server 172.16.0.12:8080 weight=1 max_fails=2 fail_timeout=30s;
}
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://backend_servers;
proxy_set_header Host $host;
}
}
这里几个参数的实际含义:
weight=2:权重越大,分配到的请求越多。上面配置里第一台机器会承担约三分之二的流量。max_fails=2:连续失败 2 次,判断该节点不可用。fail_timeout=30s:在 30 秒内统计失败次数,达到上限后在同样的时间内暂时不把请求分给它。
Nginx 默认的负载均衡算法是轮询,也就是请求依次分发。如果需要按 IP 把同一用户的请求固定到同一台后端,可以用 ip_hash。如果后端机器配置差异大,用 weight 做加权分配更直观。
5.2 容器化负载均衡的常见坑
容器场景下做负载均衡,和物理机场景有不少区别。
第一,后端实例的 IP 是动态的。容器重启后 IP 会变,如果 upstream 里写死 IP,实例一重建,转发就会失效。更稳妥的方式是让 Nginx 和后端服务在同一个自定义 Docker 网络里,用容器名或者 Docker Compose 里的服务名作为 upstream 地址:
nginx复制upstream backend_servers {
server app1:8080;
server app2:8080;
}
Nginx 容器能通过 Docker 内置 DNS 解析到这些服务名对应的当前 IP,后端重建后也能自动找到新地址。
第二,不要只靠 Nginx 层做健康检查。Nginx 判断后端失败用的是请求结果,比如连接超时、返回 5xx。真正生产环境,一般会在后端服务前面加上应用层健康检查接口,让 Nginx 定时探测。比如后端提供一个 /healthz 接口,Nginx 可以配置转发去检查。
第三,如果需求是多台物理机或者多个云服务器的流量入口,单台 Nginx 本身也会成为瓶颈。这时候要考虑在 Nginx 前面再挂一层负载均衡,或者用云厂商提供的负载均衡服务。Nginx 的配置本身可以保持不变,只是上游入口换成了更外层的流量入口。
关于连接数,Nginx 配置里有 worker_processes 和 worker_connections 两个参数。worker_connections 表示每个 worker 进程能维持的最大连接数,总连接数大致等于 worker_processes * worker_connections。遇到“并发上不去”的问题,不要只调这个参数,还要看系统层面的文件描述符限制、内核参数和后端服务能承受的并发。Nginx 调得再大,后端先崩了,结果还是 502。
6. 用 Docker Compose 编排 Nginx 和应用
6.1 一条命令拉起整个环境
单个 docker run 命令能应对一个 Nginx,但完整项目往往包含后端应用、Nginx、数据库多个组件。每个服务写一条 docker run,维护成本会越来越高。此时建议用 Docker Compose。
一个典型的 docker-compose.yml 结构如下:
yaml复制services:
app:
image: your-app-image
expose:
- "8080"
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/certs:/etc/nginx/certs:ro
- ./nginx/logs:/var/log/nginx
depends_on:
- app
restart: unless-stopped
这里有几个细节要注意。
expose 只声明容器内端口可以被其他容器访问,不会把端口发布到宿主机。所以 app 服务不需要 ports,Nginx 通过 Docker 内部网络访问 app:8080 即可。这样最安全,因为后端服务不会直接暴露给外部。
depends_on 控制启动顺序,但它只保证 app 容器先启动,不保证 app 内部的服务已经完全就绪。如果 Nginx 启动后 app 还没准备好,Nginx 会暂时返回 502,app 就绪后请求就能恢复正常。这个不算配置错误,是容器编排的正常现象。
启动命令是:
bash复制docker compose up -d
查看状态:
bash复制docker compose ps
docker compose logs -f nginx
停止并清理:
bash复制docker compose down
down 会停止容器。如果没有加 -v,挂载的宿主机目录数据不会丢。但如果你用匿名卷存储了数据库数据,down 之后数据可能丢失,所以生产场景数据库一般也要用宿主机目录挂载或命名卷持久化。
6.2 Compose 网络里 Nginx 怎么访问后端
在同一个 Compose 项目下,服务名本身就是可解析的域名。比如上面例子中,Nginx 配置里的 proxy_pass 可以写成:
nginx复制location /api/ {
proxy_pass http://app:8080;
}
Nginx 容器会通过 Docker 内置 DNS 把 app 解析成 app 容器当前的内网 IP。
这里有个路径问题:proxy_pass 后面带不带路径,效果完全不同。
proxy_pass http://app:8080;:转发时保留原始 URI。proxy_pass http://app:8080/;:转发时会按 location 前缀处理,常见做法是把匹配到的前缀去掉后拼接。
很多博客里说“加了斜杠就丢路径”,但准确说法是:proxy_pass 后面如果有 URI 部分,Nginx 会进行替换;如果没有,则原样转发。实际调试时,先用不带斜杠的方式测试,能通再加斜杠做路径重写,不要一次改太多。
7. 常见报错与排查链路
7.1 Docker 启动和网络类问题
整理一下最常遇到的几类问题:
| 现象 | 优先检查 | 常见原因 |
|---|---|---|
| Docker Desktop 启动失败 | BIOS 虚拟化、Windows 功能 | VT-x/AMD-V 未开启,或 WSL2 功能未启用 |
| 容器反复重启 | docker logs |
端口冲突、配置目录挂载错误、证书路径不对 |
| 宿主机访问不到容器服务 | 端口映射方向 | -p 宿主机端口:容器端口 写反 |
| 容器访问不了宿主机服务 | Docker 网络模式 | 容器内 127.0.0.1 不是宿主机回环地址 |
| 多个容器之间访问不通 | Compose 网络 | 服务名写错,或没有使用同一个自定义网络 |
排查这类问题,我自己的习惯顺序是:先 docker ps 看状态,再 docker logs 看错误,接着检查端口占用,最后再看配置。很多人喜欢一上来就改 Nginx 配置,结果发现容器根本没起来,改了半天也没用。
7.2 Nginx 配置类问题
典型的报错包括:
nginx: [emerg] bind() to 0.0.0.0:80 failed:端口被占用。找占用程序,或者换端口映射。nginx: [emerg] cannot load certificate:证书文件路径错误,或文件权限不对。先确认容器内路径,再确认挂载目录里有文件。invalid CORS request:这个报错多出现在反向代理层缺少必要的 Header,或者后端 CORS 配置与代理后的 Origin 不一致。需要检查proxy_set_header Origin和后端 CORS 允许列表。- 访问返回 404 但配置看起来没问题:先确认
server_name和location是否匹配上。可以用curl -H "Host: a.test" http://127.0.0.1做测试。
配置类问题的通用排查链路是:
- 执行
docker exec nginx nginx -t,先排除语法错误。 - 执行
nginx -s reload,确认新配置已加载。 - 看
logs/error.log或docker logs nginx,找到具体行号和报错内容。 - 用
curl直接访问 Nginx 入口,观察返回状态码,再逐步测试后端服务本身。
7.3 反向代理和负载均衡类问题
- 502 Bad Gateway:后端服务没启动、Nginx 访问不到后端地址、或者后端地址写错。
- 504 Gateway Timeout:转发到后端后,后端响应太慢。这时可以调整
proxy_read_timeout,但也要去查后端为什么慢,单纯调大超时只是掩盖问题。 - 负载不均衡:检查
upstream里的weight配置,以及是不是有节点被max_fails标记为不可用。 - 会话一直乱跳:如果后端需要保持用户会话,检查是否配置了
ip_hash,或者后端是否已经用独立的会话存储做共享。
负载均衡场景还有一个容易被忽略的点:请求经过 Nginx 转发后,后端日志里记录的客户端 IP 是 Nginx 的 IP,而不是真实用户 IP。所以配置 X-Real-IP 和 X-Forwarded-For 从一开始就要写完整,否则后面排查用户来源、做访问统计、设置限流策略时,数据全是错的。
7.4 安全加固放最后,但不要漏掉
把功能跑通之后,再考虑 Nginx 的基础安全项:
- 关闭或修改
server_tokens off;,不在响应头里暴露 Nginx 版本号。 - 静态目录默认不开启
autoindex,防止目录内容直接列出来。 - 管理后台、接口文档这类敏感路径,不要在公网直接暴露,可以放在内网或加访问控制。
- 如果 Nginx 代理的是本地 API 服务,比如本地模型服务的 API 接口,需要在 Nginx 层配置好访问密钥校验和访问日志,不要只依赖后端的校验。
安全相关配置必须在功能稳定之后再补。功能都没跑通就开始改安全参数,会导致排查问题时多一层干扰。
结尾:把单任务跑稳,再考虑批量化和生产化
这一套 Docker + Nginx 的组合,真正落地时最值得盯住的不是配置写了多少行,而是输入、挂载和转发路径是否清晰。先跑通一个静态站点,再跑通一个反向代理,最后再扩展负载均衡和 Compose 编排。每一层都验证过,再往上层加。
如果你只是学习,默认配置和官方镜像已经够用。如果要长期运行,建议从第一天就把配置目录、日志目录、证书目录分开管理,容器名固定,端口映射写成注释,Compose 文件纳入版本管理。踩过几次之后你会发现,很多问题不是 Docker 或 Nginx 能力不够,而是挂载路径、网络模型和输入配置没有提前理干净。
