第一次接触 Docker 时,文档很厚,概念也多。其实把一个 Node 应用跑进容器,只需要理解三个东西:镜像、容器、Compose。这篇就用一个最小例子串一遍。
准备:一个最小的 Node 服务
// server.js
import http from 'node:http'
const port = process.env.PORT || 3000
http
.createServer((_, res) => {
res.end('hello from docker')
})
.listen(port, () => console.log(`server on :${port}`))// package.json
{
"name": "demo",
"type": "module",
"scripts": { "start": "node server.js" }
}1. Dockerfile:把代码打成镜像
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
FROM node:20-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]几点值得注意:
- 多阶段构建:先装依赖,再复制源码,可以利用层缓存,代码改动不会触发重装依赖
- 用
alpine镜像体积更小,但要注意有些原生模块需要换成slim或bullseye EXPOSE只是元信息,真正暴露端口靠-p参数
2. 构建并运行
docker build -t demo:latest .
docker run -d --name demo -p 3000:3000 demo:latest
curl http://localhost:3000常用命令速查:
docker ps # 查看运行中的容器
docker logs -f demo # 跟踪日志
docker exec -it demo sh # 进入容器排查
docker stop demo && docker rm demo3. docker-compose:多容器一把梭
当你需要 Node + MySQL + Redis 一起起来时,纯命令行会很啰嗦。docker-compose.yml 把它们写在一起:
services:
app:
build: .
ports: ['3000:3000']
environment:
DB_HOST: db
DB_USER: root
DB_PASSWORD: example
depends_on: [db]
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: example
MYSQL_DATABASE: demo
volumes: ['db-data:/var/lib/mysql']
volumes:
db-data:一条命令拉起整个栈:
docker compose up -d
docker compose logs -f
docker compose down # 停止并清理(volumes 会保留)
docker compose down -v # 连数据卷一起清掉(慎用)几个新手常踩的坑
.dockerignore一定要写,不然node_modules/.git都会被复制进镜像- 不要把
package-lock.json排除掉,否则 CI 与本地装出来的依赖版本不一致 - 生产环境不要用
latesttag,锁版本可以避免某天突然破坏构建 - 容器内的端口 ≠ 主机端口,
-p 80:3000才是真正对外的映射
小结
容器化最大的价值不是“更快”,而是**“在哪都一样”**。一旦你的服务能用 docker compose up 跑起来,迁移服务器、配 CI、本地复现 bug 都会轻松很多。