← 返回文章列表

镜像与容器:Docker 里这两个不是一回事

云原生入门CI/CD

镜像 vs 容器

  • 镜像:只读模板,由多层叠加而成,可分发、可缓存;
  • 容器:镜像的一次运行实例,在镜像层之上加一个可写层。

分层的好处

相同底层被多个镜像共享,拉取和存储都更省。改一行代码通常只重建最顶层。

两个坑

  1. 把数据写进容器:容器删了数据就没了,持久化请用卷(volume);
  2. 镜像越积越大:装了构建工具又留着,用多阶段构建只留运行所需,镜像能小一倍。

实战案例:三个混淆镜像与容器的代价

  1. 删了容器以为清空了磁盘:删容器只删可写层,镜像仍占空间。清理要区分 docker rm(容器)、docker rmi(镜像)与 docker system prune。
  2. 以为改了容器就能保留:容器里改的文件只存在于可写层,重建即丢。要固化必须写进 Dockerfile 重新构建镜像。
  3. 同一镜像跑出不同行为:镜像相同但环境变量、挂载卷与网络不同,差异来自运行时而非镜像。排障先比对 docker inspect 的配置,而不是重建镜像。

常见问题(FAQ)

镜像是只读的吗?是,镜像是只读模板,容器在其上叠加一个可写层。为什么容器启动比虚拟机快?因为不需要引导完整操作系统,只是启动进程并叠加可写层。一个容器该跑几个进程?通常只跑一个主进程,多进程应拆成多个容器以便独立扩缩与重启。镜像层会重复占空间吗?相同层在不同镜像间共享,基础镜像一致时总占用远小于各镜像体积之和。

镜像瘦身:优先级从高到低

  1. 换基础镜像:alpine 或 distroless 往往能省下上百 MB,但要注意 musl 与 glibc 的兼容差异;
  2. 多阶段构建:构建依赖留在 builder 阶段,运行阶段只拷贝产物,镜像可减小一个数量级;
  3. 写好 .dockerignore:把 node_modules、.git、构建缓存排除掉,避免上下文膨胀与层失效;
  4. 合并与排序指令:把不常变动的层放前面(依赖清单先拷贝再安装),让缓存尽可能命中;
  5. 清理安装缓存: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,在构建后立即检查基础镜像与依赖漏洞,并按严重级别设置门禁:高危直接阻断合并。基础镜像应定期重建以获取上游安全更新,长期不重建的镜像即使代码没变,也会逐渐累积已知漏洞。