Docker 生产实践:从能跑到跑得稳的 Checklist
#博客更新
先说结论
「Docker 能跑」和「Docker 能稳定跑」是两回事。本地
这篇文章是我在生产环境踩过之后总结的 Checklist。不追求面面俱到,只讲高频、高影响的十件事,按重要程度排序。
1. 镜像尽量小,但要小得有理
镜像体积直接影响拉取时间和启动速度。常见手段:
● 用
● 多阶段构建:构建阶段用全量镜像,运行阶段只拷贝产物
● 清理不必要的缓存和包管理器元数据
但注意:不是越瘦越好。distroless 没有 shell,调试会很痛苦;alpine 的 musl libc 偶尔和依赖有兼容问题。先瘦到合理范围,别为了几十 MB 自找麻烦。
2. 非 root 用户运行
默认容器内是 root,这是安全大忌。一旦容器被攻破,攻击者就是 root 权限。
大多数官方镜像现在都支持直接切用户。这一步成本极低,收益很高。
3. 健康检查必须配
没有健康检查,编排系统(K8s/Swarm)就不知道你的容器是不是真的活着。进程在不代表服务可用。
注意
4. 日志打 stdout,别写文件
容器里的日志应该走 stdout/stderr,由编排系统收集,而不是写到容器内的文件。原因:
● 容器文件系统是临时的,重启就没了
● 你没法轻易拿到容器内的文件
● 云平台(CloudWatch、ELK、Loki)都从 stdout 收集
所以应用日志直接
5. 资源限制,不是可选项
不设
limits 一定要设,reservations 至少给一个合理值。这是生产环境第一道防线。
6. 环境变量和密钥,别硬编码
镜像里不要写死任何密钥。用:
● 环境变量(运行时注入)
● Docker secrets / K8s secrets
● 专门的密钥管理(Vault、云厂商的 secrets manager)
镜像一旦构建就难以「撤销」里面硬编码的密钥。密钥泄露的补救成本远高于一开始就用环境变量。
7. 固定依赖版本,别用 latest
● 基础镜像用精确 tag(如
● 依赖锁文件(
● 有需要时用 digest 固定镜像
可复现 = 可回滚 = 可调试。
8. 处理僵尸进程和信号
容器作为 PID 1 时,要正确处理信号(SIGTERM)和孤儿进程。常见方案:
● 用带 PID 1 处理的运行时(如
● 确保应用能优雅关闭(监听 SIGTERM,完成在途请求)
不然
9. 镜像构建也要考虑 .dockerignore
这也是多阶段构建里第一层就过滤掉的东西。
10. 一个容器一个职责
别在一个容器里跑 web + worker + cron。拆开:
● 各自独立扩缩容
● 一个挂了不影响其他
● 日志、健康检查、资源限制都更清晰
一个容器只做一件事,是容器化最大的架构收益。
验证清单
改完以后,跑一遍这个验证:
写在最后
这十条里,健康检查、资源限制、非 root、日志走 stdout 是成本最低、收益最高的四项,强烈建议从这些开始。
容器化不是终点,只是起点。把这些基础打牢,后面接 CI/CD、上 K8s 才会顺。
如果哪条你踩过不同的坑,欢迎交流。
via 棒无
#博客更新
先说结论
「Docker 能跑」和「Docker 能稳定跑」是两回事。本地
docker run 一次成功,和在生产集群里扛住流量、重启、扩容,中间隔着不少坑。这篇文章是我在生产环境踩过之后总结的 Checklist。不追求面面俱到,只讲高频、高影响的十件事,按重要程度排序。
1. 镜像尽量小,但要小得有理
镜像体积直接影响拉取时间和启动速度。常见手段:
● 用
alpine 或 distroless 作为基础镜像● 多阶段构建:构建阶段用全量镜像,运行阶段只拷贝产物
● 清理不必要的缓存和包管理器元数据
# 多阶段构建示例
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 /app/dist ./dist
COPY /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 \
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 棒无