知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 03 - 镜像仓库:标签规范、推拉与私有仓库 > 镜像造出来只躺在你本机,等于没交付。这一篇讲清镜像怎么被命名、怎么推到一个"大家都能拉"的地方,以及为什么"标签打错"是发布事故最常见的起点。 > 读完你应该能独立搭起一个私有仓库,并把镜像从构建机一路送到 K8s 节点上。 --- ## 1. 一个镜像名的完整结构 `registry.example.com/prod/order-service:1.4.2` 这一串看着眼熟,但它其实由五段组成,每一段都有明确职责。  *图:镜像名的五段结构:tag 只是指向 digest 的可变指针* ```text registry.example.com / prod / order-service : 1.4.2 └─────────┬────────┘ └─┬─┘ └─────┬──────┘ └─┬─┘ │ │ │ │ ① 仓库域名 ② 命名空间 ③ 名称 ④ 标签(tag) (可省略) (可省略) (必填) 完整形态还能带第 ⑤ 段 —— digest: registry.example.com/prod/order-service@sha256:9f3a1c2b7d4e... ``` | 段 | 示例 | 作用 | 省略时的默认值 | |---|---|---|---| | ① 仓库域名 | `registry.example.com` | 从哪台服务器拉/推 | 默认 `docker.io`(Docker Hub) | | ② 命名空间 | `prod` | 按组织/团队/环境隔离 | 默认 `library`(Docker Hub 官方镜像专用) | | ③ 名称 | `order-service` | 镜像名 | 必填,不能省略 | | ④ 标签 tag | `1.4.2` | 人类可读的版本指针 | 默认 `latest` | | ⑤ digest | `sha256:9f3a...` | 镜像内容的唯一哈希 | 无默认,但始终客观存在 | 省略规则要看一眼: ```bash docker pull nginx # 实际等价于 → docker pull docker.io/library/nginx:latest docker pull myteam/order-service # 实际等价于 → docker pull docker.io/myteam/order-service:latest ``` > 一句话:**digest 才是唯一标识,tag 只是一个可以被覆盖的指针。** --- ## 2. 标签规范:为什么禁用 latest `latest` 的坑在于"它只是默认值,不是版本"。同一个 tag 被重复推送覆盖后,两台机器可能拉到完全不同的代码。  *图:latest 被覆盖导致的版本漂移:同一个 tag 在不同时间指向不同代码* ```text 时间线:latest 被覆盖导致的"幽灵版本漂移" 周一 周三 │ │ │ 构建 v1,push :latest │ 构建 v2,push :latest(把 v1 覆盖了) v v registry: latest = 镜像 A registry: latest = 镜像 B (内容 sha256:aaaa) (内容 sha256:bbbb) 节点 1:周一拉过 latest,本地缓存了 A 节点 2:周三拉 latest,本地缓存了 B │ v ┌────────────────────────────────────────────────────────────┐ │ 同一个 Deployment 用 latest 滚动升级: │ │ · 节点 1 发现本地已有 latest → 不重新拉 → 继续跑 v1(旧) │ │ · 节点 2 拉了 latest → 跑 v2(新) │ │ → 两个节点跑着不同版本,谁也说不清线上到底是什么 │ │ │ │ 回滚时更糟:rollout undo 回到 latest,拉到的还是"当前 latest" │ │ → 你想要的旧版本,根本回不去 │ └────────────────────────────────────────────────────────────┘ ``` 推荐的标签规范: | 标签形式 | 示例 | 可变性 | 用途 | |---|---|---|---| | 完整语义版本 | `1.4.2` | 不可变 | 生产发布用;一次发布对应一个确定版本 | | 小版本滚动 | `1.4` | 可变 | 只想跟小版本更新的场景;生产慎用 | | git 提交短 sha | `sha-9f3a1c2` | 每次提交唯一 | 回滚定位、追溯"这个镜像对应哪次提交" | | 环境标签 | `dev` / `staging` | 可变 | 仅测试环境用;生产禁止 | | `latest` | `latest` | 可变 | **明确禁止**(等价于"没有版本") | 推荐实践:一次构建,同时打两个 tag。 ```bash docker build -t registry.example.com/prod/order-service:1.4.2 \ -t registry.example.com/prod/order-service:sha-9f3a1c2 . docker push registry.example.com/prod/order-service:1.4.2 docker push registry.example.com/prod/order-service:sha-9f3a1c2 ``` 前一个 tag 给人看、给发布用;后一个 tag 是"这一版到底是哪次提交"的铁证,回滚时最靠谱。 > 一句话:**生产环境永远用 `1.4.2` 或 `sha-9f3a1c2`,绝不用 `latest`。** --- ## 3. 公共仓库与加速器 镜像总得有个存放的地方。公共侧的选择和网络现实问题: | 类型 | 例子 | 优点 | 限制 | |---|---|---|---| | Docker Hub | `docker.io` | 官方镜像最全、是默认地址 | 匿名拉取有速率限制;国内直连慢 | | 云厂商公共镜像仓库 | 各云厂商提供的公共镜像地址 | 国内速度快 | 依赖厂商;镜像同步可能有延迟 | | 国内加速器 | `daemon.json` 里的 `registry-mirrors` | 免改镜像名,直接加速 | 只加速 Docker Hub,不管私有镜像托管 | 两个最常见的报错: ```text 现象 1:拉取超时 ERROR: net/http: request canceled while waiting for connection → 原因:直连 Docker Hub 网络不通 → 对策:配置 registry-mirrors 加速器 现象 2:被限流 ERROR: toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading... → 原因:匿名拉取超过 Docker Hub 的配额(未登录时按来源 IP 计) → 对策:docker login 后再拉(配额更高);或改用国内镜像仓库 ``` `daemon.json` 的两个关键配置: ```json { "registry-mirrors": ["https://<你的加速器地址>"], "insecure-registries": ["registry.internal:5000"] } ``` - `registry-mirrors`:只对 Docker Hub 生效,用来加速官方镜像。 - `insecure-registries`:给"HTTP 协议或自签证书"的私有仓库用。默认情况下 Docker 要求 HTTPS,这个开关是"破例"。 ```bash # Linux 改完 daemon.json 要重启守护进程 sudo systemctl restart docker # Docker Desktop:Settings → Docker Engine 里编辑,保存即自动重启 ``` > `insecure-registries` 意味着全程明文或不做证书校验,传输过程可被中间人窃听、镜像可被篡改。**它只是本地开发的方便,生产必须上 HTTPS。本地私有仓库想用,也得自己签证书、给客户端加 CA 信任。** --- ## 4. 私有仓库 公共仓库放不了公司内部镜像,得有个私有的。三种主流选择: | 方案 | 形态 | 能力 | 适合 | |---|---|---|---| | 官方 `registry:2` | 单个容器 | 只有推/拉;无 UI、无鉴权、无扫描、无回收界面 | 学习、内网极简场景 | | Harbor | 一套自建服务 | UI、RBAC、镜像扫描(Trivy)、垃圾回收、多仓库复制、镜像签名 | 企业自建的主流选择 | | 云厂商托管(TCR/ACR/CR) | 免运维 | 按量计费、天然高可用、与云上 K8s 打通 | 已经在用某朵云的公司 | 最快起一个 `registry:2` 尝尝: ```bash docker run -d -p 5000:5000 --name registry \ -v registry-data:/var/lib/registry \ registry:2 # 之后就能 docker push localhost:5000/myimage:1.0.0 ``` 登录与登出: ```bash docker login registry.example.com -u <用户名> # 密码不会进命令历史;登录成功后凭证写入 ~/.docker/config.json docker logout registry.example.com ``` 看一眼凭证文件里到底存了什么: ```json { "auths": { "registry.example.com": { "auth": "dXNlcjpwYXNzd29yZA==" } } } ``` > 注意:`auth` 只是 `用户名:密码` 的 **base64 编码,不是加密**。随手解码就能还原明文密码。所以 `~/.docker/config.json` 等同于明文凭据,绝不能提交进 git、也不能共享。 --- ## 5. 完整的推拉实操 从构建到另一台机器拉取,一条龙走一遍。 ```bash # 1. 构建,镜像名直接写成仓库地址(这一步决定了后续推到哪) docker build -t registry.example.com/prod/order-service:1.4.2 . # 2. 再补一个 sha 标签,供回滚定位 docker tag order-service:1.4.2 registry.example.com/prod/order-service:sha-9f3a1c2 # 3. 登录私有仓库 docker login registry.example.com -u <user> # 4. 推送 docker push registry.example.com/prod/order-service:1.4.2 ``` 推送时的分层输出长这样,注意 `Layer already exists`: ```text The push refers to repository [registry.example.com/prod/order-service] 9f3a1c2e8d4b: Pushed 1a2b3c4d5e6f: Layer already exists 7c8d9e0f1a2b: Pushed 3e4f5a6b7c8d: Layer already exists 1.4.2: digest: sha256:2b7e1c9a8f4d6e3b0a1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e size: 1234 ``` 含义解读: | 输出片段 | 含义 | |---|---| | `Pushed` | 这一层是新的,本次真的上传了 | | `Layer already exists` | 仓库里已有同哈希的层,跳过上传 | | `digest: sha256:...` | 推送成功后该 tag 指向的内容哈希 | 这就是"分层"在交付上的回报:**只有变化的层会被推上去**,改一行代码重新构建,通常只推一两个层。 另一台机器拉取并验证: ```bash docker login registry.example.com -u <user> docker pull registry.example.com/prod/order-service:1.4.2 docker images | grep order-service ``` 看一眼多架构信息(简单提一句): ```bash docker manifest inspect registry.example.com/prod/order-service:1.4.2 # 或 docker buildx imagetools inspect registry.example.com/prod/order-service:1.4.2 ``` 如果输出里有多个 `platform`(如 `linux/amd64`、`linux/arm64`),说明这是个多架构镜像。这在你用 Mac(arm64)构建、却要部署到 amd64 节点时至关重要——否则会撞上后文提到的 `exec format error`。 --- ## 6. 镜像仓库如何接到 K8s K8s 拉私有镜像需要凭证。做法是先把凭证做成一个 Secret,再在 Pod 里引用。 ```bash kubectl create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username=<user> \ --docker-password=<password> \ -n blog ``` 然后在 Deployment 里引用: ```yaml spec: imagePullSecrets: - name: regcred # ← 漏了这行就是 ImagePullBackOff containers: - name: app image: registry.example.com/prod/order-service:1.4.2 ``` 拉取失败的对照表: | 现象 | 原因 | 排查命令 | |---|---|---| | `ImagePullBackOff` | 拉不到镜像,K8s 正按退避策略重试 | `kubectl describe pod <pod>` 看 Events | | `ErrImagePull` | 首次拉取失败(通常是 ImagePullBackOff 的前一步) | `kubectl describe pod <pod>` | | `401 Unauthorized` / `authentication required` | 没配 `imagePullSecrets`,或凭证过期 | `kubectl get secret regcred -n blog` 确认存在;核对 `--docker-server` 是否写对 | | `manifest unknown` | tag 不存在(拼错或压根没推上去) | `docker manifest inspect <镜像>` 确认 | | `x509: certificate signed by unknown authority` | 自签证书,节点不信任 | 给节点导入 CA 信任,别指望用 insecure 糊过去 | > 这条链和前面「云原生」课程 04 篇里的私有仓库配置是同一套东西:Secret 名称、`imagePullSecrets` 字段、常见报错都一致,只是那一篇默认你已经会推镜像。 --- ## 7. 镜像安全与瘦身三招 镜像会被推送到仓库、被多个节点拉取,它本身就是一份"发布物"。三件事最该做。 **第一招:用非 root 用户跑应用。** ```dockerfile # 建一个普通用户,然后切过去 RUN groupadd -r app && useradd -r -g app app USER app ENTRYPOINT ["java", "-jar", "/app/app.jar"] ``` 容器里默认是 root。一旦内核有漏洞、或挂载了宿主机目录,root 身份会放大风险。K8s 侧如果开了 `runAsNonRoot` 校验,root 镜像会被直接拒绝。 **第二招:用扫描工具看漏洞。** ```bash trivy image registry.example.com/prod/order-service:1.4.2 ``` 期望输出(简化):  *图:镜像瘦身三档:换掉运行阶段的基础镜像比删文件更有效* ```text order-service:1.4.2 (debian 12.5) Total: 42 (UNKNOWN: 0, LOW: 20, MEDIUM: 14, HIGH: 6, CRITICAL: 2) ┌──────────┬────────────────┬──────────┬───────────────────┬───────────────┐ │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ ├──────────┼────────────────┼──────────┼───────────────────┼───────────────┤ │ openssl │ CVE-2024-XXXXX │ CRITICAL │ 3.0.11-1 │ 3.0.13-1 │ └──────────┴────────────────┴──────────┴───────────────────┴───────────────┘ ``` **第三招:别往镜像里塞不该有的东西。** | 不该出现在镜像里 | 为什么 | 怎么避免 | |---|---|---| | 密钥 / token / 密码 | 镜像会被推送到仓库、被多个节点拉取,等于公开泄漏 | 用环境变量、K8s Secret 注入 | | `.git` 目录 | 完整提交历史随之泄漏 | `.dockerignore` 里加 `.git` | | 构建中间产物(`target/`、`node_modules/`) | 体积暴涨、层数变多、推送变慢 | 多阶段构建 + `.dockerignore` | | 测试代码与测试配置 | 生产不需要,还可能带测试凭证 | 多阶段构建,只 `COPY` 运行产物 | | 本地配置(`.env`、`application-dev.yml`) | 常含数据库口令 | `.dockerignore`;配置一律外置 | --- ## 8. 踩过的坑 ```text 坑 1:docker push 报 "denied: requested access to the resource is denied" 现象 → 推送被拒 原因 → ① 没登录;② 镜像名里的仓库地址/命名空间和你账号权限不符(推到了别人的命名空间) 对策 → docker login;确认镜像名前缀 = 你的仓库地址 + 你有权限的命名空间 坑 2:K8s Pod 一直 ImagePullBackOff 现象 → kubectl get pods 显示 ImagePullBackOff 原因 → 私有仓库没配 imagePullSecrets,或 secret 里 server 写错、凭证过期 对策 → kubectl describe pod 看 Events;kubectl get secret regcred 确认存在 坑 3:本地能跑,节点拉下来跑不起来 现象 → 本地 amd64 正常,Mac(M 系列/arm64) 构建的镜像在 amd64 节点报 exec format error 原因 → 镜像架构和节点架构不匹配 对策 → 用 buildx 构建多架构镜像(--platform linux/amd64,linux/arm64) 坑 4:改了代码重新部署,节点拉到的还是旧镜像 现象 → 重新部署后行为没变 原因 → 用了可变 tag(latest / dev),节点上有本地缓存,imagePullPolicy 又不强制重拉 对策 → 生产用不可变 tag;要强制重拉可临时 imagePullPolicy: Always,但根治办法是换 tag 坑 5:daemon.json 配了 mirrors,还是 timeout 现象 → 改完没生效 原因 → 没重启 dockerd;或该镜像根本不在 Docker Hub(mirror 只代理 Docker Hub) 对策 → systemctl restart docker;非 Hub 的镜像直接写对应仓库地址 坑 6:push 到自建 registry 报 http: server gave HTTP response to HTTPS client 现象 → 自建 registry 推不上去 原因 → 自建 registry 是 HTTP,而客户端默认走 HTTPS 对策 → daemon.json 加 insecure-registries(仅开发环境);生产给 registry 配上证书走 HTTPS ``` --- ## 9. 本篇小结 ```text 构建 docker build -t registry.example.com/prod/order-service:1.4.2 . │ └─ 一次构建两个 tag:1.4.2(发布) + sha-9f3a1c2(回滚定位) v 登录 docker login registry.example.com │ v 推送 docker push registry.example.com/prod/order-service:1.4.2 │ └─ Layer already exists → 分层复用,只推变化的层 v 仓库 Harbor / registry:2 / 云厂商托管 │ v K8s imagePullSecrets(regcred) + image: ...:1.4.2 │ v 排查 ImagePullBackOff → kubectl describe pod → 看 Events 记住三条: ① tag 是可被覆盖的指针,digest 才是唯一标识 ② 生产禁用 latest,用版本号或 sha ③ 私有镜像要进 K8s,绕不开 imagePullSecrets ``` 下一篇:[04 - CI/CD:从提交代码到镜像进仓库]
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入