镜像 vs 容器
- 镜像:只读模板,由多层叠加而成,可分发、可缓存;
- 容器:镜像的一次运行实例,在镜像层之上加一个可写层。
分层的好处
相同底层被多个镜像共享,拉取和存储都更省。改一行代码通常只重建最顶层。
两个坑
- 把数据写进容器:容器删了数据就没了,持久化请用卷(volume);
- 镜像越积越大:装了构建工具又留着,用多阶段构建只留运行所需,镜像能小一倍。
镜像瘦身:优先级从高到低
- 换基础镜像:
alpine或distroless往往能省下上百 MB,但要注意 musl 与 glibc 的兼容差异; - 多阶段构建:构建依赖留在 builder 阶段,运行阶段只拷贝产物,镜像可减小一个数量级;
- 写好 .dockerignore:把
node_modules、.git、构建缓存排除掉,避免上下文膨胀与层失效; - 合并与排序指令:把不常变动的层放前面(依赖清单先拷贝再安装),让缓存尽可能命中;
- 清理安装缓存:
apt-get后删/var/lib/apt/lists,npm ci后清理缓存,且与安装写在同一层。
运行时容易忽略的三件事
- 信号处理:应用要能正确接收
SIGTERM并优雅退出,否则滚动更新时会丢请求;必要时用exec形式启动(CMD ["node","server.js"]而非 shell 形式); - 非 root 运行:用
USER指定非特权用户,缩小被攻破后的影响面; - 只读文件系统:能用
--read-only就跑只读,需要写的地方显式挂卷。
常见误区
- 把配置打进镜像:环境相关配置应通过环境变量或挂载注入,镜像保持环境无关才能一处构建多处部署;
- 用 latest 标签:无法回溯版本,回滚困难,应使用不可变标签(版本号或提交哈希);
- 在容器里跑多个进程:进程管理与日志采集都会变复杂,应拆分为多个容器。
数据卷与生命周期
- 容器文件系统随容器消亡:需要持久化的数据必须放卷或绑定挂载,否则重建即丢;
- 匿名卷容易被遗忘:
VOLUME声明的匿名卷在删除容器后可能残留,定期用docker volume prune清理; - 绑定挂载依赖宿主路径:跨机器部署会因路径不存在而失败,生产更推荐具名卷;
- 卷与镜像不同步:更新镜像不会更新卷里的数据,数据库迁移要显式执行。
镜像与容器的排障入口
docker inspect看配置与挂载,docker logs看应用输出,docker exec进容器验证;- 容器起不来时先看退出码:
137多为被 OOM 杀掉,139常见于段错误; - 镜像里没有 shell 时(如 distroless),应改用日志与探针排障,而不是临时把 shell 塞进镜像。
镜像扫描与合规
把镜像漏洞扫描接进 CI,在构建后立即检查基础镜像与依赖漏洞,并按严重级别设置门禁:高危直接阻断合并。基础镜像应定期重建以获取上游安全更新,长期不重建的镜像即使代码没变,也会逐渐累积已知漏洞。