Docker 生产实践:从能跑到跑得稳的 Checklist
#博客更新

先说结论

「Docker 能跑」和「Docker 能稳定跑」是两回事。本地 docker run 一次成功,和在生产集群里扛住流量、重启、扩容,中间隔着不少坑。

这篇文章是我在生产环境踩过之后总结的 Checklist。不追求面面俱到,只讲高频、高影响的十件事,按重要程度排序。

1. 镜像尽量小,但要小得有理

镜像体积直接影响拉取时间和启动速度。常见手段:

alpinedistroless 作为基础镜像
● 多阶段构建:构建阶段用全量镜像,运行阶段只拷贝产物
清理不必要的缓存和包管理器元数据

# 多阶段构建示例
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "dist/index.js"]

但注意:不是越瘦越好。distroless 没有 shell,调试会很痛苦;alpine 的 musl libc 偶尔和依赖有兼容问题。先瘦到合理范围,别为了几十 MB 自找麻烦。

2. 非 root 用户运行

默认容器内是 root,这是安全大忌。一旦容器被攻破,攻击者就是 root 权限。
FROM node:20-alpine
# 创建非 root 用户
RUN addgroup -S app && adduser -S app -G app
USER app

大多数官方镜像现在都支持直接切用户。这一步成本极低,收益很高。

3. 健康检查必须配

没有健康检查,编排系统(K8s/Swarm)就不知道你的容器是不是真的活着。进程在不代表服务可用。
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
  CMD wget -q -O - http://localhost:3000/healthz || exit 1

注意 start-period——给应用启动留时间,别一上来就被打回。

4. 日志打 stdout,别写文件

容器里的日志应该走 stdout/stderr,由编排系统收集,而不是写到容器内的文件。原因:

容器文件系统是临时的,重启就没了
你没法轻易拿到容器内的文件
云平台(CloudWatch、ELK、Loki)都从 stdout 收集

所以应用日志直接 console.log/print,别自己写日志文件轮转。

5. 资源限制,不是可选项

不设 --memory--cpus 限制,一个失控的容器能拖垮整个节点。
# docker-compose 或 k8s 里都要设
resources:
  limits:
    memory: 512M
    cpus: "0.5"
  reservations:
    memory: 256M

limits 一定要设,reservations 至少给一个合理值。这是生产环境第一道防线。

6. 环境变量和密钥,别硬编码

镜像里不要写死任何密钥。用:

环境变量(运行时注入)
Docker secrets / K8s secrets
专门的密钥管理(Vault、云厂商的 secrets manager)

镜像一旦构建就难以「撤销」里面硬编码的密钥。密钥泄露的补救成本远高于一开始就用环境变量。

7. 固定依赖版本,别用 latest

FROM node:20-alpine 里的 20 可能今天和明天解析到不同的小版本。生产环境要可复现

基础镜像用精确 tag(如 node:20.18.0-alpine
依赖锁文件(package-lock.json / poetry.lock)进镜像
有需要时用 digest 固定镜像

可复现 = 可回滚 = 可调试。

8. 处理僵尸进程和信号

容器作为 PID 1 时,要正确处理信号(SIGTERM)和孤儿进程。常见方案:

用带 PID 1 处理的运行时(如 tini
确保应用能优雅关闭(监听 SIGTERM,完成在途请求)

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/index.js"]

不然 docker stop 会变成 kill -9,数据库写一半就没了。

9. 镜像构建也要考虑 .dockerignore

.dockerignore 能大幅减少构建上下文和镜像体积,避免把 node_modules.git、日志、临时文件打进去。
node_modules
.git
*.log
.tmp
dist

这也是多阶段构建里第一层就过滤掉的东西。

10. 一个容器一个职责

别在一个容器里跑 web + worker + cron。拆开:

各自独立扩缩容
一个挂了不影响其他
日志、健康检查、资源限制都更清晰

一个容器只做一件事,是容器化最大的架构收益。

验证清单

改完以后,跑一遍这个验证:
# 1. 镜像构建成功且体积合理
docker build -t my-app .

# 2. 容器能启动且健康检查通过
docker run -d --name test -p 3000:3000 my-app
docker inspect --format='{{.State.Health.Status}}' test
# 期望: healthy

# 3. 优雅停止
docker stop test
# 观察日志是否有优雅关闭记录

# 4. 资源限制生效
docker run --rm -m 256m my-app
# 期望: 超出限制时 OOM,而不是拖垮宿主


写在最后

这十条里,健康检查、资源限制、非 root、日志走 stdout 是成本最低、收益最高的四项,强烈建议从这些开始。

容器化不是终点,只是起点。把这些基础打牢,后面接 CI/CD、上 K8s 才会顺。

如果哪条你踩过不同的坑,欢迎交流。

via 棒无 Docker 生产实践:从能跑到跑得稳的 Checklist - 棒无
 
 
Back to Top