作为一个后端老兵,以前每次接手新项目,最头疼的就是”环境不一致”问题。”在我的电脑上明明跑得好好的啊!”这句话简直是开发和运维之间永恒的导火索。直到我全面拥抱了 Docker,这个问题才算彻底解决。

1. 告别臃肿,拥抱多阶段构建

很多新手写 Dockerfile,喜欢把源码、编译工具、测试依赖一股脑全塞进去。结果一个简单的 Web 服务,镜像体积高达 1GB 以上。这在生产环境是不可接受的——不仅拉取慢,而且攻击面巨大。

正确的做法是使用多阶段构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ===== 阶段一:构建阶段 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# ===== 阶段二:生产阶段 =====
FROM node:20-alpine
WORKDIR /app

# 仅拷贝编译后的产物和生产依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev && npm cache clean --force

EXPOSE 3000
CMD ["node", "dist/main.js"]

这样镜像体积从 1.2GB 骤降到 80MB,不仅拉取快,而且生产环境里没有任何构建工具和测试框架,安全性大大提升。

2. 为什么推荐 Alpine?

Alpine Linux 是一个极其轻量级的发行版,基础镜像只有不到 5MB!与之对比,标准的 node:20 基础镜像超过 300MB。更小的镜像意味着:

  • 更快的 CI/CD 构建速度(镜像拉取快 10 倍以上)
  • 更小的安全攻击面(没有 systemd、cron、bash 等冗余组件)
  • 更低的存储成本

一个需要注意的坑:Alpine 使用 musl libc 而非 glibc。大部分 Node.js 原生模块已经兼容,但个别老旧的 C++ 扩展可能需要额外处理。

3. 不要以 Root 身份运行

默认情况下,Docker 容器内的进程以 root 用户运行。如果应用有漏洞导致被攻破,攻击者就拥有了容器内的 root 权限。应该创建一个非特权用户:

1
2
3
4
5
# 在生产阶段末尾添加
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

USER nodejs

4. 添加健康检查

没有健康检查的容器是运维的噩梦。Docker 提供 HEALTHCHECK 指令来告诉编排系统容器是否正常工作:

1
2
HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

在 docker-compose 中的好处尤其明显——依赖此服务的其他容器可以等待它健康后再启动。

5. 资源限制

生产环境中一定要限制容器的资源使用,防止一个容器耗尽整台机器的内存或 CPU:

1
2
3
4
5
docker run -d \
--cpus="1.5" \
--memory="512m" \
--memory-swap="512m" \
my-node-app

或在 docker-compose 中:

1
2
3
4
5
6
7
8
9
10
services:
app:
deploy:
resources:
limits:
cpus: '1.5'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M

6. 配置 .dockerignore

.gitignore 一样,千万不要忘记写 .dockerignore。把你本地的 node_modules.git、日志文件等统统排除在外。这能极大加快 Docker daemon 的构建上下文传输速度。

1
2
3
4
5
6
7
8
9
10
11
12
# .dockerignore
node_modules
.git
.gitignore
.env
*.log
dist
coverage
.vscode
Dockerfile
docker-compose.yml
README.md

7. Docker Compose 生产级编排

单容器跑应用很简单,但真实项目通常需要配合数据库、缓存、反向代理。以下是一个实用的 docker-compose 模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
version: '3.8'
services:
app:
build: .
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=postgres
depends_on:
postgres:
condition: service_healthy

postgres:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5

nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- app

volumes:
pgdata:

总结

Docker 化不是写个 FROMRUN 就完事了。精简镜像、关注安全、分离构建环境和运行环境、做好健康检查和资源限制——这才是运维老手该有的素质。这些实践不仅是技术优化,更是对生产环境的敬畏。