1. 为什么这个组合值得从零走一遍说实话Docker 和 nginx 这两个词放在一起几乎是服务端开发者绕不开的“第一课”。很多人学 Docker 的时候会犯一个典型的错误——只跑一个 hello-world 容器看到 It works 就觉得自己会了然后发现实际项目中根本用不起来。另一个极端是上来就上 Kubernetes、Service Mesh结果被一堆抽象概念劝退。我建议的路径是装好 Docker拉一个 nginx 镜像把它跑起来再配一层反向代理。这条链路覆盖了 Docker 最核心的几个操作镜像拉取、容器生命周期、端口映射、数据卷挂载、配置文件的组织方式同时 nginx 本身又是你以后一定会用到的工具学完就能落地到真实项目里。“反向代理”这四个字如果你还不熟悉可以先记住一句大白话用户访问的是你的服务器 IP 或域名但真正处理业务的是内网里跑着的多个服务nginx 在这里充当“前台接待”——根据请求的路径或域名把流量分发到不同的后端实例。它的好处太大了可以在前端统一处理 HTTPS 证书、负载均衡、静态资源缓存、请求日志后端服务只需要在自己的端口上安静地干活完全不用关心外部流量长什么样。在 Docker 场景下nginx 本身也只是一个容器但它反向代理的目标可以是宿主机上的端口也可以是 Docker 网络里的其他容器。这篇博文不会有太多“高深”的理论更多是把我实际走通这条路时的步骤、命令、配置片段和踩过的坑整理出来。不管你用的是 Windows、macOS 还是 Linux前期的安装方式略有差异但后面的命令和配置是可以完全复用的。我尽量把每一步的“为什么”也讲清楚这样你以后遇到类似问题不会只会照样敲命令而是真的懂它在干什么。2. 先把 Docker 装好三个平台各自的坑2.1 Ubuntu 环境别再用系统包里那个老古董如果你用的是 Ubuntu安装 Docker 的第一反应可能是apt install docker.io。这个包确实能装上但版本往往偏旧而且和 Docker 官方源的更新节奏不一致。更推荐的做法是走 Docker 官方提供的 apt 源把 GPG key 和软件源配好之后再安装。# 卸载可能存在的旧版本 sudo apt-get remove docker docker-engine docker.io containerd runc # 安装依赖工具 sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release # 添加 Docker 官方 GPG key sudo mkdir -p /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg # 写入软件源信息 echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.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-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin装完之后用sudo docker run hello-world验证一下。如果看到 “Hello from Docker!” 就说明引擎已经正常工作了。这里有个容易被忽略的问题docker 命令默认需要 root 权限每次都要敲sudo很烦而且如果是在脚本里用权限问题会更多。建议把当前用户加入 docker 组sudo usermod -aG docker $USER改完组之后重新登录终端或者newgrp docker切换一下之后就能直接敲docker了。这个步骤官方文档里写得比较隐晦但它直接影响你后续操作的顺畅度。2.2 Windows 和 macOSDocker Desktop 的安装与配置Windows 用户现在首选 WSL2 后端装 Docker Desktop 的时候它会自动帮你做很多配置但有一个前提——必须在 BIOS 里开启虚拟化。装完后打开 PowerShell输入wsl --status看看 WSL2 是否就绪。如果显示的是 WSL1需要手动升级。Docker Desktop 安装完成后建议立刻打开 Settings 看一眼两处配置一是 Resources 里的内存分配默认只有 2GB跑 nginx 加一两个后端服务没问题但如果以后要跑 MySQL、Redis、Elasticsearch 这种吃内存的服务建议调到 4GB 以上二是把 “Expose daemon on tcp://localhost:2375 without TLS” 保持关闭除非你真的有远程调试需求否则这就是一个安全隐患。macOS 上的 Docker Desktop 相对省心唯一要注意的是 Intel 芯片和 Apple Silicon 的镜像兼容问题。Apple Silicon 上跑 x86 镜像会經由仿真层速度有明显下降。所以拉镜像之前先看一眼docker pull的输出确认架构是arm64。实在需要 x86 镜像时再考虑docker pull --platform linux/amd64这种方式只建议做测试用。2.3 安装完成后的健康检查清单我见过太多人装完 Docker 以为万事大吉结果第一步就栽了。建议安装后依次跑这几个命令确认环境docker version docker info docker run --rm hello-world请重点看docker version里的 Client 和 Server 是否都有输出。如果只有 Client 有信息而 Server 报错说明 Docker 守护进程没有起来。Ubuntu 上常见原因是服务没启动sudo systemctl start docker可以解决。Windows 上常见原因是 WSL2 内核太旧去微软官网更新一下 wsl 内核就好。3. 镜像下载慢的根源不只是网络3.1 配置镜像加速器如果你在国内网络环境下装完 Docker第一次拉 nginx 镜像可能会被每秒几十 KB 的速度折磨到怀疑人生。Docker Hub 官方仓库在部分网络环境下访问确实不太稳定好在国内有不少镜像加速器可以配置。这个配置写在 Docker 守护进程的配置文件里{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://docker.nju.edu.cn ] }Linux 上配置文件路径是/etc/docker/daemon.jsonWindows 上是 Docker Desktop 的 Settings → Docker EnginemacOS 同样在 Settings → Docker Engine改完配置后重启 Docker用docker info检查 Registry Mirrors 是否生效。如果拉到一半中断了直接重新执行docker pullDocker 会从断点继续不需要重新开始。3.2 理解镜像分层为什么 nginx 镜像体积没想象中大配置好加速器后执行docker pull nginx:latest你会看到一堆带有Pull complete字样的输出。这里顺带讲一下 Docker 镜像的分层机制。一个 nginx 镜像并不是一整块硬盘拷贝而是由很多只读层叠加而成。每一层对应 Dockerfile 里的一条指令FROM debian是一层RUN apt-get install nginx又是一层。当你拉取镜像时如果本地已经有相同的层就会直接复用这也是为什么拉第二个基于 Debian 的镜像时会快很多。理解了分层之后你也会明白一个常见的“坑”为什么存在容器里修改文件、安装软件后如果不做处理就docker commit提交成新镜像其实是在已有的层上又加了一层镜像会越来越臃肿。真正负责的做法是写 Dockerfile用RUN指令构建新镜像让每一层的职责清晰、可复用。这也是 Docker 官方一直推荐 Dockerfile 而不是 commit 的原因。这里先不展开写 Dockerfile但记住这个概念后面很多东西会用到。4. 启动你的第一个 nginx 容器端口映射和数据卷4.1 端口映射的本质nginx 镜像拉下来之后跑一个容器非常简单docker run -d --name web -p 8080:80 nginx:latest-d是后台运行--name web给容器起个名字-p 8080:80是端口映射。这里的8080是宿主机上的端口80是容器内 nginx 监听的端口。你在浏览器访问http://服务器IP:8080流量先进宿主机的 8080 端口然后被转发到容器内的 80 端口。稍微再深挖一下这个-p参数的底层逻辑。容器默认运行在一个隔离的网络命名空间里有自己的 IP 地址和端口栈宿主机想访问它就必须在防火墙规则里加上 DNAT 规则。Docker 替你把规则写好了但你要知道宿主机监听 8080 的进程必须绑定在 0.0.0.0 上否则流量进不来。如果你本机 8080 端口已经被其他进程占了docker run会直接报port is already allocated这时候换一个宿主机端口就行容器里的 80 端口不需要动。如果你想验证容器内部的端口情况可以用docker port web查看映射关系或者docker exec -it web bash进入容器用curl localhost测试容器内的 nginx 是否正常。这种排查思路在以后遇到“容器起来了但访问不了”的时候会反复用到。4.2 数据卷挂载把配置文件放到宿主机上基础容器跑起来后你会发现一个问题nginx 的默认页面是写死的配置文件在容器里如果你要修改配置要么进入容器操作要么提交成新镜像。这两种方式都不优雅。正确做法是使用数据卷挂载把宿主机上的目录映射到容器内实现“配置在宿主机运行在容器”。docker run -d \ --name web \ -p 8080:80 \ -v /opt/nginx/html:/usr/share/nginx/html \ -v /opt/nginx/conf.d:/etc/nginx/conf.d \ nginx:latest这里的-v参数分成两部分冒号前面是宿主机路径冒号后面是容器内路径。宿主机路径必须写绝对路径否则 Docker 可能会创建出一个奇怪的命名卷导致你找不到文件。关于挂载有两个细节值得注意官方 nginx 镜像里/etc/nginx/nginx.conf会通过include指令引入/etc/nginx/conf.d/*.conf下的所有配置。所以你只需要挂载 conf.d 目录把自定义的 server 配置放进去即可主配置不需要动。-v挂载目录时宿主机目录必须事先建好并为空或作为全新配置使用。如果挂载了一个非空目录到/usr/share/nginx/html它会覆盖容器内原有的默认静态文件。这个行为在有些场景是有意的有些场景却容易让人困惑。4.3 容器管理的基础操作别再只会 docker ps进入实战后你很快会发现自己需要频繁查看容器的状态和日志。这里把最常用的几个命令列出来每个都值得熟练掌握# 查看正在运行的容器 docker ps # 查看所有容器包含已停止的 docker ps -a # 查看容器日志-f 表示持续跟踪 docker logs -f web # 进入正在运行的容器 docker exec -it web bash # 停止/启动/重启容器 docker stop web docker start web docker restart web # 删除容器 docker rm web这些命令本身不难但有一个点经常被新手忽略容器内是精简版的 Debian没有 systemd没有 init 进程所以你在容器里跑systemctl restart nginx会失败。正确的做法是要么改完配置后docker restart web重启整个容器要么进入容器直接/usr/sbin/nginx -s reload热加载配置。热加载不会中断连接适合频繁调整 nginx 配置的场景。5. 反向代理配置从静态站点到多服务分发5.1 最小配置一个 server 块搞定一个站点现在容器跑起来了默认页面能访问了我们把 nginx 从一个“只提供默认页面的服务”升级成一个“真正干活的反向代理”。在宿主机上创建/opt/nginx/conf.d/example.confserver { listen 80; server_name example.com www.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; } }这段配置的意思很直白所有发往example.com的 HTTP 请求nginx 都转发给本机的 3000 端口。server_name决定了哪些域名会命中这个 server 块这是虚拟主机Virtual Host机制的核心。如果你只有一台服务器没有域名也可以通过 IP 访问此时server_name可以写成_表示匹配所有未指定的请求。有一个经常踩的坑必须重点提示proxy_pass http://127.0.0.1:3000里的 127.0.0.1 指的是哪个机器如果 nginx 容器是单独跑的它访问宿主机上的 127.0.0.1 只会访问到容器本身因为它自己在独立的网络命名空间里localhost 就是容器自己。这时候要么在后端服务也容器化并接入同一个 Docker 网络用容器名互相访问要么在docker run时加--network host让容器共享宿主机网络。这两种方案分别对应下面两个小节的内容。5.2 nginx 和多个后端容器在 Docker 网络里协作先看比较推荐的方案把 nginx 和后端服务都接到同一个自定义 Docker 网络里。创建网络docker network create webnet启动后端服务时指定网络docker run -d --name backend-app --network webnet -p 3000:3000 your-backend-image再启动 nginx 时也指定网络docker run -d --name nginx-proxy \ --network webnet \ -p 80:80 \ -v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \ -v /opt/nginx/html:/usr/share/nginx/html \ nginx:latest此时 nginx 配置里的proxy_pass可以直接写容器名location /api/ { proxy_pass http://backend-app:3000; }Docker 自带的 DNS 解析机制会自动把backend-app解析成对应的容器 IP。这个 IP 不是固定的容器重启后可能会变但 Docker 内置 DNS 会始终追踪容器的最新 IP所以你不需要关心它。这就是“容器编排”的最朴素形态没有 K8s没有 Service Mesh但已经能解决 90% 的单机多服务问题。顺带说一句-v参数里的:ro后缀它表示只读挂载。nginx 配置文件只需要被读取不需要被容器内修改加上只读挂载可以防止容器内意外篡改配置是一个很好的安全习惯。5.3 路径匹配location 前缀匹配和代理传递的细微差异反向代理配置里最让人困惑的往往是location的匹配规则和proxy_pass的路径传递。我举个例子假设后端服务有一个接口GET /api/v1/users前端通过 nginx 访问/api/v1/users后端希望收到完整的/api/v1/users路径。那么下面两种配置的效果是不一样的# 效果1后端收到 /api/v1/users location /api/ { proxy_pass http://backend-app:3000; } # 效果2后端收到 /v1/users/api 前缀被吃掉 location /api/ { proxy_pass http://backend-app:3000/; }区别在于proxy_pass的 URI 部分。当proxy_pass后面只有协议和主机时nginx 会把完整的原始 URI 原样传给后端当proxy_pass后面带了路径哪怕是斜杠/nginx 会用location匹配的部分替换掉原始 URI 的前缀再把剩余部分拼接上去。这个行为非常容易踩坑尤其是从网上复制配置的时候一不小心就发现接口 404 了。更复杂的是location的匹配优先级。nginx 的匹配规则大致是精确匹配优先于前缀匹配^~前缀匹配优先于正则匹配~和~*但正则匹配又会反过来优先于普通前缀匹配。我见过太多人在location /里写了正则结果发现完全不生效其实就是没有理解这个优先级。建议初学者把常用的路径写成普通前缀匹配再加一两个精确匹配处理静态资源正则能不用就不用等你有真实场景需要处理动态路由时再回头补这块也不迟。5.4 静态站点和反向代理混布一个端口也能服务多个业务很多个人项目是把前端静态文件和 API 反向代理放在同一个 nginx 里管理的。比如你有一个 Vue 或 React 构建出来的 dist 目录还有一组 Java 或 Node 后端的 REST 接口。可以这么组织server { listen 80; server_name _; # 静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # API 反向代理 location /api/ { proxy_pass http://backend-app:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里的try_files $uri $uri/ /index.html是前端路由需要特别关注的配置。当用户刷新页面访问/login这个路径时静态目录下并没有名为 login 的文件try_files会按顺序尝试查找找不到就回退到/index.html交给前端路由去处理。少了这一行刷新页面就会 404。这是 SPA 部署时非常典型的问题用 nginx 托管前端项目的人基本都会遇到。如果以后业务多了一台服务器要跑多组服务可以用server_name区分域名也可以用路径前缀区分服务甚至可以结合端口区分配置不同 server 块。我的建议是能用域名区分就不要用路径区分因为路径区分会导致前端代码里到处都要写 baseURL后期维护很痛苦。6. 线上部署时最容易忽略的配置细节6.1 容器重启策略服务器重启后服务还能自己回来吗本地开发一切顺利容器运行正常但你有没有想过如果服务器意外重启了Docker 引擎会重新拉起你的容器吗默认情况下不会。所以生产环境启动容器的时候一定要加--restart参数docker run -d --name web \ --restart unless-stopped \ -p 8080:80 \ nginx:latestunless-stopped的含义是除非你手动docker stop否则不管什么原因容器退出Docker 都会自动重启它。这比always更灵活因为always连你手动 stop 后也会尝试拉起有时候反而会造成困扰。重启策略在 docker-compose 里对应的是restart: unless-stopped这个后面会提到。如果你接手了一个别人维护的环境可以先用docker inspect 容器名 | grep RestartPolicy看看现有容器的重启策略这能帮你预判服务器重启后服务能不能自愈。6.2 nginx 日志排错的第一手资料nginx 排错首先看日志不要瞎猜。日志位置取决于镜像官方 nginx 镜像的日志在容器内的/var/log/nginx/目录。如果你希望日志持久化到宿主机方便日后用 logrotate 或采集工具处理启动时再加一个挂载-v /opt/nginx/logs:/var/log/nginx访问日志和错误日志是分开的。访问日志记录每一次请求的方法、路径、状态码和响应时间你可以用它来分析流量。错误日志则记录 nginx 本身的问题比如上游连接失败、配置文件语法错误等。配置语法错误在docker restart的时候就会暴露容器会一直重启失败此时赶紧docker logs 容器名看输出nginx 会告诉你具体哪个文件哪一行有问题。这里有一个非常实用的技巧改完 nginx 配置后可以先在宿主机上执行docker exec 容器名 nginx -t它会验证配置语法是否正确输出syntax is ok和test is successful。确认没问题再 reload避免把线上服务改挂了。这个操作成本极低但能救回大量线上事故。6.3 反向代理超时和流式响应基础配置之外的进阶参数很多人在配置好反向代理后会遇到下载大文件失败、上传接口超时、日志流SSE连接被中断等问题。这通常是因为 nginx 默认的代理超时时间太短。生产环境适当调大这几个参数非常必要location /api/ { proxy_pass http://backend-app:8080; proxy_connect_timeout 60s; proxy_read_timeout 300s; proxy_send_timeout 300s; proxy_buffering off; }proxy_buffering off是给 SSE 这类需要实时推送响应的接口用的关闭后 nginx 不会缓存上游响应而是边收边转客户端能更快感知到数据变化。对于常规的 REST 接口建议保持默认的 buffering 开启状态因为 nginx 可以先拿到完整响应再发给客户端能有效降低后端压力。如果要走 HTTPS需要把证书文件位置挂载进容器并在 server 块加上listen 443 ssl; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key;。这类配置本身不复杂但证书文件的路径问题经常让新手头疼——不要写宿主机路径要写容器内的路径因为 nginx 进程运行在容器里它只能看到容器内挂载进去的文件。如果挂载目录是/opt/nginx/certs:/etc/nginx/certs那么配置里就写/etc/nginx/certs/xxx.crt。7. 多项目挂载和 docker-compose 整合7.1 一个 nginx 容器挂载多个项目目录热搜词里有“docker安装nginx并挂载多个项目目录”这是多租户或一人维护多站点场景下的刚需。本质上就是利用-v多次挂载把不同的前端项目目录映射到容器内的不同路径docker run -d \ --name nginx-proxy \ --network webnet \ -p 80:80 \ -v /opt/projects/site-a:/usr/share/nginx/site-a:ro \ -v /opt/projects/site-b:/usr/share/nginx/site-b:ro \ -v /opt/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:latest然后在配置文件里分别指定server { listen 80; server_name site-a.example.com; location / { root /usr/share/nginx/site-a; index index.html; try_files $uri $uri/ /site-a/index.html; } } server { listen 80; server_name site-b.example.com; location / { root /usr/share/nginx/site-b; index index.html; try_files $uri $uri/ /site-b/index.html; } }这个方案的好处是多个项目共享同一个 nginx 容器资源利用率高每个站点的域名和目录互不干扰配置文件按站点拆分维护时只动 one file 就行。挂载目录加:ro是安全习惯前端的构建产物只需要被读取不需要被容器修改。7.2 用 docker-compose 统一管理 nginx 和后端服务当项目多了之后逐个敲docker run命令会变得非常痛苦。这时候 docker-compose 就派上用场了。它允许你用一个 YAML 文件声明所有服务、网络、数据卷、环境变量和依赖关系一条命令完成整个环境的启动。在项目根目录创建docker-compose.ymlversion: 3.8 services: nginx: image: nginx:latest container_name: nginx-proxy restart: unless-stopped ports: - 80:80 - 443:443 volumes: - /opt/nginx/conf.d:/etc/nginx/conf.d:ro - /opt/nginx/html:/usr/share/nginx/html:ro - /opt/nginx/certs:/etc/nginx/certs:ro - /opt/nginx/logs:/var/log/nginx networks: - webnet depends_on: - backend-app backend-app: image: your-backend-image:latest container_name: backend-app restart: unless-stopped expose: - 8080 environment: - SPRING_PROFILES_ACTIVEprod networks: - webnet networks: webnet: driver: bridge在 docker-compose 文件所在目录执行docker compose up -d这条命令会检查镜像、创建网络、启动容器全部自动化完成。到这里你会发现之前手动创建网络、手动指定--network的步骤都被 compose 文件接管了。expose字段和ports字段的区别需要注意expose只让服务在 Docker 内部网络中可被访问不会映射到宿主机ports则会映射到宿主机端口外部可以直接访问。后端服务没必要对外暴露 8080 端口只有 nginx 需要通过内部网络访问它所以用expose就够了这也是减少攻击面的一个手段。depends_on能保证 backend-app 容器先启动但要注意它只保证“启动顺序”不保证“服务就绪”。比如 Java 服务启动可能需要 20 秒nginx 启动后立刻尝试转发请求到 backend-app此时可能还会得到 502。成熟的方案是在 backend-app 的镜像里加 healthcheck或者在 nginx 配置里配上proxy_next_upstream。刚入门阶段先把depends_on用起来至少能解决大部分场景的启动顺序问题。7.3 镜像命名和标签管理好习惯从第一天养成自己构建镜像时给镜像起个有辨识度的名字和标签特别重要。Docker Hub 上官方镜像的命名规则是用户名/镜像名:标签比如library/nginx:1.25.3。自己构建时建议遵循类似规则项目名/组件名:版本号。构建命令docker build -t myapp/backend:1.0.0 .不要只打latest标签因为latest是浮动的你无法通过镜像名知道当前跑的是哪个版本。线上如果出了事故排查的第一步就是看你部署的镜像版本对不对。用语义化版本号 构建日期组合更可靠比如myapp/backend:1.0.0配合myapp/backend:20241201前者给日常开发用后者给上线回滚用两者通过同样的 image ID 指向同一个镜像。8. 高频故障排查403、404、502、端口冲突的定位链路8.1 403 Forbidden权限还是 root 路径的锅如果你配置好静态站点后访问出现 403优先检查目录挂载权限。容器内的 nginx 默认以nginx用户运行它对挂载进去的宿主机目录需要有读取权限。宿主机目录权限如果太严格比如700容器内肯定读不了。用chmod -R 755 /opt/projects/site-a或者把目录归属改成 nginx 的 uid 可以解决。另一个常见的 403 来源是 index 指令没配置。如果你访问/但目录下 index.html 不叫 index或者根本没有 index 文件nginx 会拒绝列出目录内容返回 403。解决办法要么是配置index index.html;要么显式指定访问的文件路径。注意403 的情况下 nginx 错误日志里一般会写明具体拒绝原因比如 “directory index of xxx is forbidden” 或 “permission denied”。直接docker logs 容器名往往比到处搜答案更快。8.2 404 Not Found静态文件和代理路径的经典误区404 有两种典型来源。第一种是静态文件路径配错nginx 去指定的 root 目录下找不到对应文件。排查思路是进入容器确认目录映射后的实际路径docker exec -it 容器名 ls -l /usr/share/nginx/html如果能看到文件那就确认 nginx 配置文件里的 root 指令是否和实际路径一致。第二种是反向代理路径传递错误就是前面提到过的proxy_pass自带 URI 导致前缀被替换。这种 404 最迷惑人因为文件确实存在后端接口也确实能通但实际请求到的路径不对。遇到这种问题先看后端服务的访问日志对比它收到的 URL 和你期望的 URL 是不是一致。这里再补充一个更隐蔽的场景location /和location /的区别。location /是精确匹配根路径location /是前缀匹配能匹配所有以/开头的路径。如果你写了location /但用户访问的是/index.html这个精确匹配不会命中会继续找其他匹配规则。如果没找到合适的就会落到默认的 404。所以配置静态站点时根路径的 location 最好用location /除非你有非常明确的理由非用精确匹配不可。8.3 502 Bad Gateway上游连不上时的标准排查顺序502 是反向代理场景下的老朋友了。它表示 nginx 成功接收了请求但无法连接到上游服务器。标准排查顺序如下用docker ps确认后端容器是否在运行。如果容器没了或被重启了需要先解决容器状态问题。确认 nginx 容器和后端容器是否在同一个 Docker 网络里docker network inspect webnet查看容器 IP 是否都在。确认 nginx 配置里的上游地址和端口是否写对。容器名拼写错误、端口暴露不匹配都会导致连接失败。进入 nginx 容器手动测试上游连接docker exec 容器名 curl http://backend-app:8080/health。如果 curl 不通说明网络不通或后端未监听如果 curl 能通说明问题在 nginx 配置的某处重新检查 proxy_pass 地址是不是多了什么或者少了什么。查看后端应用日志确认它是否真的在监听那个端口。如果你的后端容器一直重启docker logs 后端容器名里通常会暴露应用启动失败的原因。最常见的不是配置错误而是启动时间太长导致 nginx 的 connect timeout 到点了。这时候调大proxy_connect_timeout可以临时缓解但根治方向始终是让后端服务尽快启动或者用 healthcheck 机制来避免在未就绪时接收流量。8.4 端口冲突80 端口被占用的几种处理方式很多人在服务器上启动 nginx 容器时遇到bind: address already in use因为 80 端口已经被占用了。可能占用 80 的嫌疑对象主要有宿主机上原本安装的 nginx、Apache 或其他 Web 服务也可能是系统里某个进程恰好监听了 80。先用命令查sudo lsof -i :80 # 或者 sudo ss -tlnp | grep :80如果确认是被宿主机自带的 nginx 占了有两种处理思路一种是老老实实用 8080 端口映射先跑通再说另一种是把宿主机自带的网页服务停掉把 80 端口让给容器。生产环境更推荐后者因为 80/443 是标准 Web 端口用户访问时不需要带端口号体验完全不同。停掉宿主机自带 nginxsudo systemctl stop nginx sudo systemctl disable nginx然后容器用-p 80:80启动。记住一旦宿主机上有进程占用了端口Docker 无法帮忙驱逐它只会干脆地报错所以处理端口冲突的主动权始终在宿主机上。9. 从单容器到多容器的思维升级很多人在完成“nginx 反向代理”这个目标后容易产生一种错觉Docker 不过如此。但实际项目里容器之间协作才是常态。一次常规的上线可能涉及前端容器、后端容器、MySQL 容器、Redis 容器再加上一个 nginx 容器做统一入口。如果全部用docker run一个个启动不仅命令冗长而且容易漏掉网络配置更麻烦的是升级维护的时候容易出错。docker-compose 的完整价值在这里才能真正体现出来。前面那个 YAML 文件里只有 nginx 和 backend-app实际项目中加数据库后的版本大致长这样services: mysql: image: mysql:8.0 restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: yourpassword MYSQL_DATABASE: appdb volumes: - mysql_data:/var/lib/mysql networks: - webnet healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 volumes: mysql_data:数据卷mysql_data由 Docker 管理数据不随容器删除而丢失。healthcheck让 compose 能判断数据库是否真正就绪nginx 或后端服务可以通过依赖该健康状态来决定启动时机。容器技术走到这一步本质上是在帮你把“服务器”抽象成“资源池”。你不再需要关心 MySQL 装在哪个目录、配置文件改的是哪一份只需要知道服务之间的依赖关系和端口约定。这个思维转变比我前面写到的任何一条命令都重要。10. 一套可以直接抄的主机部署方案最后分享一套我自己在个人服务器上用的完整部署案例场景是一台全新 Ubuntu 主机跑一个前端 Vite 项目加一个 Node.js 后端都由 nginx 统一入口。按第 2 节的方法装好 Docker 和 compose 插件。创建目录结构mkdir -p /opt/app/frontend mkdir -p /opt/app/backend mkdir -p /opt/nginx/conf.d mkdir -p /opt/nginx/logs mkdir -p /opt/nginx/certs把前端构建产物放到/opt/app/frontend后端项目代码放到/opt/app/backend。在/opt/nginx/conf.d/default.conf写入server { listen 80; server_name _; client_max_body_size 50m; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://backend-app: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; proxy_connect_timeout 60s; proxy_read_timeout 300s; } }在项目根目录的docker-compose.yml写入version: 3.8 services: nginx: image: nginx:latest container_name: nginx-proxy restart: unless-stopped ports: - 80:80 volumes: - /opt/app/frontend:/usr/share/nginx/html:ro - /opt/nginx/conf.d:/etc/nginx/conf.d:ro - /opt/nginx/logs:/var/log/nginx networks: - webnet depends_on: - backend-app backend-app: build: /opt/app/backend container_name: backend-app restart: unless-stopped expose: - 3000 networks: - webnet networks: webnet: driver: bridge在 compose 文件目录执行docker compose up -d --build验证docker compose ps看容器状态curl http://localhost验证前端curl http://localhost/api/health验证后端。这个方案里backend-app 不是从镜像仓库拉取的而是从本地 Dockerfile 构建的。build: /opt/app/backend告诉 compose 用该目录下的 Dockerfile 构建镜像。如果后端代码更新了重新docker compose up -d --build即可完成新版本构建和容器替换。这套配置跑通之后你已经掌握了 Docker 在单机场景下 80% 以上的实际用法。往后的方向要么是深入镜像优化和 CI/CD 自动化要么是转向 Kubernetes、Docker Swarm 这类容器编排平台但无论走哪条路今天这些基础操作和排错思路都会成为你理解更复杂系统的底子。至少在我的实际运维经验里很多线上事故的根因最后都能追溯到当初这些基础配置里某个不起眼的细节。