1. 为什么我推荐用 Docker 跑 nginx 反向代理
先聊个现实问题。很多朋友在自己的服务器上装 nginx,第一反应是apt install nginx或者直接下载编译包,然后在系统目录里改配置,改坏了还得小心翼翼备份恢复。这种传统方式不是不行,只是当你需要在同一台机器上跑好几个 Web 服务、或者需要快速迁移环境的时候,系统级安装的那套玩法就会开始拖后腿。
这篇文章的主角是 Docker 加 nginx 反向代理。简单说,就是通过 Docker 把 nginx 跑在一个独立容器里,再用它把外部请求转发给后端不同的应用服务。它的组合价值在于:nginx 负责流量入口和路由规则,Docker 负责环境隔离和快速部署。你不需要在自己电脑或服务器上装一堆依赖,也不需要担心把系统环境弄乱,所有东西都以容器方式管理,换机器、备份、回滚都变得非常直接。
这篇文章适合谁看?适合对 Docker 有一定了解、但还没真正把 nginx 反代跑起来的人;也适合那些用传统方式装过 nginx、但想换成容器化方案的人。我会从安装 Docker 开始,一路讲到 nginx 反向代理的完整配置,中间会把为什么这样做、有哪些容易踩的坑一起讲清楚。
2. 整体方案的设计思路
2.1 用容器跑 nginx 解决了什么问题
在动手之前,我想先花一点时间把设计思路理清楚,因为很多人装完了 Docker、拉完了 nginx 镜像,却不知道这套组合到底解决了自己什么问题。
首先,环境隔离是容器方案最大的价值。以前我在服务器上直接装 nginx,用的是系统自带的 OpenSSL、PCRE 等库,一旦系统升级或者别的软件改了依赖,nginx 可能就跟着出问题。用 Docker 跑 nginx,镜像里自带一整套运行时环境,和宿主机完全隔离。这意味着你在 A 机器上跑得好好的配置,迁到 B 机器上只要同样装好 Docker、同样拉取镜像,行为就是一致的,不会因为系统版本差异导致各种奇怪故障。
其次,版本切换成本极低。传统方式下,想从 nginx 1.18 升到 1.24,得折腾编译参数、配置文件兼容性;用 Docker 只需要改一下镜像 tag,启动一个新容器替换旧容器就行。我实际体验下来,整个升级过程不超过两分钟,而且可以随时回滚到旧版本。
还有一个容易被忽略的点——配置管理更清晰。传统安装方式下,nginx 配置分散在/etc/nginx/nginx.conf、/etc/nginx/conf.d/以及各种软链接之间。用容器方案,我把配置文件统一放到宿主机的一个目录里,再用挂载方式把它映射进容器。这样配置文件归配置文件、日志归日志,结构非常直观。
2.2 为什么选择挂载目录而不是改容器内部文件
这里有一个新手最容易纠结的地方:nginx 容器启动后,默认配置文件在容器内部的/etc/nginx/目录下。那我是直接docker exec进容器里改文件,还是把文件挂载进去?
我的建议是:永远用挂载,不要直接改容器内部文件。原因有三点:
第一,容器是一个临时性资源,删除重建是常态。你直接进容器改文件,容器一旦被删,所有改动全部丢失。而挂载到宿主机的目录,即使容器没了,配置还在,重启一个新容器就能恢复。
第二,直接进容器改文件不方便排查问题。容器内默认没有 vi/vim,甚至没有 bash 也不是不可能,你连个编辑器都找不到。与其在容器里折腾,不如在宿主机上用自己的编辑器改完配置,然后重启容器加载。
第三,挂载目录天然就是一个备份。我把所有服务的配置都放在/opt/docker/nginx/conf.d/这种结构下,每次改动前复制一份,出问题马上能回滚。
打个比方,容器就像一辆临时租赁的车,你可以在里面开,但别把个人物品长期留在车里。真正的行李应该放在自己家里(宿主机目录),要用的时候挂载进去。
2.3 反向代理的核心逻辑先理解再配置
很多人一上来就抄配置,proxy_pass http://localhost:8080;这种代码背得滚瓜烂熟,但遇到实际场景还是会懵:为什么有时候加斜杠、有时候不加?为什么proxy_set_header要写那一堆东西?
先理清一个基础概念:反向代理就是替客户端去访问后端服务,再把结果返回给客户端。nginx 在这里扮演的是一个“中介”角色。客户端只认识 nginx 的地址和端口,完全不知道背后其实有一堆服务在分别处理请求。
所以反向代理配置的核心就两件事:
- 监听谁:nginx 监听哪个端口,匹配哪个域名或 URL 前缀。
- 转发给谁:命中规则后,请求被转发到哪个后端地址。
这两件事搞清楚之后,剩下的诸如proxy_set_header传递真实 IP、proxy_pass结尾斜杠对路由的影响,都是在围绕这两件事做细节打磨。这篇文章后面的配置演示也会从这两个角度去拆解。
3. Docker 环境准备
3.1 Windows 场景下的 Docker Desktop 安装
如果你手头只有 Windows 电脑,想先本地体验一把,那 Docker Desktop 是最省事的选择。下载地址在 Docker 官网,装的时候保持默认选项就行。需要注意的一点是,Docker Desktop 在 Windows 上依赖 Hyper-V 或者 WSL 2 后端,安装过程中如果提示你启用相关功能,照着操作然后重启即可。
装完以后,打开命令行敲一下docker --version,看到类似Docker version 24.0.7的输出就说明安装成功了。这里还有一个很容易被忽略的步骤——在 Docker Desktop 的设置里把镜像源换成国内源。不换的话,后面拉取 nginx 镜像时你可能要等很久。
Docker Desktop 本身自带一个图形化界面,可以看到容器列表、日志、资源占用,本地开发用起来非常舒服。不过我得提醒一句,Windows 版 Docker 跑 Linux 容器本质上是靠一个轻量级的 Linux 虚拟机在幕后撑着,所以在 Windows 上做实验没问题,真要部署到服务器上,还是建议直接用 Linux 服务器。
3.2 Linux 服务器安装 Docker(Ubuntu 为例)
服务器部署我推荐使用 Ubuntu 系统,安装过程也最省心。我踩过 CentOS 上各种源、依赖的坑,后来统一换到 Ubuntu 之后,Docker 安装基本就是复制粘贴几条命令的事。
先更新包索引:
sudo apt update然后安装依赖包:
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common添加 Docker 官方 GPG 密钥和仓库:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null更新索引并安装 Docker:
sudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io安装完成后,设置 Docker 服务开机自启,并验证:
sudo systemctl enable docker sudo systemctl start docker sudo docker run hello-world如果最后一条命令能输出一段欢迎信息,说明 Docker 已经跑起来了。为了让当前用户不用每次都加sudo,你可以把用户加入 docker 组:
sudo usermod -aG docker $USER newgrp docker3.3 镜像源配置:解决下载慢的问题
这个坑几乎人人都会遇到。默认情况下,Docker 从 Docker Hub 拉取镜像,但在国内网络环境下速度非常慢,动不动就超时。我的做法是给 Docker 配置国内镜像加速器。
在 Linux 上,编辑/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.nju.edu.cn" ] }保存后重启 Docker 服务:
sudo systemctl daemon-reload sudo systemctl restart docker之后拉取镜像的速度会有明显改善。镜像源的选择其实不用太纠结,找两三个主流加速地址轮询就行。多配置几个的好处是,某个源临时挂了,Docker 会自动尝试下一个。另外提醒一句,如果你在公司的内网环境,可能还需要配置代理或者使用公司内部镜像仓库,这个要看具体环境。
Windows 上操作更简单,Docker Desktop 的设置界面里有 Registry mirrors 选项,直接填入上面的地址保存重启即可。
4. nginx 容器部署全流程
4.1 拉取 nginx 镜像并理解 tag 含义
镜像源配置好了之后,拉取 nginx 镜像就非常快了。先搜索一下有哪些版本可选:
docker search nginx不过docker search只能看到镜像的基本信息,要看具体有哪些 tag,我一般是直接去 Docker Hub 网页上查看,或者用docker pull拉取时经常用到的几个稳定版本:
nginx:latest:最新稳定版,开发学习用可以,生产环境建议指定具体版本号。nginx:1.24:某个具体大版本的最新补丁版。nginx:1.24.0:完全固定的版本号。nginx:alpine:基于 Alpine Linux 的精简版,镜像体积小很多,但用的库可能不同。
我实际项目中的选择标准其实很简单:生产环境必须指定具体版本号。比如nginx:1.24.0,因为这样可以保证你回滚的时候,拉到的镜像和之前完全一致,不会出现“我明明没改配置,升级后行为却变了”的问题。
拉取镜像:
docker pull nginx:1.24.0查看已拉取的镜像:
docker images输出列表里能看到镜像名、tag、镜像 ID、创建时间和大小。
4.2 创建配置目录结构
启动容器之前,先把宿主机的目录结构准备好。我一直强调,容器里不存放任何配置和日志,全部通过挂载映射出来。我习惯的目录结构是这样的:
/opt/docker/nginx/ ├── conf.d/ # 各站点反向代理配置 ├── nginx.conf # nginx 主配置 ├── certs/ # SSL 证书 ├── logs/ # 访问日志和错误日志 └── html/ # 静态页面创建目录:
mkdir -p /opt/docker/nginx/{conf.d,certs,logs,html}这里有一个小技巧,如果你还不确定 nginx 默认配置长什么样,可以先用下面这条命令启动一个临时容器,把默认配置拷贝出来作为基础:
docker run --rm --name nginx-temp -d nginx:1.24.0 docker cp nginx-temp:/etc/nginx/nginx.conf /opt/docker/nginx/nginx.conf docker cp nginx-temp:/etc/nginx/conf.d /opt/docker/nginx/conf.d docker stop nginx-temp这样做的意义在于,你能拿到一份和镜像版本完全匹配的默认配置,很多人在网上抄了一个新版的配置,用在旧版镜像上,就会报各种指令不存在的错。用镜像自带的配置作为起点,很大程度上可以避免这类问题。
4.3 启动第一个 nginx 容器
目录准备好之后,执行启动命令。我先把最简单的启动流程演示一遍,先不挂载配置文件,确保容器能跑起来:
docker run -d --name nginx -p 80:80 nginx:1.24.0这条命令的意思是用nginx:1.24.0镜像启动一个后台运行的容器,容器名是nginx,把宿主机的 80 端口映射到容器内的 80 端口。
启动成功后,访问http://你的服务器IP,如果看到 nginx 的欢迎页面,说明容器已经正常运行了。
然后我们停止并删除这个临时容器,用挂载方式重新启动:
docker stop nginx docker rm nginx重新创建一个带目录挂载的容器:
docker run -d \ --name nginx \ -p 80:80 \ -p 443:443 \ -v /opt/docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ -v /opt/docker/nginx/conf.d:/etc/nginx/conf.d:ro \ -v /opt/docker/nginx/certs:/etc/nginx/certs:ro \ -v /opt/docker/nginx/logs:/var/log/nginx \ -v /opt/docker/nginx/html:/usr/share/nginx/html \ --restart=always \ nginx:1.24.0这里每个参数都有自己的目的:
-p 80:80和-p 443:443:同时映射 HTTP 和 HTTPS 的默认端口。如果你服务器上 80 端口已被占用,比如还在跑系统自带的 nginx,要么先停掉那个服务,要么把宿主机的端口换成 8080。-v参数:把宿主机目录挂载到容器内目录。注意,配置文件的挂载我加了:ro后缀,意思是容器内部只能读不能写,防止在容器里不小心改到配置。--restart=always:当 Docker 服务重启或者容器意外退出时,自动拉起容器。服务器配置这个参数非常有用,否则机器重启后你得手动启动容器。
挂载配置启动后,如果没报错,依然能正常访问 nginx 欢迎页,那就说明整个目录结构已经生效了。
4.4 容器管理常用命令补充
在继续配置反向代理之前,先把容器日常管理用到的命令过一遍。这些是我平时使用频率最高的一组操作:
# 查看运行中的容器 docker ps # 查看所有容器(包括已停止的) docker ps -a # 查看容器日志 docker logs nginx # 进入容器内部的 bash docker exec -it nginx bash # 重启容器 docker restart nginx # 停止、启动、删除容器 docker stop nginx docker start nginx docker rm nginx有人会问,配置改了之后到底是docker exec去改容器里的文件,还是修改宿主机挂载目录的文件?答案很明确——修改宿主机挂载目录里的文件,然后用docker restart nginx或者docker exec nginx nginx -s reload重新加载配置。
用nginx -s reload的好处是不中断服务,只是重新加载配置。对于配置调整比较频繁的场景,用 reload 更稳妥。不过要注意,如果配置语法有错误,reload 会失败并保留旧配置继续运行,相当于有了一层安全保护。
5. 反向代理配置实战
5.1 理解 nginx 配置文件的层级结构
到了这一步,容器已经正常启动,接下来进入正题——反向代理配置。我直接以一个实际的场景为例:服务器上跑了一个前端页面(端口 3000)和一个后端 API 服务(端口 8080),希望通过 nginx 统一对外提供访问。
先把 nginx 主配置nginx.conf的关键结构理解清楚。nginx 配置是分块的:
events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; # 引入 conf.d 下所有 .conf 文件 include /etc/nginx/conf.d/*.conf; }这段配置里,events块处理的是连接相关的通用设置,http块包含所有 HTTP 服务的配置。include /etc/nginx/conf.d/*.conf是重点——它会把/etc/nginx/conf.d/目录下所有.conf文件加载进来。在我们的挂载结构里,这个目录就是宿主机上的/opt/docker/nginx/conf.d/。所以实际配置站点规则时,不要去改nginx.conf的主结构,而是在conf.d目录新建文件就行。
5.2 第一个反向代理配置:转发给单台后端服务
现在,我在conf.d目录下新建一个配置文件,就叫default.conf:
vim /opt/docker/nginx/conf.d/default.conf内容如下:
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; } # 后端 API 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; } }这段配置做了两件事。第一件事:所有访问example.com根路径的请求,会转发到宿主机的 3000 端口,也就是前端页面服务。第二件事:所有以/api/开头的请求,转发到 8080 端口,也就是后端 API 服务。
保存文件后,先测试一下配置语法是否正确:
docker exec nginx nginx -t如果输出syntax is ok和test is successful,说明配置没有问题。然后重新加载配置:
docker exec nginx nginx -s reload先解释一下几个关键配置项的作用,因为它们直接影响转发链路是否正常工作:
proxy_pass http://127.0.0.1:3000;:这是转发的目标地址。注意,在容器内部,127.0.0.1指向的是容器自身,而不是宿主机。那为什么这里能访问到宿主机的服务?因为 nginx 容器默认的网络模式是bridge,在这种模式下,容器内可以通过宿主机在 Docker 网桥上的 IP(通常是172.17.0.1)访问宿主机,也可以通过host.docker.internal访问宿主机。实际上,nginx 官方镜像在编译时加入了host.docker.internal的支持,所以这里的127.0.0.1需要特别注意——如果你在宿主机上直接运行 nginx,这个写法没问题;但在容器里,它会去找容器自身,找不到就会 502。这是我踩过的一个大坑,后面在常见问题部分会详细展开。proxy_set_header Host $host:这个非常关键。如果不设置这个 Header,后端服务收到请求时拿到的 Host 是127.0.0.1:3000,它就无法判断客户端到底是用什么域名访问的服务。对于需要判断域名做路由的 Spring Cloud Gateway 这类服务来说,这个 Header 丢失会直接导致路由失败。proxy_set_header X-Real-IP $remote_addr:传递客户端的真实 IP。因为经过 nginx 转发后,后端服务看到的连接来源是 nginx 的 IP。如果不显式传递这个 Header,后端服务记日志、做访问控制时拿到的都是 nginx 的地址,这对业务来说往往是不可接受的。X-Forwarded-For:如果请求本身带了这个 Header(比如经过了多层代理),这里会在原有值后面追加当前客户端的 IP。X-Forwarded-Proto:标识客户端原始请求的协议是 HTTP 还是 HTTPS。后端服务在生成重定向链接时经常需要用到这个信息,否则明明客户端走的是 HTTPS,后端却返回 HTTP 链接。
5.3 配置多个站点:用不同域名区分服务
实际场景里,一台服务器往往不只有一个服务。比如我自己的服务器上同时跑着博客、一个 API 服务、还有一个小型管理后台。如果都用同一个 IP + 不同端口对外,不仅记不住,而且很多服务默认只监听 80/443 端口,不太方便。
最佳实践是:一个server块对应一个域名,让 nginx 根据请求的 Host 头来区分路由到哪个后端。
还是以刚才的default.conf为例,假设我现在要新增一个 admin 服务,域名是admin.example.com,后端跑在 8081 端口。我在conf.d目录下新建一个文件admin.conf:
server { listen 80; server_name admin.example.com; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }保存后测试并 reload:
docker exec nginx nginx -t docker exec nginx nginx -s reload这样,访问example.com时进入 3000 端口的前端服务,访问admin.example.com时进入 8081 端口的管理后台服务。尼ginx 在收到请求后,会根据请求头中的 Host 字段自动匹配对应的server块。
这里有一个特别容易出问题的细节:如果server_name匹配不上任何一个 server 块,nginx 会使用默认 server(第一个 server 块或者显式标记default_server的那个)来处理请求。我遇到过这样的情况:配置了两个 server 块,结果域名 A 和域名 B 访问到的都是同一个服务的页面。排查了半天,发现是两个 server 块的server_name写错了,其中一个拼写错误导致所有请求都落到了默认 server 上。建议每次配置完,用curl -H "Host: admin.example.com" http://你的IP测试一下是否命中了正确的 server 块。
5.4 带路径前缀的反向代理:注意 proxy_pass 的坑
再讲一个非常容易掉进去的坑——location后面带路径前缀时,proxy_pass的写法对最后转发出去的 URL 影响很大。
还是以/api/为例,假设后端的真实接口地址是http://127.0.0.1:8080/users,而前端请求的是http://example.com/api/users。我们期望 nginx 能把/api前缀去掉,把请求转发到/users。
有两种写法,结果完全不同:
写法一(不带路径):
location /api/ { proxy_pass http://127.0.0.1:8080; }这种写法会保留完整的 URI,也就是说,请求/api/users会原样转发到http://127.0.0.1:8080/api/users。
写法二(带路径):
location /api/ { proxy_pass http://127.0.0.1:8080/; }注意,proxy_pass结尾多了一个/。这种情况下,nginx 会用location匹配到的前缀之外的剩余部分去拼接目标地址。也就是说,请求/api/users会被转发到http://127.0.0.1:8080/users,前缀/api被吃掉了。
这两种写法没有绝对的对错,取决于你的后端服务是否自带/api前缀。如果前端通过 nginx 访问后端 API,后端路由本身已经带/api,那用写法一;如果后端暴露的接口不带前缀,只有前端通过 nginx 路径来区分,那就用写法二。
我刚才在很多项目里看到团队因为这个问题来回折腾:要么后端路由加前缀,要么 nginx 去掉前缀,最后前端又改接口路径,整个链路改得一团糟。我的建议是:选一种方案定下来,然后所有服务都遵循同样的规则。比如统一约定“nginx 层负责路由前缀,后端不感知前缀”,那么所有反向代理都用proxy_pass http://127.0.0.1:8080/;(加斜杠)这种写法。
5.5 再加一层:WebSocket 的代理配置
现在不少运维场景会用到 WebSocket,比如实时通知、在线聊天。nginx 代理 WebSocket 的配置和普通 HTTP 代理略有不同,因为 WebSocket 协议在握手阶段需要发送Upgrade和Connection两个 Header。
配置方法是在proxy_set_header中额外加入两行:
location /ws/ { proxy_pass http://127.0.0.1:8080/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }proxy_http_version 1.1是必须的,因为 HTTP/1.0 不支持 WebSocket 升级。如果你只是搭了个普通 HTTP 反向代理,不用纠结这个问题;但只要后端涉及 WebSocket,记得把这三行加上。
5.6 HTTPS 反向代理配置要点
现在是 2024 年,HTTPS 已经是基本要求。配置 HTTPS 反向代理说难不难,说简单也容易漏东西。
首先,准备证书文件。我用的是 Let's Encrypt 的免费证书,证书文件一般分为三个:fullchain.pem(证书链)和privkey.pem(私钥)。把这两个文件放到之前挂载的/opt/docker/nginx/certs/目录下。
然后在 server 块中增加 HTTPS 监听:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; 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; } }同时,把原来的 HTTP 80 端口配置改为跳转到 HTTPS:
server { listen 80; server_name example.com; return 301 https://$host$request_uri; }这里注意一个容易犯错的地方:ssl_certificate的路径是容器内部的路径,不是宿主机的路径。因为我们挂载的是/opt/docker/nginx/certs:/etc/nginx/certs,所以配置里写的是/etc/nginx/certs/fullchain.pem。
还有一个与容器相关的问题:证书更新。Let's Encrypt 证书有效期是 90 天,如果你用 certbot 在宿主机上自动更新证书,容器内挂载的文件会自动同步更新(因为是同一个目录),但是 nginx 不会自动重新加载证书。我一般会写一个简单的定时任务,每周检查一次证书,如果证书有更新就执行docker exec nginx nginx -s reload。这一步不做的话,证书过期前最后一周你会收到很多告警邮件。
6. 常见问题与排查技巧
6.1 容器内无法访问宿主机服务(502 Bad Gateway)
这是我在 Docker 加 nginx 反向代理里遇到的最常见问题。配置没问题、容器也正常启动,访问时却返回 502。
502 的含义是 nginx 无法连接到上游服务。原因很可能是:proxy_pass里写的地址在容器内不可达。比如你在宿主机上跑了一个服务监听 8080 端口,在proxy_pass里写http://127.0.0.1:8080,容器启动后访问会 502。因为容器内的127.0.0.1指向容器自己,宿主机上的服务它自然访问不到。
解决方法有三种:
- 如果用
--network host模式启动容器,容器共享宿主机的网络栈,127.0.0.1就能正常访问宿主机服务。但代价是容器没有独立网络,端口映射相关参数就失效了。 - 在
proxy_pass中使用 Docker 网桥网关 IP:一般是172.17.0.1。这个方法有效,但 IP 可能会变,不够稳定。 - 最简单可靠的方案:把后端服务也用 Docker 跑起来,加入同一个自定义网桥网络,用服务名互相访问。
第三种方案是我目前最推荐的。创建一个自定义网络:
docker network create mynet启动后端服务时指定网络:
docker run -d --name backend --network mynet your-backend-image启动 nginx 时也加入同一网络:
docker run -d --name nginx --network mynet -p 80:80 -v ... nginx:1.24.0然后在 nginx 配置里直接写服务名:
location / { proxy_pass http://backend:8080; }nginx 会根据容器名自动解析到对应的容器 IP。这样做的优势是,容器重新创建后 IP 变化了,配置完全不用改。
6.2 端口被占用
很多朋友在自己的电脑上装了系统级 nginx,再用 Docker 起一个 nginx 容器,然后发现端口冲突。
docker run -d --name nginx -p 80:80 nginx:1.24.0报错信息类似bind: address already in use。这是因为宿主机的 80 端口已经被系统 nginx 占用了。
解决办法有两个。要么停掉系统 nginx:
sudo systemctl stop nginx要么换个宿主机端口映射:
docker run -d --name nginx -p 8080:80 nginx:1.24.0用第二种方式,访问地址就变成了http://IP:8080。
我建议系统里装过 nginx 的话,直接把系统级 nginx 停用并设置开机不自启,然后所有 Web 服务统一走 Docker 方案。否则每次开机,系统 nginx 先占用 80 端口,Docker nginx 再启动就会失败,非常烦人。
6.3 配置改了但没生效
这种问题往往是最磨人的。修改了/opt/docker/nginx/conf.d/default.conf,reload 了配置,访问还是老页面。
先别怀疑 nginx 是不是坏了,按以下顺序排查:
第一,确认改的文件是不是真的被挂载进容器了。运行:
docker exec nginx cat /etc/nginx/conf.d/default.conf看看容器里的内容和宿主机上是否一致。不一致说明挂载路径有问题。
第二,确认配置语法正确:
docker exec nginx nginx -t语法不对,reload 会失败,但 nginx 还会用旧的配置继续跑,所以页面看起来没变。
第三,确认浏览器缓存。有时候配置和容器里文件都改了,但浏览器缓存的旧页面没刷新。用无痕窗口访问,或者curl -I http://你的地址看一下响应头。
还有一个细节:include /etc/nginx/conf.d/*.conf只加载.conf结尾的文件。有些人新建了default文件没有加.conf后缀,nginx 静默忽略,没有任何报错。这种情况最容易让人懵。
6.4 403 Forbidden 问题
反向代理配置好了,访问静态文件时返回 403。出现 403 通常是权限问题,但 Docker 容器里的权限问题往往更隐蔽。
一种典型原因是挂载的宿主机目录权限过高:比如/opt/docker/nginx/html目录的所有者是 root,权限是 755,容器内 nginx 进程以 nginx 用户运行,没有读取权限。
排查时可以先看容器日志:
docker logs nginx日志里如果有Permission denied,说明就是权限问题。解决方法是把宿主机目录权限调整一下:
sudo chmod -R 755 /opt/docker/nginx/html另一种情况是目录或文件名大小写不对。nginx 对location路径是严格区分大小写的,如果页面文件是index.HTML,而配置里写的是index.html,nginx 就找不到对应文件。虽然 nginx 默认提供了try_files $uri $uri/ /index.html这类兜底策略,但实际使用中还是尽量保持文件命名和配置一致。
6.5 请求头里的 Host 异常
配置了反向代理之后,后端服务一直报错,说拿到的 Host 不对。这种情况我遇到过很多次。
比如前端应用调用后端 API 时,后端服务做域名校验,要求必须是api.example.com才能通过,否则拒绝请求。如果 nginx 配置里没有设置proxy_set_header Host $host,后端收到请求时 Host 可能就是127.0.0.1:8080,自然校验不通过。
解决办法很简单,在location或server块加上:
proxy_set_header Host $host;如果后端有特殊需求,也可以硬编码指定 Host:
proxy_set_header Host api.example.com;6.6 反向代理性能调优
配置跑通之后,还有一个话题值得关注:性能调优。nginx 默认配置其实非常适合做教学演示,但生产环境压力上来之后,最好做一些调整。
在nginx.conf的events块中:
events { worker_connections 4096; }worker_connections表示每个 worker 进程可以同时维持的最大连接数。默认 1024 对于一般场景够用,但如果并发量上来了,可以适当调大。注意,这个值受到系统文件描述符限制的影响,所以也要检查一下宿主机上的ulimit。
在http块中加入:
gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024;启用 gzip 压缩可以显著降低传输体积,尤其是 JSON 和 JavaScript 这种文本类资源。
如果代理大文件下载或者音视频流媒体,需要调大超时时间:
proxy_connect_timeout 60s; proxy_read_timeout 300s; proxy_send_timeout 300s;默认 60 秒的超时时间对于长请求可能不够。
6.7 反向代理常见问题速查表
为了方便之后快速排查,我整理了一个表格,这些是我在多个项目中实际遇到并解决的问题:
| 现象 | 可能原因 | 排查方法 | 解决方法 |
|---|---|---|---|
| 502 Bad Gateway | 代理地址无法访问 | docker logs nginx看日志 | 确认后端服务启动、网络连通 |
| 504 Gateway Timeout | 后端响应太慢 | 查看后端日志 | 调大 proxy_read_timeout |
| 403 Forbidden | 目录权限不足 | docker logs nginx看错误日志 | 调整宿主机目录权限 |
| 404 Not Found | 静态文件路径不对 | docker exec nginx ls查看容器内文件 | 调整 root 路径 |
| 配置改了没生效 | 语法错误/未 reload | docker exec nginx nginx -t | 修复语法后 reload |
| 域名访问到错误服务 | server_name 写错 | curl -H 测试 | 修正 server_name |
| 反复跳转登录 | Cookie 域不对 | 查看浏览器 Cookie | 设置 proxy_cookie_domain |
7. 写在最后的几个建议
这篇文章从 Docker 安装、目录规划、nginx 容器部署讲到反向代理配置和常见问题排查,基本上覆盖了一套可落地的完整流程。最后我想根据个人经验再分享几个建议。
第一,目录规划要趁早。如果你只是随便跑通一个 demo,目录结构无所谓;但如果你预计这台服务器上会跑多个服务、多个站点,强烈建议一开始就把目录结构理清楚。我见过很多服务器,conf.d 目录被各种命名混乱的文件塞满了,.conf、.bak、.conf.old挤在一起,维护成本极高。我的习惯是每个站点一个文件,文件名与域名相关,比如example.com.conf、admin.example.com.conf,一目了然。
第二,日志一定要挂载出来。nginx 容器如果不挂载日志目录,容器删除后日志就没了。排查线上问题的时候,日志是最重要的线索。我见过有人排障排了半天,最后发现容器的日志早就被轮转掉了。把/var/log/nginx挂载到宿主机目录,配合 logrotate 做日志清理,省心很多。
第三,始终保留一份旁路配置。每次大改之前,把当前的conf.d目录备份一份,命令就是cp -r一下的事。这个习惯救了我很多次,有时候改着改着发现自己把规则逻辑搞反了,直接回滚到之前一份备份就行,不用靠记忆恢复。
第四,如果可能,尽量把后端服务也容器化,放到同一个 Docker 网络里。这样反向代理配置里直接用服务名访问,网络层面的问题会少很多。当你有多台服务器、需要上 Docker Compose 或 Kubernetes 的时候,这套以容器为单位的部署和管理方式也能平滑迁移。
这几年用 Docker 跑 nginx 反向代理,我最大的体会是:Docker 本身并不难,难的是把它和服务编排的思维真正融入到工作流里。希望这篇文章能帮你在起步阶段少走一些弯路。