镜像 vs 容器
- 镜像:只读模板,由多层叠加而成,可分发、可缓存;
- 容器:镜像的一次运行实例,在镜像层之上加一个可写层。
分层的好处
相同底层被多个镜像共享,拉取和存储都更省。改一行代码通常只重建最顶层。
两个坑
- 把数据写进容器:容器删了数据就没了,持久化请用卷(volume);
- 镜像越积越大:装了构建工具又留着,用多阶段构建只留运行所需,镜像能小一倍。
实战案例:三个混淆镜像与容器的代价
- 删了容器以为清空了磁盘:删容器只删可写层,镜像仍占空间。清理要区分
docker rm(容器)、docker rmi(镜像)与docker system prune。 - 以为改了容器就能保留:容器里改的文件只存在于可写层,重建即丢。要固化必须写进 Dockerfile 重新构建镜像。
- 同一镜像跑出不同行为:镜像相同但环境变量、挂载卷与网络不同,差异来自运行时而非镜像。排障先比对
docker inspect的配置,而不是重建镜像。
常见问题(FAQ)
镜像是只读的吗?是,镜像是只读模板,容器在其上叠加一个可写层。为什么容器启动比虚拟机快?因为不需要引导完整操作系统,只是启动进程并叠加可写层。一个容器该跑几个进程?通常只跑一个主进程,多进程应拆成多个容器以便独立扩缩与重启。镜像层会重复占空间吗?相同层在不同镜像间共享,基础镜像一致时总占用远小于各镜像体积之和。
镜像瘦身:优先级从高到低
- 换基础镜像:
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,在构建后立即检查基础镜像与依赖漏洞,并按严重级别设置门禁:高危直接阻断合并。基础镜像应定期重建以获取上游安全更新,长期不重建的镜像即使代码没变,也会逐渐累积已知漏洞。