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.04 | 72MB | apt | glibc | 每月安全公告 | 需要完整Debian生态的Java/Python应用 |
alpine:3.18 | 5.6MB | apk | musl | 每周滚动更新 | Web服务、CLI工具、资源敏感型服务 |
distroless:nonroot | 12MB | 无 | 无(仅二进制) | 按需构建 | 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 ← 真正的缓存体积解决方案只有两个:
- 合并RUN指令:将安装与清理写在同一行,让Docker在单层内完成操作
RUN apt-get update && \ apt-get install -y curl wget git && \ rm -rf /var/lib/apt/lists/* - 使用多阶段构建:在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.confdocker 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显式声明端口映射
因此,前端调用后端必须:
- 前端构建时注入环境变量
VUE_APP_API_BASE_URL=http://localhost:8080(开发环境) - 或在
docker-compose.yml中为前端添加extra_hosts: ["host.docker.internal:host-gateway"],使其能访问宿主机服务 - 生产环境用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映射端口 | 开发测试环境 |
host | network_mode: "host" | 容器共享宿主机网络命名空间,无网络隔离 | 性能敏感型服务(如Envoy代理) |
none | network_mode: "none" | 容器无网络接口,仅lo回环 | 安全沙箱(如离线数据处理) |
| 自定义bridge | networks: { 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,保留原始IP3.3 volumes挂载:不是文件同步,是存储生命周期管理
volumes常被误用为“让代码热更新”,但bind mount(如./src:/app/src)在生产环境是灾难。某次某图像处理Demo项目在CI中用volumes: ["./logs:/app/logs"]收集日志,结果因宿主机磁盘满,导致所有容器I/O阻塞。
根本原因在于:bind mount使容器存储生命周期与宿主机强绑定。正确方案是区分三种挂载类型:
| 类型 | 语法 | 生命周期 | 适用场景 |
|---|---|---|---|
| Bind Mount | ./data:/app/data | 与宿主机目录同寿 | 开发环境代码热加载、配置文件注入 |
| Named Volume | mydata:/app/data | 由Docker管理,docker volume prune清理 | 数据库数据持久化、日志归档 |
| tmpfs | tmpfs:/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+.gitignoreservices: 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阻塞
因此采用四层解耦:
gateway:Nginx反向代理,处理HTTPS终止、限流(limit_req)、静态资源缓存web:Vue前端+Flask后端,处理用户交互与任务分发worker:Python Celery Worker,消费RabbitMQ队列,调用AI模型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编译环境,体积780MBpip install未指定--no-cache-dir,缓存占120MBrequirements.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+devtoolsdocker compose --profile staging up→ 启动web+prometheusdocker compose --profile prod up→ 启动web+nginx(严格资源限制)
实操心得:
profiles配合extends可实现配置继承。某次某项目将common.yaml定义基础网络和卷,prod.yaml通过extends复用并追加安全策略,避免配置重复。
4.4 构建与部署流水线:CI/CD中不可妥协的5个检查点
在GitLab CI中,某次某图像处理Demo项目因忽略以下检查点,导致生产事故:
- 镜像签名验证:
docker build后必须cosign sign,CI中cosign verify确保镜像未被篡改 - CVE扫描:
trivy image --severity CRITICAL myapp:latest,发现libjpeg-turbo高危漏洞立即阻断 - 端口冲突检测:
docker-compose config | grep "ports:" | wc -l> 10时告警(防端口爆炸) - 环境变量完整性:
docker-compose config | grep "environment:" | grep -v "DB_PASSWORD\|API_KEY",确保敏感变量未明文出现 - 网络策略合规:
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: - tags5. 常见故障排查手册:95%的问题都藏在这5个地方
5.1 “Connection refused”不是网络问题,是服务发现失效
现象:docker-compose logs web显示requests.exceptions.ConnectionError: HTTPConnectionPool(host='api', port=8000): Max retries exceeded with url: /health
排查路径:
- 确认服务是否启动:
docker compose ps | grep api,状态应为running - 验证DNS解析:
docker compose exec web nslookup api,返回Name: api Address: 172.20.0.3才正常 - 检查端口监听:
docker compose exec api ss -tlnp | grep :8000,确认进程监听0.0.0.0:8000而非127.0.0.1:8000 - 验证防火墙:
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 -rf5.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配置