容器化技術 ch04 Registry 與 Image 座標:那串很長的名字在說什麼
下一章→
CH 04 日常必備

Registry 與 Image 座標:那串很長的名字在說什麼

座標四段式拆解latest 的最大誤解Tag 會變、Digest 不會多架構映像與 M1 惡夢推自己的映像上去

你的 compose 檔裡有這麼一行:

image: mcr.microsoft.com/mssql/server:2022-latest

這串東西不是隨便取的名字,而是一組精確的座標——它告訴 Docker 去哪個倉庫、找哪個櫃子、拿哪個版本。這一章把它拆到最後一個字元,順便回答幾個天天遇到卻很少人講清楚的問題:為什麼 nginx 可以只寫三個字母、latest 為什麼不是「最新版」、為什麼同事的 Mac 建出來的映像丟到公司伺服器會噴 exec format error。最後教你把自己的 Spring Boot 映像推上去。

映像名稱是一組四段式座標,完整格式長這樣:

[registry 主機]/[命名空間]/[映像名]:[標籤]

mcr.microsoft.com  /  mssql  /  server  :  2022-latest
       ①              ②        ③          ④
  • ① Registry 主機:去哪個倉庫拿。mcr.microsoft.com 是微軟自家的容器倉庫(Microsoft Container Registry)。省略時預設是 docker.io(Docker Hub)。
  • ② 命名空間 / 帳號:倉庫裡哪個櫃子。通常是組織或使用者名稱,例如 mssql、bitnami、你的 Docker Hub 帳號。
  • ③ 映像名(Repository):櫃子裡哪個抽屜。同一個抽屜裡放著這個軟體的所有版本。
  • ④ 標籤(Tag):抽屜裡哪一份。省略時預設是 latest。

所以為什麼 nginx 可以只寫三個字母?

因為 Docker 會自動補完:

你寫的:      nginx
實際上是:    docker.io/library/nginx:latest
              ↑          ↑       ↑      ↑
          預設 registry  官方   映像名  預設 tag
                       命名空間

library 是 Docker Hub 保留給官方映像(Official Images)的特殊命名空間——由 Docker 官方或軟體原廠維護的那批(nginx、redis、postgres、eclipse-temurin…)。這也是一個安全訊號:

  • redis(=library/redis)→ 官方維護,可信度高。
  • someguy/redis → 某個路人自己建的,內容你完全不知道。名字只差一個斜線,信任等級天差地遠。
🚨 常見的供應鏈攻擊手法叫 typosquatting:攻擊者上傳一個叫 ngnix(故意拼錯)或 mysqI(大寫 i 假裝小寫 L)的映像,等你手殘打錯就中招。複製貼上官方文件的映像名,不要憑印象手打。
💡 所以你那行 mcr.microsoft.com/mssql/server:2022-latest 的白話翻譯是:「去微軟的官方倉庫,找 mssql 這個櫃子裡的 server 抽屜,拿標著 2022-latest 的那一份。」
核心事實:latest 只是一個「預設標籤名」,沒有任何「最新」的含義。它不會自動指向最新版本,Docker 也不會幫你更新它。它就是一個字串,跟 v1、stable、banana 沒有本質差別。

它為什麼危險?三個現實後果

  1. 它可能非常舊。如果維護者發布 v2.0 時忘了同時更新 latest,那 latest 就永遠停在 v1.0。「latest 指向最新」純粹是靠維護者自律,不是機制保證。
  2. 它會偷偷改變,破壞可重現性。標籤只是一個「指向某個映像的可變指標」,維護者隨時可以把它重新指到別的映像。
    你今天:   docker pull myapp:latest   → 拿到 v1.0
    (維護者推了新版,latest 被重新指向 v2.0)
    同事明天: docker pull myapp:latest   → 拿到 v2.0
    同一行指令、同一個名字、不同的東西。這正是「在我電腦上可以跑」的容器版翻版——你以為容器解決了環境不一致,結果被 latest 又打回原形。
  3. 它讓回滾變得不可能。正式環境爆炸想退回上一版,但你只知道「我們部署的是 latest」——那到底是哪一版?無從查起。

實務規則

  • 開發環境:用明確的次版本標籤,例如 eclipse-temurin:17.0.9_9-jre-jammy,至少 17-jre。
  • 正式環境:釘死到 digest(下一張卡),或至少釘死到不會被覆寫的完整版本號。
  • 你自己的映像:用不可重複的識別碼當標籤——Git commit SHA 或建置編號,例如 myapp:1.4.2、myapp:a3f91c2。這樣「正式環境跑的是哪一版程式碼」永遠有答案。
⭐ 你 compose 裡那個 mssql/server:2022-latest 也是同樣狀況:它代表「SQL Server 2022 這條線的最新累積更新」,會隨時間變動。開發環境無妨(甚至是好事,自動拿到修補),但如果你要精確重現某次的環境,就得釘死更明確的版本。

既然標籤會變,那有沒有「絕對指向同一個東西」的方式?有,就是 digest。

docker pull nginx@sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31

digest 是什麼

  • 它是映像內容(manifest)算出來的 SHA-256 雜湊值。內容決定 ID,ID 決定內容——這種設計叫做內容尋址(content-addressable)。
  • 只要內容變了一個位元組,digest 就完全不同。所以 digest 無法被「重新指向」,它天生不可變。
  • 拉下來之後 Docker 會重算一次雜湊比對,順便驗證了完整性——傳輸過程被竄改或損毀都會被抓出來。

標籤 vs Digest 的類比

💡 標籤像 Git 的分支名,digest 像 commit SHA。
「main 分支」今天和明天可能指向不同的 commit;但 a3f91c2 這個 commit 永遠是同一份程式碼。要精確描述「是哪一份東西」,只能用後者。

實務怎麼用

# 查出目前 tag 對應的 digest
docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25

# compose 檔釘死(正式環境建議)
services:
  app:
    image: myregistry.io/myapp@sha256:abc123...
  • Kubernetes 的正式環境部署普遍要求釘 digest,理由完全一樣:保證每個節點拉到的是同一份東西。
  • 缺點很明顯:難讀、難記、更新時要改一長串。所以實務上常見折衷——CI 建置時用語意化標籤,部署時把它解析成 digest 再寫進部署設定,兩全其美。
⭐ 一句話:tag 是給人看的便利標籤(可變),digest 是給機器用的身分證(不可變)。要可讀性用 tag,要可重現性用 digest。

你打 docker pull nginx 之後,實際上跟 registry 進行了三輪對話:

  1. 要 manifest(清單):「請給我 nginx:latest 的清單。」
    manifest 是一份小小的 JSON,記載著:這個映像由哪幾層組成(每層的 digest 和大小)、設定檔在哪。它本身不含實際資料,就是一張目錄。
  2. 要 config(設定檔):記載 CMD、ENV、USER、WORKDIR、架構等 metadata。
  3. 逐層下載:比對本機已有哪些層(digest 相同就跳過,顯示 Already exists),只下載缺的。ch03 講的分層下載,就發生在這一步。

多架構映像:一個名字,好幾種 CPU 版本

問題來了:nginx:latest 要給 Intel/AMD 的 x86 伺服器用,也要給 M 系列 Mac 和樹莓派的 ARM 用。同一個名字,怎麼可能同時是兩種東西?

答案是 manifest list(又稱 image index)——一份「清單的清單」:

nginx:latest  (manifest list)
  ├── linux/amd64   → 指向 A 這份 manifest
  ├── linux/arm64   → 指向 B 這份 manifest
  └── linux/arm/v7  → 指向 C 這份 manifest

你 pull 的時候,Docker 會自動報上自己的作業系統與 CPU 架構,registry 就給對應的那份。所以你的 M1 Mac 拿到 arm64 版、公司的 x86 伺服器拿到 amd64 版——同一行指令,各取所需。

🚨 M1 Mac 的經典災難(極常見,務必記住)
你在 M 系列 Mac 上 docker build 出自己的 Spring Boot 映像 → 它是 arm64 的 → 推上 registry → 部署到公司 x86 Linux 伺服器 → 容器起不來,錯誤訊息是:
exec format error
或 no matching manifest for linux/amd64。

原因:你建的映像裡是 ARM 指令集的執行檔,x86 CPU 根本讀不懂。
解法:建置時指定目標架構,或直接建多架構映像:
# 指定單一目標架構
docker build --platform linux/amd64 -t myapp:1.0 .

# 一次建出多架構並推送(需要 buildx)
docker buildx build --platform linux/amd64,linux/arm64 \
  -t myregistry.io/myapp:1.0 --push .
💡 純 Java 應用有個容易誤導的地方:你的 .jar 檔本身是跨平台的位元組碼沒錯,但映像裡的 JVM、glibc、以及所有原生函式庫都是有架構之分的。所以「Java 跨平台」救不了你——映像的架構還是要對。

推送的關鍵觀念只有一個:映像的名字就是它的位址。要推到哪裡,就得先把名字改成那個位址。

四個步驟

# ① 建置(本機名稱,還沒有 registry 位址)
docker build -t myapp:1.0 .

# ② 重新命名成「目的地座標」——這步是關鍵
docker tag myapp:1.0 yourname/myapp:1.0
#          ↑本機名     ↑帳號/映像名:版本

# ③ 登入
docker login

# ④ 推送
docker push yourname/myapp:1.0
  • 推到非 Docker Hub 的 registry,名字前面要加主機:
    docker tag myapp:1.0 myharbor.company.com/team/myapp:1.0
    docker push myharbor.company.com/team/myapp:1.0
  • docker tag 不會複製任何資料,只是替同一個映像多取一個名字(多一個指標)——所以它是瞬間完成的。

認證憑證存在哪裡(安全提醒)

  • docker login 之後,憑證預設存在 ~/.docker/config.json,而且可能只是 base64 編碼,不是加密。任何能讀到這個檔案的人就拿到了你的推送權限。
  • 正式環境的正解:用 credential helper(例如 docker-credential-ecr-login),或在 CI 用短期權杖(AWS ECR 的登入權杖 12 小時就過期)。

Docker Hub 的拉取次數限制(會突然咬你一口)

  • Docker Hub 對匿名拉取有次數上限(以來源 IP 計算)。公司或 CI 環境常常是一整棟樓共用一個對外 IP——於是額度很快用完,CI 突然開始噴 toomanyrequests: You have reached your pull rate limit。
  • 對策:登入後拉(額度較高)、在內網架 pull-through cache(本地鏡像快取)、或把常用映像複製一份到公司自己的 registry。
⭐ Registry 選擇速覽:
• Docker Hub:公開映像的預設來源,私有庫額度有限。
• 雲端商私有 registry:AWS ECR、Azure ACR、Google Artifact Registry——與雲端的權限系統(IAM)整合,部署到該雲端時的首選(見 AWS ch08)。
• 自架:Harbor(企業級,含漏洞掃描、簽章、複製)、或最陽春的 registry:2。

docker pull 這個動作的本質是:從網路上下載一整套別人準備的檔案系統,然後在你的機器上執行它。用這個角度想,就知道該謹慎了。

四個實務原則

  1. 優先用官方或已驗證的映像。Docker Hub 上有 Official Image(library/ 命名空間)與 Verified Publisher 標記;沒有這兩者的映像,等於來路不明的執行檔。
  2. 釘死版本,並定期更新。這兩件事看似矛盾,其實是一體兩面:釘死是為了可重現,定期更新是為了修補漏洞。作法是把「更新基底映像版本」變成一個明確的、有人負責的動作(例如 Dependabot 自動送 PR),而不是靠 latest 隨機決定。
  3. 掃描漏洞。基底映像帶著整套系統函式庫,舊版本必然累積 CVE:
    docker scout cves myapp:1.0     # Docker 內建
    trivy image myapp:1.0           # 業界常用的開源工具
    接到 CI 裡,有高風險漏洞就擋下建置。
  4. 用小的基底映像。ubuntu 基底裝了幾百個套件,每個都是潛在漏洞;alpine 或 distroless 只有必要的東西,攻擊面小一個數量級(ch10 詳談,含 Java 的選擇建議)。

進階:怎麼確認映像沒被掉包?

  • digest 保證完整性(內容沒被改),但不保證來源(不能證明是誰做的)。
  • 要證明來源,需要簽章:現代作法是 Sigstore / cosign,發布者用私鑰簽映像,你用公鑰驗證。
  • 再上一層是 SBOM(軟體物料清單):映像附上一份「我裡面裝了哪些套件與版本」的清單,出漏洞時能立刻查出自己是否受影響。
📌 這幾件事聽起來像大公司才需要,但 2024 年之後多數企業的資安稽核都會問到「你們的容器映像從哪來、有沒有掃描」。以 SRE 職涯來說,供應鏈安全是必答題——現在知道有這些名詞,之後遇到就不會陌生。
Image 座標四段式拆解
段落作用省略時的預設範例
① Registry 主機去哪個倉庫拿docker.io(Docker Hub)mcr.microsoft.com
② 命名空間哪個組織/帳號的櫃子library(官方映像專用)mssql、bitnami、你的帳號
③ 映像名哪個軟體(抽屜)必填server、nginx、redis
④ 標籤哪個版本latest2022-latest、17-jre、1.4.2
Tag vs Digest:可重現性的關鍵
面向Tag(標籤)Digest(摘要)
長相nginx:1.25nginx@sha256:0d17b565c37b…
本質可變的指標(像 Git 分支名)內容算出的雜湊(像 commit SHA)
會不會變會!維護者隨時可重新指向永遠不會(變了就是另一個 digest)
可讀性好記好讀又長又難記
可重現性無保證100% 保證同一份東西
適用場景開發、日常操作正式環境部署、K8s、稽核
新手最容易踩的四個座標地雷
地雷症狀原因正解
用 latest 上正式環境不同機器跑到不同版本,無法回滾latest 只是預設標籤名,會被覆寫釘明確版本或 digest;自家映像用 commit SHA 當 tag
M1 Mac 建的映像上 x86 伺服器exec format error建出來的是 arm64 映像docker build --platform linux/amd64,或 buildx 建多架構
拉到 typosquatting 映像映像內含惡意程式手打名字打錯一個字母(ngnix、mysqI)複製官方文件的名稱;確認是 library/ 官方映像
CI 突然拉不動映像toomanyrequests: pull rate limit整棟樓共用對外 IP,匿名拉取額度用光登入後拉、架 pull-through cache、或複製到自家 registry

練習題 點選選項查看解析

0 / 8
01 / 8
你只寫了 image: redis,Docker 實際上去拉的完整座標是什麼?
A redis.io/redis:stable
B docker.io/library/redis:latest
C docker.io/redis/redis:1.0
D localhost/redis:latest
解析
省略的部分會被自動補完:registry 主機預設 docker.io、命名空間預設 library(Docker Hub 保留給官方映像)、標籤預設 latest。順帶一提這也是安全判斷點:redis(=library/redis)是官方維護,someguy/redis 則是路人自建,名字只差一個斜線,信任等級天差地遠。
02 / 8
關於 latest 這個標籤,正確的理解是?
A Docker 會自動讓它指向最新版本
B 它只是「省略標籤時的預設名稱」,沒有任何『最新』的機制保證——維護者忘了更新它就會一直停在舊版
C 它代表最穩定的版本
D 它會每天自動同步
解析
latest 是 Docker 命名史上最誤導人的設計。它就是一個字串,跟 v1、stable、banana 沒有本質差別。「latest 指向最新」純粹靠維護者自律。更糟的是它會被覆寫:你今天拉到 v1.0,同事明天拉到 v2.0,同一行指令卻是不同的東西——容器本來要解決的不一致問題又回來了。
03 / 8
為什麼正式環境建議把映像釘死到 digest(sha256:…)?
A digest 下載速度比較快
B digest 是內容算出的雜湊,天生不可變,能 100% 保證每次、每台機器拉到的都是同一份映像,並可驗證未被竄改
C digest 佔用空間較小
D digest 可以自動更新到最新版
解析
tag 像 Git 分支名(可變),digest 像 commit SHA(不可變)。要精確描述「是哪一份東西」只能用後者。拉取後 Docker 會重算雜湊比對,順便驗證了完整性。缺點是難讀難更新,所以常見折衷是:CI 用語意化標籤建置,部署時解析成 digest 寫進部署設定。
04 / 8
docker pull 時 registry 回傳的 manifest 是什麼?
A 映像的完整壓縮檔
B 一份小 JSON 目錄,記載這個映像由哪幾層組成(各層的 digest 與大小)以及設定檔位置,本身不含實際資料
C 使用者的授權憑證
D 容器的執行紀錄
解析
pull 是三輪對話:先要 manifest(目錄)、再要 config(CMD/ENV/USER 等 metadata)、最後照著目錄逐層下載。因為 manifest 列出了每層的 digest,Docker 才能比對本機已有哪些層而跳過(Already exists)——ch03 的分層下載就發生在這一步。
05 / 8
同事在 M1 Mac 上建的映像,部署到公司 x86 Linux 伺服器時噴 exec format error。原因是?
A 映像檔損毀
B 在 M 系列 Mac 上建出來的是 arm64 架構映像,裡面的執行檔是 ARM 指令集,x86 CPU 讀不懂
C 伺服器的 Docker 版本太舊
D 少裝了 Java
解析
映像是分架構的。解法是建置時指定目標架構 docker build --platform linux/amd64,或用 buildx 一次建多架構並推送。注意 Java 開發者容易誤判:jar 是跨平台位元組碼沒錯,但映像裡的 JVM、glibc、原生函式庫都有架構之分——「Java 跨平台」救不了你。
06 / 8
一個名字(如 nginx:latest)怎麼同時提供 x86 和 ARM 兩種版本?
A Docker 會即時轉譯指令集
B 透過 manifest list(image index):一份『清單的清單』,列出各架構對應的 manifest,pull 時客戶端報上自己的 OS/架構,registry 回傳對應那份
C 映像裡同時包含兩套執行檔
D 需要手動指定不同的映像名
解析
manifest list 讓同一個座標對應多個實際映像。你的 M1 Mac 拿到 arm64 版、x86 伺服器拿到 amd64 版,同一行指令各取所需。反過來說,你自己 build 的映像若沒特別處理,就只有你這台機器的架構那一種。
07 / 8
你要把本機建好的 myapp:1.0 推到公司的 Harbor(myharbor.company.com)。哪一步是必要的?
A 直接 docker push myapp:1.0 即可
B 先 docker tag myapp:1.0 myharbor.company.com/team/myapp:1.0,把名字改成目的地座標,再 push
C 先把映像匯出成 tar 檔再上傳
D 必須先在 Harbor 建立同名容器
解析
關鍵觀念:映像的名字就是它的位址。沒有 registry 主機前綴,Docker 會以為要推到 Docker Hub。docker tag 不會複製任何資料,只是替同一個映像多加一個指標,所以是瞬間完成的。
08 / 8
CI 突然開始出現 toomanyrequests: You have reached your pull rate limit。最可能的原因與對策是?
A 網路頻寬不足,需升級線路
B Docker Hub 對匿名拉取以來源 IP 計算次數上限,而公司/CI 常共用一個對外 IP 導致額度耗盡;對策是登入後拉、架 pull-through cache 或複製映像到自家 registry
C 映像太大,需要壓縮
D Docker 版本過舊,升級即可
解析
這個錯誤在企業 CI 環境非常常見,而且往往是「昨天還好好的今天突然壞掉」,因為額度是以 IP 計算的共用資源。三個對策由簡到繁:登入後拉(額度較高)、在內網架本地鏡像快取、把常用映像複製一份到公司自己的 registry(同時也解決了外部依賴風險)。

點擊卡片翻面查看答案,共 10 張。

QUESTION
Image 座標的四段式格式?
點擊翻面
ANSWER
[registry 主機]/[命名空間]/[映像名]:[標籤]
mcr.microsoft.com / mssql / server : 2022-latest
省略預設:docker.io / library / — : latest
點擊翻回
QUESTION
為什麼 nginx 可以只寫三個字母?
點擊翻面
ANSWER
自動補完成 docker.io/library/nginx:latest
library = Docker Hub 官方映像專用命名空間
(redis vs someguy/redis:信任等級天差地遠)
點擊翻回
QUESTION
latest 的真相?
點擊翻面
ANSWER
只是「省略標籤時的預設名稱」,沒有『最新』的機制保證
可能很舊、會被覆寫、讓回滾變不可能
自家映像請用 commit SHA 或版本號當 tag
點擊翻回
QUESTION
Tag vs Digest 的類比?
點擊翻面
ANSWER
Tag = Git 分支名(可變指標)
Digest = commit SHA(內容雜湊,不可變)
可讀性用 tag,可重現性用 digest
點擊翻回
QUESTION
docker pull 的三輪對話?
點擊翻面
ANSWER
① 要 manifest(層清單目錄)
② 要 config(CMD/ENV/USER 等 metadata)
③ 逐層下載,已有的跳過(Already exists)
點擊翻回
QUESTION
manifest list 解決什麼問題?
點擊翻面
ANSWER
一個名字對應多種 CPU 架構
pull 時客戶端報上自己的 OS/架構,registry 給對應那份
(M1 拿 arm64、x86 伺服器拿 amd64)
點擊翻回
QUESTION
exec format error 的成因與解法?
點擊翻面
ANSWER
在 M 系列 Mac build 出 arm64 映像,丟到 x86 伺服器跑
解法:docker build --platform linux/amd64
或 docker buildx build --platform linux/amd64,linux/arm64 --push
點擊翻回
QUESTION
推送映像到私有 registry 的關鍵步驟?
點擊翻面
ANSWER
映像的名字就是它的位址
docker tag myapp:1.0 myharbor.com/team/myapp:1.0
→ docker login → docker push
(tag 不複製資料,只多一個指標)
點擊翻回
QUESTION
Docker Hub 拉取限制的成因與對策?
點擊翻面
ANSWER
匿名拉取以來源 IP 計次,公司/CI 共用對外 IP 很快用完
對策:登入後拉、架 pull-through cache、複製到自家 registry
點擊翻回
QUESTION
供應鏈安全四原則?
點擊翻面
ANSWER
① 用官方/已驗證映像(防 typosquatting)
② 釘死版本 + 定期更新(可重現 vs 修漏洞)
③ 掃描漏洞(docker scout / trivy 接進 CI)
④ 用小基底(alpine/distroless)縮小攻擊面
點擊翻回