这是 DevOps 四大支柱的第一根:容器化。学完这节课,你能把任何项目打包成一个在任何机器上都能跑的 Docker 镜像。
你肯定经历过:项目在你电脑上跑得好好的,换个环境就炸了——Node 版本不对、系统库缺失、环境变量忘配。Docker 解决这个问题的方式很简单:
把应用和它的全部依赖(OS 库、运行时、配置)打包在一起,变成一个「集装箱」。 这个集装箱在任何装了 Docker 的机器上跑,结果一模一样。
跟你熟悉的 npm 类比:package.json 锁定了 JS 依赖 → Docker 镜像锁定了一切依赖,包括操作系统级别的。
| 概念 | 类比 | 是什么 |
|---|---|---|
| Dockerfile | 食谱 / package.json | 一个文本文件,定义如何构建镜像。列出每一步操作。 |
| Image(镜像) | 类 (class) / 模板 | Dockerfile 构建出来的只读模板。包含完整文件系统。 |
| Container(容器) | 实例 (instance) / 进程 | 镜像的运行实例。一个镜像可以启动多个容器。 |
Dockerfile ──(build)──► Image ──(run)──► Container
以你的 ~/code/podcast(Astro 项目)为例。这个项目的构建流程是:
npm install → npm run build → 启动静态文件服务器
翻译成 Dockerfile:
# === Stage 1: Build(构建阶段)===
FROM node:22-alpine AS builder # 以 Node.js 22 Alpine 为基础镜像
WORKDIR /app # 设工作目录(没有就自动创建)
COPY package*.json ./ # 先复制依赖清单(利用缓存)
RUN npm ci # 安装依赖(ci = 按 lock 文件精确安装)
COPY . . # 复制其余源代码
RUN npm run build # 构建
# === Stage 2: Run(运行阶段)===
FROM node:22-alpine AS runner # 换一个干净镜像(不要构建工具)
WORKDIR /app
COPY --from=builder /app/dist ./dist # 只从构建阶段复制产物
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 4321 # 声明端口(文档作用,不实际开放)
CMD ["node", "dist/server/entry.mjs"] # 启动命令
逐行解释:
| 指令 | 作用 | 为什么这样写 |
|---|---|---|
FROM node:22-alpine | 指定基础镜像 | Alpine = 最小 Linux(~5MB),够用且安全 |
WORKDIR /app | 设置工作目录 | 后续命令都在这个目录下执行 |
COPY pkg*.json ./ | 先复制依赖清单 | 关键优化:只有 package.json 变了才重新 npm ci。代码变了不影响这层缓存 |
RUN npm ci | 运行命令 | npm ci 比 npm install 更快更严格 |
COPY . . | 复制源代码 | 后复制,这样日常改代码时只重建这层 |
--from=builder | 多阶段构建 | 最终镜像只含运行时需要的东西,不包含 npm、构建工具等垃圾 |
EXPOSE 4321 | 声明端口 | 文档性质的,实际端口映射在 docker run -p 时指定 |
CMD [...] | 容器启动时执行的命令 | JSON 数组格式(exec form),推荐用这个 |
COPY . . 在前面,你每次改一行代码,整层缓存都失效,后面 RUN npm ci 还得重跑。把 COPY package*.json 提前,依赖层就缓存住了——除非你改了 package.json。
打开终端,跟着敲。
# 1. 构建镜像
docker build -t my-app:latest . # -t = 名字:标签 . = Dockerfile 所在目录
# 2. 查看本地镜像
docker images # 列出所有本地镜像
# 3. 运行容器
docker run -d -p 8080:4321 --name my-app my-app:latest
# ↑ ↑ ↑ ↑
# 后台跑 端口映射 容器名 镜像名
# 宿主:容器
# 4. 查看运行中的容器
docker ps # 运行中的
docker ps -a # 包括已停止的
# 5. 查看日志
docker logs my-app # 一次性输出
docker logs -f my-app # 实时追踪(Ctrl+C 退出)
# 6. 进入容器内部
docker exec -it my-app sh # -it = 交互式终端 sh = shell
# 7. 停止 / 启动 / 删除
docker stop my-app # 停止
docker start my-app # 重新启动
docker rm my-app # 删除容器(先 stop)
docker rm -f my-app # 强制删除(stop + rm)
# 8. 清理
docker system prune -a # 删除所有未使用的镜像/容器/网络
# ⚠️ 这条会清掉很多东西,确认后再跑
真实项目很少只有一个容器。比如你的应用可能需要 Node.js + PostgreSQL + Redis。手动 docker run 三个容器再连网络太痛苦了。Docker Compose 用一个 YAML 文件定义所有服务。
# docker-compose.yml
services:
app:
build: . # 用当前目录的 Dockerfile 构建
ports:
- "4321:4321"
environment:
- DATABASE_URL=postgres://db:5432/myapp # db = 下面的服务名,自动 DNS
depends_on:
- db
db:
image: postgres:16-alpine # 直接用官方镜像,不需要自己写 Dockerfile
environment:
- POSTGRES_PASSWORD=secret
volumes:
- pgdata:/var/lib/postgresql/data # 持久化数据(否则容器删了数据就没了)
volumes:
pgdata: # 声明一个命名卷
启动:docker compose up -d。停止:docker compose down。
任务:创建一个最小的 Node.js HTTP 服务,写 Dockerfile,构建并运行。
# 1. 创建项目
mkdir /tmp/docker-demo && cd /tmp/docker-demo
# 2. 创建 server.js
cat > server.js << 'EOF'
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Docker!\n');
});
server.listen(3000, () => console.log('Listening on :3000'));
EOF
# 3. 创建 package.json(极简版)
echo '{"name":"docker-demo","private":true}' > package.json
# 4. 创建 Dockerfile
cat > Dockerfile << 'EOF'
FROM node:22-alpine
WORKDIR /app
COPY package.json ./
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
EOF
# 5. 构建并运行
docker build -t docker-demo .
docker run -d -p 3000:3000 --name demo docker-demo
# 6. 验证
curl http://localhost:3000
# 输出: Hello from Docker!
# 7. 进容器看看
docker exec -it demo sh
# 现在你在容器里!ls 看看,只有 package.json 和 server.js
# exit 退出
# 8. 清理
docker rm -f demo
-p 3456:3000,然后 curl http://localhost:3456。
docker run -d -p 80:80 docker/getting-started 然后打开 http://localhost — Docker 官方自带的交互教程下一课我们学习 CI/CD 基础——GitHub Actions。你将学会:每次 push 代码,自动构建 Docker 镜像、跑测试、部署。这是 DevOps 最核心的自动化实践。
docker logs 容器名 是你的第一排查工具。