Docker实战:用nginx反向代理容器化服务完整指南 📅 发布时间:2026/9/20 10:41:27 👁 浏览次数: 前几天有个朋友问我说想学 Docker但折腾了一周还没把环境跑通。我给他的建议很直接别一上来就整 K8s也别急着啃官方文档先装好 Docker把一个 nginx 容器跑起来再给它配一个反向代理你对容器的感觉就会完全不一样。docker nginx 反向代理这个组合几乎覆盖了容器技术里最核心的几个概念镜像、容器、端口映射、数据卷挂载、容器间网络通信再加上一份 nginx 配置等于把日常运维里最高频的一类需求完整走了一遍。这篇文章就把我实测的完整过程写出来从安装 Docker 开始到你用 nginx 给多个后端服务做反向代理结束中间所有命令、配置和坑都会给到。适合刚接触 Docker 的开发者也适合想快速搭一套本地多环境代理的运维同学。我会尽量把每个操作步骤背后的原因也讲清楚这样你不仅能把命令敲出来还能理解为什么这么写换个场景也知道怎么改。1. 为什么我建议从这套组合开始1.1 Docker 到底解决了什么问题很多人刚开始接触 Docker 时第一反应是“又一个虚拟机”。这个理解不算错但不准确。虚拟机虚拟的是整台机器每个虚拟机里都有完整的操作系统内核启动慢、占用高Docker 虚拟的是运行环境所有容器共享宿主机内核只隔离进程、文件系统和网络空间所以一个容器往往几十兆几秒就能启动。拿我们最熟悉的场景举例以前你在本地写了一个 Python 项目依赖 Python 3.8。后来服务器上装的是 Python 3.11跑起来一堆语法报错。再往后别人接手这个项目配置环境又花了两天。Docker 的做法是把 Python 3.8、所有依赖包、项目代码一起打包进一个镜像里任何人拿到这个镜像在任何安装了 Docker 的机器上跑起来环境完全一致。这就叫“一次构建到处运行”。容器技术真正的价值在于把“应用”和“环境”打包成了同一个东西。你不再需要关心服务器上有没有装 nginx、Redis、MySQL只需要拉取对应的镜像启动容器就行。这也是为什么现代后端开发里Docker 几乎是必会技能。1.2 为什么第一个容器项目选 nginx选 nginx 当入门项目有几个非常实际的原因。一是轻量。nginx 的官方镜像 alpine 版本只有二十多兆拉取快启动只要一两秒特别适合新手反复折腾。你随便改配置、删容器、重建容器成本几乎为零。二是贴近真实场景。nginx 在服务端扮演的角色非常多静态资源服务器、反向代理、负载均衡、SSL 终止层。你学会用容器跑起来一个 nginx等于同时学会了部署一个生产级组件后面直接可以迁移到服务器上使用。三是它天然适合演示 Docker 的核心机制。比如“端口映射”nginx 容器默认监听 80 端口你怎么把宿主机的 8080 端口映射到容器里的 80再比如“数据卷挂载”nginx 的配置文件和静态资源目录都在容器里你怎么把宿主机上的配置目录挂载进去让修改不依赖容器内部。这些概念全部可以拿 nginx 当载体直观又容易验证。我给自己的建议是跟着这篇文章走完一遍然后删掉所有容器自己从头再搭一遍这次不看文档。能独立搭出来才算真的入门了。2. 环境准备主流平台装 Docker 的实操记录2.1 Windows 平台Docker Desktop 安装全流程Windows 上装 Docker最省事的方案是 Docker Desktop。它自带图形界面右下角有个鲸鱼图标点击就能管理所有容器。安装前有两个前置条件一是系统版本需要 Windows 10 64 位专业版或更高版本二是必须开启 CPU 虚拟化也就是 BIOS 里的 Intel VT-x 或 AMD-V。下载 Docker Desktop 安装包后一路点下一步就能装完。装完会提示你重启电脑然后启动软件。第一次启动如果提示需要启用 WSL 2比较快的处理方式是打开管理员权限的 PowerShell执行下面这条命令wsl --update执行完重启 Docker Desktop界面右下角鲸鱼图标变成绿色就说明 Docker 引擎已经正常启动了。在 PowerShell 里输入docker version能看到 Client 和 Server 两段信息。这里有个新手特别容易踩的坑如果只看到 Client 内容Server 报错或者根本没有说明 Docker 引擎没起来多半是 WSL 2 没配好或虚拟化没开。Docker Desktop 的资源占用不算低默认分配的内存可能比较高。如果你是 8GB 内存的机器建议在 Docker Desktop 的设置里把内存调到 4GB 左右避免电脑变卡。如果公司电脑有安全策略限制装不了 Docker Desktop可以退而求其次用 Podman Desktop但这就是另一个话题了。2.2 Linux 平台用命令行完成安装Linux 上安装 Docker 更接近生产服务器上的实际操作我以最常用的 Ubuntu 为例。其实安装流程官方给出了一键脚本但生产环境我不推荐用脚本因为你不知道它到底执行了什么。更可控的方式是走官方 apt 源sudo apt update sudo apt install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod ar /etc/apt/keyrings/docker.gpg echo \ deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.gpg] 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 update sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin装完后执行sudo systemctl status docker能看到 active (running) 就说明服务已经起来了。Linux 下执行 docker 命令需要 root 权限为了避免每次敲 sudo可以把当前用户加入 docker 用户组sudo usermod -aG docker $USER这条命令执行完需要重新登录终端才能生效。有个安全上的细节必须要提醒docker 用户组相当于 root 权限因为容器可以把宿主机目录挂载进来一旦有 docker 组权限意味着可以访问宿主机文件系统。所以在生产服务器上一定不要把不信任的用户加入 docker 组。CentOS 系统的安装方式略有不同最明显的是默认包管理器是 yum/dnf需要的依赖名称也不同。但配置逻辑一致按官方文档走一遍就行。macOS 的安装和 Windows 基本一样也是用 Docker Desktop选 Apple Silicon 还是 Intel 芯片对应的安装包即可。2.3 镜像加速配置解决 pull 镜像慢的问题第一次拉取镜像很多人会卡在“pull access denied”或长时间无响应。这不是因为你网络有问题而是默认的 Docker Hub 服务器在国外国内访问确实很慢。解决办法是配置国内镜像加速器也就是 registry mirror。Docker Desktop 用户直接在 Settings - Docker Engine 里的 JSON 配置中加入 mirror 字段{ registry-mirrors: [ https://docker.m.daocloud.io, https://dockerproxy.com ] }保存后 Docker 会自动重启。Linux 用户则需要修改/etc/docker/daemon.json没有这个文件就新建一个然后将上面的 JSON 内容写进去重启 docker 服务sudo systemctl restart docker配置完成后再执行docker pull nginx:alpine速度会有明显提升。这里要提醒一点镜像加速只影响拉取镜像的速度不影响镜像来源的可靠性你拉下来的镜像依然来自 Docker Hub。我个人的经验是多配置两到三个镜像源作为备选有些源偶尔会不稳定切换一下就好。3. 镜像、容器与仓库先把这三件事搞清楚3.1 镜像、容器、仓库三者关系Docker 的三个核心概念用吃饭来类比特别好懂。镜像相当于菜谱它定义了这道菜的全部内容食材清单、烹饪步骤、最终形态。容器相当于按菜谱做出来的一道菜你可以做一份也可以做十份每份互相独立。仓库相当于存放菜谱的图书馆你需要时从中取一本。具体到命令层面docker pull nginx就是从 Docker Hub 这个公共仓库把 nginx 镜像下载到你本地docker run nginx就是基于这个镜像启动一个容器docker build是写自己的菜谱也就是自定义镜像。在这个过程中你还可以用docker commit把一个容器的状态固化成新的镜像但在实际生产里不推荐这么做应该用 Dockerfile 来描述镜像构建过程。有个细节值得注意同一个镜像可以启动无数个容器容器之间完全隔离。你启动的 nginx 容器里改了index.html不会影响到其他容器也不会影响镜像本身。这个特性在你做实验的时候特别方便随便折腾坏了删除容器重新启动即可镜像还是最初的样子。3.2 常用命令速查够用就好入门阶段命令不需要背太多掌握下面这些足够支撑从安装到反向代理的完整流程。docker image ls # 查看本地镜像 docker ps # 查看运行中的容器 docker ps -a # 查看所有容器包括已停止的 docker pull nginx:alpine # 拉取镜像alpine 是发行版标签 docker run -d -p 8080:80 nginx:alpine # 后台启动 nginx 容器并映射端口 docker stop 容器ID # 停止容器 docker rm 容器ID # 删除已停止的容器 docker rmi 镜像ID # 删除镜像 docker exec -it 容器ID bash # 进入容器内部执行命令 docker logs -f 容器ID # 实时查看容器日志看到docker run后面的参数新手会比较懵。-d表示后台运行-p 8080:80表示把宿主机的 8080 端口映射到容器内部的 80 端口nginx:alpine中冒号前是镜像名冒号后是标签。没写标签时默认用latest但这在正式项目里是有风险的因为 latest 会变动你无法确定部署的是哪个版本。这些命令不需要死记硬背多敲几遍自然就记住了。我现在写脚本时还经常查docker run --help这很正常重要的是理解每个参数到底影响了什么。4. 用 Docker 部署第一个 nginx 容器4.1 拉取镜像与启动容器先执行这条命令把 nginx 镜像拉下来docker pull nginx:alpine如果镜像加速配置正常十几秒就能拉取完成。执行docker image ls确认一下能看到nginx和alpine两列信息SIZE 只有几十兆。现在启动第一个容器docker run -d --name my-nginx -p 8080:80 nginx:alpine--name参数给容器起个名字后面管理容器时不用记一串随机 ID。执行完回到浏览器访问http://localhost:8080能看到 nginx 的欢迎页面。这里有个关键点容器内部只有 80 端口它本身不知道宿主机 8080 端口的存在所有访问 8080 端口的请求都会被 Docker 转发到容器内的 80 端口。如果你是第一次配置页面一直打不开先执行docker ps看看容器是不是退出状态。如果容器状态是 Up那大概率是防火墙拦截了端口如果容器已经退出执行docker logs my-nginx看日志里输出什么错误再针对性解决。4.2 端口映射与目录挂载上一步的端口映射已经能让 nginx 跑起来但离真实使用还差一步现在你想改 nginx 的配置得进入容器内部但容器一旦被删除所有修改都会丢失。这显然不符合实际需求我们需要把宿主机的配置目录挂载到容器里。先看一下 nginx 镜像里的目录结构。执行docker exec -it my-nginx ls /etc/nginx能看到 conf.d、nginx.conf、sites-enabled 等目录。在实际部署中我习惯把宿主机上的一个目录比如~/docker/nginx/conf.d挂载到容器的/etc/nginx/conf.d。这样处理前提是挂载目录里已经有配置文件了否则容器里对应目录是空的nginx 默认配置里引用了该目录下的 default.conf空目录会导致配置加载失败。先停止并删除刚才的容器重新启动一个带挂载参数的容器docker stop my-nginx docker rm my-nginx mkdir -p ~/docker/nginx/conf.d ~/docker/nginx/html ~/docker/nginx/logs docker run -d --name my-nginx -p 8080:80 \ -v ~/docker/nginx/conf.d:/etc/nginx/conf.d:ro \ -v ~/docker/nginx/html:/usr/share/nginx/html \ -v ~/docker/nginx/logs:/var/log/nginx \ nginx:alpine这里有三组挂载参数。conf.d目录我加了:ro表示只读防止容器内部误修改配置html目录是 nginx 网站的根目录你把index.html放进去访问时就能看到自己的页面logs目录把容器日志输出到宿主机排错时直接看宿主机文件不用进入容器。每次修改宿主机文件容器内立即生效不需要重启容器。这是容器开发调试中非常重要的一个能力。4.3 修改 nginx 默认页面挂载好目录后在宿主机~/docker/nginx/html下新建一个index.html写入任意内容!DOCTYPE html html headtitleDocker Nginx Test/title/head body h1Hello from Docker nginx/h1 /body /html刷新http://localhost:8080页面会显示你写的内容。如果你看到的是 nginx 默认欢迎页大概率是挂载目录不对nginx 容器内实际读取的根目录是/usr/share/nginx/html只要宿主机目录正确挂载到这个路径就会显示宿主机里的文件。这个实验能直观理解“数据卷挂载”的价值容器里的代码、配置、日志都与宿主机共享你在宿主机上的任何修改容器内部立即可见。这样做的另一个好处是部署新版本时只需要替换宿主机上的文件然后重启容器即可不需要重新构建镜像。5. 重头戏配置 nginx 反向代理5.1 反向代理是什么为什么用得上反向代理这个概念很多新手听着高大上理解了其实不复杂。正向代理是替客户端访问外部资源比如你在浏览器里配置代理网页请求先经过代理服务器再转发到目标网站服务器看到的是代理服务器的地址。反向代理恰恰相反它在服务器端工作替服务器接收外部请求再把请求转发到内部的多台服务器上。举个例子你有一个域名www.example.com背后有三台不同的服务前端页面跑在 3000 端口API 接口跑在 8080 端口图片服务跑在 9000 端口。如果直接让用户访问不同端口体验差而且暴露了服务结构。用 nginx 做反向代理后所有请求都走 80 端口nginx 根据路径或域名把请求转发到对应服务。在 Docker 场景下nginx 反向代理尤其重要。因为你可能用容器部署了多个应用这些应用每个占用不同端口但对外只暴露一个 80 或 443 端口。nginx 容器就成了流量的统一入口用户访问 80 端口nginx 负责把/api开头的请求转发到后端服务容器把/开头的请求转发到前端容器。5.2 修改 nginx 配置实现反向代理现在我把刚才启动的 nginx 容器改造成一个简单的反向代理。先在宿主机~/docker/nginx/conf.d里新建一个关键配置文件名字叫reverse-proxy.confserver { listen 80; server_name localhost; location / { proxy_pass http://my-nginx-html:80; 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; } }这段配置的意思是所有到本机 80 端口的请求都会转发给上游地址http://my-nginx-html:80。但这里有个问题my-nginx-html这个服务名还不能直接访问需要在 Docker 内创建一个自定义网络让容器之间可以通过服务名互通。先创建一个网络docker network create webnet然后启动一个新的 nginx 容器命名为my-nginx-html加入 webnet 网络docker run -d --name my-nginx-html --network webnet \ -v ~/docker/nginx/html:/usr/share/nginx/html:ro \ nginx:alpine接着把我原来的my-nginx容器也加入 webnetdocker network connect webnet my-nginx此时两个容器在同一个自定义网络中my-nginx就可以通过容器名访问到my-nginx-html。重启my-nginx浏览器访问http://localhost:8080请求会转发到my-nginx-html容器的 80 端口最终显示的还是之前那个自定义页面。这个验证虽然看起来只是绕了一圈但它把 Docker 容器间通信的关键讲清楚了。容器互访不使用 IP而使用容器名。Docker 内置 DNS 会自动解析这个机制让服务发现变得非常简单你不需要维护 IP 映射只要容器在同一个网络里名字就是地址。5.3 多项目反向代理路径分流与域名分流实际项目中一个 nginx 往往要代理多个服务。两种常见策略是不同域名分流和不同路径分流。域名分流是这样的www.example.com和api.example.com都解析到同一台服务器但需要把请求转发到不同后端。nginx 配置里 server_name 来区分server { listen 80; server_name www.example.com; location / { proxy_pass http://web-frontend:3000; } } server { listen 80; server_name api.example.com; location / { proxy_pass http://web-backend:8080; } }域名分流的前提是你有这个域名的解析权限本地测试时可以在/etc/hosts里把域名指到127.0.0.1来模拟。路径分流则是在同一个域名下按 URI 前缀分流到不同服务server { listen 80; server_name localhost; location /api/ { proxy_pass http://api-backend:8080; } location /static/ { alias /usr/share/nginx/html/static/; } location / { proxy_pass http://web-frontend:3000; } }路径分流的顺序有讲究nginx 会按最长前缀匹配所以/api/的请求会先命中location /api/而不是location /。这个特性在配置时要特别留意我曾经因为把location /写在了/api/前面导致所有请求都被转发到同一个后端排查了半天才发现是顺序错了。6. 用 docker-compose 编排整套代理环境6.1 为什么需要 docker-compose手动docker run跑几个容器还能应付但服务一多命令就变得又长又容易出错。而且容器之间还有先后关系比如 nginx 必须在后端服务启动之后启动否则启动时会报“host not found”。管理这些依赖和参数的更好的方式是 docker-compose它用一份 YAML 文件描述整套服务的拓扑关系一条命令启动全部。docker-compose 的定位是“单机多容器的编排工具”。K8s 解决的问题更大但入门阶段不需要上 K8scompose 是最合适的中间层。你不需要学会 YAML 的复杂语法照着写就能跑起来发现错误也能用docker compose logs快速定位。6.2 一个完整的 docker-compose.yml 示例我写一个实际可用的例子一个 nginx 反向代理代理两个后端静态站点。version: 3.8 services: nginx-proxy: image: nginx:alpine ports: - 8080:80 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro depends_on: - site-a - site-b networks: - webnet site-a: image: nginx:alpine volumes: - ./site-a:/usr/share/nginx/html:ro networks: - webnet site-b: image: nginx:alpine volumes: - ./site-b:/usr/share/nginx/html:ro networks: - webnet networks: webnet: driver: bridge在这个配置文件里我用networks定义了一个名为webnet的自定义网络所有服务默认加入这个网络。depends_on指定了服务启动顺序nginx-proxy 会在 site-a 和 site-b 之后启动。这个配置有一个非常明显的优势不需要手动设置--network参数服务之间可以直接通过服务名互相访问。对应 nginx 的反向代理配置./nginx/conf.d/default.confupstream site_a_backend { server site-a:80; } upstream site_b_backend { server site-b:80; } server { listen 80; server_name localhost; location /a/ { proxy_pass http://site_a_backend; proxy_set_header Host $host; } location /b/ { proxy_pass http://site_b_backend; proxy_set_header Host $host; } }目录结构需要提前创建mkdir -p docker-demo/nginx/conf.d docker-demo/site-a docker-demo/site-b在site-a里放一个index.html写上“我是站点A”在site-b里放一个index.html写上“我是站点B”。然后执行docker compose up -d浏览器访问http://localhost:8080/a/会看到站点A的内容访问http://localhost:8080/b/会看到站点B的内容。到这里你就完成了一套用 nginx 做多服务反向代理的完整环境。6.3 管理容器生命周期compose 的常用命令非常少而且很直观docker compose up -d # 启动所有服务 docker compose ps # 查看服务状态 docker compose logs -f nginx-proxy # 查看某个服务的日志 docker compose restart nginx-proxy # 重启某个服务 docker compose down # 停止并删除所有容器 docker compose down -v # 同时删除数据卷慎用这里要特别注意docker compose down -v会把 compose 文件里定义的数据卷一起删除如果里面存了数据库数据那数据就没了。我在测试环境的 MySQL 容器上栽过一次生产的教训是要重视的。还有个细节修改了 compose 文件或挂载的配置文件后只执行docker compose restart不一定生效因为 restart 只是重启容器不会重新读取 compose 配置。正确的做法是执行docker compose up -dcompose 会检测到配置变化并重新创建容器。7. 常见问题排查与避坑实录7.1 端口占用导致容器启动失败启动 nginx 容器时最常见的报错是Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use这说明宿主机 80 端口已经被占用。常见原因是你之前装了系统自带的 apache或者另一个 nginx 在挂着。解决方式一改映射端口比如-p 8080:80解决方式二停掉宿主机的服务。Linux 上可以执行sudo lsof -i :80 sudo systemctl stop apache2这个报错也提醒我一件事生产环境部署 nginx 容器时80 和 443 端口几乎是必须用的所以提前在服务器上检查端口占用很有必要。我一般拿到新服务器第一件事就是netstat -tlnp看一下常用端口的状态。7.2 容器修改配置不生效很多人在宿主机改了挂载的 nginx 配置文件但刷新网页没变化。排查思路是这样的先确认挂载是否真的生效执行docker exec -it my-nginx cat /etc/nginx/conf.d/default.conf对比容器内的文件内容是否与宿主机一致。如果不一致说明挂载目录不对。如果一致再看看 nginx 有没有加载新配置。nginx 配置修改后需要重载而不是重启容器docker exec my-nginx nginx -s reloadreload 是平滑重载不会中断正在处理的请求适合生产环境。这也说明了一个重要的习惯挂载配置到容器之后修改配置、reload、验证是一个完整的流程缺一不可。我曾经改完配置直接刷新页面发现没变化就怀疑挂载有问题折腾半天结果只是忘了 reload。7.3 镜像拉取过慢或超时镜像拉取慢绝大多数情况是镜像源没配置好。在终端执行docker info找到 Registry Mirrors 一行看有没有列出镜像源地址。如果为空回到第 2.3 节配置镜像加速。配置完记得重启 Docker然后再试。还有一个经验是不要在高峰期反复重试同一个镜像有时换个时段拉取会快很多。如果你多次尝试还是失败可以检查一下磁盘空间docker image ls看看是不是缓存了太多无用镜像执行docker system prune -a清理后重试。7.4 反向代理 502 Bad Gateway反向代理配置好后访问出现 502说明 nginx 无法连接到上游服务。排查步骤很简单按顺序来先确认上游容器是否在运行docker ps再确认两个容器是否在同一个网络docker network inspect webnet然后进入 nginx 容器测试连通性docker exec -it my-nginx ping site-a如果 ping 不通说明网络没连上。如果 ping 通了还 502那就是 nginx 配置里的proxy_pass地址的端口不对。比如上游 nginx 暴露的是 80 端口你写了 8080就会连接失败。还有一个隐蔽的坑proxy_pass后面的 URL 有没有带 URI。proxy_pass http://site-a;和proxy_pass http://site-a/;在带 location 前缀的情况下转发路径是不同的。前者会保留完整的原始路径后者会去掉匹配的前缀再转发。如果前端和后端路由对不上多数是这里出了问题。下面整理一张速查表方便收藏场景常见原因快速定位命令容器启动失败端口被占用 / 挂载目录不存在docker logs 容器ID访问不到页面端口映射错误 / 防火墙拦截docker ps、curl localhost:端口配置修改不生效挂载路径错误 / 未执行 reloaddocker exec 容器ID cat 配置文件502 Bad Gateway上游服务不可达 / 网络不在同一网段docker network inspect 网络名镜像拉取超时未配置镜像加速 / 磁盘空间不足docker info查看 Registry Mirrors排错时有个基本思路从底层往上查。先确认容器在不在跑然后确认网络通不通接着确认服务监听端口最后检查配置文件。一层层缩小范围比瞎猜快得多。我个人的体会是这套从安装到反向代理的流程本质上是给你建立了一根“容器思维”的脊柱。以后无论你接触什么容器化项目跑起来一个服务、暴露端口、挂载配置、容器间通信思路都是一样的。如果跟着文章走到了最后一步说明你已经能独立掌控一个容器化的 nginx 了剩下的就是多动手、多折腾、多写配置踩坑多了自然就熟练了。