Docker 生产实践:从能跑到跑得稳的 Checklist docker best practices Docker DevOps Container Production 工具运维
1196 字
6 分钟

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。拆开:

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

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

验证清单#

改完以后,跑一遍这个验证:

Terminal window
# 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 才会顺。

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

Docker 生产实践:从能跑到跑得稳的 Checklist
https://bangwu.me/posts/docker-best-practices/
作者
棒无
发布于
2026-08-06
许可协议
CC BY-NC-SA 4.0