ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

从Dockerfile到Compose与私有仓库:容器交付链路避坑指南

从Dockerfile到Compose与私有仓库:容器交付链路避坑指南 Dockerfile 自定义镜像、Docker Compose 编排、Docker 镜像仓库这三件事几乎是每个后端或运维同学绕不开的一组基础能力。单看每一个都不难难的是把它们串成一条顺滑的交付链路代码改了镜像能稳定重建本地跑通换台机器也能一键起来镜像推到仓库别人拉下来不会因为标签乱飞而拉错版本。我见过太多项目卡在中间某一环——Dockerfile 写得像流水账、Compose 文件复制粘贴改了三个月没人敢动、私有仓库起了一个却因为证书问题全组改用别的方式传镜像。这篇就把这三块按我实际踩过的顺序拆开讲从分层缓存的底层逻辑到 Compose 的健康检查写法再到私有仓库的 TLS 配置和存储回收每个环节都给出能直接抄的参数和命令也会说明我为什么这么选。刚接触容器的新手可以照着做一遍已经在用但总觉得别扭的老手大概率能在避坑那几节找到共鸣。1. 先把三个东西的边界理清楚很多人上手就把这三样东西混在一起学结果脑子里是一锅粥。我用一句话概括它们的分工Dockerfile 负责造Compose 负责组仓库负责存和分发。造出来的东西要能被组起来组起来的东西要能被别人拿到这三步是一条线上的三个节点任何一环偷懒后面都会加倍还回来。1.1 一条镜像从代码到运行的完整链路我把一次完整的交付拆成六步你对照自己的项目看看断在哪一步。第一步开发者本地写代码改完提交第二步CI 或者本机执行docker buildDockerfile 逐条指令生成一层层文件系统快照最终产出一个带标签的镜像第三步docker push把镜像分层推到仓库注意只有本地不存在的层才会真正上传这也是为什么第二次推送往往快得多第四步目标机器docker pull按 manifest 里的 digest 把各层拉下来组装第五步Compose 读取 yaml创建网络、挂载卷、按依赖顺序起容器第六步健康检查通过流量切进来。这条链路上有三个时间点特别关键构建时的缓存命中率、拉取时的层复用率、启动时的就绪判断。缓存命中率高构建就从几分钟降到几十秒层复用率高一次发布只传改动的那几十兆就绪判断做对了滚动更新才不会出现新容器还没连上数据库老容器已经被干掉的尴尬。后面所有优化本质上都是在抠这三个点。我见过一个典型的反面案例某团队把COPY . .放在 Dockerfile 第二行紧接着才RUN npm install。结果每次改一行 README整个依赖安装层全部失效构建时间稳定在四分钟以上。把这两行的顺序调过来配合.dockerignore构建时间直接掉到二十几秒。改动量不到五行收益却是几十倍这就是理解链路顺序的价值。1.2 为什么我劝你彻底放弃 docker commit新手最容易走的捷径是起一个基础容器进去apt install、改配置、装依赖折腾满意了docker commit一下镜像就有了。第一次这么干会觉得特别爽但它埋的雷比省的时间多得多。最直接的问题是不可复现。三个月后你需要在这个镜像上加一个包谁也不知道当初在容器里手敲过哪些命令是改了/etc/apt/sources.list还是临时下过一个二进制。第二个问题是体积失控commit 会把容器运行期间产生的所有临时文件、日志、缓存全部固化进去一个本该 200MB 的镜像能做到 1.5GB。第三个问题是没有分层每次 commit 都是一整层新的推一次仓库传几百兆团队网速直接被拖垮。Dockerfile 的价值不在于能构建镜像而在于它是一份可审查、可版本控制、可被他人复现的构建说明书。谁改了什么、为什么加这个环境变量git blame一目了然。所以我的态度很明确除了临时排查问题任何时候都不要用 commit 产出要交付的镜像。真要临时进容器调试用docker exec -it进去看完就退别把改动当成成果。1.3 Compose 的适用边界什么时候该上什么时候别硬上Compose 解决的问题是一台机器上多个容器的协作关系。典型场景是应用 数据库 缓存 反向代理四五个服务本地开发、测试环境、小规模单机部署都用得上。它的好处是把docker run那一长串参数固化进文件端口、卷、环境变量、网络别名全部声明式表达新人克隆仓库就能跑起来不用问你那个数据库密码是多少。但它有明确的边界。第一只管单机。多节点调度、跨主机网络、自动扩缩容这些不是它的职责硬要往上叠就得引入额外的编排系统。第二没有内置的滚动更新和回滚docker compose up -d是停旧起新中间有短暂中断对可用性要求高的服务得接受这点。第三配置漂移难发现如果有人docker exec进去手改了配置重启就没了这既是缺点也是优点。我的判断标准是单机、服务数量在十个以内、不需要跨节点容灾Compose 完全够用别为了显得专业提前上重编排运维成本会反过来咬你。等真的出现多机需求了再迁移那时候你对服务间的依赖关系已经摸得很清楚迁移反而快。1.4 镜像仓库不只是存镜像的地方很多人对仓库的理解停留在能 push 能 pull 就行于是起个最简陋的服务放内网。用着用着就出问题谁都能推 latest某天线上拉到的 latest 和昨天的不一样磁盘涨到 100% 才发现没人清理出了安全问题想查某个镜像里到底装了什么版本的基础镜像是哪天的无从下手。一个真正可用的仓库至少要提供四件事访问控制谁能推、谁能拉、按项目隔离、标签规范配合 CI 强制约束不允许裸推 latest、存储治理保留策略 垃圾回收、可追溯镜像 digest、推送人、推送时间。前两项靠流程和仓库自身能力后两项靠定时任务。这几件事在单机 registry 上要自己搭在企业级方案里通常是现成的。选型的时候别只看能不能跑起来要看半年后谁来维护它。2. Dockerfile把镜像当代码写写 Dockerfile 的正确心态是写代码——它会被 review、会被 diff、会在半年后被修改。所以可读性、层数控制、缓存友好度都得在写的时候就考虑进去而不是等构建慢到受不了再回头重构。2.1 分层缓存是理解一切优化行为的钥匙Docker 构建镜像时每条指令生成一层每层带一个基于父层 ID 指令内容算出来的哈希。构建时逐层比对一旦某层的哈希在本地缓存里存在就直接复用不再执行。注意这里的关键哈希只跟指令文本和父层有关跟指令执行的结果无关。这意味着只要指令字符串变了哪怕只是多一个空格这层和它之后的所有层全部失效。由此推导出的三条铁律变化频率低的指令放前面变化频率高的放后面把安装依赖和拷贝源码彻底分开能用一条RUN干完的事别拆成三条。第三条的原理是每条RUN都是一层层数多了镜像的元数据开销变大而且中间产物没法在同一条指令内清理。举个最常见的 Node 项目写法我会这样排FROM node:20-alpine AS base WORKDIR /app # 只拷贝依赖清单源码变化不影响这一层缓存 COPY package.json package-lock.json ./ RUN npm ci --omitdev npm cache clean --force # 源码放在最后 COPY . . CMD [node, server.js]npm ci比npm install更适合构建场景它严格按 lock 文件安装结果确定而且会先清空 node_modules避免上次残留污染。--omitdev跳过开发依赖npm cache clean --force在同一层里清掉缓存两者加起来通常能省一百多兆。对比一下 Java 项目逻辑完全一样只是清单文件换成pom.xmlFROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn -B dependency:go-offline COPY src ./src RUN mvn -B clean package -DskipTestsdependency:go-offline这一句是精髓它把依赖提前拉到本地仓库形成一个独立缓存层。实测下来没加这句时改一行 Java 代码要重下全部依赖加了之后增量构建基本在十秒级。注意COPY . .之前一定要有.dockerignore否则node_modules、.git、target这些目录会被打进构建上下文光是传输上下文就能耗掉几十秒而且可能覆盖掉容器内刚装好的依赖。2.2 指令选择的细节账COPY和ADD的区别被讲烂了但真正要紧的部分是ADD会偷偷解压本地 tar 包、会去拉远程 URL这两个贴心功能会让构建行为变得不可预测。远程 URL 的下载不进缓存校验内容变了也不会触发重建。所以除非你确实需要解压一个本地压缩包否则一律用COPY。RUN的写法有个流传很久但其实是错误的做法把apt-get update和apt-get install分成两条RUN。分开写的话update 那层会被缓存很久等你半年后新增一个包时install 用的是半年前的索引轻则装到旧版本重则 404 直接失败。正确写法是合并成一条并在末尾清理RUN apt-get update \ apt-get install -y --no-install-recommends curl tzdata \ rm -rf /var/lib/apt/lists/*--no-install-recommends能挡掉一大批推荐包rm -rf必须在同一条指令里执行否则删除动作在下一层上一层的大小照样算进镜像。ENV和ARG常被混用。ARG只在构建期有效构建完就消失适合传版本号、镜像源地址这类构建参数ENV会写进镜像的配置里容器运行时也能读到适合传时区、语言编码、运行模式。这里有个安全提醒不要把密钥、令牌通过ARG或ENV传进去它们会留在镜像历史里docker history一条命令就能看见。CMD和ENTRYPOINT的 shell 形式与 exec 形式的差异是信号处理问题的根源。写成CMD node server.jsshell 形式实际启动的是/bin/sh -c node server.js你的应用是 sh 的子进程PID 1 是 sh。sh 不会转发 SIGTERMdocker stop发信号后应用收不到只能等十秒超时被 SIGKILL 强杀优雅关闭逻辑全部失效。写成CMD [node, server.js]exec 形式node 直接是 PID 1信号正常。EXPOSE只是文档声明不实际开放端口真正映射靠-p或 Compose 的ports。但它是很好的自描述信息建议写上。HEALTHCHECK后面在 Compose 那节细说它在镜像里定义一次所有用这个镜像的地方都继承。2.3 多阶段构建一个 Java 服务的体积从 800MB 到 180MB多阶段构建是 Dockerfile 里收益最大的一招。核心思想编译环境臃肿但只在构建期需要运行环境要精简。用多个FROM阶段最后一个阶段只COPY --from前一个阶段的产物。以 Spring Boot 为例不用多阶段时通常是把 jar 拷进一个带 JDK 的镜像动辄七八百兆。改成下面这样FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build COPY pom.xml . RUN mvn -B dependency:go-offline COPY src ./src RUN mvn -B clean package -DskipTests FROM eclipse-temurin:17-jre-alpine WORKDIR /app RUN addgroup -S app adduser -S -G app app COPY --frombuilder /build/target/*.jar app.jar USER app EXPOSE 8080 ENTRYPOINT [java, -XX:MaxRAMPercentage75, -jar, app.jar]最终镜像只有 JRE 加一个 jar实测一般在一百八到两百二十兆之间取决于依赖多少。里面有几点值得说MaxRAMPercentage75让 JVM 按容器内存限制的百分比估算堆大小比写死-Xmx更适应不同规格的部署adduser创建非 root 用户USER切过去后面第 2.5 节细讲COPY --frombuilder只搬 jarMaven 的本地仓库、源码、编译中间件一概不带。前端项目同理构建阶段用 node 打包出静态文件运行阶段换成反向代理镜像只把dist目录拷过去镜像能从 1GB 降到 50MB 左右。这种编译环境和运行环境彻底分离的思路适用面比想象中广任何需要编译产物的语言都能用。2.4 基础镜像选型对照表与 musl 的坑基础镜像选错后面要么体积下不来要么跑起来就崩。我按常见选项列个对照基础镜像类型体积量级优点需要注意的地方alpine5–10MB极小攻击面小用 musl libc某些预编译二进制跑不了装包用 apk命令和 apt 不同debian-slim25–40MB兼容性好glibc 完整比 alpine 大一圈包管理器是 apt完整发行版70–120MB工具齐全调试方便体积大无用包多distroless类20–60MB无 shell、无包管理器安全性高没法进容器排查日志全靠应用输出scratch0绝对最小完全空只适合静态编译的二进制Alpine 的坑值得单独说因为我踩过。它的 C 运行库是 musl不是广泛使用的 glibc。纯 Go 静态编译的程序、Python 的解释器都没问题但某些依赖 glibc 特性的预编译二进制比如一些原生扩展、某些数据库客户端放进去会报not found或者奇怪的符号错误排查起来非常费劲因为报错信息完全不指向根因。我的经验是不确定就先上 debian-slim等确认没问题再往 alpine 迁。为了省那二十兆搭进去半天排查时间不划算。distroless适合安全要求高的生产环境但没有 shell 意味着出问题时你没法exec进去看进程、看文件只能靠完善的日志和监控。团队排查能力没到那个水平之前别用。2.5 安全性收尾非 root、只读、健康检查镜像默认以 root 运行容器内进程这在单机开发环境问题不大但在多租户或者对外服务的场景里是明显风险点。加一个普通用户的成本极低RUN useradd -u 1000 -m -s /bin/bash appuser \ chown -R appuser:appuser /app USER appuser注意chown要在USER之前完成顺序反了会因为没有权限而失败。另外创建用户这一步尽量和后续的chown合并在同一条指令里避免多一层。时区是另一个高频问题。容器默认 UTC日志时间比东八区差八小时排查问题时对时间戳能对到怀疑人生。做法是装tzdata并设置环境变量ENV TZAsia/Shanghai LANGC.UTF-8 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime echo $TZ /etc/timezoneLANG也别漏某些应用在非 UTF-8 环境下处理中文会出乱码尤其是日志输出和文件读写。HEALTHCHECK健康检查在镜像里定义一次即可HEALTHCHECK --interval30s --timeout3s --start-period40s --retries3 \ CMD curl -fsS http://localhost:8080/actuator/health || exit 1start-period很关键它告诉 Docker 在这段时间内的失败不算数给应用留足启动时间。没有这个参数启动慢的 Java 服务会被反复判定为不健康。注意检查命令用的工具必须在镜像里存在alpine 下curl默认没装要么装要么换成wget。2.6 .dockerignore 与构建上下文.dockerignore是被严重低估的文件。它的作用和.gitignore类似但影响的是发送给 Docker 守护进程的构建上下文。这个上下文在构建开始时会被整个打包传输一个带node_modules的前端项目上下文轻松几百兆每次构建光传输就要几十秒。我常用的模板.git .gitignore node_modules target build dist *.log logs .env.local .idea .vscode README.md docker-compose*.yml两个细节一是不要忽略package-lock.json或pom.xml那是构建必需的二是如果一个文件既在.dockerignore里又被COPY引用构建会直接失败报file not found很多人排查半天以为是路径写错了其实是忽略了。3. Docker Compose把一组容器当一台机器用Compose 文件写得好不好看一个标准就够了新人克隆仓库docker compose up -d一条命令所有服务起来并进入可用状态不需要看任何文档。达不到这个标准说明还有环境变量、依赖顺序或者初始化数据没声明清楚。3.1 compose.yaml 的结构骨架新版 Compose 已经不需要顶层version字段写了反而会收到一条废弃警告。基本骨架是这样的services: api: build: context: . dockerfile: Dockerfile image: registry.internal.example.com/team/api:1.4.2 ports: - 8080:8080 environment: TZ: Asia/Shanghai SPRING_PROFILES_ACTIVE: docker env_file: - .env.docker volumes: - app-logs:/app/logs networks: - backend depends_on: db: condition: service_healthy restart: unless-stopped healthcheck: test: [CMD, curl, -fsS, http://localhost:8080/actuator/health] interval: 30s timeout: 3s retries: 3 start_period: 40s db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db-data:/var/lib/postgresql/data networks: - backend healthcheck: test: [CMD-SHELL, pg_isready -U postgres] interval: 10s timeout: 5s retries: 5 volumes: app-logs: db-data: networks: backend: driver: bridge几个点展开说。build和image同时存在时docker compose build会按 Dockerfile 构建并打上image指定的标签docker compose pull则直接拉这个标签。这个组合在开发和生产之间切换很方便本地改代码就 build服务器上直接 pull。environment和env_file可以并存前者优先级更高。密码这类敏感信息放env_file里并把该文件加入.gitignore操作用${DB_PASSWORD}变量引用Compose 会自动从同目录的.env里读。restart: unless-stopped是我最常用的策略。always的问题是手动停了也会被拉起来调试时很烦on-failure遇到正常退出就不重启有些场景不合适。unless-stopped兼顾两者崩溃自动拉起手动停止后保持停止。3.2 depends_on 与 healthcheck 的正确组合这是 Compose 里被误解最深的一处。很多人以为depends_on: [db]就代表等 db 启动好了再起 api实际上它只保证容器启动顺序db 容器一创建api 容器立刻开始启动此时数据库进程可能还在初始化、端口根本没监听。结果 api 连接失败重试几次后放弃日志里一堆连接拒绝。正确做法是配合健康检查depends_on: db: condition: service_healthy配合 db 服务上的healthcheck定义Compose 会等到健康检查通过才启动依赖方。pg_isready这类专用探针比curl更准确因为它检查的是数据库真实可接受连接的状态不只是端口开着。健康检查的参数需要根据服务特性调。数据库这种启动快但初始化慢的interval: 10s、retries: 5大概够Java 应用启动慢start_period: 40s甚至更长。start_period内的失败不计数这是给慢启动服务的保护期设短了会误判。还有一个常见误解健康检查通过只代表进程能响应探针不代表业务逻辑完全就绪。比如应用可能探针通了但缓存还没预热完。这种场景下更靠谱的做法是在应用里实现真正的就绪检查接口把关键依赖数据库连接池、配置加载、缓存连接都纳入判断探针打这个接口。健康检查的质量直接决定编排的可靠性。3.3 网络、卷与环境变量的三层隔离Compose 默认会创建一个以项目名命名的网络所有服务自动加入服务之间可以直接用服务名当主机名互访。这个默认行为很方便但也容易造成全都通的混乱。我的习惯是显式定义网络并分组networks: frontend: backend: internal: true把数据库和缓存放backend应用同时加入两个网络反向代理只加入frontend。internal: true让这个网络无法访问外网数据库完全没有对外出口安全性上一个台阶。卷分两类命名卷和绑定挂载。命名卷由 Docker 管理存在/var/lib/docker/volumes/下适合数据库数据、应用日志这类需要持久化但不方便直接编辑的内容。绑定挂载直接把宿主机目录映射进去适合开发时挂载源码实现热更新volumes: - ./src:/app/src - ./config:/app/config:ro:ro只读挂载配置目录防止容器内误改宿主机文件。这个细节在多人共用一台开发机时尤其重要。环境变量我按三层管理镜像内置默认值ENV写在不敏感的配置上、Compose 声明的环境变量区分不同环境的差异项、运行时注入密码密钥通过外部文件或编排平台注入绝不进仓库。三层各司其职不会出现改个数据库地址要重新构建镜像这种事。3.4 多环境覆盖override 文件与 profilesCompose 支持文件合并compose.yaml加上compose.override.yaml会自动叠加后者优先级更高。这个机制天然适合做环境差异化基础文件写通用配置override 写本机特定的端口、卷、调试参数。# compose.override.yaml本地开发用加入 .gitignore services: api: volumes: - ./src:/app/src environment: LOG_LEVEL: debug ports: - 8080:8080 - 5005:5005 # 远程调试端口服务器上不放这个文件就是生产配置本地开发放进去自动获得热更新和调试端口。也可以显式指定多个文件docker compose -f compose.yaml -f compose.prod.yaml up -d后者覆盖前者。profiles解决的是可选服务的问题。比如监控组件、数据库管理工具这些平时不需要的服务services: adminer: image: adminer:latest profiles: [tools] ports: - 8081:8080不带--profile tools启动时这个服务完全不会被创建不会浪费资源。需要时docker compose --profile tools up -d临时拉起来。比起注释掉再取消注释这种方式干净太多也不会因为忘记改回来而出事。3.5 我常用的命令清单与排查路径命令不在多在于用对时机。下面这几个是我每天都会碰的命令用途使用时机docker compose config展开并校验最终配置改完 yaml 先跑一遍变量没解析、语法错都能立刻发现docker compose up -d --build重建镜像并后台启动改了代码或 Dockerfile 后docker compose ps查看服务状态和健康情况起完之后确认是否全部 healthydocker compose logs -f --tail100 api跟踪某个服务日志排查启动失败docker compose exec api sh进入运行中的容器需要看文件、验证网络连通性docker compose down -v停止并删除容器与卷需要彻底清空环境重来docker compose up -d --force-recreate强制重建容器配置改了但没生效时docker compose config这个命令被严重低估。它把变量替换后的最终配置整个打印出来很多时候配置明明改了为什么不生效跑一下就能看到真相——可能是变量名拼错、可能是 override 文件覆盖了、可能是.env文件位置不对。排查起不来的服务我的固定顺序是先docker compose ps看状态是 Exit 还是 Restarting再docker compose logs --tail200 服务名看报错然后docker compose config确认配置没被覆盖最后才考虑进容器。绝大多数问题在前两步就能定位直接钻容器里翻往往是浪费时间。4. 私有镜像仓库从单机 registry 到团队级方案镜像仓库这块很多团队吃过起步太简陋、后来迁移成本高的亏。我的建议是先想清楚使用规模和治理需求再决定投入多少。4.1 先想清楚仓库要解决什么问题三类需求对应三种方案。个人或小团队内网共享几十个镜像两三个人推拉一个单机 registry 就够了十分钟能跑起来维护成本几乎为零。团队级使用需要按项目隔离权限、需要知道谁推了什么、需要定期清理这时候得上带管理界面的方案配置项多但也省心。有合规或审计要求需要镜像签名、漏洞扫描、拉取审计日志那就得选企业级套件前期投入大但后面省事。选型时我最看重的是存储后端是否可替换。早期的仓库把数据存在本地磁盘用着用着磁盘满了扩容就得停机迁移。支持对象存储的仓库扩容只是改个配置。这一点在起步时容易被忽略等数据涨到几百 GB 才发现迁移就是一次大工程。4.2 十分钟起一个可用的私有仓库单机方案起步极快services: registry: image: registry:2 ports: - 5000:5000 environment: REGISTRY_STORAGE_DELETE_ENABLED: true volumes: - ./registry-data:/var/lib/registry - ./config.yml:/etc/docker/registry/config.yml:ro restart: unless-stoppedREGISTRY_STORAGE_DELETE_ENABLED这个环境变量一定要开否则后面想删镜像标签会直接被拒绝。配置文件里可以追加保留策略和存储后端storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true cache: blobdescriptor: inmemory http: addr: :5000起来之后先给镜像打上带仓库地址的完整标签再推docker tag myapp:1.0.0 192.168.10.20:5000/team/myapp:1.0.0 docker push 192.168.10.20:5000/team/myapp:1.0.0标签的完整格式是仓库地址/命名空间/镜像名:版本。这里最容易出的错是忘了加仓库地址前缀直接docker push myapp:1.0.0会被推到默认的公共仓库本地私有仓库里什么都没有。4.3 TLS 与 insecure-registries 的正确姿势默认情况下Docker 客户端要求仓库必须走 HTTPS。你用 HTTP 起在 5000 端口push 时会看到http: server gave HTTP response to HTTPS client意思很直白客户端发的是 HTTPS 请求服务端回的是 HTTP 明文。两种解法。生产环境必须用证书把 CA 证书放到/etc/docker/certs.d/仓库地址:端口/ca.crt客户端就会信任。自签证书的关键是 SAN 字段必须包含你实际访问用的地址——很多人签证书时只填了域名实际访问用的是 IP照样报x509: certificate is valid for ...。开发和测试环境可以临时把仓库标记为不安全仓库在/etc/docker/daemon.json里加{ insecure-registries: [192.168.10.20:5000] }改完必须重启 Docker 服务生效。这里有个坑insecure-registries只对 HTTP 仓库有效如果你的仓库确实是 HTTPS 但证书不被信任加这个配置也没用得老老实实放 CA 证书。我见过有人在这上面绕了两小时一直在反复重启 Docker 服务。注意不要为了图省事把内网所有地址都塞进insecure-registries。一旦形成习惯某天真的需要走加密传输时排查范围会大得离谱。4.4 命名、打标签与保留策略标签策略是我见过最混乱的一环。典型现场是仓库里每个镜像都有latest、test、v1、v1-new、v1-最终版、v1-真最终版没人敢删任何一个。半年后想回滚某个版本发现根本对不上哪次发布。我推行的规则只有三条。第一latest只在一个地方使用本地开发随手构建时用CI 和发布流程严禁推latest。第二正式版本用语义化版本号1.4.2这种配合 git tag能精确对应到某次提交。第三额外打一个 git 短哈希标签方便排查docker tag myapp:1.4.2 192.168.10.20:5000/team/myapp:1.4.2 docker tag myapp:1.4.2 192.168.10.20:5000/team/myapp:sha-a1b2c3d版本号给人看哈希给机器查。生产环境部署时用 digest 而不是标签这是最稳的方式docker pull 192.168.10.20:5000/team/myappsha256:xxxx因为标签可以被移动同一个1.4.2今天推的和明天推的可能是两个不同的镜像digest 是内容哈希永远指向同一份内容。这点在出故障回滚时特别关键——你确定拉回来的就是当初那个。4.5 存储回收为什么删了 tag 磁盘没变小这个问题几乎每个自建仓库的人都会遇到。执行了删除标签的命令curl也返回成功但du -sh一看磁盘占用纹丝不动。原因是 Registry 的删除是两段式的删标签只是删掉了 manifest 的引用底层的 layer 文件blob还在因为可能被其他标签共享。要真正回收得跑垃圾回收。Registry 自带了一个命令在容器里执行docker exec registry /bin/registry garbage-collect \ --delete-untagged /etc/docker/registry/config.yml在这之前必须先删干净所有指向无用 manifest 的标签而且回收过程最好停写或只读因为并发推送可能导致新镜像的层被误判为无引用而清掉。我的做法是每天凌晨跑一次定时任务先停掉对外推送权限再执行回收最后恢复。听起来麻烦但比起某天磁盘满了导致所有构建失败这点麻烦值。保留策略建议按数量 时间双维度控制每个仓库保留最近 10 个版本同时删除所有超过 90 天且非正式版本的标签。只按数量控制低频更新的仓库会保留很久只按时间控制高频更新的仓库会积攒太多。5. 实战问题速查与避坑记录下面这些几乎都是我或者身边同事真实撞上过的按阶段整理出问题时可以先扫一眼表格。5.1 构建阶段高频故障表现象根因处理方式COPY failed: file not found文件被.dockerignore排除或路径超出构建上下文检查.dockerignore确认context目录包含该文件构建越来越慢改一行代码就要几分钟COPY . .在依赖安装之前缓存全失效调整指令顺序依赖清单先拷源码后拷镜像体积远超预期缓存文件、包管理器索引没在同一层清理RUN合并清理命令用多阶段构建not found或符号相关错误程序跑不起来Alpine 的 musl libc 与预编译二进制不兼容换成debian-slim基础镜像时区不对日志差八小时容器默认 UTC没设TZ装tzdata设ENV TZ构建时报no space left on device本地镜像和构建缓存占满磁盘docker system prune -af配合--filter until72h磁盘那条多说一句docker system prune默认不清理正在被容器使用的镜像加-a才会清掉所有未使用镜像加-f免确认。执行前确认没有正在跑的重要容器用docker ps看一眼再动手。5.2 运行与编排阶段高频故障表现象根因处理方式应用启动就连不上数据库depends_on只保证启动顺序不等就绪加healthcheckcondition: service_healthydocker stop要等十秒才停CMD用了 shell 形式PID 1 是 sh信号没转发改成 exec 形式[node,server.js]非 root 用户写卷报permission denied宿主机目录属主与容器内 UID 不匹配构建时chown或启动时用user: 1000:1000容器内访问localhost连不上另一个服务每个容器有独立网络栈用服务名当主机名服务需在同一网络exec format error镜像架构与宿主机不一致构建时指定--platform linux/amd64改了配置重启不生效容器被复用新配置没进容器docker compose up -d --force-recreateport is already allocated端口被占用ss -lntp查占用进程或换端口exec format error这个在 M 系列芯片的机器上构建、部署到 x86 服务器时特别常见报错信息很短容易误判成镜像损坏。构建时加--platform linux/amd64或者用buildx同时产出多架构镜像能一次性解决。5.3 仓库阶段的三个经典报错http: server gave HTTP response to HTTPS client——前面说过客户端用 HTTPS 请求服务端是明文 HTTP。要么给仓库配证书要么把地址加进insecure-registries并重启服务。x509: certificate signed by unknown authority——客户端不信任仓库证书。把 CA 证书放到/etc/docker/certs.d/地址/ca.crt。注意目录名要和访问用的地址完全一致带端口的话端口不能少192.168.10.20:5000和192.168.10.20是两个不同的目录。manifest unknown或not found——拉取时找不到对应的标签。要么是标签打错了要么是推送失败了但命令返回被忽略。用仓库的 API 列一下现有标签确认curl http://地址:5000/v2/命名空间/镜像名/tags/list能直接看到实际存在哪些标签比猜测快得多。5.4 几条踩出来的经验关于调试信息镜像里装curl和netstat这类工具体积会增加十几兆但排查网络问题时能省下大量时间。如果实在不想装进生产镜像可以在构建时开一个带工具的 debug 阶段出问题时临时构建那个阶段进去看。关于构建速度如果依赖下载慢是网络层面的问题加构建参数换源比优化 Dockerfile 更有效。另外--cache-from配合 CI 缓存能显著加速把上一次构建的镜像拉下来当缓存源远程构建时效果明显。关于Compose 文件的可维护性超过五个服务之后尽量按职责拆成多个文件用-f组合。一个两百行的 yaml 没人愿意改改成三个七十行的人人敢动。另外把公共部分网络、时区、日志配置用x-开头的锚点抽出来复用能减少大量重复。关于仓库的可用性私有仓库挂掉的时候如果所有机器都依赖它拉镜像整个发布流程就停了。至少保证每台机器本地缓存了最近几个可用版本docker image ls里能看到紧急时可以直接用本地镜像启动不必等仓库恢复。关于版本管理的心态我见过太多人觉得镜像仓库反正内网标签随便打没关系等到要回滚某个两周前的版本发现latest已经更新了七次根本找不回来。标签规范这件事在只有三个人的时候建立成本几乎为零在三十个人的时候推行的阻力大得离谱。早做比什么都强。最后分享一个我自己一直在用的小习惯每写完一个 Dockerfile先用docker build跑两遍。第一遍看不到什么特别第二遍观察哪些层显示CACHED如果只有最后一两层命中说明指令顺序还有优化空间把变化最频繁的内容再往后挪。这个动作花不到一分钟但能让你对每一层的边界有肌肉记忆写第三个 Dockerfile 的时候顺序自然就对了。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表