<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Registry on Tarragon</title><link>https://tarrragon.github.io/blog/tags/registry/</link><description>Recent content in Registry on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/registry/index.xml" rel="self" type="application/rss+xml"/><item><title>Image build、scan、registry 與 promotion 流程</title><link>https://tarrragon.github.io/blog/ci/docker-deploy/image-supply-chain-flow/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/docker-deploy/image-supply-chain-flow/</guid><description>&lt;p>Image 供應鏈流程的核心責任是讓 container image 從 build 到 runtime 都可追溯。Image 同時包含 application、runtime、OS package 與 dependency；CI/CD 需要把 Dockerfile、base image、tag、scan、registry 與 deployment manifest 串成同一條供應鏈。&lt;/p>
&lt;h2 id="流程定位">流程定位&lt;/h2>
&lt;p>Image deployment 的風險集中在「看似同名、實際不同」的產物漂移。&lt;code>latest&lt;/code>、mutable tag、重新 build 與跨 registry promotion 都可能讓 staging 測過的 image 不等於 production 跑的 image。嚴謹流程應以 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a> 或 immutable tag 作為 artifact 身分。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>階段&lt;/th>
 &lt;th>責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>從 Dockerfile 產生 image&lt;/td>
 &lt;td>base image、lockfile、build arg 是否固定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tag&lt;/td>
 &lt;td>建立查詢與推進入口&lt;/td>
 &lt;td>commit SHA、semver、digest 是否可追&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scan&lt;/td>
 &lt;td>顯性化漏洞、secret、&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a> 風險&lt;/td>
 &lt;td>阻擋門檻與例外流程是否存在&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry&lt;/a>&lt;/td>
 &lt;td>保存 image 並控制 promotion&lt;/td>
 &lt;td>immutable、retention、權限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime handoff&lt;/td>
 &lt;td>讓 deployment 使用已驗證 image&lt;/td>
 &lt;td>manifest 是否指向已掃描 digest&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責封裝 runtime。Multi-stage build、dependency cache、base image pinning 與 build secret 處理會直接影響安全性；CI 應能在乾淨 runner 上重建 image，避免開發機狀態被帶入。&lt;/p>
&lt;p>Tag 階段負責支援不同查詢情境。Commit SHA 適合事故追溯，semver 適合 release 溝通，&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest&lt;/a> 適合 runtime 精準鎖定；production 判讀應以 digest 為準，tag 只作為人類入口。&lt;/p>
&lt;p>Scan 階段負責把風險分流。Vulnerability scan、secret scan、license scan 與 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM&lt;/a> 不應只是報表；流程要定義哪些風險阻擋發布、哪些風險允許例外、例外誰審核、何時重新評估。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry&lt;/a> 階段負責保存與推進 image。Registry 要處理權限、retention、immutability、promotion 與垃圾回收；若 production 直接從 feature branch push 的 tag 拉 image，供應鏈邊界就失去治理。&lt;/p>
&lt;p>Runtime handoff 階段負責把已驗證 image 交給部署平台。Kubernetes、ECS、Compose 或其他 runtime 都應指向已驗證 digest 或 immutable tag，並把 health、readiness、resource limit 與 rollback 連到同一次 release。&lt;/p>
&lt;h2 id="tag-與-digest-策略">Tag 與 digest 策略&lt;/h2>
&lt;p>Tag 策略的責任是讓人查得到、機器鎖得住。單一 tag 很難同時滿足可讀性、可追溯與不可變三個需求，因此實務上常搭配多個 tag 與 digest。&lt;/p></description><content:encoded><![CDATA[<p>Image 供應鏈流程的核心責任是讓 container image 從 build 到 runtime 都可追溯。Image 同時包含 application、runtime、OS package 與 dependency；CI/CD 需要把 Dockerfile、base image、tag、scan、registry 與 deployment manifest 串成同一條供應鏈。</p>
<h2 id="流程定位">流程定位</h2>
<p>Image deployment 的風險集中在「看似同名、實際不同」的產物漂移。<code>latest</code>、mutable tag、重新 build 與跨 registry promotion 都可能讓 staging 測過的 image 不等於 production 跑的 image。嚴謹流程應以 <a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 或 immutable tag 作為 artifact 身分。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>從 Dockerfile 產生 image</td>
          <td>base image、lockfile、build arg 是否固定</td>
      </tr>
      <tr>
          <td>Tag</td>
          <td>建立查詢與推進入口</td>
          <td>commit SHA、semver、digest 是否可追</td>
      </tr>
      <tr>
          <td>Scan</td>
          <td>顯性化漏洞、secret、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 風險</td>
          <td>阻擋門檻與例外流程是否存在</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry</a></td>
          <td>保存 image 並控制 promotion</td>
          <td>immutable、retention、權限</td>
      </tr>
      <tr>
          <td>Runtime handoff</td>
          <td>讓 deployment 使用已驗證 image</td>
          <td>manifest 是否指向已掃描 digest</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責封裝 runtime。Multi-stage build、dependency cache、base image pinning 與 build secret 處理會直接影響安全性；CI 應能在乾淨 runner 上重建 image，避免開發機狀態被帶入。</p>
<p>Tag 階段負責支援不同查詢情境。Commit SHA 適合事故追溯，semver 適合 release 溝通，<a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 適合 runtime 精準鎖定；production 判讀應以 digest 為準，tag 只作為人類入口。</p>
<p>Scan 階段負責把風險分流。Vulnerability scan、secret scan、license scan 與 <a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 不應只是報表；流程要定義哪些風險阻擋發布、哪些風險允許例外、例外誰審核、何時重新評估。</p>
<p><a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container registry</a> 階段負責保存與推進 image。Registry 要處理權限、retention、immutability、promotion 與垃圾回收；若 production 直接從 feature branch push 的 tag 拉 image，供應鏈邊界就失去治理。</p>
<p>Runtime handoff 階段負責把已驗證 image 交給部署平台。Kubernetes、ECS、Compose 或其他 runtime 都應指向已驗證 digest 或 immutable tag，並把 health、readiness、resource limit 與 rollback 連到同一次 release。</p>
<h2 id="tag-與-digest-策略">Tag 與 digest 策略</h2>
<p>Tag 策略的責任是讓人查得到、機器鎖得住。單一 tag 很難同時滿足可讀性、可追溯與不可變三個需求，因此實務上常搭配多個 tag 與 digest。</p>
<table>
  <thead>
      <tr>
          <th>標識</th>
          <th>適合用途</th>
          <th>風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commit SHA</td>
          <td>從 runtime 回查 source</td>
          <td>對使用者不友善</td>
      </tr>
      <tr>
          <td>Semver</td>
          <td>對外 release 溝通</td>
          <td>tag 可能被覆寫，需搭配 immutability</td>
      </tr>
      <tr>
          <td>Branch tag</td>
          <td>preview / staging 快速迭代</td>
          <td>不適合作為 production 依據</td>
      </tr>
      <tr>
          <td>Digest</td>
          <td>runtime 精準鎖定</td>
          <td>人類閱讀成本高</td>
      </tr>
  </tbody>
</table>
<p>Production deployment 應能從 running pod 或 task 反查 image digest，再反查 registry metadata、scan report、workflow run 與 source commit。這條查詢路徑是 incident response 的基本能力。</p>
<h2 id="scan-gate-分流">Scan gate 分流</h2>
<p>Scan gate 的責任是讓安全訊號變成可操作路由。掃描工具會產生大量結果，沒有分流規則時，團隊會在兩種壞狀態間搖擺：全部阻擋導致發不出去，全部忽略導致掃描失去信任。</p>
<table>
  <thead>
      <tr>
          <th>結果類型</th>
          <th>策略</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Critical exploitable</td>
          <td>阻擋 production promotion</td>
          <td>升級 dependency / base image</td>
      </tr>
      <tr>
          <td>High with mitigation</td>
          <td>需要審核例外與到期日</td>
          <td>記錄風險、設定重新掃描</td>
      </tr>
      <tr>
          <td>Base image aging</td>
          <td>排入 base image refresh</td>
          <td>建立定期更新節奏</td>
      </tr>
      <tr>
          <td>Secret in layer</td>
          <td>阻擋並輪替 secret</td>
          <td>重建 image、撤銷已暴露 credential</td>
      </tr>
      <tr>
          <td><a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> missing</td>
          <td>阻擋高治理環境，低風險環境警告</td>
          <td>補 provenance / SBOM 產出</td>
      </tr>
  </tbody>
</table>
<p>這個分流讓 scan 成為 gate。例外流程要有 owner 與到期日，讓例外維持可追蹤、可重新評估。</p>
<h2 id="常見反模式">常見反模式</h2>
<p>反模式的共同問題是讓 image 身分失去穩定錨點。當 image 身分漂移，測試結果、掃描結果與 runtime 狀態會彼此分叉。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>替代做法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>production 使用 <code>latest</code></td>
          <td>running image 缺少精準身分</td>
          <td>使用 <a href="/blog/ci/knowledge-cards/image-digest/" data-link-title="Image Digest" data-link-desc="說明 container image digest 如何作為不可變產物身分，支撐掃描、推進與 runtime 追溯">Image Digest</a> 或 immutable tag</td>
      </tr>
      <tr>
          <td>staging 與 production 各自 build</td>
          <td>測試產物與上線產物分叉</td>
          <td>build once，promote same image</td>
      </tr>
      <tr>
          <td>build secret 留在 layer</td>
          <td>secret 進入 registry 與節點</td>
          <td>使用 BuildKit secret mount</td>
      </tr>
      <tr>
          <td>scan 只報告不阻擋</td>
          <td>高風險漏洞仍進 production</td>
          <td>定義阻擋門檻與例外流程</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Image 部署總覽：回 <a href="../">Docker / Image 部署 CI/CD</a>。</li>
<li>Registry 術語：讀 <a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a>。</li>
<li>後端 runtime 部署：讀 <a href="../../backend-deploy/">後端部署 CI/CD</a>。</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的容器與映像管理</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-container/</guid><description>&lt;p>容器化應用在斷網環境的主要挑戰不是容器本身——Docker 和 containerd 不需要網路就能啟動容器。挑戰在映像的取得和更新：沒有 Docker Hub、沒有 ECR、沒有 ghcr.io，每一個 base image 和應用映像都要經過搬運路徑進入隔離網路。映像的管理在斷網環境裡需要一條完整的 pipeline：外部下載 → 安全掃描 → 搬運 → 推送到內部 registry → 各節點 pull。&lt;/p>
&lt;h2 id="private-registry">Private Registry&lt;/h2>
&lt;p>隔離網路裡需要一個容器映像倉庫，讓內部的 Docker host / Kubernetes 節點能 pull image。&lt;/p>
&lt;h3 id="harbor">Harbor&lt;/h3>
&lt;p>Harbor 是 VMware 開源的企業級 registry，功能包含：映像儲存、漏洞掃描（整合 Trivy）、存取控制（RBAC）、映像簽章（Cosign / Notary）、複製策略。適合中大規模的斷網環境。&lt;/p>
&lt;p>離線安裝：Harbor 提供 offline installer（&lt;code>.tgz&lt;/code>，約 600MB），包含所有需要的容器映像。搬進隔離網路後解壓、跑 &lt;code>install.sh&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：下載 offline installer&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，在內部解壓安裝&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">tar xzf harbor-offline-installer-v2.11.0.tgz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> harbor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">cp harbor.yml.tmpl harbor.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 編輯 harbor.yml：設定 hostname、HTTPS 憑證、admin 密碼&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">./install.sh&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-registry官方輕量版">Docker Registry（官方輕量版）&lt;/h3>
&lt;p>如果不需要 Harbor 的進階功能（RBAC、掃描），官方的 Docker Registry 是單一容器、設定最簡單：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># registry image 也要先搬進來&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker load &amp;lt; registry-2.8.3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker run -d -p 5000:5000 --restart&lt;span class="o">=&lt;/span>always --name registry &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v /data/registry:/var/lib/registry &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> registry:2.8.3&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>內部機器的 Docker daemon 要設定信任這個 registry（如果是 HTTP 而非 HTTPS）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;insecure-registries&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;registry.internal:5000&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="映像搬運">映像搬運&lt;/h2>
&lt;h3 id="docker-save--load">docker save / load&lt;/h3>
&lt;p>最直接的搬運方式——把映像匯出成 tar 檔、搬運後匯入：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：匯出&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker pull nginx:1.25-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker save nginx:1.25-alpine -o nginx-1.25-alpine.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，內部匯入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">docker load &amp;lt; nginx-1.25-alpine.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 重新 tag 指向內部 registry&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">docker tag nginx:1.25-alpine registry.internal:5000/nginx:1.25-alpine
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">docker push registry.internal:5000/nginx:1.25-alpine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>多個映像可以打包成一個 tar：&lt;code>docker save img1 img2 img3 -o bundle.tar&lt;/code>。&lt;/p>
&lt;h3 id="skopeo-copy">skopeo copy&lt;/h3>
&lt;p>skopeo 是不需要 Docker daemon 的映像操作工具，適合 CI 環境或沒有裝 Docker 的工作站：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部：從 Docker Hub 複製到本地目錄&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">skopeo copy docker://nginx:1.25-alpine dir:/path/to/export/nginx-1.25
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運後，從本地目錄推送到內部 registry&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">skopeo copy dir:/path/to/export/nginx-1.25 docker://registry.internal:5000/nginx:1.25-alpine&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>skopeo 的優勢是不需要 pull 整個映像到本地 Docker（省磁碟空間）、支援 OCI layout、且可以在沒有 root 權限的環境執行。&lt;/p>
&lt;h3 id="搬運清單管理">搬運清單管理&lt;/h3>
&lt;p>映像搬運容易變成「需要什麼才搬什麼」的臨時操作。建議維護一份搬運清單（manifest），列出所有需要的 base image 和版本：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c"># image-manifest.yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">images&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1.25&lt;/span>-&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/nginx&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16.3&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">node&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tag&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">20&lt;/span>-&lt;span class="l">alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">source&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">docker.io/library/node&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>搬運腳本讀這份清單自動 pull + save，確保每次搬運的內容一致且可追蹤。&lt;/p></description><content:encoded><![CDATA[<p>容器化應用在斷網環境的主要挑戰不是容器本身——Docker 和 containerd 不需要網路就能啟動容器。挑戰在映像的取得和更新：沒有 Docker Hub、沒有 ECR、沒有 ghcr.io，每一個 base image 和應用映像都要經過搬運路徑進入隔離網路。映像的管理在斷網環境裡需要一條完整的 pipeline：外部下載 → 安全掃描 → 搬運 → 推送到內部 registry → 各節點 pull。</p>
<h2 id="private-registry">Private Registry</h2>
<p>隔離網路裡需要一個容器映像倉庫，讓內部的 Docker host / Kubernetes 節點能 pull image。</p>
<h3 id="harbor">Harbor</h3>
<p>Harbor 是 VMware 開源的企業級 registry，功能包含：映像儲存、漏洞掃描（整合 Trivy）、存取控制（RBAC）、映像簽章（Cosign / Notary）、複製策略。適合中大規模的斷網環境。</p>
<p>離線安裝：Harbor 提供 offline installer（<code>.tgz</code>，約 600MB），包含所有需要的容器映像。搬進隔離網路後解壓、跑 <code>install.sh</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：下載 offline installer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運後，在內部解壓安裝</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">tar xzf harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">cd</span> harbor
</span></span><span class="line"><span class="ln">7</span><span class="cl">cp harbor.yml.tmpl harbor.yml
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># 編輯 harbor.yml：設定 hostname、HTTPS 憑證、admin 密碼</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">./install.sh</span></span></code></pre></div><h3 id="docker-registry官方輕量版">Docker Registry（官方輕量版）</h3>
<p>如果不需要 Harbor 的進階功能（RBAC、掃描），官方的 Docker Registry 是單一容器、設定最簡單：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># registry image 也要先搬進來</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker load &lt; registry-2.8.3.tar
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run -d -p 5000:5000 --restart<span class="o">=</span>always --name registry <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  -v /data/registry:/var/lib/registry <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  registry:2.8.3</span></span></code></pre></div><p>內部機器的 Docker daemon 要設定信任這個 registry（如果是 HTTP 而非 HTTPS）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;insecure-registries&#34;</span><span class="p">:</span> <span class="p">[</span><span class="s2">&#34;registry.internal:5000&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h2 id="映像搬運">映像搬運</h2>
<h3 id="docker-save--load">docker save / load</h3>
<p>最直接的搬運方式——把映像匯出成 tar 檔、搬運後匯入：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：匯出</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker pull nginx:1.25-alpine
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker save nginx:1.25-alpine -o nginx-1.25-alpine.tar
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搬運後，內部匯入</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">docker load &lt; nginx-1.25-alpine.tar
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 重新 tag 指向內部 registry</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">docker tag nginx:1.25-alpine registry.internal:5000/nginx:1.25-alpine
</span></span><span class="line"><span class="ln">9</span><span class="cl">docker push registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>多個映像可以打包成一個 tar：<code>docker save img1 img2 img3 -o bundle.tar</code>。</p>
<h3 id="skopeo-copy">skopeo copy</h3>
<p>skopeo 是不需要 Docker daemon 的映像操作工具，適合 CI 環境或沒有裝 Docker 的工作站：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：從 Docker Hub 複製到本地目錄</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy docker://nginx:1.25-alpine dir:/path/to/export/nginx-1.25
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運後，從本地目錄推送到內部 registry</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">skopeo copy dir:/path/to/export/nginx-1.25 docker://registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>skopeo 的優勢是不需要 pull 整個映像到本地 Docker（省磁碟空間）、支援 OCI layout、且可以在沒有 root 權限的環境執行。</p>
<h3 id="搬運清單管理">搬運清單管理</h3>
<p>映像搬運容易變成「需要什麼才搬什麼」的臨時操作。建議維護一份搬運清單（manifest），列出所有需要的 base image 和版本：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># image-manifest.yaml</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">images</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="m">1.25</span>-<span class="l">alpine</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/nginx</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">postgres</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;16.3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/postgres</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">node</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">tag</span><span class="p">:</span><span class="w"> </span><span class="m">20</span>-<span class="l">alpine</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">source</span><span class="p">:</span><span class="w"> </span><span class="l">docker.io/library/node</span></span></span></code></pre></div><p>搬運腳本讀這份清單自動 pull + save，確保每次搬運的內容一致且可追蹤。</p>
<h2 id="base-image-更新週期">Base Image 更新週期</h2>
<p>斷網環境的 base image 不會自動更新——<code>nginx:1.25-alpine</code> 搬進去之後就是那個版本，裡面的 Alpine 套件不會收到安全補丁。需要定期用新版 base image 替換舊的。</p>
<h3 id="更新流程">更新流程</h3>
<ol>
<li><strong>外部</strong>：pull 最新版 base image</li>
<li><strong>外部</strong>：用 Trivy 掃描漏洞（見下一節）</li>
<li><strong>搬運</strong>：走 content ferry 帶進內部</li>
<li><strong>內部</strong>：push 到內部 registry、更新 tag</li>
<li><strong>內部</strong>：重新 build 所有依賴這個 base image 的應用映像</li>
<li><strong>內部</strong>：部署更新後的應用映像</li>
</ol>
<p>更新頻率：安全敏感環境月更、一般環境季更。每次更新都要記錄哪些 base image 換了、從哪個版本換到哪個版本。</p>
<h3 id="helm-chart-離線">Helm Chart 離線</h3>
<p>如果內部有 Kubernetes 且使用 Helm，chart 也要離線管理：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：下載 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">helm pull bitnami/postgresql --version 15.5.0
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運後，內部用本地檔案安裝</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">helm install pg ./postgresql-15.5.0.tgz -f values.yaml</span></span></code></pre></div><p>或架設 ChartMuseum 作為內部 Helm repo：chart 搬進來後 push 到 ChartMuseum，<code>helm repo add</code> 指向它。</p>
<h2 id="離線漏洞掃描">離線漏洞掃描</h2>
<p>連網環境的 Trivy 會自動下載漏洞資料庫（CVE DB）。斷網環境要先在外部下載 DB、搬進來。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：下載 Trivy 漏洞資料庫</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">trivy image --download-db-only --cache-dir /path/to/trivy-db/
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運 DB 檔案（~30MB）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># db.tar.gz 在 /path/to/trivy-db/db/ 裡</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 內部：用離線 DB 掃描</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">trivy image --skip-db-update --cache-dir /path/to/trivy-db/ registry.internal:5000/nginx:1.25-alpine</span></span></code></pre></div><p>掃描結果的處理方式跟連網環境相同——critical 和 high 的 CVE 要評估是否影響、是否有 base image 更新可修。差別是斷網環境的修復週期更長（要走搬運流程），所以掃描要更頻繁（至少跟 base image 更新同步）。</p>
<p>Harbor 整合 Trivy 後可以在 push 時自動掃描——Trivy DB 的更新同樣需要定期搬運。</p>
<p>時程參考：Private registry 建置（Harbor offline）約需 1 天。映像搬運流程建立約需半天。第一批 base image 搬運 + 掃描約需半天。之後每次更新約 2-4 小時。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：映像搬運走 content ferry 模式</li>
<li>→ <a href="/blog/infra/05-core-services/compute-ecs-eks/" data-link-title="運算平台上 IaC — ECS 與 EKS" data-link-desc="容器運算平台的 IaC 描述：ECS 與 EKS 選型、task definition 與映像版本解耦、IAM task role 分離、auto-scaling 策略">模組五：核心服務上 IaC — 運算</a>：連網環境的容器部署</li>
<li>→ <a href="/blog/infra/knowledge-cards/ecs/" data-link-title="ECS" data-link-desc="AWS Elastic Container Service — 受管的容器編排服務，用 task definition 描述容器配置、由平台負責排程與健康管理">ECS 知識卡</a>：容器編排的基礎概念</li>
</ul>
]]></content:encoded></item><item><title>斷網環境的套件與容器映像 Registry</title><link>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-package-registry/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/air-gapped/air-gapped-package-registry/</guid><description>&lt;p>連網環境的套件安裝和映像拉取，背後都有一個公開的 registry 在服務：apt 走 archive.ubuntu.com、npm 走 registry.npmjs.org、Docker 走 Docker Hub。斷網環境裡這些 endpoint 全部不可達，每一條 &lt;code>apt install&lt;/code>、&lt;code>npm install&lt;/code>、&lt;code>pip install&lt;/code>、&lt;code>docker pull&lt;/code> 都會失敗。替代做法是在內網部署自己的 registry，把需要的套件和映像從外部下載、經過安全審查後搬進來。&lt;/p>
&lt;p>本篇涵蓋兩個 registry 的部署與操作：Nexus Repository（多格式套件）和 Harbor（容器映像）。兩者可以獨立運作，也可以搭配使用——Nexus 管套件依賴、Harbor 管容器映像，各自負責不同的 artifact 類型。&lt;/p>
&lt;h2 id="nexus-repository統一的離線套件-proxy">Nexus Repository：統一的離線套件 proxy&lt;/h2>
&lt;p>Nexus Repository OSS（開源版）支援 apt、yum、npm、PyPI、Maven、NuGet、Go modules 等多種格式，用一個實例取代多個獨立的離線 repo mirror。部署在內網後，所有開發機器和 CI runner 把套件 source 指向 Nexus。&lt;/p>
&lt;h3 id="部署">部署&lt;/h3>
&lt;p>Nexus 本身是一個 Java 應用，用 Docker 部署最簡單。映像需要事先從外部搬進來：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部機器下載映像&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker pull sonatype/nexus3:latest
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">docker save sonatype/nexus3:latest -o nexus3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運到內網後載入&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">docker load -i nexus3.tar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">docker run -d -p 8081:8081 --name nexus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -v nexus-data:/nexus-data &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> sonatype/nexus3:latest&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>初始管理員密碼在容器內 &lt;code>/nexus-data/admin.password&lt;/code>，首次登入後強制修改。&lt;/p>
&lt;h3 id="hosted-repo-模式">Hosted repo 模式&lt;/h3>
&lt;p>連網環境的 Nexus 通常用 proxy repo（代理公開 registry、快取下載過的套件）。斷網環境 proxy 模式無法運作，改用 hosted repo——手動上傳套件到 Nexus，Nexus 作為唯一的分發來源。&lt;/p>
&lt;p>以 npm 為例，workflow 是在外部機器打包、搬運、上傳：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 外部機器：打包專案的所有依賴&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">npm pack --pack-destination ./npm-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># 或用 npm-offline-packager 批次下載整棵依賴樹&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">npx npm-offline-packager --package ./package.json --output ./npm-packages/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 搬運到內網後上傳到 Nexus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">for&lt;/span> pkg in ./npm-packages/*.tgz&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> curl -u admin:password &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --upload-file &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pkg&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;http://nexus.internal:8081/repository/npm-hosted/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>apt 和 yum 的做法類似：外部機器用 &lt;code>apt-get download&lt;/code> 或 &lt;code>yumdownloader&lt;/code> 抓 .deb / .rpm 檔案，搬進來後上傳到 Nexus 的 hosted repo。&lt;/p>
&lt;h3 id="客戶端設定">客戶端設定&lt;/h3>
&lt;p>開發機器和 CI runner 的套件 source 指向 Nexus：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># npm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">npm config &lt;span class="nb">set&lt;/span> registry http://nexus.internal:8081/repository/npm-hosted/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># pip&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">pip install --index-url http://nexus.internal:8081/repository/pypi-hosted/simple/ package-name
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># apt（在 /etc/apt/sources.list.d/ 加一份）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">deb http://nexus.internal:8081/repository/apt-hosted/ focal main&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="harbor容器映像的-private-registry">Harbor：容器映像的 private registry&lt;/h2>
&lt;p>Harbor 是 CNCF 畢業專案的企業級容器 registry，支援映像簽章、漏洞掃描（Trivy）、存取控制、映像複製。在斷網環境裡它是 Docker Hub 和 ECR 的替代品。&lt;/p></description><content:encoded><![CDATA[<p>連網環境的套件安裝和映像拉取，背後都有一個公開的 registry 在服務：apt 走 archive.ubuntu.com、npm 走 registry.npmjs.org、Docker 走 Docker Hub。斷網環境裡這些 endpoint 全部不可達，每一條 <code>apt install</code>、<code>npm install</code>、<code>pip install</code>、<code>docker pull</code> 都會失敗。替代做法是在內網部署自己的 registry，把需要的套件和映像從外部下載、經過安全審查後搬進來。</p>
<p>本篇涵蓋兩個 registry 的部署與操作：Nexus Repository（多格式套件）和 Harbor（容器映像）。兩者可以獨立運作，也可以搭配使用——Nexus 管套件依賴、Harbor 管容器映像，各自負責不同的 artifact 類型。</p>
<h2 id="nexus-repository統一的離線套件-proxy">Nexus Repository：統一的離線套件 proxy</h2>
<p>Nexus Repository OSS（開源版）支援 apt、yum、npm、PyPI、Maven、NuGet、Go modules 等多種格式，用一個實例取代多個獨立的離線 repo mirror。部署在內網後，所有開發機器和 CI runner 把套件 source 指向 Nexus。</p>
<h3 id="部署">部署</h3>
<p>Nexus 本身是一個 Java 應用，用 Docker 部署最簡單。映像需要事先從外部搬進來：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部機器下載映像</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker pull sonatype/nexus3:latest
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker save sonatype/nexus3:latest -o nexus3.tar
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搬運到內網後載入</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">docker load -i nexus3.tar
</span></span><span class="line"><span class="ln">7</span><span class="cl">docker run -d -p 8081:8081 --name nexus <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  -v nexus-data:/nexus-data <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  sonatype/nexus3:latest</span></span></code></pre></div><p>初始管理員密碼在容器內 <code>/nexus-data/admin.password</code>，首次登入後強制修改。</p>
<h3 id="hosted-repo-模式">Hosted repo 模式</h3>
<p>連網環境的 Nexus 通常用 proxy repo（代理公開 registry、快取下載過的套件）。斷網環境 proxy 模式無法運作，改用 hosted repo——手動上傳套件到 Nexus，Nexus 作為唯一的分發來源。</p>
<p>以 npm 為例，workflow 是在外部機器打包、搬運、上傳：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 外部機器：打包專案的所有依賴</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">npm pack --pack-destination ./npm-packages/
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># 或用 npm-offline-packager 批次下載整棵依賴樹</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">npx npm-offline-packager --package ./package.json --output ./npm-packages/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 搬運到內網後上傳到 Nexus</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">for</span> pkg in ./npm-packages/*.tgz<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  curl -u admin:password <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>    --upload-file <span class="s2">&#34;</span><span class="nv">$pkg</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>    <span class="s2">&#34;http://nexus.internal:8081/repository/npm-hosted/&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><p>apt 和 yum 的做法類似：外部機器用 <code>apt-get download</code> 或 <code>yumdownloader</code> 抓 .deb / .rpm 檔案，搬進來後上傳到 Nexus 的 hosted repo。</p>
<h3 id="客戶端設定">客戶端設定</h3>
<p>開發機器和 CI runner 的套件 source 指向 Nexus：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># npm</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">npm config <span class="nb">set</span> registry http://nexus.internal:8081/repository/npm-hosted/
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># pip</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pip install --index-url http://nexus.internal:8081/repository/pypi-hosted/simple/ package-name
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># apt（在 /etc/apt/sources.list.d/ 加一份）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">deb http://nexus.internal:8081/repository/apt-hosted/ focal main</span></span></code></pre></div><h2 id="harbor容器映像的-private-registry">Harbor：容器映像的 private registry</h2>
<p>Harbor 是 CNCF 畢業專案的企業級容器 registry，支援映像簽章、漏洞掃描（Trivy）、存取控制、映像複製。在斷網環境裡它是 Docker Hub 和 ECR 的替代品。</p>
<h3 id="部署-1">部署</h3>
<p>Harbor 用 Docker Compose 部署。安裝包需要從外部下載後搬進來：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 外部機器下載離線安裝包</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 搬運到內網後解壓</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">tar xzf harbor-offline-installer-v2.11.0.tgz
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">cd</span> harbor
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 複製並編輯設定</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">cp harbor.yml.tmpl harbor.yml
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 修改 hostname、storage 路徑、HTTPS 憑證（內部 CA 簽發）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">./install.sh --with-trivy</span></span></code></pre></div><p><code>--with-trivy</code> 啟用內建的漏洞掃描。Trivy 的漏洞資料庫需要離線更新——從外部下載 DB 檔案、搬進來放到指定路徑。</p>
<h3 id="專案與存取控制">專案與存取控制</h3>
<p>Harbor 用「專案」（project）組織映像。每個專案可以設定獨立的存取控制：</p>
<ul>
<li><code>library</code>：公開專案、所有使用者可 pull</li>
<li><code>platform</code>：平台團隊專用、限定成員可 push</li>
<li><code>vendor</code>：第三方 base image、由 infra 團隊管理更新</li>
</ul>
<p>robot account 提供 CI/CD 用的非互動式認證（限定 pull / push 權限、可設定到期時間）。</p>
<h2 id="映像搬運-sop">映像搬運 SOP</h2>
<p>映像從外部搬進斷網環境是一個需要標準化的操作，涉及格式、大小、多架構支援：</p>
<h3 id="搬運工具比較">搬運工具比較</h3>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>優點</th>
          <th>限制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>docker save/load</code></td>
          <td>最直覺、不需要額外安裝</td>
          <td>只能處理本地已 pull 的映像、不支援跨 registry 直接搬</td>
      </tr>
      <tr>
          <td><code>skopeo copy</code></td>
          <td>不需要 Docker daemon、支援跨 registry、支援 manifest list</td>
          <td>需要安裝 skopeo</td>
      </tr>
      <tr>
          <td><code>crane</code></td>
          <td>輕量 CLI、支援 manifest 操作</td>
          <td>功能比 skopeo 少</td>
      </tr>
  </tbody>
</table>
<p>skopeo 的操作流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部機器：從 Docker Hub 複製到本地目錄</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy docker://nginx:1.25-alpine dir:./images/nginx-1.25-alpine
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 搬運到內網後：從本地目錄推到 Harbor</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">skopeo copy dir:./images/nginx-1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  docker://harbor.internal/library/nginx:1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --dest-tls-verify<span class="o">=</span><span class="nb">false</span>  <span class="c1"># 如果 Harbor 用內部 CA</span></span></span></code></pre></div><h3 id="多架構映像">多架構映像</h3>
<p>如果環境同時有 amd64 和 arm64 的機器，搬運時要帶整個 manifest list：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部：複製所有架構</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">skopeo copy --all docker://nginx:1.25-alpine <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  dir:./images/nginx-1.25-alpine-multiarch
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 內網：推送所有架構</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">skopeo copy --all dir:./images/nginx-1.25-alpine-multiarch <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  docker://harbor.internal/library/nginx:1.25-alpine</span></span></code></pre></div><p><code>--all</code> flag 確保 manifest list 裡的每個架構都被複製，而非只複製本機架構。</p>
<h2 id="套件與映像的更新週期">套件與映像的更新週期</h2>
<p>斷網環境的套件和映像不會自動更新——每一次更新都是一次有意識的搬運操作。更新週期的頻率由安全需求決定：</p>
<table>
  <thead>
      <tr>
          <th>安全等級</th>
          <th>更新頻率</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>一般</td>
          <td>每月一次</td>
          <td>開發工具、非直接面對外部的服務</td>
      </tr>
      <tr>
          <td>中等</td>
          <td>每兩週</td>
          <td>有外部接口的服務、包含網路元件</td>
      </tr>
      <tr>
          <td>高</td>
          <td>每週或 CVE 驅動</td>
          <td>安全敏感環境、合規要求</td>
      </tr>
  </tbody>
</table>
<p>每次更新的標準流程：</p>
<ol>
<li><strong>外部機器下載</strong>：按清單下載指定版本的套件和映像</li>
<li><strong>安全掃描</strong>：在外部（或 staging gateway）跑 Trivy / Snyk 掃描，確認沒有已知的高風險 CVE</li>
<li><strong>審查核准</strong>：掃描報告給安全團隊或負責人簽核</li>
<li><strong>搬運</strong>：核准的 artifact 寫入唯讀媒體或加密通道搬進內網</li>
<li><strong>上傳到 registry</strong>：推到 Nexus 和 Harbor</li>
<li><strong>通知團隊</strong>：哪些套件/映像有新版本可用</li>
</ol>
<p>這個流程的產出是一份更新清單（什麼版本、掃描結果、核准人），存進版控作為稽核紀錄。</p>
<h2 id="helm-chart-離線管理">Helm chart 離線管理</h2>
<p>Kubernetes 環境用 Helm 部署應用。斷網時 Helm chart 需要離線管理：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 外部機器：下載 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">helm repo add bitnami https://charts.bitnami.com/bitnami
</span></span><span class="line"><span class="ln">3</span><span class="cl">helm pull bitnami/postgresql --version 15.5.0
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搬運到內網後有兩個存放選項</span></span></span></code></pre></div><p><strong>選項一：Harbor 內建 chart 支援</strong>。Harbor 2.0+ 支援 OCI artifact，Helm chart 可以直接推到 Harbor：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">helm push postgresql-15.5.0.tgz oci://harbor.internal/charts</span></span></code></pre></div><p><strong>選項二：ChartMuseum</strong>。獨立的 chart repository server：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 上傳 chart</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">curl --data-binary <span class="s2">&#34;@postgresql-15.5.0.tgz&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  http://chartmuseum.internal:8080/api/charts</span></span></code></pre></div><p>Harbor 的 OCI 方式較簡單（不需要額外維護 ChartMuseum），但需要 Helm 3.8+ 的 OCI 支援。</p>
<h2 id="時程與管理層溝通">時程與管理層溝通</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>初次部署時間</th>
          <th>持續維護</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Nexus Repository</td>
          <td>1 天（部署 + 初始套件上傳）</td>
          <td>每次更新週期 2-4 小時</td>
      </tr>
      <tr>
          <td>Harbor</td>
          <td>1 天（部署 + 初始映像搬運）</td>
          <td>每次更新週期 2-4 小時</td>
      </tr>
      <tr>
          <td>搬運 SOP 建立</td>
          <td>半天（腳本化 + 文件）</td>
          <td>每次執行 1-2 小時</td>
      </tr>
      <tr>
          <td>Trivy 離線 DB 更新</td>
          <td>含在 Harbor 部署內</td>
          <td>每次更新週期 30 分鐘</td>
      </tr>
  </tbody>
</table>
<p>管理層需要知道的成本：registry 的維護不是一次性投入，每個更新週期都需要工程師時間執行搬運和掃描。這筆成本在連網環境裡由公開 registry 和自動更新吸收，斷網環境裡由團隊承擔。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-principles/" data-link-title="斷網環境的通用原則" data-link-desc="離線套件管理、內容搬運、變更追蹤的共通操作模式 — 所有斷網情境都要先建立的基礎能力">斷網環境的通用原則</a>：content ferry pattern 和安全審查流程</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-container/" data-link-title="斷網環境的容器與映像管理" data-link-desc="Private registry 架設、映像搬運（docker save/load、skopeo）、base image 更新週期、離線漏洞掃描">斷網環境的容器與映像管理</a>：映像搬運的更完整討論（本篇聚焦 registry 部署、該篇聚焦映像生命週期）</li>
<li>→ <a href="/blog/infra/air-gapped/air-gapped-iac/" data-link-title="斷網環境的 IaC" data-link-desc="Terraform provider mirror、離線 plugin cache、本地 state backend、沒有雲端時的 plan/apply 流程與內網 CI">斷網環境的 IaC</a>：Terraform provider 也需要離線 mirror、可用 Nexus 的 raw hosted repo 存放</li>
</ul>
]]></content:encoded></item><item><title>Container Registry</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/container-registry/</guid><description>&lt;p>Container Registry 的核心概念是「管理可部署 image 的供應鏈節點」。它負責保存、授權、保留與推進已驗證影像。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Container Registry 位在 image build、scan、promotion 與 runtime deploy 之間，連接 CI 產物與環境發布。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>同一 tag 在不同環境對應內容不一致。&lt;/li>
&lt;li>部署因拉取權限或鏡像不存在失敗。&lt;/li>
&lt;li>線上 image 缺少來源與掃描紀錄的反查路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>團隊以 immutable digest 推進 staging 與 production，並透過 registry policy 控制 retention、pull 權限與 promotion 路徑。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Container Registry 要定義命名策略、權限模型、保留策略與來源追溯，讓 image 發布具備可審計性。&lt;/p></description><content:encoded><![CDATA[<p>Container Registry 的核心概念是「管理可部署 image 的供應鏈節點」。它負責保存、授權、保留與推進已驗證影像。</p>
<h2 id="概念位置">概念位置</h2>
<p>Container Registry 位在 image build、scan、promotion 與 runtime deploy 之間，連接 CI 產物與環境發布。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li>同一 tag 在不同環境對應內容不一致。</li>
<li>部署因拉取權限或鏡像不存在失敗。</li>
<li>線上 image 缺少來源與掃描紀錄的反查路徑。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>團隊以 immutable digest 推進 staging 與 production，並透過 registry policy 控制 retention、pull 權限與 promotion 路徑。</p>
<h2 id="設計責任">設計責任</h2>
<p>Container Registry 要定義命名策略、權限模型、保留策略與來源追溯，讓 image 發布具備可審計性。</p>
]]></content:encoded></item></channel></rss>