最近有不少朋友问我,工作环境里总是要跟环境部署打交道,Docker 到底该怎么上手?装好了之后又怎么把服务跑起来、怎么让外部请求正确打到内部服务上?这些问题如果只靠零散搜索,很容易一头扎进细节里出不来。
这篇文章我就用一条完整的实战路径来带你过一遍:从零开始装好 Docker,再拉取 nginx 镜像、启动容器、配置反向代理,最后把常见坑都给你指出来。这条路径我已经在不同机器上反复走过很多次,无论你是后端开发想本地起环境,还是运维同学要快速部署服务,还是刚接触容器化想系统入门,按照文章的顺序操作一遍,基本就能摸清 Docker + nginx 的核心玩法。
1. 核心思路拆解:为什么是 Docker + nginx 这台组合
1.1 容器化到底解决了什么问题
先聊一个底层问题:为什么现在部署服务都爱用 Docker?说白了,就是“环境一致性”这四个字。
我经常打一个比方:以前部署一个 web 项目,你要先装操作系统依赖、装运行时、装中间件、配环境变量,每一步都可能因为系统版本、软件源差异或者某个库的版本不对而挂掉。换一台机器等于重新折腾一遍,这就像搬家的时候连水电管道都要重新铺,累不累?
Docker 做的事,就是把你的应用连同它需要的运行环境一起打包成一个“集装箱”。这个集装箱在任何装了 Docker 的机器上都能跑,不管底层是 Windows、Ubuntu 还是 CentOS。对于 nginx 来说也一样,你不需要关心宿主机上有没有编译依赖、有没有 openssl 版本冲突——容器里自带一套干净、确定性的运行环境。这样你本地测出来的行为和生产环境是一致的,调试成本直线下降。
1.2 为什么反向代理要用 nginx 来做
你可能会有个疑问:我已经有应用服务了,为什么还要在前面挡一层 nginx?
这里得先分清楚两个概念:正向代理和反向代理。正向代理是替客户端去访问服务器,你访问不了某个资源,找一个能访问的代理服务器帮你取回来,这是“代理客户端”;反向代理是替服务器接收请求,客户端只知道 nginx 的地址,不知道背后的应用服务器是谁,这是“代理服务器”。
反向代理在实际部署里有几个非常实用的场景,我在生产环境中都踩过:
- 统一入口:多个应用(比如前端、后端 API、管理后台)分别跑在不同端口,通过 nginx 按路径或域名分发到不同服务,外部只需要暴露 80/443 一个入口。
- 负载均衡:同一个服务起了多个实例,nginx 可以把请求轮询或按权重分发到不同实例上,实现水平扩容。
- SSL 终结:证书配置在 nginx 这一层,内部服务继续走 http,既省事又安全。
- 静态资源服务:前端打包后的静态文件直接交给 nginx 托管,性能好,还不用占用应用进程的资源。
所以 Docker 负责把你的 nginx 和环境一起打包,nginx 负责流量调度和转发,两者结合,刚好覆盖了从“环境交付”到“流量管理”的完整环节。这也是为什么这套组合在微服务、前后端分离项目里几乎成了标配。
2. 实操准备:在不同系统上把 Docker 装好并跑通
2.1 Windows 篇:使用 Docker Desktop 快速上手
Windows 上目前最正规的姿势就是装 Docker Desktop。它自带图形界面,集成了 Docker Engine、命令行工具、Kubernetes 集群(可选),对新手来说很友好。
安装步骤大致是这样的:
- 去 Docker 官网下载 Docker Desktop for Windows 的安装包。
- 双击安装,在配置界面勾选 “Use WSL 2 instead of Hyper-V”(推荐,性能更好且兼容性高),然后一路 Next。
- 安装完成后重启系统,打开 Docker Desktop,等待右下角鲸鱼图标变成绿色的 Running 状态。
- 打开 PowerShell 或 CMD,输入
docker version验证安装。
需要注意的坑:安装之前请确保 Windows 的 BIOS 里开启了虚拟化(VT-x / AMD-V),并且系统功能里启用了 “适用于 Linux 的 Windows 子系统” 和 “虚拟机平台”。这两个没开,Docker Desktop 会各种报错。
2.2 Linux 篇:Ubuntu / CentOS 的安装命令
Linux 上装 Docker 就一句话的事,但不同发行版还是有区别。
Ubuntu 上推荐使用官方脚本安装,方便快捷:
curl -fsSL https://get.docker.com | sh这个脚本会自动配置好软件源、安装 docker-ce 和 containerd,装完以后记得把当前用户加入 docker 组,不然每次执行 docker 命令都要加 sudo:
sudo usermod -aG docker $USER改完组之后必须重新登录或者执行newgrp docker才能生效,否则会提示 permission denied。
CentOS / RHEL 系可以用官方仓库安装:
sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo yum install -y docker-ce docker-ce-cli containerd.io sudo systemctl start docker sudo systemctl enable docker2.3 安装验证与基础命令速记
无论哪个平台,装完之后都建议先跑一遍基础检查:
# 查看版本,确认客户端和服务端都正常 docker version # 查看当前 Docker 运行状态 docker info # 跑一个 hello-world 测试容器 docker run --rm hello-worldhello-world镜像能正常拉取并打印提示,说明 Docker 引擎已经正常工作。
这里顺便把接下来会反复用到的几个核心命令整理一下,你可以先收藏:
# 镜像操作 docker pull nginx:latest # 拉取镜像 docker images # 查看本地镜像列表 docker rmi nginx # 删除镜像 # 容器操作 docker ps # 查看运行中的容器 docker ps -a # 查看所有容器(包括已停止) docker start/stop/restart <容器名> # 启动/停止/重启容器 docker rm -f <容器名> # 强制删除容器 docker logs -f <容器名> # 跟踪容器日志 # 进入容器内部 docker exec -it <容器名> bash2.4 一个绕不开的问题:镜像下载慢怎么办
第一次拉取镜像的时候,很多人会被下载速度气得摔键盘。这主要是因为官方仓库在国外,跨海传输带宽有限。
解决方案是配置国内镜像加速器。在 Docker Desktop 的 Settings → Docker Engine 里,或者在 Linux 的/etc/docker/daemon.json里,添加镜像源配置:
{ "registry-mirrors": [ "https://docker.m.daocloud.io", "https://dockerproxy.com", "https://docker.nju.edu.cn" ] }修改之后重启 Docker 服务让配置生效:
sudo systemctl restart docker配置好之后再用docker pull nginx,速度会有天壤之别。镜像源如果失效了,可以换一个,网上随时能找到可用的公共加速地址,但注意一定要选大厂或者高校维护的,稳定性有保障。
3. nginx 容器部署与反向代理的完整配置流程
3.1 拉取镜像与首次启动
环境搞定之后,我们正式进入主题。第一步先拉取 nginx 官方镜像:
docker pull nginx:latest拉取完成后,先简单启动一个临时容器,验证一下 nginx 能不能正常工作:
docker run -d --name test-nginx -p 8080:80 nginx这条命令的含义拆开解释:
-d:后台运行容器,不占住当前终端。--name test-nginx:给容器起个名字,后面操作方便。-p 8080:80:端口映射,宿主机 8080 端口接收到的请求转发到容器内部的 80 端口。nginx 默认监听 80,所以这里必须映射 80,宿主机的端口可以随意换。nginx:指定使用的镜像名。
启动后在浏览器访问http://宿主机IP:8080,能看到 nginx 默认的欢迎页,说明容器已经跑起来了。这个欢迎页的意义在于:Docker 容器内部的网络是隔离的,能通过宿主机端口访问到容器内服务,说明端口映射链路是通的,后续反向代理配置有了一个可靠的起点。
3.2 理解 nginx 容器内的目录结构
直接启动容器虽然简单,但你随即会遇到一个尴尬的问题:nginx 的配置文件在容器里面,如果直接在容器里改配置,容器一删配置就全没了。这不符合我们的“可复现、可迁移”需求。
所以正式使用前,必须先把容器内的关键目录挂载到宿主机上。nginx 镜像里核心路径有三个:
/etc/nginx/nginx.conf:主配置文件,控制全局行为,引用其他配置。/etc/nginx/conf.d/:用来放各个 server 块的配置文件,通常一个站点或一个应用对应一个.conf文件。/usr/share/nginx/html/:默认的静态网页目录。
在宿主机上创建好对应目录,然后启动容器时用-v参数挂载进去:
mkdir -p /opt/nginx/conf.d mkdir -p /opt/nginx/html mkdir -p /opt/nginx/logs docker run -d \ --name web-nginx \ -p 80:80 \ -v /opt/nginx/conf.d:/etc/nginx/conf.d \ -v /opt/nginx/html:/usr/share/nginx/html \ -v /opt/nginx/logs:/var/log/nginx \ nginx这样之后修改宿主机上的配置和网页文件,容器内会直接同步生效,不需要再跑进容器里折腾编辑器。
3.3 反向代理的核心配置详解
接下来是这篇文章的重头戏:配置反向代理。
假设你的服务器上跑了一个后端应用,监听在 8081 端口(比如一个 Java 服务或者 Node.js 服务),现在需求是:用户访问http://你的服务器/,nginx 把请求转发到http://你的服务器:8081/,同时用户感知不到后端服务的存在。
在宿主机/opt/nginx/conf.d/下新建一个配置文件,命名为myapp.conf,内容如下:
server { listen 80; server_name your-domain.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; proxy_set_header X-Forwarded-Proto $scheme; } }逐行说下关键配置的含义:
server_name:匹配域名。如果本地测试,填localhost或留空默认也行。proxy_pass:转发目标地址。注意这里写的是127.0.0.1:8081,因为 nginx 容器和宿主机是共享网络的?——不对,这里有个大坑,如果你用默认的 bridge 网络模式,容器内的127.0.0.1指的是容器自己,不是宿主机。所以反向代理后端应用时,地址要写宿主机的内网 IP,或者使用 Docker 的魔法地址host.docker.internal,或者干脆把网络模式设为 host。
这个坑很多人都会踩,我单独在下一节详细说。
proxy_set_header系列:这些 header 是让后端服务能拿到客户端真实的 IP 和协议信息。否则后端看到的请求来源全是 nginx 容器,日志排查会一头雾水。
改完配置后重载 nginx 让配置生效:
docker exec web-nginx nginx -s reload这种方式的优雅之处在于,nginx -s reload可以在不中断服务的情况下应用新配置,不会因为配置错误造成长时间停机。
3.4 bridge 网络模式下代理宿主机服务的正确姿势
如果你按照 3.3 的配置把proxy_pass写成http://127.0.0.1:8081,访问时大概率会得到502 Bad Gateway。原因就是上面提到的:nginx 跑在容器里,容器内的 127.0.0.1 是容器自己的网卡,宿主机上监听 8081 的服务它根本够不着。
解决办法有三条路,按推荐程度排序:
方案一:使用 host 网络模式
启动容器时加--network host,让容器直接共享宿主机的网络栈:
docker run -d \ --name web-nginx \ --network host \ -v /opt/nginx/conf.d:/etc/nginx/conf.d \ nginx这种模式下,nginx 的127.0.0.1:8081就是宿主机的127.0.0.1:8081,配置简单直接。缺点是端口隔离能力变弱,nginx 占用的是宿主机端口,不过对于单机部署 nginx 的场景,这是最省心的选择。
方案二:使用特殊 DNS 名称
在 Linux 上,Docker 默认没有host.docker.internal这个解析。但在 Docker Desktop(跨平台)和较新版本的 Docker Engine 中,加一段配置可以启用:
docker run -d \ --name web-nginx \ --add-host host.docker.internal:host-gateway \ -p 80:80 \ -v /opt/nginx/conf.d:/etc/nginx/conf.d \ nginx配置里就可以这样写:
proxy_pass http://host.docker.internal:8081;方案三:使用宿主机局域网 IP
直接写宿主机的局域网 IP,比如proxy_pass http://192.168.1.100:8081;。缺点是这个 IP 可能变化,不够灵活,但如果你只是本地测试,完全可行。
我个人的建议是:如果只想跑一个 nginx 容器做网关层,优先用 host 网络模式;如果还要和其他容器做联动,再考虑 bridge +host.docker.internal的方案。
3.5 反向代理多站点场景的配置示例
很多时候一个 nginx 要做多个站点的分发,比如:
app1.example.com分发到后端 8081app2.example.com分发到后端 8082测试环境根据路径/api/转发到某个服务
这种场景下,只要在conf.d目录下新建多个.conf文件即可,每个文件一个 server 块,nginx 启动时会自动加载所有配置文件。
再举个例子,一个前端页面 + 后端 API 分离的典型配置:
server { listen 80; server_name www.example.com; # 前端静态资源直接由 nginx 托管 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # API 请求全部反向代理到后端服务 location /api/ { proxy_pass http://host.docker.internal: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; } }这里有个容易踩的细节:proxy_pass的路径结尾是否带斜杠,会影响转发后路径拼接。location /api/搭配proxy_pass http://xxx:8081/,请求/api/users会被转发为/users,也就是location前缀会被替换掉;如果proxy_pass不带斜杠写http://xxx:8081,请求会被转发为/api/users,路径原样保留。具体用哪种,取决于后端接口的设计,你需要跟后端同事确认好再写。
这种一个 nginx 里同时托管静态资源和处理接口转发的模式,就是典型的前后端分离部署形态,生产环境里大量项目都是这么玩的。
4. 进阶玩法:docker-compose 编排 nginx 和多个后端服务
4.1 为什么要用 docker-compose
当你的部署体量变大,比如要同时启动 nginx、后端服务、数据库、Redis,每启一个容器都要敲一长串docker run命令,这就变得不可维护了。这时候就该上 docker-compose。
docker-compose 的核心思想是“用一份 YAML 文件描述整个应用栈的拓扑结构”,一个命令就能把整套服务启起来。它还解决了服务间通信的问题:在同一 compose 项目里,容器可以通过服务名互相访问,不需要记 IP,不需要做端口映射到宿主机才能互通。
4.2 实战:写一个多服务编排文件
假设我们要编排一个典型 Web 应用:nginx + 后端 API + MySQL。对应docker-compose.yml文件如下:
version: '3.8' services: nginx: image: nginx:latest container_name: gateway-nginx ports: - "80:80" - "443:443" volumes: - /opt/nginx/conf.d:/etc/nginx/conf.d - /opt/nginx/html:/usr/share/nginx/html - /opt/nginx/logs:/var/log/nginx depends_on: - backend networks: - app-net backend: image: my-backend:latest container_name: backend-service environment: - DB_HOST=mysql - DB_PORT=3306 - DB_PASSWORD=secret depends_on: - mysql networks: - app-net mysql: image: mysql:8.0 container_name: app-mysql ports: - "3306:3306" environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: myapp volumes: - mysql-data:/var/lib/mysql networks: - app-net volumes: mysql-data: networks: app-net: driver: bridge重点看几个设计要点:
networks自定义了一个 bridge 网络app-net,三个服务都在这个网络里。nginx 配置文件里代理后端时,proxy_pass可以直接写http://backend:8081——backend就是 compose 里服务名,由 Docker 内置 DNS 自动解析,这是和单容器部署最大的区别。depends_on控制容器的启动顺序,先启动数据库再启动后端。注意它只保证启动顺序,不保证数据库完全可用,所以后端应用代码里最好有重连机制。- mysql 的
ports这里映射了 3306,这是为了方便你在宿主机上用数据库客户端连上去调试。实际生产环境如果不希望数据库暴露到外部,可以把ports去掉,只保留内部网络访问。
启动方式和单容器不同,准备工作做完后:
docker-compose up -d查看服务状态:
docker-compose ps查看日志:
docker-compose logs -f停止并删除所有服务:
docker-compose down这套玩法一旦用熟了,你会发现环境交付的复杂度被压得极低。新同事入职,把代码和这条 compose 文件给他,一条命令搞定环境,省下多少排障时间,不可估量。
5. 常见问题与故障排查技巧实录
5.1 镜像拉取慢、超时
现象:docker pull nginx卡很久,或者报net/http: TLS handshake timeout。
排查思路:先确认镜像源是否配置成功。执行docker info,在Registry Mirrors一栏能看到当前使用的镜像加速地址。如果没有配置,参照 2.4 节配置后再重启 Docker;如果配置了还是慢,把镜像源换成另一个再试。不同地区对不同镜像源的访问速度差异很大,多试几个总有一个快的。
5.2 端口被占用导致容器启动失败
现象:执行docker run报Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use。
排查思路:这说明宿主机 80 端口已经被别的进程占用了。执行:
sudo lsof -i :80或者:
sudo netstat -tlnp | grep :80查看是什么进程占用了端口。处理方法有两种:停掉占用端口的程序,或者把 nginx 容器的端口映射换成一个未被占用的端口(比如-p 8080:80)。
5.3 访问 nginx 页面返回 502 Bad Gateway
现象:浏览器能访问到 nginx,但页面显示 502。
排查思路:502 意味着 nginx 无法连接到它要代理的后端服务。按这个顺序排查:
1.后端服务是否还在运行?docker ps看看容器状态。 2.后端服务监听端口是否正确?进入后端容器执行curl http://127.0.0.1:8081验证。 3.nginx 容器里访问后端地址是否通?执行:
docker exec web-nginx curl http://host.docker.internal:8081这条命令如果超时,大概率是网络模式或地址写法问题。回到 3.4 节检查你的转发目标地址,是不是写了容器自己感知不到的地址。
5.4 配置改了但没生效
现象:修改了宿主机挂载的.conf文件,刷新页面没变化。
排查思路:nginx 不会自动热加载新配置。每次改完配置要执行重载命令:
docker exec web-nginx nginx -s reload执行后配置立即生效。如果重载时报错,先检查配置文件里是不是少了分号或括号。可以用这条命令在容器里校验配置文件的语法:
docker exec web-nginx nginx -t5.5 容器日志里反复报链接被拒绝
现象:docker logs web-nginx里出现大量connect() failed (111: Connection refused)。
排查思路:这个错误表明 nginx 尝试连接后端的 IP 和端口,但没人监听那个端口。最可能的原因就是反向代理地址写错,或者后端服务没有在该地址上启动。用docker exec进入容器手动 curl 一下代理目标,能快速定位是网络不通还是端口不对。
5.6 容器启动后立刻退出
现象:执行docker run后容器没几秒就退出了,docker ps -a看到状态是 Exited。
排查思路:查日志:
docker logs 容器名nginx 容器如果启动即退出,通常是挂载的配置文件有问题,目录挂载到了错误的位置,或者容器内的配置引用了不存在的路径。此时可以把-v挂载先去掉,用默认配置启动,再逐步挂载排查,定位是哪个挂载项导致问题。
5.7 实际操作中最容易被忽略的三个细节
最后分享几个我踩过多次的坑,提醒你注意:
第一,挂载文件而不是挂载目录时,宿主机的文件必须存在。如果把单个配置文件挂载到容器里,但宿主机上这个文件不存在,Docker 会帮你创建一个目录,结果容器内路径变成了目录,nginx 直接拒绝启动。
第二,修改完配置记得先执行nginx -t再 reload。配置文件写错了直接 reload 会导致 nginx 拒绝加载,整个服务还可能挂掉。先检查语法,再应用,这个习惯能帮你省掉至少一半的排障时间。
第三,容器删除之前确认数据已经持久化。如果你启动容器没有挂载任何数据卷,直接docker rm -f之后,容器内所有配置和数据都会消失。尤其是 nginx 的配置文件,一定挂载到宿主机再操作,否则每次重建容器都要重新配置一遍,那是真的血泪教训。
写在最后
做运维和部署这件事,工具一直在变,但底层的思路没有变:环境要一致,配置要可管,状态要可控。Docker 把这三个诉求收敛成了一套标准化的操作方式,而 nginx 反向代理则是流量入口最基础也最重要的一环。
按我个人的习惯,刚接触这套东西时不用急着啃官方文档,先照着文章把 Docker 装好、把 nginx 容器跑起来、把反向代理配置通。当你亲手把一个请求从浏览器经过 nginx 转发到后端服务、再原路返回看到正确响应的时候,整个链路的工作原理就自然刻在脑子里了。之后再去看官方文档里更细的参数,你会发现一切都是顺理成章的事。
如果你照着这篇实操下来遇到了文章里没提到的问题,建议先执行docker logs 容器名看一下日志,再对照docker inspect 容器名检查网络和挂载配置,90% 的问题都能通过这些线索找到答案。祝愿一次跑通。