<?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>Container on Tarragon</title><link>https://tarrragon.github.io/blog/tags/container/</link><description>Recent content in Container 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/container/index.xml" rel="self" type="application/rss+xml"/><item><title>容器化資源設計</title><link>https://tarrragon.github.io/blog/devops/05-capacity-planning/container-resource-design/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/05-capacity-planning/container-resource-design/</guid><description>&lt;p>Container 的資源限制是容量規劃在容器化環境的落地。每個 container 設定 memory limit、CPU limit 和磁碟 I/O 控制，確保單一 container 不會吃光 host 資源影響其他服務。限制設太緊觸發 OOMKill 或 CPU throttle，設太鬆等於沒有限制。&lt;/p>
&lt;h2 id="memory-限制設計">Memory 限制設計&lt;/h2>
&lt;h3 id="觀察-baseline">觀察 baseline&lt;/h3>
&lt;p>在限制之前先觀察服務的真實記憶體使用。用 &lt;code>docker stats&lt;/code> 看 container 的 MEM USAGE，跑至少 24 小時涵蓋日常操作和定期 job（降採樣、清理）。&lt;/p>
&lt;p>Baseline 包含：&lt;/p>
&lt;ul>
&lt;li>應用程式本身的 heap + stack&lt;/li>
&lt;li>Runtime 開銷（Go 的 GC metadata、JVM 的 metaspace、Python 的 interpreter）&lt;/li>
&lt;li>內嵌資料庫的 page cache（如 SQLite 的 &lt;code>PRAGMA cache_size&lt;/code>）&lt;/li>
&lt;li>HTTP server 的連線 buffer&lt;/li>
&lt;/ul>
&lt;h3 id="設定-limit">設定 limit&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Memory limit = baseline peak × 1.5（安全係數）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>安全係數 1.5 是經驗值 — 預留 burst 時的記憶體波動（如大 batch 的 JSON 反序列化、查詢結果集暫存）。安全係數太大浪費資源、太小在 burst 時 OOMKill。&lt;/p>
&lt;h3 id="oomkill-排查">OOMKill 排查&lt;/h3>
&lt;p>OOMKill 的症狀是 container 突然消失、沒有 application log。排查步驟：&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">docker inspect &amp;lt;container&amp;gt; &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.[0].State.OOMKilled&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># true = 被 OOM killer 終止&lt;/span>
&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">dmesg &lt;span class="p">|&lt;/span> grep -i oom
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># kernel log 中的 OOM 記錄、包含被殺的 process 和當時的記憶體使用&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OOMKill 後的處理：提高 memory limit，或找出記憶體使用異常的原因（memory leak、unbounded cache、大結果集查詢）。&lt;/p>
&lt;h3 id="不同-runtime-的記憶體特性">不同 runtime 的記憶體特性&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Runtime&lt;/th>
 &lt;th>特性&lt;/th>
 &lt;th>注意事項&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Go&lt;/td>
 &lt;td>GC 自動管理、GOGC 控制觸發頻率&lt;/td>
 &lt;td>&lt;code>GOMEMLIMIT&lt;/code> 讓 Go runtime 感知 container 的 memory limit、避免 GC 不積極&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JVM&lt;/td>
 &lt;td>heap + metaspace + native memory&lt;/td>
 &lt;td>設 &lt;code>-Xmx&lt;/code> 小於 container limit（留空間給 native memory）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Python&lt;/td>
 &lt;td>無 GC 上限、依賴 OS&lt;/td>
 &lt;td>大 DataFrame / 大 dict 可能瞬間超限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Node.js&lt;/td>
 &lt;td>V8 heap limit 預設 ~1.5GB&lt;/td>
 &lt;td>設 &lt;code>--max-old-space-size&lt;/code> 配合 container limit&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="cpu-限制設計">CPU 限制設計&lt;/h2>
&lt;h3 id="--cpus-vs---cpu-shares">&lt;code>--cpus&lt;/code> vs &lt;code>--cpu-shares&lt;/code>&lt;/h3>
&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>&lt;code>--cpus=0.5&lt;/code>&lt;/td>
 &lt;td>Hard limit — 最多用 0.5 個 CPU core&lt;/td>
 &lt;td>嚴格隔離、多 container 共用一台主機&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>--cpu-shares=512&lt;/code>&lt;/td>
 &lt;td>Relative weight — 和其他 container 按比例分 CPU&lt;/td>
 &lt;td>彈性分配、host 閒置時可用更多&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="cpu-throttle-症狀">CPU throttle 症狀&lt;/h3>
&lt;p>CPU throttle 不會 crash（和 OOMKill 不同）。症狀是延遲上升 — request 處理時間從 10ms 變成 100ms，因為 container 的 CPU time 被 cgroup 暫停。&lt;/p></description><content:encoded><![CDATA[<p>Container 的資源限制是容量規劃在容器化環境的落地。每個 container 設定 memory limit、CPU limit 和磁碟 I/O 控制，確保單一 container 不會吃光 host 資源影響其他服務。限制設太緊觸發 OOMKill 或 CPU throttle，設太鬆等於沒有限制。</p>
<h2 id="memory-限制設計">Memory 限制設計</h2>
<h3 id="觀察-baseline">觀察 baseline</h3>
<p>在限制之前先觀察服務的真實記憶體使用。用 <code>docker stats</code> 看 container 的 MEM USAGE，跑至少 24 小時涵蓋日常操作和定期 job（降採樣、清理）。</p>
<p>Baseline 包含：</p>
<ul>
<li>應用程式本身的 heap + stack</li>
<li>Runtime 開銷（Go 的 GC metadata、JVM 的 metaspace、Python 的 interpreter）</li>
<li>內嵌資料庫的 page cache（如 SQLite 的 <code>PRAGMA cache_size</code>）</li>
<li>HTTP server 的連線 buffer</li>
</ul>
<h3 id="設定-limit">設定 limit</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Memory limit = baseline peak × 1.5（安全係數）</span></span></code></pre></div><p>安全係數 1.5 是經驗值 — 預留 burst 時的記憶體波動（如大 batch 的 JSON 反序列化、查詢結果集暫存）。安全係數太大浪費資源、太小在 burst 時 OOMKill。</p>
<h3 id="oomkill-排查">OOMKill 排查</h3>
<p>OOMKill 的症狀是 container 突然消失、沒有 application log。排查步驟：</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">docker inspect &lt;container&gt; <span class="p">|</span> jq <span class="s1">&#39;.[0].State.OOMKilled&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># true = 被 OOM killer 終止</span>
</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">dmesg <span class="p">|</span> grep -i oom
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># kernel log 中的 OOM 記錄、包含被殺的 process 和當時的記憶體使用</span></span></span></code></pre></div><p>OOMKill 後的處理：提高 memory limit，或找出記憶體使用異常的原因（memory leak、unbounded cache、大結果集查詢）。</p>
<h3 id="不同-runtime-的記憶體特性">不同 runtime 的記憶體特性</h3>
<table>
  <thead>
      <tr>
          <th>Runtime</th>
          <th>特性</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go</td>
          <td>GC 自動管理、GOGC 控制觸發頻率</td>
          <td><code>GOMEMLIMIT</code> 讓 Go runtime 感知 container 的 memory limit、避免 GC 不積極</td>
      </tr>
      <tr>
          <td>JVM</td>
          <td>heap + metaspace + native memory</td>
          <td>設 <code>-Xmx</code> 小於 container limit（留空間給 native memory）</td>
      </tr>
      <tr>
          <td>Python</td>
          <td>無 GC 上限、依賴 OS</td>
          <td>大 DataFrame / 大 dict 可能瞬間超限</td>
      </tr>
      <tr>
          <td>Node.js</td>
          <td>V8 heap limit 預設 ~1.5GB</td>
          <td>設 <code>--max-old-space-size</code> 配合 container limit</td>
      </tr>
  </tbody>
</table>
<h2 id="cpu-限制設計">CPU 限制設計</h2>
<h3 id="--cpus-vs---cpu-shares"><code>--cpus</code> vs <code>--cpu-shares</code></h3>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>--cpus=0.5</code></td>
          <td>Hard limit — 最多用 0.5 個 CPU core</td>
          <td>嚴格隔離、多 container 共用一台主機</td>
      </tr>
      <tr>
          <td><code>--cpu-shares=512</code></td>
          <td>Relative weight — 和其他 container 按比例分 CPU</td>
          <td>彈性分配、host 閒置時可用更多</td>
      </tr>
  </tbody>
</table>
<h3 id="cpu-throttle-症狀">CPU throttle 症狀</h3>
<p>CPU throttle 不會 crash（和 OOMKill 不同）。症狀是延遲上升 — request 處理時間從 10ms 變成 100ms，因為 container 的 CPU time 被 cgroup 暫停。</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">cat /sys/fs/cgroup/cpu/cpu.stat
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># nr_throttled: 被限制的次數</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># throttled_time: 累計被暫停的時間（奈秒）</span></span></span></code></pre></div><p>I/O bound 的服務（如監控 collector — 主要時間花在 SQLite 寫入和 HTTP 收發）通常不需要嚴格 CPU 限制。CPU 只在查詢處理（JSON 反序列化、聚合計算）時短暫使用。</p>
<h2 id="磁碟-io-考量">磁碟 I/O 考量</h2>
<h3 id="overlay-filesystem-的寫入放大">Overlay filesystem 的寫入放大</h3>
<p>Docker 的 overlay2 storage driver 把 container 的寫入操作分層管理。每次寫入新檔案或修改檔案，overlay 在上層（upper layer）建立副本再修改（copy-on-write）。對 SQLite 這類頻繁 fsync 的嵌入式資料庫，overlay 層增加 20-40% 的寫入延遲。</p>
<h3 id="volume-mount-繞過-overlay">Volume mount 繞過 overlay</h3>
<p>把需要高 I/O 效能的目錄掛載為 host volume（<code>-v /host/path:/container/path</code>），寫入直接到 host 檔案系統、繞過 overlay。</p>
<p>適用 volume mount 的場景：</p>
<ul>
<li>嵌入式資料庫的資料目錄（SQLite、BoltDB）</li>
<li>需要持久化的 log 檔案</li>
<li>大量小檔案寫入（cache 目錄）</li>
</ul>
<p>不適用 volume mount 的場景（用 overlay 即可）：</p>
<ul>
<li>暫存檔（處理完就刪）</li>
<li>只讀的設定檔（<code>-v config:/config:ro</code>，overlay 讀取開銷小）</li>
</ul>
<h3 id="tmpfs-mount">tmpfs mount</h3>
<p>記憶體中的暫存目錄，不寫磁碟。適合不需要持久化的高頻寫入（如 SDK 的離線 buffer、session 暫存）：</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">docker run --tmpfs /tmp:size<span class="o">=</span>64m ...</span></span></code></pre></div><h2 id="health-check-設計">Health Check 設計</h2>
<p>Container 的 health check 告訴 orchestrator「這個 container 是否正常運作」。Process 活著但 HTTP 不回應的場景（deadlock、資源耗盡）只靠 process 監控抓不到。</p>
<h3 id="dockerfile-healthcheck">Dockerfile HEALTHCHECK</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">HEALTHCHECK</span> --interval<span class="o">=</span>30s --timeout<span class="o">=</span>5s --retries<span class="o">=</span><span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  <span class="k">CMD</span> wget -q --spider http://localhost:8080/health <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span></span></span></code></pre></div><h3 id="docker-compose-healthcheck">Docker Compose healthcheck</h3>





<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="nt">healthcheck</span><span class="p">:</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">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;wget&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-q&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--spider&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:8080/health&#34;</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">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</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">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</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">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</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">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">10s</span></span></span></code></pre></div><p><code>start_period</code> 是啟動寬限期 — container 啟動後前 10 秒的 health check 失敗不算。避免服務還在初始化時就被標記 unhealthy。</p>
<h3 id="kubernetes-probe-對應">Kubernetes probe 對應</h3>
<table>
  <thead>
      <tr>
          <th>Docker</th>
          <th>Kubernetes</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>HEALTHCHECK</td>
          <td>livenessProbe</td>
          <td>container 是否活著（失敗 → 重啟）</td>
      </tr>
      <tr>
          <td>—</td>
          <td>readinessProbe</td>
          <td>container 是否準備好接流量（失敗 → 從 service 移除）</td>
      </tr>
      <tr>
          <td>—</td>
          <td>startupProbe</td>
          <td>container 是否完成啟動（失敗 → 重啟、比 liveness 寬容）</td>
      </tr>
  </tbody>
</table>
<p>Docker 的 HEALTHCHECK 只有一種、等同 Kubernetes 的 livenessProbe。Kubernetes 的 readinessProbe 和 startupProbe 在 Docker 單機環境沒有對應物 — 它們是多 pod 場景下的流量控制機制。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>監控 collector 的 container 部署實例 → <a href="/blog/monitoring/04-collector/container-deployment/" data-link-title="Container 部署設計" data-link-desc="Docker 部署 collector 的設計 — SQLite 在 overlay filesystem 的 I/O 考量、volume mount、graceful shutdown、資源限制">Container 部署設計</a></li>
<li>服務探活與自動恢復 → <a href="/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">DevOps 服務探活</a></li>
<li>負載平衡設計 → <a href="/blog/devops/01-load-balancing/" data-link-title="模組一：負載平衡與反向代理" data-link-desc="流量進來怎麼分給多個服務實例 — nginx / HAProxy / DNS round-robin 的選型和健康檢查路由設計">DevOps 負載平衡</a></li>
</ul>
]]></content:encoded></item><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>5.1 container 與 runtime</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/container-runtime/</guid><description>&lt;p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。&lt;/p>
&lt;h2 id="image-與建置責任">image 與建置責任&lt;/h2>
&lt;p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。&lt;/p>
&lt;p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。&lt;/p>
&lt;h3 id="基底映像選擇">基底映像選擇&lt;/h3>
&lt;p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語言官方映像&lt;/strong>（&lt;code>python:3.12&lt;/code>、&lt;code>node:20&lt;/code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。&lt;/li>
&lt;li>&lt;strong>slim / alpine 變體&lt;/strong>（&lt;code>python:3.12-slim&lt;/code>、&lt;code>node:20-alpine&lt;/code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。&lt;/li>
&lt;li>&lt;strong>distroless&lt;/strong>（&lt;code>gcr.io/distroless/base&lt;/code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。&lt;/li>
&lt;li>&lt;strong>自建基底&lt;/strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。&lt;/li>
&lt;/ul>
&lt;p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。&lt;/p>
&lt;h3 id="建置可重現性">建置可重現性&lt;/h3>
&lt;p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。&lt;/p>
&lt;p>可重現建置的關鍵實踐：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>鎖定依賴版本&lt;/strong>：&lt;code>go.sum&lt;/code>、&lt;code>package-lock.json&lt;/code>、&lt;code>poetry.lock&lt;/code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。&lt;/li>
&lt;li>&lt;strong>Multi-stage build&lt;/strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。&lt;/li>
&lt;li>&lt;strong>避免 image 中殘留敏感資訊&lt;/strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。&lt;/li>
&lt;li>&lt;strong>image 標記策略&lt;/strong>：&lt;code>latest&lt;/code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。&lt;/li>
&lt;/ol>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration&lt;/a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。&lt;/p>
&lt;h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為&lt;/h2>
&lt;p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。&lt;/p>
&lt;p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。&lt;/p>
&lt;h3 id="pid-1-與信號處理">PID 1 與信號處理&lt;/h3>
&lt;p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：&lt;/p>
&lt;p>&lt;strong>Shell 作為 PID 1&lt;/strong>：&lt;code>ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;-c&amp;quot;, &amp;quot;java -jar app.jar&amp;quot;]&lt;/code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 &lt;code>exec&lt;/code> 或直接用 exec form：&lt;code>ENTRYPOINT [&amp;quot;java&amp;quot;, &amp;quot;-jar&amp;quot;, &amp;quot;app.jar&amp;quot;]&lt;/code>。&lt;/p>
&lt;p>&lt;strong>多進程容器&lt;/strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 &lt;code>shareProcessNamespace: true&lt;/code> 讓 kubelet 處理。&lt;/p></description><content:encoded><![CDATA[<p>容器執行環境（container runtime）的核心責任是把應用執行環境做成可重現、可限制、可觀測的交付單位。它是部署可靠性的起點——後續的 probe、canary、rollback 都假設 runtime 產物行為可預測。</p>
<h2 id="image-與建置責任">image 與建置責任</h2>
<p>image 的責任是固定依賴、執行入口與檔案結構，讓同一版本在不同環境行為一致。建置流程要回答三件事：基底映像是否可維護、建置產物是否可追溯、敏感資訊是否被隔離。</p>
<p>映像層數、套件來源、編譯參數都會影響啟動時間與安全邊界。部署策略在後面才有效，前提是 runtime 產物本身可預測。</p>
<h3 id="基底映像選擇">基底映像選擇</h3>
<p>基底映像（base image）決定 image 的安全維護基線與啟動時體積。選擇的核心取捨是體積 / 啟動速度與除錯便利性：</p>
<ul>
<li><strong>語言官方映像</strong>（<code>python:3.12</code>、<code>node:20</code>）：套件齊全、除錯方便，但體積大（通常 800MB+）、攻擊面廣。適合開發環境與 CI。</li>
<li><strong>slim / alpine 變體</strong>（<code>python:3.12-slim</code>、<code>node:20-alpine</code>）：體積壓到 100-200MB、啟動快、攻擊面小。代價是缺少除錯工具（strace、curl、dig），生產事故時 exec 進容器排查會受限。Alpine 用 musl libc 而非 glibc，某些 C extension 需要額外處理。</li>
<li><strong>distroless</strong>（<code>gcr.io/distroless/base</code>）：只包含 runtime 必要檔案，無 shell、無套件管理器。攻擊面最小，但除錯只能靠 ephemeral debug container 或外部觀測。適合安全要求高且觀測基礎建設完備的生產環境。</li>
<li><strong>自建基底</strong>：組織統一維護的基底映像，可以固定安全基線、預裝觀測 agent、統一 timezone / locale。代價是基底維護本身是持續工作，版本更新節奏要有明確 owner。</li>
</ul>
<p>選完基底後要確認兩件事：upstream 的更新節奏是否可追蹤（CVE 修補從上游到自家 image 的時間），以及團隊是否有能力在基底更新後快速重建並驗證所有服務 image。</p>
<h3 id="建置可重現性">建置可重現性</h3>
<p>同一份 source code 在不同時間建置出不同 image，會讓 rollback 的假設失效——「回退到上一版」回退的是哪一版，取決於當時 build 環境的狀態。</p>
<p>可重現建置的關鍵實踐：</p>
<ol>
<li><strong>鎖定依賴版本</strong>：<code>go.sum</code>、<code>package-lock.json</code>、<code>poetry.lock</code> 要進 git。依賴解析在建置時不從 registry 重新 resolve。</li>
<li><strong>Multi-stage build</strong>：把建置環境（compiler、dev dependencies）和執行環境分開。最終 image 只包含 runtime 必要檔案，體積小且攻擊面收窄。</li>
<li><strong>避免 image 中殘留敏感資訊</strong>：build arg、環境變數、中間層都可能殘留 secret。secret 不進 Dockerfile，用 runtime mount 或 secret manager 注入。</li>
<li><strong>image 標記策略</strong>：<code>latest</code> tag 不可重現——同一個 tag 指向的 image 會隨時間改變。用 git commit SHA 或語意版本號標記，讓每個 tag 指向唯一 image。</li>
</ol>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/orbitera-managed-kubernetes-migration/" data-link-title="5.C3 Orbitera：遷移到 Managed Kubernetes" data-link-desc="平台重置時如何讓產品不中斷地完成編排層轉換。">5.C3 Orbitera managed K8s migration</a>：揭露「跨平台遷移本質是能力遷移」。遷移到新平台時，CI/CD pipeline 可能換了 runner 環境、換了 registry——建置可重現性的前提是依賴鎖定與 multi-stage build 本身不依賴特定 CI 環境。</p>
<h2 id="entrypoint-與啟動行為">entrypoint 與啟動行為</h2>
<p>entrypoint/command 的責任是定義容器如何啟動與退出。啟動流程應顯式處理初始化步驟、配置載入、依賴檢查與失敗退出。退出流程應處理信號中斷、在途請求收斂與資源釋放。</p>
<p>若啟動行為隱藏在 shell script 且無可觀測訊號，部署平台很難判斷 readiness 與失敗原因。</p>
<h3 id="pid-1-與信號處理">PID 1 與信號處理</h3>
<p>容器內 PID 1 有特殊語意：它是 init process，負責接收平台送來的 SIGTERM / SIGINT 並轉發給子進程。PID 1 的問題出在三種情境：</p>
<p><strong>Shell 作為 PID 1</strong>：<code>ENTRYPOINT [&quot;sh&quot;, &quot;-c&quot;, &quot;java -jar app.jar&quot;]</code> 讓 sh 成為 PID 1。SIGTERM 送到 sh、sh 預設不轉發、java 進程收不到信號、等到 terminationGracePeriodSeconds 到期後被 SIGKILL 強殺。修法是用 <code>exec</code> 或直接用 exec form：<code>ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code>。</p>
<p><strong>多進程容器</strong>：一個容器跑多個進程時，PID 1 要負責信號轉發與子進程回收（zombie reaping）。如果 PID 1 不做 wait()，結束的子進程會變成 zombie。解法是用 tini 或 dumb-init 作為輕量 init，或在 Kubernetes 設 <code>shareProcessNamespace: true</code> 讓 kubelet 處理。</p>
<p><strong>啟動腳本的信號遮蔽</strong>：entrypoint script 在初始化階段（下載 config、等依賴就緒）捕捉 SIGTERM 做清理，但如果清理邏輯卡住，整個 shutdown 會被阻塞。啟動腳本的 trap handler 要有 timeout，避免把 graceful shutdown 變成 ungraceful hang。</p>
<h3 id="啟動時間對部署策略的影響">啟動時間對部署策略的影響</h3>
<p>啟動時間直接影響 rollout 的最短觀察窗。一個啟動需 60 秒的服務，rollout 每批至少要等 60 秒 + 觀察窗口才能確認新版本穩定。啟動時間的組成與壓縮策略見 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
<p>image 體積也影響啟動時間——image pull 在冷啟動（節點上沒有這個 image 的快取）時占啟動時間的顯著比例。1GB image 在 100Mbps 網路下需要 ~80 秒 pull。壓縮 image 體積同時改善啟動速度與節省 registry 頻寬。</p>
<h2 id="resource-limit">resource limit</h2>
<p>CPU/memory <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a> 隔離資源競爭並保護叢集穩態。限制過低會導致頻繁節流與重啟，過高會壓縮同節點容量並放大鄰近工作負載風險。</p>
<p>限制設計要依服務流量型態與 GC/執行時特性調整，並與 autoscaling、rollout 批次策略一起評估。</p>
<h3 id="cpu-request-與-limit-的設定策略">CPU request 與 limit 的設定策略</h3>
<p>CPU 限制有兩個參數：request（排程保證）與 limit（硬上限）。兩者的關係決定服務在負載變動下的行為：</p>
<ul>
<li><strong>request = limit</strong>（guaranteed QoS）：CPU 用量穩定可預測，不會被 throttle 也不會超用。代價是無法在閒時借用節點剩餘 CPU。適合延遲敏感的 API 服務。</li>
<li><strong>request &lt; limit</strong>（burstable QoS）：平時用 request 保證的份額，高峰時可用到 limit。代價是當節點 CPU 競爭激烈時，所有 burstable pod 同時被 throttle，延遲會一起劣化。適合批次處理或對延遲要求不高的服務。</li>
<li><strong>不設 limit</strong>（只設 request）：服務可用到節點全部剩餘 CPU。Kubernetes 社群近年傾向這個做法——CPU throttle 常比 CPU contention 更難排查。代價是需要良好的觀測來偵測 noisy neighbor。</li>
</ul>
<h3 id="memory-limit-與-oom-的判讀">Memory limit 與 OOM 的判讀</h3>
<p>memory limit 是硬邊界——超過就 OOM kill，不走 graceful shutdown。OOM kill 的判讀分兩種情境：</p>
<p><strong>真正的 memory leak</strong>：記憶體使用量隨時間單調上升，GC 無法回收。修法在程式碼層。memory limit 只是延後問題爆發，不是解法。</p>
<p><strong>memory limit 設太低</strong>：服務在高峰流量下的正常記憶體使用超過 limit。常見於 JVM 服務——JVM heap + metaspace + native memory + thread stack 的總和超出 container memory limit。設 limit 時要用「峰值實際使用 + headroom」而非「平均使用」。</p>
<p>GC-based runtime（JVM、.NET、Go）要注意 container-aware memory 設定。早期 JVM 不認 cgroup memory limit，會按宿主機記憶體計算 heap 大小，導致 heap 配置超過 container limit。現代 JVM（Java 10+）預設啟用 container awareness（<code>-XX:+UseContainerSupport</code>），Go runtime 1.19+ 支援 <code>GOMEMLIMIT</code>。</p>
<h3 id="資源設定與-autoscaling-的協同">資源設定與 autoscaling 的協同</h3>
<p>resource request 同時決定 HPA（Horizontal Pod Autoscaler）的觸發基線。request 設太高時，CPU utilization % 會偏低，HPA 不會觸發擴容，導致服務在真正需要擴容前已經出現延遲。request 設太低時，utilization % 容易衝高，HPA 頻繁擴容，造成 pod 數量抖動。</p>
<p>穩定做法是先在 staging 環境跑負載測試確認服務的實際資源消耗曲線，再以 p90 負載的 CPU / memory 使用作為 request 基線。</p>
<h2 id="runtime-config">runtime config</h2>
<p>環境差異要顯式化才能追蹤——<a href="/blog/backend/knowledge-cards/runtime-config/" data-link-title="Runtime Config" data-link-desc="說明服務在啟動與執行時如何讀取與組合設定">Runtime Config</a> 承擔這個責任。配置來源、版本、更新節奏都應可追蹤。高風險設定需配合 <a href="/blog/backend/knowledge-cards/config-rollout/" data-link-title="Config Rollout" data-link-desc="說明設定如何安全下發到正在運作的服務實例">Config Rollout</a> 策略，避免同批大規模變更。</p>
<p>runtime 配置與映像版本要保留相容窗口，讓部署與回退可分步進行。</p>
<h3 id="配置注入方式與取捨">配置注入方式與取捨</h3>
<p>配置注入容器有三條路徑，各自有不同的版本追蹤與更新語意：</p>
<table>
  <thead>
      <tr>
          <th>注入方式</th>
          <th>版本追蹤</th>
          <th>更新行為</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>環境變數</td>
          <td>跟 deployment spec 一起版控</td>
          <td>需要 pod restart 才生效</td>
          <td>啟動時固定的設定（DB URL、port）</td>
      </tr>
      <tr>
          <td>ConfigMap mount</td>
          <td>ConfigMap 版本</td>
          <td>自動更新（kubelet sync period 內）</td>
          <td>需要動態更新的非敏感設定</td>
      </tr>
      <tr>
          <td>Secret mount</td>
          <td>Secret 版本</td>
          <td>自動更新（同 ConfigMap）</td>
          <td>credential、cert、API key</td>
      </tr>
      <tr>
          <td>外部 config store</td>
          <td>config store 內版本</td>
          <td>應用主動拉取或 sidecar push</td>
          <td>feature flag、複雜設定邏輯</td>
      </tr>
  </tbody>
</table>
<p>環境變數最簡單但更新需要 restart。ConfigMap mount 可以動態更新但應用要能偵測檔案變化並 reload。外部 config store（Consul KV、AWS AppConfig、Feature Flag service）最靈活但引入了額外依賴。</p>
<p>設定變更跟 image 變更走不同路徑時，要確保兩者的版本可以交叉相容。版本 v2 的 image 搭版本 A 的 config 能跑、版本 v1 的 image 搭版本 B 的 config 也能跑——rollback image 但 config 沒回退、或 rollback config 但 image 沒回退的情境下、服務不應崩潰。這個相容窗口的設計責任見 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">5.7 Config Boundary</a>。</p>
<h2 id="遷移期的-runtime-穩定性">遷移期的 Runtime 穩定性</h2>
<p>對應 <a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a>：揭露「平台託管化的價值在讓團隊把心力從底層維護轉到交付效率與可靠性策略」。遷移到 managed 平台後，runtime 層面的變化包含 container runtime 版本（containerd vs Docker shim）、node OS、storage driver、network plugin。這些變化可能改變 image pull 速度、filesystem 行為、DNS 解析路徑。</p>
<p>遷移前後的 runtime 驗證應包含：</p>
<ol>
<li><strong>image pull 時間比較</strong>：新 registry / 新 node 的 pull 速度是否在 startup timeout 內。</li>
<li><strong>filesystem 行為</strong>：log 寫入路徑、tmp 目錄、volume mount 行為在新 runtime 下是否一致。</li>
<li><strong>DNS 解析</strong>：新叢集的 CoreDNS / node-local DNS 設定是否影響服務的依賴連線建立速度。</li>
<li><strong>resource 行為</strong>：新 node type 的 CPU 架構（x86 vs ARM）、memory page size 是否影響服務性能特性。</li>
</ol>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新版本容器啟動時間顯著增加</td>
          <td>image 體積或初始化步驟膨脹</td>
          <td>優化映像層、拆分初始化流程</td>
      </tr>
      <tr>
          <td>rollout 初期出現 OOM/CPU throttle</td>
          <td>resource limit 與實際負載不匹配</td>
          <td>重設 request/limit、調整併發與批次</td>
      </tr>
      <tr>
          <td>配置變更後特定環境異常</td>
          <td>runtime config 管理不一致</td>
          <td>統一配置來源、補版本追蹤與差異檢查</td>
      </tr>
      <tr>
          <td>容器停止時請求中斷率上升</td>
          <td>signal/drain 協調不足</td>
          <td>補 shutdown hook、對齊 termination 流程</td>
      </tr>
      <tr>
          <td>同版本在不同節點行為差異大</td>
          <td>runtime 依賴未固定或環境漂移</td>
          <td>收斂基底映像、鎖定依賴與建置流程</td>
      </tr>
      <tr>
          <td>JVM 服務 OOM 但 heap 未用滿</td>
          <td>native memory / metaspace 超出 limit</td>
          <td>調整 MaxMetaspaceSize、限制 thread 數</td>
      </tr>
      <tr>
          <td>冷啟動節點上服務啟動超慢</td>
          <td>image pull 時間在啟動時間中占比高</td>
          <td>壓縮 image 體積、啟用 image cache</td>
      </tr>
      <tr>
          <td>rollback 後行為跟上次部署不同</td>
          <td>建置不可重現、tag 覆蓋</td>
          <td>改用 commit SHA 標記、鎖定依賴版本</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>Container 常被簡化成「打包完就好」的步驟，結果是部署風險被後移到 rollout 階段。runtime 產物穩定性不足時，後續 probe、canary、rollback 都只能被動補救。</p>
<p>把資源限制設成平台預設值，也常造成高峰期不穩。限制應反映服務真實耗用模式，不應只追求表面資源利用率。</p>
<p>把 <code>latest</code> tag 當成版本標記，會讓 rollback 指向無法預測的 image。image tag 在 registry 上是 mutable——同一個 tag 可以被覆蓋指向新 image。用 immutable tag（commit SHA、content digest）才能保證 rollback 的確定性。</p>
<p>把所有配置都用環境變數注入，會讓設定變更跟 image 部署綁在一起。需要動態更新的設定（feature flag、rate limit 閾值）應該用 ConfigMap mount 或外部 config store，讓設定變更不需要 pod restart。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>runtime 穩定性可用 <a href="/blog/backend/05-deployment-platform/cases/tradeshift-self-managed-k8s-to-eks/" data-link-title="5.C1 Tradeshift：self-managed Kubernetes 遷移到 EKS" data-link-desc="零停機平台遷移的分段策略案例。">5.C1 Tradeshift：self-managed K8s -&gt; EKS</a> 回寫。先看遷移期內啟動行為與資源限制如何影響切流，再對照本章檢查 image、entrypoint、limit 與 config 相容窗口。這個案例主要支撐的是「執行環境可重現性」判讀——遷移到新叢集時，image 不變但 runtime 環境變了（node OS、container runtime 版本、network plugin），runtime 穩定性的前提是 image 本身不依賴特定宿主環境的行為。</p>
<p><a href="/blog/backend/05-deployment-platform/cases/miro-managed-eks-migration/" data-link-title="5.C5 Miro：Managed EKS 遷移" data-link-desc="從自維運平台轉向 managed EKS 的組織與技術協同案例。">5.C5 Miro managed EKS 遷移</a> 從另一個角度支撐：managed 平台接管 runtime 基礎設施後，container runtime 版本升級由平台控制，團隊要能驗證自家 image 在新 runtime 版本下行為一致。</p>
<p>若同版容器在不同節點出現分歧行為，先追建置來源與 runtime config 版本鏈，確認是依賴漂移還是環境漂移，再把關鍵證據收斂到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。不直接支撐 service discovery TTL 或 queue replay 邏輯；若根因在定位鏈路或重播流程，應轉到 5.4 或 3.4。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 5.2 的交接：部署批次與探針策略回到 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes 部署策略</a>。</li>
<li>與 5.3 的交接：流量進出與連線收斂回到 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">load balancer 合約</a>。</li>
<li>與 5.6 的交接：startup / readiness / drain 的生命週期定義回到 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Platform Lifecycle Contract</a>。</li>
<li>與 4.20 的交接：啟動與資源證據回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>。</li>
<li>與 6.8 的交接：放行與回退條件回到 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a>。</li>
<li>與 7.3 的交接：image 安全基線與攻擊面回到 <a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">7.3 入口治理與伺服器防護</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 runtime 行為接到部署收斂，接著讀 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">5.2 Kubernetes 部署策略</a>。要看切流與退場條件，接著讀 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">5.3 load balancer 合約</a>。要看 runtime 層的生命週期如何被平台表達，接著讀 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">5.6 Platform Lifecycle Contract</a>。</p>
]]></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>Container 部署設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/container-deployment/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/container-deployment/</guid><description>&lt;p>Container 部署讓 collector 完全隔離於 host 環境，開源使用者用 &lt;code>docker run&lt;/code> 一行部署，不需要安裝 Go 或管理 binary 版本。但 SQLite 在 container 中有特殊的 I/O 和持久化考量 — overlay filesystem 的寫入延遲和 container 生命週期對資料持久性的影響需要在部署設計中處理。&lt;/p>
&lt;h2 id="dockerfile-設計">Dockerfile 設計&lt;/h2>
&lt;p>Multi-stage build 把編譯環境和執行環境分離。Build stage 用 Go 官方 image 編譯 binary，runtime stage 只包含 binary 和必要的 CA 憑證。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">FROM&lt;/span>&lt;span class="s"> golang:1.22-alpine AS build&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /src&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> go.mod go.sum ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> go mod download&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> . .&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> &lt;span class="nv">CGO_ENABLED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> go build -o /collector ./cmd/collector&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> alpine:3.20&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apk add --no-cache ca-certificates tzdata&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>build /collector /usr/local/bin/collector&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> adduser -D -u &lt;span class="m">1000&lt;/span> monitor&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> monitor&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">EXPOSE&lt;/span>&lt;span class="s"> 8080&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;collector&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最終 image 包含 Go binary（~15MB）+ alpine base（~7MB）+ ca-certificates，總大小目標 &amp;lt; 25MB。用 &lt;code>scratch&lt;/code> 替代 &lt;code>alpine&lt;/code> 可以再小 7MB，但失去 shell debug 能力。&lt;/p>
&lt;h2 id="sqlite-在-container-中的-io-考量">SQLite 在 Container 中的 I/O 考量&lt;/h2>
&lt;p>Docker 的 overlay2 storage driver 在每次 fsync 時經過 overlay 層。SQLite 的 WAL mode 依賴 fsync 確保寫入持久性 — 每筆 transaction commit 觸發一次 fsync。Overlay 層增加的延遲讓每筆 fsync 慢 20-40%（取決於 host 的 storage driver 和檔案系統）。&lt;/p>
&lt;h3 id="volume-mount-繞過-overlay">Volume mount 繞過 overlay&lt;/h3>
&lt;p>把 SQLite 的資料目錄掛載為 host volume（&lt;code>-v /host/data:/data&lt;/code>），SQLite 直接寫 host 檔案系統、繞過 overlay 層。寫入效能和同機部署的 binary 版本相當。&lt;/p>
&lt;p>不用 volume mount 的風險：container 刪除時 overlay 層的資料一起消失。&lt;code>docker rm&lt;/code> = 所有事件資料消失。即使只是 &lt;code>docker run&lt;/code> 新版本的 image 也會建立新 container，舊 container 的資料不會自動遷移。&lt;/p>
&lt;h2 id="volume-mount-設計">Volume Mount 設計&lt;/h2>
&lt;p>兩個目錄分開掛載，職責和權限不同：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Mount&lt;/th>
 &lt;th>Container 路徑&lt;/th>
 &lt;th>Host 路徑（範例）&lt;/th>
 &lt;th>權限&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>資料&lt;/td>
 &lt;td>&lt;code>/data&lt;/code>&lt;/td>
 &lt;td>&lt;code>./monitor-data&lt;/code>&lt;/td>
 &lt;td>read-write&lt;/td>
 &lt;td>SQLite DB + WAL + 匯出檔&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>設定&lt;/td>
 &lt;td>&lt;code>/config&lt;/code>&lt;/td>
 &lt;td>&lt;code>./monitor-config&lt;/code>&lt;/td>
 &lt;td>read-only&lt;/td>
 &lt;td>retention config + rule config + sensor config&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Container 內用非 root user（UID 1000）執行。Host 的 volume 目錄 ownership 需要對應：&lt;/p></description><content:encoded><![CDATA[<p>Container 部署讓 collector 完全隔離於 host 環境，開源使用者用 <code>docker run</code> 一行部署，不需要安裝 Go 或管理 binary 版本。但 SQLite 在 container 中有特殊的 I/O 和持久化考量 — overlay filesystem 的寫入延遲和 container 生命週期對資料持久性的影響需要在部署設計中處理。</p>
<h2 id="dockerfile-設計">Dockerfile 設計</h2>
<p>Multi-stage build 把編譯環境和執行環境分離。Build stage 用 Go 官方 image 編譯 binary，runtime stage 只包含 binary 和必要的 CA 憑證。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">FROM</span><span class="s"> golang:1.22-alpine AS build</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /src</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="err"></span><span class="k">COPY</span> go.mod go.sum ./<span class="err">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="err"></span><span class="k">RUN</span> go mod download<span class="err">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="err"></span><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="err"></span><span class="k">RUN</span> <span class="nv">CGO_ENABLED</span><span class="o">=</span><span class="m">0</span> go build -o /collector ./cmd/collector<span class="err">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="err"></span><span class="k">FROM</span><span class="s"> alpine:3.20</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="err"></span><span class="k">RUN</span> apk add --no-cache ca-certificates tzdata<span class="err">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="err"></span><span class="k">COPY</span> --from<span class="o">=</span>build /collector /usr/local/bin/collector<span class="err">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="err"></span><span class="k">RUN</span> adduser -D -u <span class="m">1000</span> monitor<span class="err">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="err"></span><span class="k">USER</span><span class="s"> monitor</span><span class="err">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8080</span><span class="err">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;collector&#34;</span><span class="p">]</span></span></span></code></pre></div><p>最終 image 包含 Go binary（~15MB）+ alpine base（~7MB）+ ca-certificates，總大小目標 &lt; 25MB。用 <code>scratch</code> 替代 <code>alpine</code> 可以再小 7MB，但失去 shell debug 能力。</p>
<h2 id="sqlite-在-container-中的-io-考量">SQLite 在 Container 中的 I/O 考量</h2>
<p>Docker 的 overlay2 storage driver 在每次 fsync 時經過 overlay 層。SQLite 的 WAL mode 依賴 fsync 確保寫入持久性 — 每筆 transaction commit 觸發一次 fsync。Overlay 層增加的延遲讓每筆 fsync 慢 20-40%（取決於 host 的 storage driver 和檔案系統）。</p>
<h3 id="volume-mount-繞過-overlay">Volume mount 繞過 overlay</h3>
<p>把 SQLite 的資料目錄掛載為 host volume（<code>-v /host/data:/data</code>），SQLite 直接寫 host 檔案系統、繞過 overlay 層。寫入效能和同機部署的 binary 版本相當。</p>
<p>不用 volume mount 的風險：container 刪除時 overlay 層的資料一起消失。<code>docker rm</code> = 所有事件資料消失。即使只是 <code>docker run</code> 新版本的 image 也會建立新 container，舊 container 的資料不會自動遷移。</p>
<h2 id="volume-mount-設計">Volume Mount 設計</h2>
<p>兩個目錄分開掛載，職責和權限不同：</p>
<table>
  <thead>
      <tr>
          <th>Mount</th>
          <th>Container 路徑</th>
          <th>Host 路徑（範例）</th>
          <th>權限</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料</td>
          <td><code>/data</code></td>
          <td><code>./monitor-data</code></td>
          <td>read-write</td>
          <td>SQLite DB + WAL + 匯出檔</td>
      </tr>
      <tr>
          <td>設定</td>
          <td><code>/config</code></td>
          <td><code>./monitor-config</code></td>
          <td>read-only</td>
          <td>retention config + rule config + sensor config</td>
      </tr>
  </tbody>
</table>
<p>Container 內用非 root user（UID 1000）執行。Host 的 volume 目錄 ownership 需要對應：</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">mkdir -p monitor-data monitor-config
</span></span><span class="line"><span class="ln">2</span><span class="cl">chown 1000:1000 monitor-data</span></span></code></pre></div><h2 id="graceful-shutdown">Graceful Shutdown</h2>
<p><code>docker stop</code> 送 SIGTERM → collector 收到後執行 shutdown 序列：</p>
<ol>
<li>停止接受新的 HTTP request（listener close）</li>
<li>等待 in-flight request 完成（5 秒 context timeout）</li>
<li>Flush pending writes（尚未寫入 storage 的事件，5 秒）</li>
<li>停止定期 job（downsample / purge / rule engine 定期評估）</li>
<li>SQLite WAL checkpoint（TRUNCATE mode，15 秒）</li>
<li>關閉 DB connection</li>
<li>退出</li>
</ol>
<p>步驟 2-5 合計超時上限 25 秒。這個序列對應 <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Backend 5.6 Platform Lifecycle Contract</a> 的 shutdown → drain 狀態：步驟 1-2 是 drain（停接新工作、等在途完成），步驟 3-6 是 shutdown（flush 狀態和釋放資源）。Collector 屬於短 request API 的 workload 類型（drain 窗口 5-30 秒），但多了 WAL checkpoint 步驟，讓 shutdown 時間可能超過一般 HTTP 服務。PID 1 信號處理的設計考量（exec form、避免 shell 攔截 SIGTERM）見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 PID 1 與信號處理</a>。</p>
<p><code>docker stop</code> 預設等 10 秒後送 SIGKILL。如果 WAL checkpoint 在大量未 checkpoint 的資料下需要超過 10 秒，Docker Compose 可以調 <code>stop_grace_period: 30s</code>。</p>
<p>SQLite 的 WAL 設計支援 crash recovery — SIGKILL 後 WAL 檔案仍在，下次開啟 DB 時自動 replay。但非 graceful shutdown 可能丟失 channel 中尚未寫入的事件（已收到 HTTP 202 但還在 buffer 中的事件）。</p>
<h2 id="資源限制">資源限制</h2>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>建議值（自用）</th>
          <th>建議值（小團隊）</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Memory</td>
          <td>256MB</td>
          <td>512MB</td>
          <td>Collector + SQLite page cache + Go runtime</td>
      </tr>
      <tr>
          <td>CPU</td>
          <td>0.5 核</td>
          <td>1 核</td>
          <td>I/O bound、CPU 通常不是瓶頸</td>
      </tr>
      <tr>
          <td>磁碟</td>
          <td>volume mount 容量</td>
          <td>volume mount 容量</td>
          <td>保留策略控制、和 host 磁碟共享</td>
      </tr>
  </tbody>
</table>
<p>Memory 限制設太緊會觸發 OOMKill — container 突然消失且無 log。設定 memory limit 前先觀察 collector 的 baseline 記憶體使用（<code>docker stats</code>），再乘以 1.5 安全係數。CPU request/limit 的設定策略（guaranteed vs burstable QoS）和 memory limit 與 OOM 的判讀見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 Resource Limit</a>。</p>
<h2 id="docker-compose-範例">Docker Compose 範例</h2>





<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="nt">services</span><span class="p">:</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">collector</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">image</span><span class="p">:</span><span class="w"> </span><span class="l">tarrragon/monitor:latest</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">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:8080&#34;</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">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="l">./monitor-data:/data</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">      </span>- <span class="l">./monitor-config:/config:ro</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">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span>- <span class="l">MONITOR_STORAGE=sqlite</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">      </span>- <span class="l">MONITOR_DB_PATH=/data/events.db</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">stop_grace_period</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="nt">deploy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span><span class="nt">resources</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">        </span><span class="nt">limits</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">          </span><span class="nt">memory</span><span class="p">:</span><span class="w"> </span><span class="l">256M</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">          </span><span class="nt">cpus</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;0.5&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;wget&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-q&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;--spider&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;http://localhost:8080/health&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">3</span></span></span></code></pre></div><p><code>restart: unless-stopped</code> 讓 container 在 crash 或 host 重啟後自動恢復。<code>healthcheck</code> 讓 Docker 偵測 collector 是否真的在回應 — 只有 process 活著但 HTTP 不回應的場景也會被標記為 unhealthy。</p>
<h2 id="和同機部署的效能對照">和同機部署的效能對照</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>同機 binary</th>
          <th>Container + volume mount</th>
          <th>Container 無 volume（overlay）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>寫入吞吐（Mac SSD）</td>
          <td>~5,000/sec</td>
          <td>~4,500/sec（-10%）</td>
          <td>~3,000/sec（-40%）</td>
      </tr>
      <tr>
          <td>寫入吞吐（Linux VPS）</td>
          <td>~3,000/sec</td>
          <td>~2,700/sec（-10%）</td>
          <td>~1,800/sec（-40%）</td>
      </tr>
      <tr>
          <td>查詢延遲</td>
          <td>baseline</td>
          <td>baseline（volume = 直接讀 host）</td>
          <td>+20%（overlay 讀取開銷小）</td>
      </tr>
      <tr>
          <td>啟動時間</td>
          <td>&lt; 100ms</td>
          <td>&lt; 500ms（container 啟動開銷）</td>
          <td>同左</td>
      </tr>
      <tr>
          <td>記憶體額外開銷</td>
          <td>0</td>
          <td>~10-20MB（container runtime）</td>
          <td>同左</td>
      </tr>
  </tbody>
</table>
<p>Volume mount 後效能差異只有 ~10%（Go HTTP handler 的 overhead 大於 volume mount 的 overhead）。不用 volume mount 時 overlay fs 的 fsync 開銷顯著 — 寫入吞吐降 40%。</p>
<h2 id="何時用-container何時用-binary">何時用 container、何時用 binary</h2>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>建議</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>開源使用者快速試用</td>
          <td>Container</td>
          <td><code>docker run</code> 一行、不需裝 Go</td>
      </tr>
      <tr>
          <td>長期自用部署</td>
          <td>Binary + systemd</td>
          <td>效能最佳、無 container overhead</td>
      </tr>
      <tr>
          <td>CI/CD 測試環境</td>
          <td>Container</td>
          <td>可拋棄式、每次乾淨環境</td>
      </tr>
      <tr>
          <td>Kubernetes 部署</td>
          <td>Container</td>
          <td>pod spec 標準化</td>
      </tr>
      <tr>
          <td>Raspberry Pi / 邊緣設備</td>
          <td>Binary</td>
          <td>低資源環境避免 container overhead</td>
      </tr>
  </tbody>
</table>
<h2 id="斷網環境的部署考量">斷網環境的部署考量</h2>
<p>Collector 在斷網環境（air-gapped）裡的部署跟連網環境的主要差異有三點。第一，SDK 的 endpoint 從外部 URL（<code>https://collect.example.com</code>）改為內網地址（<code>http://collector.internal:8080</code>），SDK 設定檔裡的 endpoint 要能按環境切換。第二，Collector 的 container image 無法從 Docker Hub 拉取——需要透過 content ferry 搬運映像、推送到內網的 private registry（Harbor 或 Docker Registry），Dockerfile 的 base image 來源也要改指 private registry。第三，Collector 的 storage backend 只能用本地磁碟或 NFS，不能用雲端物件儲存——SQLite backend 在斷網環境反而是優勢（零外部依賴），儲存容量規劃要在部署前就確定，因為斷網環境的磁碟擴容流程可能需要數週。</p>
<p>SDK 的 offline buffer（見<a href="/blog/monitoring/03-sdk-design/offline-buffer/" data-link-title="離線 buffer 與重試" data-link-desc="網路不可用時的事件保存策略 — FIFO 丟棄、本地 persistence、恢復後補發的取捨">SDK 設計：offline-buffer</a>）在斷網環境更重要——如果 Collector 重啟或暫時不可達，SDK 端的 buffer 是唯一能保住事件的機制。</p>
<p>斷網環境的 infra 層監控（Prometheus / Grafana / Loki）設定見<a href="/blog/infra/air-gapped/air-gapped-monitoring/" data-link-title="斷網環境的監控與可觀測性" data-link-desc="Self-hosted 監控（Prometheus &#43; Grafana）、離線 log 收集（Loki / ELK）、不能 phone home 的告警、NTP 時間同步">斷網環境的監控與可觀測性</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>SQLite 效能基準的詳細數字 → <a href="/blog/monitoring/04-collector/sqlite-performance-baseline/" data-link-title="SQLite Backend 效能基準" data-link-desc="寫入吞吐 / 查詢延遲 / 資源消耗的量化預期 — 不同硬體環境下 SQLite 能撐多少、邊界在哪、怎麼實測">SQLite Backend 效能基準</a></li>
<li>可插拔 Storage Backend 架構 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>Container runtime 通用原則（base image 選擇、build 可重現性、PID 1 信號處理）→ <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">Backend 5.1 Container 與 Runtime</a></li>
<li>生命週期合約（startup / readiness / drain / shutdown 的責任分類）→ <a href="/blog/backend/05-deployment-platform/platform-lifecycle-contract/" data-link-title="5.6 Platform Lifecycle Contract" data-link-desc="說明 runtime、startup、readiness、liveness、shutdown 與 drain 如何組成平台生命週期合約。">Backend 5.6 Platform Lifecycle Contract</a></li>
<li>容器化資源設計的通用原則 → <a href="/blog/devops/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="Container 的 memory / CPU / 磁碟限制設計 — 資源限制設太緊 OOMKill、設太鬆擠壓其他服務、overlay filesystem 的 I/O 影響">DevOps 容器化資源設計</a></li>
<li>服務探活和自動恢復 → <a href="/blog/devops/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">DevOps 服務探活</a></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><item><title>Docker / Image 部署 CI/CD</title><link>https://tarrragon.github.io/blog/ci/docker-deploy/</link><pubDate>Wed, 06 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/docker-deploy/</guid><description>&lt;p>Docker / image 部署 CI/CD 的核心責任是把可執行環境封裝成可追溯的 image。Image 同時承載 application、runtime、OS package、dependency 與安全掃描結果，因此它是可以被推進、掃描與回溯的部署產物；而 &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;/p>
&lt;h2 id="場域定位">場域定位&lt;/h2>
&lt;p>Image 部署常出現在後端、worker、batch job 與自架服務。它把「在哪個環境跑」前移到 build 階段，但也引入 registry、tag、base image、vulnerability 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> 與 promotion 流程（platform 概念可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container&lt;/a>）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>Image 部署常見責任&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Build&lt;/td>
 &lt;td>Dockerfile、multi-stage build&lt;/td>
 &lt;td>image 是否可重現、layer 是否合理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tag&lt;/td>
 &lt;td>semver、commit SHA、release tag&lt;/td>
 &lt;td>tag 是否能追到 source&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Scan&lt;/td>
 &lt;td>vulnerability、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>Registry&lt;/td>
 &lt;td>push、retention、promotion&lt;/td>
 &lt;td>prod image 是否來自已驗證 artifact&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Runtime&lt;/td>
 &lt;td>Kubernetes、Compose、ECS 等&lt;/td>
 &lt;td>health、readiness、rollback 是否存在&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Build 階段負責把 application 與 runtime 封裝成 image。Multi-stage build、dependency cache、base image 與 layer 順序會影響速度、安全性與可重現性；CI 應能從 Dockerfile 與 lockfile 重建同一類產物。&lt;/p>
&lt;p>Tag 階段負責讓 image 可追溯。Commit SHA、release tag 與 semver 各自服務不同查詢情境；production 需要能從 running image 反查 source、workflow run 與掃描結果。&lt;/p>
&lt;p>Scan 階段負責讓 image 風險可見。Vulnerability scan、secret scan 與 SBOM 能把 base image、OS package 與 dependency 風險顯性化；阻擋門檻要和例外流程一起定義，讓掃描結果能被分流處理。&lt;/p>
&lt;p>Registry 階段負責保存與推進 image。真實流程通常需要 retention、immutability、promotion 與權限控管；production image 應來自已驗證 &lt;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff&lt;/a>，讓各環境推進同一份產物（供應鏈治理可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance&lt;/a>）。&lt;/p>
&lt;p>Runtime 階段負責把 image 轉成可運行服務。Kubernetes、Compose、ECS 或其他平台都需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit&lt;/a>、secret injection（可對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management&lt;/a>）與 rollback 設計，否則 image 成功不等於服務可用。&lt;/p>
&lt;h2 id="常見注意事項">常見注意事項&lt;/h2>
&lt;ul>
&lt;li>&lt;code>latest&lt;/code> 不適合當 production 追溯依據。&lt;/li>
&lt;li>Base image 要有更新節奏，否則掃描結果會持續惡化。&lt;/li>
&lt;li>Build secret 不應留在 image layer。&lt;/li>
&lt;li>Scan gate 要區分阻擋門檻與可接受例外。&lt;/li>
&lt;li>Promotion 應推進同一份 image，讓 staging 與 production 的差異集中在設定與流量。&lt;/li>
&lt;/ul>
&lt;h2 id="學習路線">學習路線&lt;/h2>
&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>&lt;a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程&lt;/a>&lt;/td>
 &lt;td>Image supply chain&lt;/td>
 &lt;td>建立可追溯 tag、掃描 gate 與 registry 推進&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Image 供應鏈流程：讀 &lt;a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程&lt;/a>。&lt;/li>
&lt;li>後端部署：讀 &lt;a href="../backend-deploy/">後端部署 CI/CD&lt;/a>。&lt;/li>
&lt;li>Gate 原理：讀 &lt;a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界&lt;/a>。&lt;/li>
&lt;li>Backend deployment platform：讀 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口&lt;/a>。&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>Docker / image 部署 CI/CD 的核心責任是把可執行環境封裝成可追溯的 image。Image 同時承載 application、runtime、OS package、dependency 與安全掃描結果，因此它是可以被推進、掃描與回溯的部署產物；而 <a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a> 提供保存與推進的供應鏈節點。</p>
<h2 id="場域定位">場域定位</h2>
<p>Image 部署常出現在後端、worker、batch job 與自架服務。它把「在哪個環境跑」前移到 build 階段，但也引入 registry、tag、base image、vulnerability scan、<a href="/blog/ci/knowledge-cards/sbom/" data-link-title="SBOM" data-link-desc="說明 Software Bill of Materials 如何揭露 artifact 內含元件，支撐供應鏈掃描與例外治理">SBOM</a> 與 promotion 流程（platform 概念可對照 <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container</a>）。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Image 部署常見責任</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Build</td>
          <td>Dockerfile、multi-stage build</td>
          <td>image 是否可重現、layer 是否合理</td>
      </tr>
      <tr>
          <td>Tag</td>
          <td>semver、commit SHA、release tag</td>
          <td>tag 是否能追到 source</td>
      </tr>
      <tr>
          <td>Scan</td>
          <td>vulnerability、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>Registry</td>
          <td>push、retention、promotion</td>
          <td>prod image 是否來自已驗證 artifact</td>
      </tr>
      <tr>
          <td>Runtime</td>
          <td>Kubernetes、Compose、ECS 等</td>
          <td>health、readiness、rollback 是否存在</td>
      </tr>
  </tbody>
</table>
<p>Build 階段負責把 application 與 runtime 封裝成 image。Multi-stage build、dependency cache、base image 與 layer 順序會影響速度、安全性與可重現性；CI 應能從 Dockerfile 與 lockfile 重建同一類產物。</p>
<p>Tag 階段負責讓 image 可追溯。Commit SHA、release tag 與 semver 各自服務不同查詢情境；production 需要能從 running image 反查 source、workflow run 與掃描結果。</p>
<p>Scan 階段負責讓 image 風險可見。Vulnerability scan、secret scan 與 SBOM 能把 base image、OS package 與 dependency 風險顯性化；阻擋門檻要和例外流程一起定義，讓掃描結果能被分流處理。</p>
<p>Registry 階段負責保存與推進 image。真實流程通常需要 retention、immutability、promotion 與權限控管；production image 應來自已驗證 <a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">artifact handoff</a>，讓各環境推進同一份產物（供應鏈治理可對照 <a href="/blog/backend/knowledge-cards/artifact-provenance/" data-link-title="Artifact Provenance" data-link-desc="說明交付物的來源、完整性與簽章關聯如何建立信任">Artifact Provenance</a>）。</p>
<p>Runtime 階段負責把 image 轉成可運行服務。Kubernetes、Compose、ECS 或其他平台都需要 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">health check</a>、<a href="/blog/backend/knowledge-cards/readiness/" data-link-title="Readiness" data-link-desc="說明 instance 何時可以安全接收流量，以及 readiness 如何和部署平台協作">readiness</a>、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a>、secret injection（可對照 <a href="/blog/backend/knowledge-cards/secret-management/" data-link-title="Secret Management" data-link-desc="說明 token、key、password 與憑證如何保存、輪替與撤銷">Secret Management</a>）與 rollback 設計，否則 image 成功不等於服務可用。</p>
<h2 id="常見注意事項">常見注意事項</h2>
<ul>
<li><code>latest</code> 不適合當 production 追溯依據。</li>
<li>Base image 要有更新節奏，否則掃描結果會持續惡化。</li>
<li>Build secret 不應留在 image layer。</li>
<li>Scan gate 要區分阻擋門檻與可接受例外。</li>
<li>Promotion 應推進同一份 image，讓 staging 與 production 的差異集中在設定與流量。</li>
</ul>
<h2 id="學習路線">學習路線</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程</a></td>
          <td>Image supply chain</td>
          <td>建立可追溯 tag、掃描 gate 與 registry 推進</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Image 供應鏈流程：讀 <a href="image-supply-chain-flow/">Image build、scan、registry 與 promotion 流程</a>。</li>
<li>後端部署：讀 <a href="../backend-deploy/">後端部署 CI/CD</a>。</li>
<li>Gate 原理：讀 <a href="../ci-gate-workflow-boundary/">CI gate 與 workflow 邊界</a>。</li>
<li>Backend deployment platform：讀 <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">模組五：部署平台與網路入口</a>。</li>
</ul>
]]></content:encoded></item><item><title>Image Digest</title><link>https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ci/knowledge-cards/image-digest/</guid><description>&lt;p>Image Digest 的核心概念是「用內容雜湊識別不可變 image」。它補足 &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;a href="https://tarrragon.github.io/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff&lt;/a> 可以鎖定精準產物。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Image Digest 位在 image build、scan、registry promotion 與 runtime deploy 之間，通常以 &lt;code>sha256:...&lt;/code> 形式標識 image manifest 或 image index。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;ul>
&lt;li>&lt;code>latest&lt;/code> 或 mutable tag 造成 staging 與 production 內容分叉。&lt;/li>
&lt;li>production runtime 需要反查實際跑的 image。&lt;/li>
&lt;li>掃描結果需要和部署內容精準對齊。&lt;/li>
&lt;/ul>
&lt;h2 id="接近真實服務的例子">接近真實服務的例子&lt;/h2>
&lt;p>CI build image 後推到 registry，scan 報告綁定 digest。Kubernetes manifest 在 production 使用同一個 digest，事故時可從 running pod 反查 workflow run 與 source commit。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Image Digest 要納入 deployment manifest、scan report、release note 與 rollback 記錄，讓 image 發布具備可追溯與可審計能力。&lt;/p></description><content:encoded><![CDATA[<p>Image Digest 的核心概念是「用內容雜湊識別不可變 image」。它補足 <a href="/blog/ci/knowledge-cards/container-registry/" data-link-title="Container Registry" data-link-desc="說明容器產物儲存、權限與推進流程在 CD 中的責任">Container Registry</a> 的命名治理，讓 <a href="/blog/ci/knowledge-cards/artifact-handoff/" data-link-title="Artifact Handoff" data-link-desc="說明測試與部署如何共用同一份可追溯產物">Artifact Handoff</a> 可以鎖定精準產物。</p>
<h2 id="概念位置">概念位置</h2>
<p>Image Digest 位在 image build、scan、registry promotion 與 runtime deploy 之間，通常以 <code>sha256:...</code> 形式標識 image manifest 或 image index。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<ul>
<li><code>latest</code> 或 mutable tag 造成 staging 與 production 內容分叉。</li>
<li>production runtime 需要反查實際跑的 image。</li>
<li>掃描結果需要和部署內容精準對齊。</li>
</ul>
<h2 id="接近真實服務的例子">接近真實服務的例子</h2>
<p>CI build image 後推到 registry，scan 報告綁定 digest。Kubernetes manifest 在 production 使用同一個 digest，事故時可從 running pod 反查 workflow run 與 source commit。</p>
<h2 id="設計責任">設計責任</h2>
<p>Image Digest 要納入 deployment manifest、scan report、release note 與 rollback 記錄，讓 image 發布具備可追溯與可審計能力。</p>
]]></content:encoded></item><item><title>Container</title><link>https://tarrragon.github.io/blog/backend/knowledge-cards/container/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/knowledge-cards/container/</guid><description>&lt;p>Container 的核心概念是「把應用程式與執行環境封裝成可交付單位」。它通常承載 application binary、runtime 依賴、config 與啟動命令。 可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Container 位在 build、deploy、runtime 與 platform 之間，是服務交付與資源限制的基本單位。 可先對照 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit&lt;/a>。&lt;/p>
&lt;h2 id="可觀察訊號">可觀察訊號&lt;/h2>
&lt;p>系統需要 container 化的訊號是服務需要一致的啟動方式、相同的 runtime 環境、可複製的部署流程，或多個 instance 要共用同一套交付模型。&lt;/p>
&lt;h2 id="接近真實網路服務的例子">接近真實網路服務的例子&lt;/h2>
&lt;p>當服務要被放進 Kubernetes、CI pipeline 或 VM 上的標準化部署流程時，container 可以把 binary、system dependency 與啟動參數打包成固定形狀，降低環境差異。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>設計時要定義 image 內容、啟動命令、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit&lt;/a> 與環境變數來源。Container 是讓服務交付、擴容與回滾更一致的手段，目的在服務層面。&lt;/p></description><content:encoded><![CDATA[<p>Container 的核心概念是「把應用程式與執行環境封裝成可交付單位」。它通常承載 application binary、runtime 依賴、config 與啟動命令。 可先對照 <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>Container 位在 build、deploy、runtime 與 platform 之間，是服務交付與資源限制的基本單位。 可先對照 <a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a>。</p>
<h2 id="可觀察訊號">可觀察訊號</h2>
<p>系統需要 container 化的訊號是服務需要一致的啟動方式、相同的 runtime 環境、可複製的部署流程，或多個 instance 要共用同一套交付模型。</p>
<h2 id="接近真實網路服務的例子">接近真實網路服務的例子</h2>
<p>當服務要被放進 Kubernetes、CI pipeline 或 VM 上的標準化部署流程時，container 可以把 binary、system dependency 與啟動參數打包成固定形狀，降低環境差異。</p>
<h2 id="設計責任">設計責任</h2>
<p>設計時要定義 image 內容、啟動命令、<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">resource limit</a> 與環境變數來源。Container 是讓服務交付、擴容與回滾更一致的手段，目的在服務層面。</p>
]]></content:encoded></item></channel></rss>