Dockerfile 是"做奶茶的配方"?用 todos 全栈项目看懂 Docker 构建与发布
上篇文章我们讲了一条请求在 Docker 里怎么走(nginx 反向代理 + 端口映射),评论区不少朋友说想看"怎么把一个项目真正发布成 Docker 镜像"。这篇就接着来。
先看一句话:
蜜雪冰城的 SOP:先加奶、再加奶、放 3 勺糖、摇匀。 任何人照着做,出来的味道都一样,就成了连锁店。
Dockerfile 就是这份 SOP 的程序化版本。
它是一个纯文本的"配方文件",里面按步骤写着"怎么做菜"。Docker 拿到这个文件,就能自动做出一模一样的镜像------不靠人记忆、不靠口口相传,任何人、任何电脑,产出完全一致。
这篇文章用一个真实的 todos 全栈项目(前端 React + 后端 NestJS + nginx),带你把它从头构建成镜像、再发布的完整流程。
一、Dockerfile:一份能自动执行的食谱
普通的菜谱是给人看的,Dockerfile 是给 Docker 引擎 读的。它长这样:
dockerfile
# 1. 选一个基础镜像:上面装好了 Node 环境
FROM node
# 2. 在容器里切到 /app 文件夹,设为工作目录
WORKDIR /app
# 3. 复制"原料"(代码)进来
COPY package*.json ./
# 4. 按步骤"下锅":安装依赖
RUN npm install
# 5. 把剩余代码全部拷进容器
COPY . .
# 6. 告诉 Docker 这个容器该暴露哪个端口
EXPOSE 3000
# 7. 上桌:容器启动时执行这个命令
CMD ["node", "server.js"]
每一行都是一个指令,从上到下顺序执行,就像配方里"先加奶、再加奶、放 3 勺糖、摇匀"。
| Dockerfile 指令 | 类比蜜雪 SOP |
|---|---|
FROM |
选什么牌子的原料 / 锅底 |
WORKDIR |
在工作台上切菜(切到哪个目录) |
COPY |
把准备好的食材放进锅 |
RUN |
下锅烹饪(执行命令) |
EXPOSE |
告诉打包箱这个服务会开哪个窗口 |
CMD |
上桌要做什么(容器启动命令) |
记法 :
FROM起头,COPY/RUN中间干活,CMD收尾启动。看到 Dockerfile 先找这三个,结构就清楚了。
二、构建到发布:四步走
把食谱变成真正能跑起来的服务,核心就这 4 条命令:
bash
docker build -t my-todos . # 1. 按 Dockerfile 构建镜像
docker login # 2. 登录镜像仓库(比如 Docker Hub / 私有仓库)
docker push my-todos # 3. 把镜像推到远程仓库
docker pull my-todos # 4. 在别的机器上拉下来跑
一条条拆开看:
① docker build -t my-todos .
-t 给镜像起名字,末尾的 . 表示"用当前目录下的 Dockerfile"。
text
docker build -t 镜像名 .
↑ 名字中间是命令 ↑ 找当前目录的 Dockerfile
理解点 :build 不是"看一遍文件",而是把 Dockerfile 的每一条指令执行一遍,每一层都生成一个缓存层。所以改个小配置再 build 会很快------只有变化的层会重做。
② docker login → ③ docker push
镜像构建好后,光在本地没用。要发布给团队 / 服务器用,就推去镜像仓库(类似 git 的远程仓库,但是装的是"光盘"而不是代码)。
bash
docker login # 登录仓库,验证身份
docker push my-todos # 把本地镜像上传到仓库
上传之后,任何能访问仓库的人 / 机器都能 docker pull my-todos 拉下来。
④ docker pull + 跑起来
把仓库里的镜像拉下来,run 一个容器就完成了部署:
bash
docker pull my-todos
docker run -p 3000:3000 my-todos
Dockerfile 是发布项目的标准方式之一 ------它的价值在于:"构建一次,到处运行"。 本地开发环境、测试环境、生产环境,都是同一个镜像,杜绝"我这能跑你那不能跑"。
三、实战:todos 全栈项目怎么组成镜像
前面是理论,来看我们真实的 todos 项目长什么样。它是一个三件套:
text
todos-fullstack/
├── todos/ # 前端:React + TypeScript + zustand
├── todos-backend/ # 后端:NestJS + Todo Module
└── (nginx) # 反向代理:把前端 80 → 后端 3000
项目架构非常典型:
text
浏览器
│ :80
▼
nginx(反向代理)
│ 转发
▼
NestJS 后端 :3000
▲
└── Todo Module(CRUD:增删改查)
每个部分都有自己的 Dockerfile
前端(React + Vite):
dockerfile
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build # 打包出静态文件
FROM nginx
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
后端(NestJS):
dockerfile
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build # 编译 TS 到 dist
EXPOSE 3000
CMD ["node", "dist/main.js"]
跨域问题在这套架构里怎么解
前端请求后端时,浏览器会拦跨域(前端是 80,后端是 3000,端口不同 = 跨域)。两种主流解法:
解法一:Nginx 反向代理(推荐,生产用) 让前端和后端同源------所有请求都走 80,由 nginx 转发:
nginx
server {
listen 80;
location /api/ {
proxy_pass http://backend:3000; # 80 → 3000,前端无感知
}
}
同源后浏览器根本不认为在跨域,一劳永逸。
解法二:后端开启 CORS(开发用) 后端直接允许跨域,适合本地调试。NestJS 里一行:
typescript
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({ origin: true, credentials: true });
await app.listen(3000);
}
记住:生产环境多用 Nginx 反代(同源),开发环境可以偷懒用 CORS。 两者目的都是解决跨域,只是放的位置不同。
四、命令速查
收尾把构建发布相关的命令整理成一张表:
| 命令 | 作用 | 类比 |
|---|---|---|
docker build -t 名 . |
按 Dockerfile 构建镜像 | 按 SOP 做了一杯奶茶 |
docker login |
登录镜像仓库 | 出示员工卡进连锁店后台 |
docker push 名 |
镜像推到远程仓库 | 把配方发到总部中央库 |
docker pull 名 |
从仓库拉镜像 | 从总部取配方 |
docker run 名 |
跑一个容器 | 照配方做一杯递给你 |
docker images |
看本地镜像列表 | 看看库存 |
结尾
回到开头那个比喻:
Dockerfile 就是蜜雪冰城的 SOP。 它把"怎么从零做一个能跑的 todo 应用"写成了人(引擎)能照做的指令:FROM 选环境 → COPY 拷代码 → RUN 装依赖 → CMD 启动。构建成镜像后 push / pull,就完成了标准化发布。
一句话收尾:
Dockerfile 是发布项目的标准方式之一------把"我这能跑"变成"哪都能跑"。
留个问题给你:我们演示的后端用 node dist/main.js 直接跑。如果要把后端也压进 nginx 反代后面的容器网络 里(而不是端口直通),proxy_pass 应该写 localhost 还是别的?这正好衔接上一篇的 Docker 网络话题,欢迎评论区聊聊。