Docker + Nginx 反向代理:从“我电脑能跑”到“哪里都能跑”

不知道你有没有遇到过这种场景:接手一个三年前的老项目,文档上写着"请使用 Node 16 + npm 8 启动",而你本机装的是 Node 22。跑起来报错,降级又怕影响手头其他项目。这时候你心里大概率会冒出一句: "我电脑能跑,你电脑怎么就跑不起来?"

这不是玄学,这是环境依赖在作祟。今天的文章就用一个最小实战,带你用 Docker 和 Nginx 反向代理,把"能跑"这件事从个人电脑上拆下来,变成一份可以分发的配置。


一、Docker 到底解决了什么问题?

先抛开那些厚重的概念,记住一个类比:

Docker = 应用 + 运行环境

就像海运的集装箱,你把货物(应用)和必要的保护措施(运行环境)打包进一个标准箱子,然后这艘"万吨巨轮"可以把这个箱子运到任何港口。无论目的港是深圳、鹿特丹还是洛杉矶,开箱即用。

放到开发里也一样。一个完整的 Web 应用往往不止有代码,还有:

  • Node.js 的版本
  • npm/yarn 的版本
  • Redis、MySQL 等中间件
  • 各种系统级依赖

任何一个环节不一致,都可能导致"你电脑能跑,我电脑不能跑"。Docker 做的就是把这些依赖连同代码一起,打成一个镜像 ,然后在你任何设备上以容器的形式运行起来。

如果你关注 AI Agent,会发现一个有趣的类比:

Agent = LLM + Harness(tool + mcp + rag + skill...)

Docker = 应用 + 运行环境

本质上,它们都是在解决"能力 + 环境"的组合问题。

两个必须理解的概念:Image 和 Container

  • Image(镜像) :类似于一张光盘。它包含了应用程序和它需要的环境,是只读的模板。
  • Container(容器) :类似于光盘放进 DVD 播放器后正在播放的内容。它是镜像运行起来的实例,可读可写。

所以你会发现,Docker 的日常操作,基本就是围绕这两个东西展开:

bash 复制代码
docker pull nginx      # 拉取镜像,相当于下载光盘
docker run nginx       # 运行镜像,相当于把光盘放进播放器
docker stop xxx        # 停止容器
docker rm xxx          # 删除容器
docker rmi nginx       # 删除镜像

先记住这两组概念,后面实战中你会反复看到它们。


二、实战:用 Nginx 反向代理一个 Node 服务

假设我们现在有一个最简单的 Node 服务:

javascript 复制代码
// index.js
const http = require('http')

const server = http.createServer((req, res) => {
    res.end('hello world')
})

server.listen(1314, '0.0.0.0', () => {
    console.log('node service run on 1314')
})

这个服务监听在 1314 端口,注意这里监听的是 0.0.0.0,而不是 127.0.0.1。为什么要这样?后面你会看到,因为 Nginx 容器需要通过宿主机访问这个服务,如果只监听回环地址,容器可能无法访问。

现在,你在本机启动它:

复制代码
node index.js

浏览器访问 http://localhost:1314,会看到熟悉的 hello world

但问题来了:用户会输入 localhost:1314 吗?不会。用户只会输入 http://localhost,甚至是一个域名。而 HTTP 协议默认端口是 80。也就是说,用户永远只会从 80 端口进来,但你的服务跑在 1314 端口

这就需要一位"前台接待员"------Nginx。

为什么需要 Nginx?

因为在实际生产环境中,你需要一个统一入口来处理请求:

  • 用户访问 80 端口
  • Nginx 监听 80 端口
  • Nginx 根据配置把请求转发到真正运行服务的 1314 端口

这就是反向代理 。说白了就是:用户不知道你的服务在哪个端口,也不该知道,Nginx 帮你挡在前面。

准备 Nginx 配置文件

ini 复制代码
# nginx.conf
events {}

http {
    server {
        listen 80;

        location / {
            proxy_pass http://host.docker.internal:1314;
            proxy_set_header Host $host;
        }
    }
}

这里有一个关键点:host.docker.internal。这是 Docker Desktop 提供的一个特殊 DNS 名称,它指向宿主机。因为我们的 Node 服务直接跑在宿主机上,而 Nginx 跑在容器里,所以 Nginx 需要通过这个地址访问宿主机的 1314 端口。

用 Docker 启动 Nginx

bash 复制代码
docker run --name my-nginx-demo \
  -p 80:80 \
  -v /path/to/nginx.conf:/etc/nginx/nginx.conf \
  -d nginx

这条命令拆开来看,就是 Docker 的日常操作核心:

  • --name my-nginx-demo:给容器起个名字,方便后续管理。
  • -p 80:80:端口映射,本机的 80 端口映射到容器的 80 端口。
  • -v /path/to/nginx.conf:/etc/nginx/nginx.conf:把本机的配置文件挂载到容器内 Nginx 的默认配置路径。
  • -d nginx:后台运行 nginx 镜像。如果不设置,会阻塞运行,需要重开一个进程。

现在,浏览器访问 http://localhost,请求会先到本机 80 端口,然后通过 Docker 端口映射进入 Nginx 容器,再根据配置文件代理到宿主机的 1314 端口,最终返回 hello world


三、请求链路拆解:一次请求的完整旅程

这个例子虽然简单,但把 Docker 和 Nginx 反向代理的核心链路全串起来了。你可以把下面这条链路在脑子里过一遍:

csharp 复制代码
浏览器 http://localhost:80
        ↓
宿主机 80 端口
        ↓
Docker 端口映射 -p 80:80
        ↓
Nginx 容器内 80 端口
        ↓
nginx.conf 反向代理规则
        ↓
host.docker.internal:1314(宿主机 Node 服务)
        ↓
返回 hello world

你会发现,整个过程中,用户完全不知道服务真正跑在 1314 端口。对于用户来说,只有一个入口:http://localhost

这就是反向代理和正向代理的区别:

  • 正向代理:代理的是客户端,比如你挂了个代理去访问外网,服务器不知道你是谁。
  • 反向代理:代理的是服务端,比如 Nginx 挡在服务前面,用户不知道真正的服务在哪。

反向代理的意义不只是端口转发,它把"服务真正在哪里"这件事藏了起来,对外只暴露一个干净的入口。


四、踩坑点与解决方案

在实际操作中,有几个点需要特别注意,不然很容易卡住。

1. host.docker.internal 的兼容性问题

host.docker.internal 是 Docker Desktop(Windows/Mac)自带的特性。如果你在 Linux 服务器上运行 Docker,这个 DNS 名称可能不存在。

解决方案是添加 --add-host 参数:

lua 复制代码
docker run --name my-nginx-demo \
  -p 80:80 \
  -v /path/to/nginx.conf:/etc/nginx/nginx.conf \
  --add-host=host.docker.internal:host-gateway \
  -d nginx

host-gateway 会让 Docker 自动解析到宿主机 IP,这样 Nginx 就能访问宿主机的 Node 服务了。

2. 端口映射的顺序

-p 80:80 里面,第一个 80 是宿主机端口,第二个 80 是容器端口。这个顺序别搞反了。

如果你本机 80 端口已经被占用(比如本机已经有 Nginx 在跑),可以改成 -p 8080:80,然后访问 http://localhost:8080

3. 挂载路径的格式

在 Windows 上,挂载路径可能需要写成 E:\workspace...,但更推荐使用绝对路径或者 Docker 的 volume。上面的示例中我用了 /path/to/nginx.conf,你需要替换成自己本机的实际路径。

4. 容器清理

如果折腾了一堆容器,想快速清理环境,可以用以下命令:

makefile 复制代码
# 停止所有运行中的容器
docker stop $(docker ps -q)

# 删除所有容器
docker rm $(docker ps -aq)

# 删除 nginx 镜像
docker rmi nginx

注意:docker rm 只能删除已停止的容器,如果容器还在运行,需要先执行 docker stop,或者直接用 docker rm -f 强制删除。


五、扩展:用 Docker 跑一个 MySQL

Docker 的便利性在数据库这种强环境依赖的场景下体现得更加明显。比如你本机已经装了一个 MySQL 5.7,但项目要求 MySQL 8.0,你不可能卸载重装。这时候 Docker 就派上用场了。

ini 复制代码
# 拉取 MySQL 8.0 镜像
docker pull mysql:8.0

# 运行 MySQL 容器,映射到宿主机 3307 端口
docker run -d --name mysql-demo \
  -p 3307:3306 \
  -e MYSQL_ROOT_PASSWORD=123456 \
  mysql:8.0

这里有两个点需要注意:

  • -p 3307:3306:MySQL 默认监听 3306 端口,但如果本机已有 MySQL 占用了 3306,就映射到 3307,避免冲突。
  • -e MYSQL_ROOT_PASSWORD=123456:通过环境变量设置 MySQL 的 root 密码,-e 就是 --env 的缩写。

进入容器内部,可以像操作普通 Linux 一样操作:

bash 复制代码
docker exec -it mysql-demo /bin/bash
mysql -uroot -p123456

docker exec -it 是进入一个正在运行的容器并打开交互式终端,之后你就可以在容器内部执行命令了。

  • -i --interactive:保持输入流开启,能接收键盘输入
  • -t --tty:分配虚拟终端,显示命令行提示符
  • -it 几乎永远成对使用,用来做交互式操作

六、总结:Docker 的核心就三件事

回顾一下这篇文章的内容,你会发现 Docker 的核心其实就是三件事:

  1. 打包:把应用和运行环境打包成镜像。
  2. 分发:镜像可以轻松 pull/push,任何设备都能拿到同一份环境。
  3. 隔离:容器之间互不影响,不会因为版本冲突而崩溃。

而 Nginx 反向代理,则是把"用户入口"和"服务端口"解耦,让你可以灵活地在后面挂任何服务。

环境问题不再是玄学,容器化让你把"能跑"这件事固化下来,变成一份可移植的配置。

下次再遇到"我电脑能跑,你电脑跑不起来"的情况,别急着怀疑人生,先问问自己:这件事,是不是该交给 Docker 了?

相关推荐
深念Y1 小时前
Wine 运行 HiTool 踩坑记录
linux·windows·容器·桌面·wine·虚拟器
coder_lorraine1 小时前
手把手教你用 Docker Compose 部署 Twikoo 评论系统
mongodb·docker·容器
微擎应用市场1 小时前
微擎面板 W7Panel:一站式云原生管理平台,让 Kubernetes 触手可及
云原生·容器·kubernetes
张洛闻Eren1 小时前
云原生k8s【第一课】: Docker 容器技术
运维·云原生·容器·k8s
量化分析1 小时前
迁移docker部署的wordpress
运维·docker·容器
懒人82 小时前
8月18日更新 | 2026 最新可用 Docker 国内镜像源加速列表(Linux 环境下拉取镜像的方式进行演示)
docker
三言老师2 小时前
K8s集群运行时自动化运维全覆盖落地实操(上)
docker·容器·kubernetes
ly76893 小时前
负载均衡详解:从四层、七层到 LVS、Keepalived、Nginx 与 HAProxy
nginx·负载均衡·lvs
梅孔立3 小时前
Docker 部署 Python3\.11 Flask爬虫项目 终极稳定教程(解决线程报错/挂载失效/pip报错)
爬虫·docker·flask