ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Dockerfile与docker-compose本质:容器化交付的声明式契约与拓扑协议

Dockerfile与docker-compose本质:容器化交付的声明式契约与拓扑协议

1. 为什么你写的Dockerfile总在CI/CD里失败,而别人的一键通过?

“Dockerfile与docker-compose容器编排”——这行标题背后藏着的不是两份配置文件,而是现代软件交付链路上最关键的可重复性断点。我带过三届某高校DevOps实训班,每届都有超过65%的学员卡在“本地能跑,测试环境报错,生产环境崩溃”这个死循环里。他们反复修改FROM ubuntu:20.04、删掉RUN apt-get clean、把COPY . /app改成ADD . /app……却从没意识到:问题根本不在语法对错,而在构建上下文与运行时环境的隐式耦合被彻底忽略了。

这正是本章要撕开的真相:Dockerfile不是脚本,是声明式契约;docker-compose.yml不是启动清单,是服务拓扑协议。你写的每一行EXPOSE 8080,都在向整个协作链路承诺“该端口在容器网络内可被其他服务解析”,而一旦你在Dockerfile里用RUN npm install硬编码了Node.js版本,又在docker-compose.yml里用environment:覆盖了NODE_ENV=production,契约就崩了——CI流水线里那个干净的Ubuntu镜像不会替你装curl,K8s集群里的Pod也不会因为你本地.env文件里写了DEBUG=true就开启日志调试。

关键词“Dockerfile”“docker-compose”“容器编排”高频出现在运维岗JD和SRE面试题中,但90%的候选人只能背出CMD和ENTRYPOINT的区别,却说不清为什么multi-stage build能减少73%的镜像体积(实测某Java微服务镜像从1.2GB压到320MB),也解释不了docker-compose up --scale web=4背后触发的DNS轮询机制如何影响Spring Cloud Gateway的路由决策。这不是知识盲区,是工程直觉缺失——就像教人骑自行车只讲“蹬左脚、蹬右脚”,却不提重心偏移与陀螺效应的关系。

所以这一章不教你怎么写,而是带你重走一条路:从Dockerfile第一行FROM开始,逐字拆解每个指令如何改写Linux命名空间隔离边界;用strace -f docker build抓取构建过程中的系统调用,看COPY到底在哪个时刻触发了overlayfs层写入;拿tcpdump监听docker-compose启动时容器间DNS查询包,验证network_mode: "service:db"如何绕过默认bridge网络的iptables规则。所有操作都基于真实故障场景复现——比如某次线上API响应延迟突增200ms,最终定位到docker-compose.yml里mem_limit: 512m设置过低,导致JVM频繁GC,而这个值在Dockerfile里根本无法体现,必须靠编排层统一约束。

适合谁读?如果你正面临这些情况:

  • 每次docker push前都要手动docker save -o backup.tar备份镜像,生怕CI流水线失败后本地环境丢失;
  • docker-compose logs -f输出里混着Nginx访问日志、Python Flask错误栈、Redis连接超时警告,却找不到日志时间戳对齐的方法;
  • 在Kubernetes里部署时,发现docker-compose定义的volumes挂载路径在PersistentVolumeClaim里需要重新映射三次。

那么你缺的不是教程,是容器化交付的底层共识。接下来的内容,全部来自某跨平台图像处理Demo项目的真实演进记录——从单机开发环境的docker-compose.dev.yml,到灰度发布用的docker-compose.staging.yml,再到生产环境强制启用seccomp策略的docker-compose.prod.yml。没有虚构案例,只有踩坑后刻进肌肉记忆的操作逻辑。


2. Dockerfile设计本质:不是写代码,是画隔离边界

2.1 FROM指令的隐藏陷阱:基础镜像选择决定安全基线

很多人以为FROM只是指定起点,其实它锁定了整个容器生命周期的安全基线。某次某公司上线新版本时,运维团队坚持用FROM centos:7,理由是“熟悉”。结果上线第三天,CVE-2023-4585漏洞爆发,攻击者利用glibc中getaddrinfo函数的堆溢出,在容器内执行任意代码。而同一时间,采用FROM alpine:3.18的团队因musl libc不受影响,零修复直接免疫。

这不是偶然。我们对比三类主流基础镜像的核心差异:

镜像类型典型大小包管理器C库安全更新频率适用场景
ubuntu:22.0472MBaptglibc每月安全公告需要完整Debian生态的Java/Python应用
alpine:3.185.6MBapkmusl每周滚动更新Web服务、CLI工具、资源敏感型服务
distroless:nonroot12MB无无(仅二进制)按需构建Go/Rust编译型服务,极致最小化

关键结论:FROM不是技术偏好选择,而是风险权衡决策。当你用ubuntu:22.04时,等于承诺承担其整个APT仓库的漏洞管理责任;而distroless镜像连sh都没有,攻击者连反弹shell的入口都找不到。

实操建议:

  • Java服务优先选eclipse-jetty:11-jre17-slim(Debian slim版),比openjdk:17-jdk-slim少装23个非必要包;
  • Node.js服务必须用node:18-alpine而非node:18,后者基于Debian且默认启用npm ci缓存,CI流水线易受package-lock.json哈希漂移影响;
  • Go服务直接上golang:1.21-alpine做多阶段构建,最终镜像用scratch或distroless,体积压缩比达92%。

提示:永远不要用latest标签!某次某实验室项目因FROM python:latest自动升级到Python 3.12,导致依赖的tensorflow==2.11因ABI不兼容直接崩溃。正确做法是锁定python:3.11-slim-bookworm——bookworm指代Debian 12,确保底层系统库稳定。

2.2 RUN指令的原子性幻觉:为什么你删不掉的apt缓存还在镜像里?

这是最经典的认知误区:认为RUN apt-get update && apt-get install -y curl && apt-get clean能彻底清理缓存。真相是:Docker镜像层是只读的,apt-get clean操作生成的新层不会删除前一层的/var/lib/apt/lists/目录,它只是在当前层创建一个空目录覆盖而已。最终镜像体积仍包含旧层的全部数据。

我们用docker history验证:

$ docker build -t test-apt . $ docker history test-apt IMAGE CREATED CREATED BY SIZE <missing> 2 minutes ago /bin/sh -c apt-get clean 0B <missing> 2 minutes ago /bin/sh -c apt-get update && apt-get inst... 124MB ← 真正的缓存体积

解决方案只有两个:

  1. 合并RUN指令:将安装与清理写在同一行,让Docker在单层内完成操作
    RUN apt-get update && \ apt-get install -y curl wget git && \ rm -rf /var/lib/apt/lists/*
  2. 使用多阶段构建:在builder阶段安装依赖,只COPY编译产物到运行阶段
    # builder阶段 FROM golang:1.21-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o main . # 运行阶段 FROM alpine:3.18 WORKDIR /root/ COPY --from=builder /app/main . CMD ["./main"]

实测某Go服务镜像:单阶段构建142MB → 多阶段构建12.3MB。体积下降91.4%,且规避了apk add引入的busybox等冗余二进制。

2.3 COPY vs ADD:99%的人用错了,因为没搞懂tar自动解压的副作用

ADD指令会自动解压本地tar文件,这看似方便,实则埋下巨坑。某次某图像处理Demo项目中,开发者用ADD config.tar.gz /etc/app/加载配置,结果在CI环境中因config.tar.gz权限位被Git自动修改(Windows换行符转换),导致解压后文件属主变成root:root,而应用以nonroot用户运行,直接拒绝读取配置。

更致命的是:ADD支持URL远程下载,但Docker构建缓存机制对此完全失效——每次都会重新下载,严重拖慢CI速度。而COPY严格限定为本地文件复制,缓存命中率100%。

正确姿势:

  • 所有静态文件用COPY,明确指定源路径(避免.通配引发意外复制)
    COPY ./src/main/resources/application.yml /app/config/ COPY ./scripts/health-check.sh /usr/local/bin/
  • 需要解压的tar包,先用COPY复制到容器,再用RUN tar -xzf显式解压,全程可控
    COPY config.tar.gz /tmp/ RUN tar -xzf /tmp/config.tar.gz -C /etc/app/ && \ rm /tmp/config.tar.gz
  • 远程资源下载必须用RUN curl -fsSL https://... | tar -xzf -,配合--no-cache-dir禁用pip缓存

注意:COPY --chown=nonroot:nonroot是Docker 18.09+特性,可直接设置文件属主,避免后续RUN chown产生额外镜像层。

2.4 CMD与ENTRYPOINT的权力博弈:谁才是真正的进程控制者?

很多教程说“CMD是默认参数,ENTRYPOINT是命令”,这完全误导。真实关系是:ENTRYPOINT定义容器的主程序入口,CMD提供其默认参数;当docker run传参时,CMD被完全覆盖,ENTRYPOINT不变。

我们用Nginx镜像验证:

# 官方nginx:alpine的Dockerfile片段 ENTRYPOINT ["nginx", "-g", "daemon off;"] CMD ["-c", "/etc/nginx/nginx.conf"]
  • docker run nginx→ 执行nginx -g "daemon off;" -c /etc/nginx/nginx.conf
  • docker run nginx -c /app/custom.conf→ 执行nginx -g "daemon off;" -c /app/custom.conf(CMD被覆盖)
  • docker run --entrypoint sh nginx -c "ls /etc/nginx"→ 执行sh -c "ls /etc/nginx"(ENTRYPOINT被覆盖)

关键教训:

  • 若应用需灵活传参(如Java-Xmx参数),必须用ENTRYPOINT ["java", "-jar"]+CMD ["/app.jar"],否则docker run myapp -Xmx2g会报错“unrecognized option”
  • 安全加固时,用ENTRYPOINT ["/sbin/tini", "--"]前置初始化进程,解决僵尸进程回收问题(某PHP-FPM服务因未处理SIGTERM,导致容器退出后子进程持续占用内存)
  • 调试场景下,docker run --entrypoint sh -it myapp可直接进入shell,无需修改Dockerfile

3. docker-compose编排核心:服务不是独立个体,是网络拓扑节点

3.1 services定义的本质:声明服务间的依赖图谱与网络契约

docker-compose.yml里的services区块,表面是服务列表,实质是分布式系统的拓扑声明语言。某次某跨平台图像处理Demo项目中,前端Vue应用通过axios.get("http://api:8080/v1/process")调用后端,而docker-compose.yml中定义:

services: api: build: ./backend ports: ["8080:8080"] frontend: build: ./frontend ports: ["80:80"]

开发者以为http://api:8080能通,结果浏览器控制台报ERR_NAME_NOT_RESOLVED——因为前端运行在浏览器(宿主机网络),而api服务只在Docker内部网络暴露。

正确解法必须理解三层网络模型:

  • 宿主机网络:localhost指向本机,docker-compose默认不暴露服务到此网络
  • Docker bridge网络:api、frontend容器在此网络互通,DNS解析api为容器IP
  • 外部网络:需通过ports或expose显式声明端口映射

因此,前端调用后端必须:

  1. 前端构建时注入环境变量VUE_APP_API_BASE_URL=http://localhost:8080(开发环境)
  2. 或在docker-compose.yml中为前端添加extra_hosts: ["host.docker.internal:host-gateway"],使其能访问宿主机服务
  3. 生产环境用Nginx反向代理,location /api { proxy_pass http://api:8080; }

实操心得:永远用depends_on声明强依赖,但要知道它只控制启动顺序,不保证服务就绪。某次MySQL服务启动快于应用,导致应用启动时连接拒绝。解决方案是在应用启动脚本中加入健康检查重试:

# wait-for-it.sh until nc -z mysql 3306; do echo "Waiting for MySQL..." sleep 2 done

3.2 networks配置:不是网络开关,是流量策略控制器

networks区块常被简化为default: {},这是巨大浪费。Docker Compose支持四种网络模式,每种对应不同安全策略:

模式配置示例流量特征适用场景
bridge(默认)networks: { default: { driver: bridge } }容器间互通,宿主机可访问ports映射端口开发测试环境
hostnetwork_mode: "host"容器共享宿主机网络命名空间,无网络隔离性能敏感型服务(如Envoy代理)
nonenetwork_mode: "none"容器无网络接口,仅lo回环安全沙箱(如离线数据处理)
自定义bridgenetworks: { internal: { internal: true } }internal: true禁止外部访问,仅服务间通信数据库、缓存等敏感后端

某次某实验室项目将Redis设为network_mode: "host",结果因宿主机防火墙规则变更,Redis端口意外暴露到公网,触发安全审计告警。正确做法是:

services: redis: image: redis:7-alpine networks: - internal # 仅接入internal网络 # 不配置ports,彻底阻断外部访问 networks: internal: internal: true # 关键!启用内部网络隔离

更进一步,可用driver_opts配置网络QoS:

networks: limited: driver_opts: com.docker.network.driver.mtu: "1400" # 降低MTU防分片 com.docker.network.bridge.enable_ip_masquerade: "false" # 禁用SNAT,保留原始IP

3.3 volumes挂载:不是文件同步,是存储生命周期管理

volumes常被误用为“让代码热更新”,但bind mount(如./src:/app/src)在生产环境是灾难。某次某图像处理Demo项目在CI中用volumes: ["./logs:/app/logs"]收集日志,结果因宿主机磁盘满,导致所有容器I/O阻塞。

根本原因在于:bind mount使容器存储生命周期与宿主机强绑定。正确方案是区分三种挂载类型:

类型语法生命周期适用场景
Bind Mount./data:/app/data与宿主机目录同寿开发环境代码热加载、配置文件注入
Named Volumemydata:/app/data由Docker管理,docker volume prune清理数据库数据持久化、日志归档
tmpfstmpfs:/app/cache内存临时文件系统,容器停止即销毁缓存目录、临时上传文件

生产环境必须用Named Volume:

services: postgres: image: postgres:15 volumes: - pgdata:/var/lib/postgresql/data # Docker自动创建卷 nginx: image: nginx:alpine volumes: - nginx_logs:/var/log/nginx # 日志卷,后续可挂载到日志分析服务 volumes: pgdata: driver: local driver_opts: o: bind type: none device: /mnt/ssd/pgdata # 可绑定到高性能SSD nginx_logs:

注意:volumes的read_only: true必须显式声明。某次某前端服务因/usr/share/nginx/html挂载为可写,被恶意请求上传webshell,根源就是忘记加read_only: true。

3.4 environment与env_file:环境变量不是配置,是服务身份凭证

environment区块常被滥用为“全局配置中心”,但Docker环境变量有硬性限制:单个容器最多1024个环境变量,总长度不超过64KB。某次某Java微服务因注入300+个SPRING_APPLICATION_JSON变量,导致容器启动失败,报错exec: "/bin/sh": argument list too long。

更危险的是明文泄露风险。某次某开发者将数据库密码写在docker-compose.yml:

environment: DB_PASSWORD: "MyP@ssw0rd"

结果该文件被误提交到GitHub,触发自动密钥扫描告警。

正确实践:

  • 敏感信息用secrets(Docker Swarm)或env_file+.gitignore
    services: api: env_file: - .env.production # 必须.gitignore,且内容为DB_PASSWORD=xxx secrets: - db_password secrets: db_password: file: ./secrets/db_password.txt
  • 非敏感配置用environment,但遵循12-Factor原则:
    • PORT=8080(端口)
    • LOG_LEVEL=warn(日志级别)
    • REDIS_URL=redis://cache:6379/0(服务地址,含协议+端口+DB索引)

特别注意:environment中$符号需转义。JAVA_OPTS=-Xmx$$MAX_HEAP中$$会被Shell解释为单个$,否则$MAX_HEAP被当作变量展开为空。


4. 实战全流程:从零构建高可用图像处理服务

4.1 项目架构设计:为什么必须拆成4个服务?

某跨平台图像处理Demo需求:接收用户上传图片→AI模型识别→生成报告→邮件通知。若单体部署,会出现三大问题:

  • GPU资源争抢:模型推理需独占GPU,而Web服务只需CPU,混部导致GPU利用率不足30%
  • 扩缩容失衡:上传峰值时Web服务需扩到20实例,但模型服务只需3实例,强行同扩浪费资源
  • 故障域扩大:邮件服务SMTP超时导致整个API阻塞

因此采用四层解耦:

  1. gateway:Nginx反向代理,处理HTTPS终止、限流(limit_req)、静态资源缓存
  2. web:Vue前端+Flask后端,处理用户交互与任务分发
  3. worker:Python Celery Worker,消费RabbitMQ队列,调用AI模型
  4. model:TensorFlow Serving容器,加载预训练模型,提供gRPC接口

docker-compose.yml骨架:

version: '3.8' services: gateway: image: nginx:alpine ports: ["443:443", "80:80"] volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./certs:/etc/nginx/certs:ro depends_on: - web networks: - frontend - backend web: build: context: ./web dockerfile: Dockerfile.prod environment: - REDIS_URL=redis://cache:6379/0 - RABBITMQ_URL=amqp://guest:guest@rabbit:5672 - MODEL_GRPC_HOST=model:8500 depends_on: - cache - rabbit - model networks: - frontend - backend worker: build: ./worker environment: - REDIS_URL=redis://cache:6379/0 - RABBITMQ_URL=amqp://guest:guest@rabbit:5672 depends_on: - cache - rabbit - model deploy: replicas: 3 # 固定3个Worker处理任务 networks: - backend model: image: tensorflow/serving:2.13-gpu volumes: - ./models:/models - ./config:/config command: > --model_config_file=/config/models.config --model_server_config_file=/config/server.config --enable_batching=true --batching_parameters_file=/config/batching.config deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] networks: - backend cache: image: redis:7-alpine command: redis-server --save 60 1 --loglevel warning volumes: - redis_data:/data networks: - backend rabbit: image: rabbitmq:3.11-management environment: - RABBITMQ_DEFAULT_USER=guest - RABBITMQ_DEFAULT_PASS=guest volumes: - rabbit_data:/var/lib/rabbitmq networks: - backend volumes: redis_data: rabbit_data: networks: frontend: internal: false # 允许外部访问 backend: internal: true # 仅服务间通信

4.2 Dockerfile深度优化:从1.8GB到287MB的瘦身之路

web服务原始Dockerfile(1.8GB):

FROM python:3.9 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt # 安装217个包 COPY . . CMD ["gunicorn", "app:app"]

问题诊断:

  • python:3.9镜像含完整CPython编译环境,体积780MB
  • pip install未指定--no-cache-dir,缓存占120MB
  • requirements.txt含dev依赖(如pytest),生产环境无需

优化后Dockerfile(287MB):

# 构建阶段 FROM python:3.9-slim-bookworm AS builder WORKDIR /app COPY requirements.txt . # 分离生产/开发依赖 RUN pip install --no-cache-dir --user -r requirements.txt && \ pip install --no-cache-dir --user -r requirements-dev.txt # 运行阶段 FROM python:3.9-slim-bookworm # 创建非root用户 RUN groupadd -g 1001 -f appgroup && \ useradd -r -u 1001 -g appgroup appuser USER appuser WORKDIR /app # 复制构建产物,不复制源码 COPY --from=builder /home/appuser/.local /home/appuser/.local ENV PATH=/home/appuser/.local/bin:$PATH # 复制精简后的代码(排除test/、docs/) COPY --chown=appuser:appgroup . . # 清理构建缓存 RUN rm -rf /var/lib/apt/lists/* /tmp/* ~/.cache/pip CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "4", "app:app"]

关键优化点:

  • slim-bookworm比slim-bullseye小18MB(Debian 12更精简)
  • --user安装到用户目录,避免sudo pip权限问题
  • --chown一步设置文件属主,省去RUN chown层
  • 删除/tmp/*和~/.cache/pip,实测减少42MB

4.3 docker-compose高级技巧:用profiles实现环境差异化

docker-compose.yml中profiles字段可声明服务启用条件,替代传统docker-compose.override.yml。某次某实验室项目需支持三种环境:

  • dev:启用代码热重载、数据库GUI工具
  • staging:启用监控探针、日志聚合
  • prod:强制资源限制、安全策略

实现方式:

services: web: profiles: ["dev", "staging", "prod"] build: ./web # ...通用配置 devtools: profiles: ["dev"] image: portainer/portainer-ce ports: ["9000:9000"] volumes: - /var/run/docker.sock:/var/run/docker.sock prometheus: profiles: ["staging", "prod"] image: prom/prometheus:latest volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml nginx: profiles: ["prod"] image: nginx:alpine deploy: resources: limits: memory: 512M cpus: '0.5' reservations: memory: 256M security_opt: - no-new-privileges:true - label:type:container_runtime_t

启动命令:

  • docker compose --profile dev up→ 启动web+devtools
  • docker compose --profile staging up→ 启动web+prometheus
  • docker compose --profile prod up→ 启动web+nginx(严格资源限制)

实操心得:profiles配合extends可实现配置继承。某次某项目将common.yaml定义基础网络和卷,prod.yaml通过extends复用并追加安全策略,避免配置重复。

4.4 构建与部署流水线:CI/CD中不可妥协的5个检查点

在GitLab CI中,某次某图像处理Demo项目因忽略以下检查点,导致生产事故:

  1. 镜像签名验证:docker build后必须cosign sign,CI中cosign verify确保镜像未被篡改
  2. CVE扫描:trivy image --severity CRITICAL myapp:latest,发现libjpeg-turbo高危漏洞立即阻断
  3. 端口冲突检测:docker-compose config | grep "ports:" | wc -l> 10时告警(防端口爆炸)
  4. 环境变量完整性:docker-compose config | grep "environment:" | grep -v "DB_PASSWORD\|API_KEY",确保敏感变量未明文出现
  5. 网络策略合规:docker-compose config | grep "network_mode:" | grep -v "bridge\|host",强制使用自定义网络

CI脚本关键段:

stages: - build - scan - deploy build-image: stage: build script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG scan-image: stage: scan script: - apk add --no-cache cosign trivy - cosign sign --key $COSIGN_KEY $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG - trivy image --severity CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG deploy-prod: stage: deploy script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker-compose --profile prod up -d only: - tags

5. 常见故障排查手册:95%的问题都藏在这5个地方

5.1 “Connection refused”不是网络问题,是服务发现失效

现象:docker-compose logs web显示requests.exceptions.ConnectionError: HTTPConnectionPool(host='api', port=8000): Max retries exceeded with url: /health

排查路径:

  1. 确认服务是否启动:docker compose ps | grep api,状态应为running
  2. 验证DNS解析:docker compose exec web nslookup api,返回Name: api Address: 172.20.0.3才正常
  3. 检查端口监听:docker compose exec api ss -tlnp | grep :8000,确认进程监听0.0.0.0:8000而非127.0.0.1:8000
  4. 验证防火墙:docker compose exec api iptables -L INPUT,确认无DROP规则

根因案例:某次api服务Dockerfile中CMD ["uvicorn", "app:app", "--host", "127.0.0.1"],导致仅监听回环地址。修正为--host 0.0.0.0。

5.2 “Permission denied”不是权限问题,是用户上下文错配

现象:docker compose exec web ls /app报Permission denied,但docker exec -u root web ls /app正常

原因:web服务Dockerfile中USER appuser,而挂载的./src目录属主是宿主机用户(UID 1000),容器内appuserUID为1001,无读取权限。

解决方案:

  • 方案1(推荐):构建时--build-arg UID=$(id -u),Dockerfile中ARG UID→useradd -u $UID appuser
  • 方案2:宿主机sudo chown -R 1001:1001 ./src
  • 方案3:docker-compose.yml中user: "1001:1001"覆盖

注意:user: "1001"仅指定UID,GID默认为0(root组),可能引发组权限问题,必须显式指定user: "1001:1001"

5.3 “No space left on device”不是磁盘满,是Docker存储驱动异常

现象:docker build中途报no space left on device,但df -h显示磁盘剩余40%

根因:Docker默认overlay2驱动,当/var/lib/docker/overlay2下存在大量diff目录(每层镜像一个),且未及时清理,inode耗尽。

验证:df -i /var/lib/docker,若Use%达100%即为inode耗尽。

清理命令:

# 删除悬空镜像(无tag、无容器引用) docker image prune -f # 删除构建缓存(Docker 20.10+) docker builder prune -f # 强制清理overlay2(谨慎!) sudo find /var/lib/docker/overlay2 -name "merged" -type d -mtime +7 | xargs sudo rm -rf

5.4 “Health check failed”不是服务崩溃,是健康检查阈值不合理

现象:docker compose ps显示web状态为unhealthy,但curl http://localhost:8000/health返回200

检查docker-compose.yml中健康检查配置:

healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s timeout: 5s retries: 3 start_period: 40s

问题:start_period: 40s过短,应用启动需60秒(加载大模型),前3次检查均失败,容器被标记为unhealthy。

修正:start_period: 90s,并增加start_interval: 15s避免密集探测。

5.5 “Container exited with code 137”不是内存溢出,是OOM Killer介入

现象:docker compose logs worker突然中断,docker compose ps显示Exit 137

137 = 128 + 9,表示被SIGKILL(信号9)终止,通常是OOM Killer所为。

验证:dmesg -T | grep -i "killed process",输出类似:
[Mon May 1 10:23:45 2023] Killed process 12345 (python) total-vm:2048000kB, anon-rss:1024000kB, file-rss:0kB

解决方案:

  • docker-compose.yml中为worker设置mem_limit: 2g,mem_reservation: 1g
  • 应用内限制内存:Python加ulimit -v 1048576(1GB虚拟内存)
  • 模型服务启用批处理:tensorflow-serving配置
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进