前一阵子帮朋友梳理一个线上小项目发现他们还在用最原始的方式部署——两台服务器轮流手动改 nginx 配置改完nginx -s reload偶尔还会出现端口被占、配置写错导致整站 502 的情况。我当时就建议他们把 nginx 搬进 Docker用一套容器化的反向代理体系来统一管理结果朋友的第一反应是Docker 我能理解但 nginx 在容器里跑配置文件挂载来挂载去听着就很绕。这个反应其实挺典型的。Docker 刚入门的人最容易卡住的不是docker run本身而是容器里的 nginx 和宿主机上的 nginx 到底有什么区别、配置写在容器里还是写在宿主机里这类问题。这篇博文我就从头到尾走一遍完整链路从 Docker 安装开始一直到用 nginx 容器做反向代理并且会把我在实际项目里踩过的坑、排错的思路一并写出来。不管你是刚接触 Docker 的新手还是已经在用 Docker 跑项目但没怎么碰过反向代理的人这篇文章都能帮你把这条链路打通。1. 为什么我坚持用 Docker 跑 nginx 反向代理很多人的第一反应是宿主机上直接apt install nginx不就完事了吗配置文件放在/etc/nginx/改完systemctl reload nginx几十秒就能上手何必再套一层 Docker这个说法在单机单项目场景下没毛病但一旦你开始同时维护多个项目、多套环境或者需要频繁测试不同版本的 nginx 配置Docker 的优势会非常明显。1.1 反向代理到底解决了什么问题先说反向代理本身。假设你有一台服务器上面跑着三个服务一个 Java 后端占 8080 端口一个 Node.js 服务占 3000 端口还有一个前端静态页面占 80。用户访问时如果直接面对这些端口会碰到两个麻烦一是端口暴露得太多安全面变大二是不同服务的入口地址不一样总不能让用户去记http://xxx:8080这种地址。反向代理就是那个统一入口。它在最前面监听 80 或 443 端口根据域名或 URL 路径把请求转发给后面的真实服务。用户只认准一个地址服务怎么分布在后面都无所谓。nginx 是这个领域最常用的工具没有之一。1.2 Docker 容器化带来的实际好处用 Docker 跑 nginx 比直接在宿主机装多出来的收益我总结成三点第一环境隔离。nginx 依赖的库、版本、配置文件全部打包在镜像里不会污染宿主机。我在一台服务器上同时跑过 nginx 1.20 和 1.24 的容器只为了对比某个 rewrite 规则在不同版本下的行为差异这在宿主机直接安装的方式下基本做不到。第二迁移成本极低。本机跑通的配置把镜像和挂载的配置目录打个包搬到另一台服务器docker run一条命令拉起来行为完全一致。我做过一个实验把整套环境从阿里云迁到腾讯云整个过程没超过一刻钟中间还包括重新拉镜像的时间。第三模版化管理。多个项目共用一个 nginx 容器只需挂载不同的配置文件目录即可。新增一个域名转发规则时丢一个.conf文件进去然后 reload完事。这种操作方式比在宿主机上一层层找配置文件要直观得多。当然Docker 也有它的学习曲线特别是数据卷挂载和网络模式这两个概念。但反过来看恰恰是这两个概念让你对配置属于谁这件事想得比以前更清楚。2. 环境准备Windows 和 Linux 下装 Docker 的不同打开方式Docker 的安装本身不难难的是装完之后环境不工作。这一节把 Windows 和 Linux 两条路线都说清楚特别是一些容易漏掉的细节。2.1 Windows 上安装 Docker Desktop 的关键步骤Windows 装 Docker 基本就是装 Docker Desktop。但很多人装上之后发现docker run hello-world卡住不动或者提示cannot start service大部分原因是底层的运行环境没准备好。Docker Desktop 在 Windows 上依赖 WSL 2Windows Subsystem for Linux安装前需要确保两件事Windows 10 版本在 2004 及以上或者 Windows 11BIOS 里开启了虚拟化支持Intel VT-x 或 AMD-V确认方式很简单打开任务管理器切到性能标签页看虚拟化那一项是不是已启用。如果显示未启用就要进 BIOS 把它打开否则 Docker Desktop 起不来。安装包下载好之后一路 Next 即可。安装完成后会在系统托盘出现 Docker 图标。首次启动会提示是否启用 WSL 2建议直接选使用 WSL 2 而不是 Hyper-V因为 WSL 2 的资源占用和启动速度都比 Hyper-V 方案好。如果之前没装过 WSL 2 的内核组件会弹窗提示下载更新按提示操作就行。有一个很容易被忽略的点Docker Desktop 默认的资源配置里内存可能只有 2GB。如果你打算后跑 MySQL、Redis、nginx 等多个容器建议在 Docker Desktop 的 Settings → Resources 里把内存调到 4-6GB。我见过不少朋友环境装好了结果一跑多个容器就频繁重启调完内存就好了。2.2 Ubuntu 下安装 Docker Engine 的推荐姿势Linux 上推荐装 Docker Engine 而不是 Docker Desktop这也更贴近生产环境的使用方式。以 Ubuntu 22.04 为例官方推荐的安装方式是配置 Docker 的 apt 仓库然后通过 apt 安装。流程大致是sudo apt-get update sudo apt-get install -y ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod ar /etc/apt/keyrings/docker.asc echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release echo $VERSION_CODENAME) stable | sudo tee /etc/apt/sources.list.d/docker.list /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io装完验证sudo systemctl enable docker sudo systemctl start docker sudo docker run hello-world看到Hello from Docker!就说明环境正常。另外每次用sudo docker有点烦。可以把当前用户加进 docker 组之后就不用加 sudo 了sudo usermod -aG docker $USER改完需要重新登录一次终端才能生效。这一条属于常见操作但很多教程没提新手经常被 sudo 搞得心态崩。2.3 镜像下载慢怎么处理热搜词里有一大堆docker镜像下载慢、docker镜像源的问题这确实是个绕不开的话题。解决办法是配置镜像加速器也就是把 Docker Hub 的请求分流到国内的镜像仓库。Linux 上编辑/etc/docker/daemon.json如果文件不存在就新建{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com, https://docker.nju.edu.cn ] }Windows 的 Docker Desktop 里直接在 Settings → Docker Engine 里编辑同一个格式的 JSON 配置即可。改完重启 Dockerdocker info里能看到 Registry Mirrors 一栏列出了你配置的地址说明生效了。镜像加速器不是什么魔法它只是把 Docker Hub 的常用镜像做了一级缓存。如果某个镜像比较冷门加速器缓存里没有还是要回源去拉速度就没那么理想。但在绝大多数场景下配置了加速器之后mysql、redis、nginx 这些常用镜像的拉取速度都会有明显改善。3. 先建立正确的容器心智模型很多教程会直接让你docker run nginx然后告诉你看nginx 跑起来了。但你真正需要理解的不是那条命令而是容器和镜像的关系、端口映射的原理、数据卷的机制。这三个概念搞不明白后面挂载配置、做反向代理一定会出各种莫名其妙的问题。3.1 镜像和容器的关系就像程序和进程镜像Image是只读的模板包含了你运行某个服务需要的全部文件可执行程序、依赖库、配置文件。容器Container是镜像的一个运行实例多了一个可写的薄层用来记录运行时的变化。类比一下镜像就像是安装包解压后的目录容器就像是你实际启动的那个进程。同一个镜像可以启动多个容器容器之间互不干扰。所以修改容器里的文件和修改镜像里的配置是两回事——容器删掉之后所有修改全部丢失但镜像永远干净如初。这也是为什么我不推荐直接在容器里改配置你改完配置容器一删全部归零。正确做法是把配置文件通过数据卷挂载进去让配置文件归属于宿主机容器只是读这些文件。3.2 端口映射到底做了什么事nginx 容器默认监听容器内的 80 端口。容器内部是一个隔离的网络命名空间宿主机访问不到这个 80。-p 8080:80这个参数的含义是把宿主机的 8080 端口映射到容器的 80 端口。外部请求到达宿主机的 8080Docker 会把它转给容器内的 80 端口。这个映射方向很容易被人忽略。一句话记住-p 宿主机端口:容器端口。实际使用中有一个高频需求是端口冲突。当你在宿主机上装了 MySQL 占用了 3306而容器里的 MySQL 也想用 3306那么映射时就必须换成宿主机的其他端口比如-p 3307:3306。Docker 本身不会主动帮你检查宿主机端口是否被占只有在启动容器报port is already allocated的时候你才会知道。3.3 数据卷容器和宿主机之间的共享文件夹数据卷Volume解决的是容器内外数据交换的问题。它的本质就是把宿主机上的一个目录挂载到容器里的某个路径。容器对挂载目录的所有读写都会直接反映到宿主机上。在 nginx 这个场景里常用挂载有三个挂载项宿主机路径示例容器内路径作用nginx 配置目录/data/nginx/conf//etc/nginx/conf.d/存放各站点的 server 配置主配置/data/nginx/nginx.conf/etc/nginx/nginx.conf覆盖主配置文件静态站点目录/data/www//usr/share/nginx/html/存放静态网页搞清楚这个挂载关系之后反向代理的逻辑就非常透明了你不需要进入容器只需要在宿主机上写配置文件reload 一下nginx 就按新配置工作了。这个思路贯穿全文建议花点时间消化。4. 部署 nginx 容器从最小可用到规范挂载理论铺垫完毕开始实际操作。先从最小可用配置开始再逐步过渡到规范挂载多个配置目录。4.1 一条命令跑出一个 nginx先拉镜像docker pull nginx:1.26-alpine我习惯使用 alpine 版本原因很简单体积小默认只有几十 MB比完整版省一半多。跑容器docker run -d --name web-nginx -p 80:80 nginx:1.26-alpine参数说明-d表示后台运行--name web-nginx给容器起个名字-p 80:80把宿主机的 80 端口映射到容器内的 80 端口。浏览器打开http://localhost能看到 nginx 的欢迎页就算成功了。这是一个纯裸跑没有挂载任何东西。数据都在容器里适合测试不适合持久化使用。验证容器状态docker ps docker logs web-nginxdocker logs会输出 nginx 的访问日志和错误日志后续排错时非常有用。4.2 挂载配置文件的标准做法上面那种裸跑方式最大的问题是配置没法改。真正用于生产环境推荐用目录挂载的方式管理配置。我的习惯是把所有和 nginx 相关的文件集中放在一个根目录下结构如下/data/nginx/ ├── nginx.conf ├── conf.d/ │ ├── project1.conf │ ├── project2.conf │ └── project3.conf ├── html/ └── logs/然后启动容器docker run -d \ --name web-nginx \ -p 80:80 \ -p 443:443 \ -v /data/nginx/nginx.conf:/etc/nginx/nginx.conf:ro \ -v /data/nginx/conf.d:/etc/nginx/conf.d:ro \ -v /data/nginx/html:/usr/share/nginx/html \ -v /data/nginx/logs:/var/log/nginx \ nginx:1.26-alpine几个细节敲黑板:ro表示只读挂载。nginx 运行期间不应该修改配置文件只读挂载可以防止误操作也暗示了配置文件在宿主机上改容器只负责读这一设计。nginx 镜像里默认的/etc/nginx/nginx.conf会include /etc/nginx/conf.d/*.conf。所以你把站点配置放在conf.d目录下nginx 会自动加载不需要改主配置。日志目录挂载到宿主机用tail -f实时看日志非常方便。首次挂载有一个常见误区宿主机上的空目录和容器里的非空目录挂载后容器里原来的文件会被遮住。也就是说如果/data/nginx/html是空的容器里/usr/share/nginx/html里默认的index.html就看不到了。解决办法是先把镜像里的默认文件复制出来docker cp web-nginx:/usr/share/nginx/html/index.html /data/nginx/html/index.html这步操作只需要执行一次。复制完之后宿主机上就能看到熟悉的 nginx 欢迎页文件了。4.3 重启容器后配置会丢吗这里要说一个重要概念数据卷的持久性和容器的生命周期是解耦的。容器可以被删除、重新创建但只要你挂载的是同一个宿主机目录配置和数据都在。所以当你改了一个.conf文件不需要删掉整个容器重跑只需要重新加载服务docker exec web-nginx nginx -s reload或者懒人一点直接重启容器docker restart web-nginx区别在于reload是热加载不会断掉正在处理的请求restart会先停止再启动会短暂中断服务。线上环境里我基本只用reload。那什么时候需要把容器删掉重建呢当你改了端口映射、改了挂载路径、升级了镜像版本这些属于容器定义层面的变化才需要重新docker run。4.4 多项目目录挂载的一种实用扩展热搜词里有docker安装nginx并挂载多个项目目录这在同一台服务器上跑多个前端项目时很常见。常规做法是每个项目一个子目录都放在/data/nginx/html下面/data/nginx/html/ ├── project-a/ │ └── index.html ├── project-b/ │ └── index.html └── project-c/ └── index.htmlnginx 配置里用location区分server { listen 80; server_name _; location /a/ { alias /usr/share/nginx/html/project-a/; } location /b/ { alias /usr/share/nginx/html/project-b/; } }还有一种场景是不同项目挂在不同的宿主机目录不想都塞到/data/nginx/html下面。那就可以把宿主机上的多个目录分别挂载到容器内不同路径docker run -d \ --name web-nginx \ -p 80:80 \ -v /home/user/projects/a:/usr/share/nginx/html/a:ro \ -v /home/user/projects/b:/usr/share/nginx/html/b:ro \ -v /data/nginx/conf.d:/etc/nginx/conf.d:ro \ nginx:1.26-alpine然后配置里分别指定 root 路径即可。这种多挂载的方式在团队协作时特别有用每个人负责自己的项目目录不需要碰 nginx 的全局配置。5. 反向代理核心配置让 nginx 把请求转发给后端服务nginx 做反向代理的精髓就在于proxy_pass这一行指令。但具体怎么配置里面有很多门道这一节说说我在实际项目里的完整配置方案。5.1 设计一个典型多服务场景假设现在服务器上有三个服务服务内部端口对外路径前端静态页面80由容器内 nginx 直接服务https://example.comJava API 服务8080https://api.example.com文件上传服务8081https://api.example.com/upload/目标是用一个 nginx 容器接收所有外部请求根据域名不同做分流。5.2 完整的配置文件示例在/data/nginx/conf.d/project.conf中写入upstream backend_api { server 127.0.0.1:8080; } server { listen 80; server_name example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend_api; 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; } location /upload/ { proxy_pass http://127.0.0.1:8081/upload/; proxy_set_header Host $host; } }这段配置做了三件事example.com的请求直接到静态页面不再转给其他服务。api.example.com的请求转给名为backend_api的上游组上游组里就是你的 Java 服务。/upload/这个特定路径单独转给文件上传服务。upstream块的好处是可以配置多个后端节点实现简单的负载均衡。比如upstream backend_api { server 127.0.0.1:8080 weight2; server 127.0.0.1:8081 weight1; }意思是请求会按 2:1 的比例分发到两个节点。当然这里只是演示生产环境里更常见的是指向内网的其他服务器 IP而不是 127.0.0.1。5.3 proxy_pass 结尾斜杠的迷惑行为这是 nginx 反向代理里最经典的坑没有之一。看这两个配置location /api/ { proxy_pass http://backend; } location /api/ { proxy_pass http://backend/; }第一个请求/api/user会被原样转发为/api/user后端收到的是包含/api前缀的完整路径。第二个请求/api/user会把location匹配的部分替换掉实际转发的路径是/user。这个行为的本质是proxy_pass后面如果带 URI即路径以/结尾nginx 会把请求的 URI 去掉 location 前缀部分然后拼接上proxy_pass的 URI。如果不带 URI则保持原始 URI 不变。这个细节我踩过一次大坑。当时配置了一个网关服务后端接口路径本身已经是/api/user了我在proxy_pass后面多加了一个/结果后端收到/user直接 404。排查到半夜才意识到是斜杠的问题。经验准则如果后端接口路径已经包含/api前缀proxy_pass后面不要加/如果后端接口路径不含前缀需要去掉那就在proxy_pass结尾加/。5.4 修改配置后的验证流程修改完配置照着下面这套流程走一遍能最大程度避免改完配置后站点挂掉# 1. 检查nginx配置文件语法 docker exec web-nginx nginx -t # 2. 语法没问题再reload docker exec web-nginx nginx -s reload # 3. 看日志确认请求是否正常转发 docker logs -f web-nginxnginx -t是检查配置语法的命令输出syntax is ok和test is successful才说明没问题。这个检查一定不能跳过。很多时候我改了配置reload 时没有报错但实际请求却报 502回头一看是语法检查时忽略了某个警告。还有一个技巧用 curl 测试转发是否生效注意连接的是容器映射到宿主机的端口curl -H Host: api.example.com http://127.0.0.1/api/user这样做是为了让 curl 请求到达 nginx 时携带正确的 Host 头nginx 才会根据server_name匹配到正确的 server 块。6. 用 Docker Compose 管理整套服务编排单靠docker run命令管理多个容器会有一个明显的痛点命令太长、参数太多环境完全不同。比如跑了 nginx、MySQL、Redis 三个容器每条命令都是几十个参数新环境部署时要一条条重新敲敲完还可能漏掉某个挂载。这时候就该用 Docker Compose 了。6.1 Compose 解决的痛点Compose 是一种结构化声明容器定义的方式。你写一个docker-compose.yml文件把所有容器的镜像、端口、挂载、网络都描述清楚然后用一条命令启动全部服务。它的价值在于配置即代码docker-compose.yml可以进 Git环境变更都有记录一键启停docker compose up -d启动所有docker compose down停止所有统一网络Compose 会自动创建一个网络服务之间可以用服务名互访不需要关心 IP 地址6.2 一个同时包含 nginx 反向代理和多后端的 compose 示例这个示例模拟了一个常见场景nginx 充当反向代理把请求转发给两个后端服务Go 和 Node.js同时项目还依赖 MySQL 和 Redis。version: 3.8 services: nginx: image: nginx:1.26-alpine container_name: gw-nginx ports: - 80:80 - 443:443 volumes: - /data/nginx/conf.d:/etc/nginx/conf.d:ro - /data/nginx/html:/usr/share/nginx/html - /data/nginx/logs:/var/log/nginx networks: - app-net depends_on: - go-api - node-api go-api: image: golang:1.22-alpine container_name: go-api working_dir: /app volumes: - ./go-service:/app command: go run main.go networks: - app-net node-api: image: node:20-alpine container_name: node-api working_dir: /app volumes: - ./node-service:/app command: npm start networks: - app-net mysql: image: mysql:8.0 container_name: mysql-db environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: app_db ports: - 3306:3306 volumes: - /data/mysql-data:/var/lib/mysql networks: - app-net redis: image: redis:7-alpine container_name: redis-cache ports: - 6379:6379 volumes: - /data/redis-data:/data networks: - app-net networks: app-net: driver: bridge注意几个细节container_name指定了容器名但更推荐用depends_on来控制启动顺序。Compose 里不推荐容器名互访而是用服务名互访。比如 nginx 配置里写proxy_pass http://go-api:8080;Compose 网络会解析go-api这个名字到对应的容器 IP。MySQL 的数据目录挂载到宿主机/data/mysql-data容器删了数据还在。networks配置把所有服务放进同一个自定义 bridge 网络这样服务之间才能互相发现。6.3 Compose 常用命令速查# 启动所有服务后台运行 docker compose up -d # 查看服务状态 docker compose ps # 查看某个服务的日志 docker compose logs -f nginx # 进入某个容器 docker compose exec nginx sh # 停止并删除所有容器数据卷默认保留 docker compose down # 停止并删除所有容器及数据卷慎用 docker compose down -v # 配置有变化后重建某个服务 docker compose up -d --build nginx6.4 Compose 环境下的配置继承与复用当你有多个环境测试、预发、生产时可以用环境变量配合.env文件来复用 Compose 配置。在docker-compose.yml里写environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}然后在同目录下创建.env文件MYSQL_ROOT_PASSWORDroot123456不同环境只需替换.env文件内容不需要修改 Compose 文件本身。这是团队协作时非常自然的配置管理方式。7. 高频问题排查我把踩过的坑集中说一遍Docker 加 nginx 的组合排查问题时会遇到一些经典款每次遇到都能让新手头疼半天。我把自己实际踩过和一些朋友遇到过的坑整理一下每一条都是真实场景。7.1 403 Forbidden现象访问 nginx 首页是正常的但访问某个子路径返回 403。排查思路第一步确定 403 是 nginx 返回的还是后端服务返回的。看响应头里的Server字段如果写着nginx说明是 nginx 挡下的。第二步检查这个路径对应的文件是否存在、权限是否正确。在 Docker 挂载场景下最常见的 403 原因是宿主机目录权限问题。比如/data/www/html的属主是 root其他用户没有读权限容器内的 nginx 进程以 nginx 用户运行访问文件时就会 403。解决办法chmod -R 755 /data/www/html chown -R root:root /data/www/html一般设置目录 755、文件 644 就够。用ls -l确认一下文件模式不对就立刻能发现。7.2 404 Not Found这个 404 分两种情况。第一种是静态文件 404多半是root和try_files的路径配错了。比如你把项目放在宿主机的/data/www/project-a但配置里写的是/data/www/project-b自然会 404。检查一下挂载路径和 root 路径是否一致。第二种是反向代理后的 404多半就是我前面说的proxy_pass斜杠问题。后端接口路径是/api/user你代理出去变成/user后端找不到就 404 了。这个直接用 5.3 小节的方法排查即可。7.3 502 Bad Gateway502 表示 nginx 成功收到请求但转发给后端时连接失败了。原因通常是三类后端服务没起来看后端容器状态docker ps确认容器是 Up 状态。后端服务起来了但监听端口不对Java 服务监听 8080你配置转发到 8081连不上自然 502。网络不通Compose 场景跨服务的代理除了配好还要确认两个容器在同一个自定义网络内或者用服务名而不是 IP。另外一个常见的隐蔽原因后端服务绑定的是127.0.0.1如果 nginx 和后端不在同一个容器或同一个宿主机就无法访问。Docker 容器之间互访时后端服务必须监听0.0.0.0才行。7.4 端口冲突的排查提示driver failed programming external connectivity on endpoint或port is already allocated时说明宿主机端口被占用了。排查方式# 看哪个进程占用了端口 lsof -i :80 # 或者 netstat -tlnp | grep :80如果是别的容器占用的用docker ps能看到。这种情况最简单停掉占用的容器或者改你新容器的宿主机端口映射即可。7.5 容器重启后配置不生效有些朋友遇到配置文件在宿主机上改了curl 一下发现还是旧行为。这大概率是因为没执行 reload。修改conf.d下的.conf文件后nginx 不会自动加载必须手动docker exec web-nginx nginx -s reload。另外注意一点检查一下你的挂载方式是不是:ro只读挂载。如果是只读容器内确实无法修改文件但宿主机仍然可以改所以正常情况不影响 reload。我之前碰到过一个案例同事直接进容器用 vi 改了/etc/nginx/conf.d/xxx.conf以为改完了结果容器重启后全部还原——因为他改的是容器内文件不是宿主机挂载目录下的文件。7.6 镜像拉取时提示no matching manifest这种情况通常发生在跨架构场景。比如在树莓派ARM 架构上拉取nginx:latest如果 Docker 默认拉的是 amd64 版本可能就会报错。解决办法是拉取支持多架构的镜像或指定架构版本比如nginx:latestsha256:xxx或者直接在 Docker Desktop 里开启Use containerd for pulling and storing images等选项视版本而定。不过在 x86 服务器上这个问题很少见遇到了知道是怎么回事就行。8. 最后说几个我个人的实践习惯文章写到这里Docker 从安装到 nginx 反向代理的完整链路基本都覆盖了。最后分享几个我在实际项目里形成的习惯谈不上标准答案但至少帮我少踩了不少坑。第一个习惯所有配置文件都放在宿主机上容器里永远不修改任何东西。不管是 nginx 配置、MySQL 配置还是应用代码一律通过挂载目录管理。这样容器本身就是一个临时执行者随时可以删除重建数据和配置都安全地留在宿主机上。第二个习惯给容器做镜像版本锁定。nginx:1.26-alpine这种写法比nginx:latest要稳得多。latest标签在你重启容器或重新部署时可能会拉到新版本行为变化难以预知。锁版本虽然不能完全避免升级但至少升级动作是可控的你明确知道自己在用什么版本。第三个习惯遇到问题先看日志不要瞎猜。docker logs web-nginx里能看到 nginx 的访问日志和错误日志错误信息里通常直接写着具体是哪个配置文件、哪一行有语法错误。哪怕是 403、404 这种看起来跟日志无关的问题日志里往往也能看到那个请求实际落在了哪个 location 上。按日志的信息去排查比在配置里瞎翻效率高得多。这篇文章从安装开始走了一遍纯命令行搭建 nginx 反向代理的完整流程也覆盖了 Compose 编排和常见故障排查。把文章里的实践操作自己动手跑一遍比读十遍都管用。尤其建议拿一台闲置的云服务器把 5.2 小节的配置真实跑起来体验一下域名分流、路径转发、reload 生效这几个操作的实际手感。踩过一两次坑之后这套东西就真正属于你了。