Docker - 02 - 生成镜像的文件Dockerfile

写Dockerfile之前先懂 3 个概念

概念 一句话
镜像 Image 一套已经装好的迷你操作系统 + 程序文件 + 启动方式,可以直接运行
容器 Container 镜像跑起来就是容器
层 Layer Dockerfile 每一行指令 ≈ 一层;某层输入变了,该层及之后全部重做

两个关键指令的区别:

  • RUN --- 构建镜像时执行(装依赖、generate 等)
  • CMD --- 容器启动时执行(跑你的应用)

第 0 步:新建文件 + 配套 .dockerignore

在项目根目录创建 Dockerfile(无后缀,首字母大写常见)。

同时准备 .dockerignore,避免把不该拷进镜像的东西带进去:

text 复制代码
node*modules
.env
.git
tests
*.md

为什么需要?nodemodules 会在容器里用 npm ci 重装;.env 含密钥不应进镜像;tests、.md 与运行无关,减小镜像体积。

第 1 步:选基础镜像 --- FROM

dockerfile 复制代码
FROM node:22-bookworm-slim

你在做什么: 告诉 Docker「从哪个现成环境开始搭」。

怎么选:

部分 含义
node:22 Node.js 22
bookworm-slim Debian 12 精简版,体积小

为什么不用 Alpine?

Prisma 在 Alpine(musl)上常出兼容问题,Debian 更稳

小白检查点: 本地 node -v 尽量与镜像大版本一致(本项目用 22)。

第 2 步:定工作目录 --- WORKDIR

dockerfile 复制代码
WORKDIR /app

你在做什么: 在容器里创建并进入 /app,后面 COPY、RUN 默认都在这里执行。

类比: 相当于先 mkdir /app && cd /app。

第 3 步:先只拷依赖清单 --- COPY(第一次)

dockerfile 复制代码
COPY package.json package-lock.json ./

你在做什么: 只复制 package.json 和 package-lock.json,还不拷源码。

为什么先拷这两个?

Docker 按层缓存:依赖很少变、源码经常变。先装依赖、后拷代码,改 .ts 文件时不必重新 npm ci,构建更快。

  • 改 package.json → 从第 3 步起全部重做
  • 只改 src/*.ts → 第 3~5 步缓存命中,只重做后面的 COPY

第 4 步:安装依赖 --- RUN npm ci

dockerfile 复制代码
RUN npm ci

你在做什么: 构建镜像时,按 package-lock.json 精确安装依赖。RUN npm ci --omit=dev,只安装生产依赖

npm ci vs npm install:

npm ci npm install
依据 严格按 lock 文件 可能改 lock
场景 CI / Docker 构建 本地开发

为什么装 devDependencies?

本项目 "start": "tsx ./index.ts",tsx 在 devDependencies;构建时 prisma generate 也需要 prisma CLI。容器启动走 npm start,没有这些会失败。

生产环境若改用 node dist/index.js 编译产物,可再考虑多阶段构建;本项目直接用 tsx,所以需要 dev 依赖。

第 5 步:Prisma 单独处理 --- COPY + RUN generate

dockerfile 复制代码
COPY prisma ./prisma
RUN npx prisma generate

你在做什么:

  • 只拷 prisma/(含 schema.prisma 等)
  • 在容器内生成 Prisma Client → src/generated/prisma

两个常见误区:

误区 真相
COPY . . 会自带 Client COPY 只复制文件,不会执行 generate
本地已 generate,拷进去就行 Windows 生成的是 Windows 引擎,Linux 容器里往往跑不了

为什么放在 COPY . . 之前?

  • 功能:clone/CI 里通常没有生成物(.gitignore 已忽略),必须在镜像里 generate
  • 缓存:只改业务代码时不重复 generate
改了什么 影响
只改 src/*.ts generate 层缓存命中
改 prisma/schema 重跑 generate
改 package.json 从 npm ci 起全重跑

第 6 步:复制其余源码 --- COPY . .

dockerfile 复制代码
COPY . .

你在做什么: 把项目其余文件拷进 /app(受 .dockerignore 过滤)。

不会覆盖什么?

  • 已在容器里 npm ci 的 node_modules 不会被宿主机覆盖(.dockerignore 排除了 node_modules)
  • 第 5 步已 generate 的 Client 保留(除非本地有旧生成物被拷进去------本项目 ignore 了,一般没问题)

第 7 步:声明端口 --- EXPOSE

dockerfile 复制代码
EXPOSE 8080

你在做什么: 文档性声明「这个容器内的 API 监听 8080」(与 index.ts 里 PORT = 8080 一致)。

注意:

  • EXPOSE 不会自动映射到宿主机
  • 不影响 PostgreSQL :5432、Redis :6379(它们是别的容器)
  • 访问需手动映射:
bash 复制代码
docker run -p 3000:8080 your-image

# 浏览器 localhost:3000 → 容器内 8080

第 8 步:启动命令 --- CMD

dockerfile 复制代码
CMD ["npm", "start"]

你在做什么: 容器启动时执行 npm start,等价于 tsx ./index.ts。

推荐写法: JSON 数组形式 "npm", "start",避免 shell 解析问题。

与 RUN 再记一次:

指令 时机
RUN npm ci 构建镜像时
CMD "npm", "start" 每次 docker run 启动容器时

完整 Dockerfile 一览

dockerfile 复制代码
FROM node:22-bookworm-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY prisma ./prisma
RUN npx prisma generate
COPY . .
EXPOSE 8080
CMD ["npm", "start"]

写完后:验证步骤

bash 复制代码
# 1. 构建镜像(在项目根目录,有 Dockerfile 的地方)

docker build -t nodejs-api .

对应到你的命令:

部分 含义
docker build 构建镜像
-t nodejs-api 给镜像打标签,名字叫 nodejs-api
. 构建上下文 = 当前目录
bash 复制代码
# 2. 运行容器(需配好 DATABASE_URL、REDIS 等环境变量)

docker run -p 8080:8080 --env-file .env my-node-api

# 3. 浏览器或 curl 访问

curl http://localhost:8080

构建时观察输出:若只改了源码,应看到前几步 CACHED,说明层缓存生效。

小白易错清单

错误 后果 正确做法
先 COPY . . 再 npm ci 改一行代码就重装全部依赖 先拷 lock,再 npm ci,最后拷源码
省略 prisma generate 容器启动报找不到 Client 构建时在 Linux 容器内 generate
用 npm install 代替 npm ci 依赖版本不稳定 Docker 构建用 npm ci
以为 EXPOSE 就能访问 宿主机访问不到 加 -p 宿主机端口:8080
把 .env 打进镜像 密钥泄露 写进 .dockerignore,运行时 -e 或 --env-file 传入
用 Alpine + Prisma 各种 libc/引擎报错 本项目选 Debian slim

推荐写作顺序(记忆口诀)

text 复制代码
FROM 选环境
↓
WORKDIR 定目录
↓
COPY 依赖清单 → RUN 装依赖 ← 利用缓存
↓
COPY prisma → RUN generate ← 跨平台 + 缓存
↓
COPY 源码
↓
EXPOSE 声明端口
↓
CMD 启动命令