<?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>Docker on Tarragon</title><link>https://tarrragon.github.io/blog/tags/docker/</link><description>Recent content in Docker on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/docker/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>CI 中的服務 fixture 管理</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/service-fixture-management/</guid><description>&lt;p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。&lt;/p>
&lt;h2 id="三種服務管理方案">三種服務管理方案&lt;/h2>
&lt;h3 id="processstart直接啟動程序">Process.start（直接啟動程序）&lt;/h3>
&lt;p>在 test 的 setUp 中用 &lt;code>Process.start&lt;/code> 啟動服務程序，tearDown 中用 &lt;code>process.kill&lt;/code> 停止。&lt;/p>
&lt;p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&amp;lt; 2 秒），不需要持久化狀態。&lt;/p>
&lt;p>app_tunnel 的 ttyd 就是這個模式。&lt;code>ttyd bash&lt;/code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：&lt;/p>





&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">setUp: process = Process.start(&amp;#39;ttyd&amp;#39;, [&amp;#39;--port&amp;#39;, &amp;#39;7681&amp;#39;, &amp;#39;bash&amp;#39;])
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> await waitForPort(7681, timeout: 3s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">tearDown: process.kill()&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="docker-compose">Docker Compose&lt;/h3>
&lt;p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 &lt;code>docker compose up&lt;/code>，after_all 階段 &lt;code>docker compose down&lt;/code>。&lt;/p>
&lt;p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。&lt;/p>
&lt;p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。&lt;/p>
&lt;h3 id="testcontainers">Testcontainers&lt;/h3>
&lt;p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。&lt;/p>
&lt;p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。&lt;/p>
&lt;p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。&lt;/p>
&lt;h2 id="健康檢查">健康檢查&lt;/h2>
&lt;p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。&lt;/p>
&lt;p>健康檢查的方式依服務類型而定：&lt;/p>
&lt;p>&lt;strong>TCP port 可達&lt;/strong>：&lt;code>waitForPort(port, timeout)&lt;/code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。&lt;/p>
&lt;p>&lt;strong>HTTP health endpoint&lt;/strong>：對 &lt;code>/health&lt;/code> 或 &lt;code>/ready&lt;/code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。&lt;/p>
&lt;p>&lt;strong>特定操作成功&lt;/strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。&lt;/p>
&lt;h2 id="服務狀態隔離">服務狀態隔離&lt;/h2>
&lt;p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。&lt;/p>
&lt;p>三種隔離策略：&lt;/p>
&lt;p>&lt;strong>每 test 重啟服務&lt;/strong>：最強隔離，最慢。適合服務啟動快（&amp;lt; 1 秒）的場景。&lt;/p>
&lt;p>&lt;strong>每 test 重設狀態&lt;/strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。&lt;/p>
&lt;p>&lt;strong>每 test 用獨立 namespace&lt;/strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。&lt;/p>
&lt;p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。&lt;/p></description><content:encoded><![CDATA[<p>Protocol integration test 需要真實的外部服務實例。在 CI 中管理這些服務實例的啟動、初始化、健康檢查和停止，是 protocol integration test 基礎設施的核心問題。</p>
<h2 id="三種服務管理方案">三種服務管理方案</h2>
<h3 id="processstart直接啟動程序">Process.start（直接啟動程序）</h3>
<p>在 test 的 setUp 中用 <code>Process.start</code> 啟動服務程序，tearDown 中用 <code>process.kill</code> 停止。</p>
<p>適合的前提：服務是單一二進位檔（不需要 Docker），啟動速度快（&lt; 2 秒），不需要持久化狀態。</p>
<p>app_tunnel 的 ttyd 就是這個模式。<code>ttyd bash</code> 一行指令啟動，不需要設定檔，不需要資料庫，啟動到可接受連線約 500ms。Test harness 只需要：</p>





<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">setUp: process = Process.start(&#39;ttyd&#39;, [&#39;--port&#39;, &#39;7681&#39;, &#39;bash&#39;])
</span></span><span class="line"><span class="ln">2</span><span class="cl">       await waitForPort(7681, timeout: 3s)
</span></span><span class="line"><span class="ln">3</span><span class="cl">tearDown: process.kill()</span></span></code></pre></div><h3 id="docker-compose">Docker Compose</h3>
<p>用 Docker Compose 定義服務堆疊，CI 的 before_all 階段 <code>docker compose up</code>，after_all 階段 <code>docker compose down</code>。</p>
<p>適合的前提：服務有依賴（database + cache + app server）、需要特定 OS 環境、需要精確的版本控制。</p>
<p>Docker Compose 的成本是 image pull 時間（首次或 image 更新時）和容器啟動時間。CI 中可以用 image cache 減少 pull 時間，但冷啟動仍比直接啟動程序慢。</p>
<h3 id="testcontainers">Testcontainers</h3>
<p>在 test 程式碼中用 testcontainers 套件管理 Docker 容器。每個 test class 或 test suite 啟動自己的容器，test 結束後自動清理。</p>
<p>適合的前提：和 Docker Compose 類似，但需要更細粒度的控制（不同 test 用不同的服務設定），或需要在 test 程式碼中動態決定服務的啟動參數。</p>
<p>Testcontainers 的優勢是 test 和 fixture 在同一個程式碼檔案中，容易理解每個 test 需要什麼環境。缺點是每個 test suite 啟動自己的容器，比共用容器慢。</p>
<h2 id="健康檢查">健康檢查</h2>
<p>服務啟動後到可以接受請求之間有延遲。直接在啟動後發送 test request 會因為服務尚未 ready 而失敗。</p>
<p>健康檢查的方式依服務類型而定：</p>
<p><strong>TCP port 可達</strong>：<code>waitForPort(port, timeout)</code> 反覆嘗試 TCP 連線，成功即表示服務在監聽。最簡單，適合所有 TCP 服務。</p>
<p><strong>HTTP health endpoint</strong>：對 <code>/health</code> 或 <code>/ready</code> 發送 GET request，收到 200 表示服務 ready。比 port check 更可靠 — port 監聽不代表應用層 ready。</p>
<p><strong>特定操作成功</strong>：執行一個輕量的業務操作（例如 WebSocket 連線 + 簡單指令），成功表示服務完全 ready。最可靠但最慢。</p>
<h2 id="服務狀態隔離">服務狀態隔離</h2>
<p>不同 test 之間的服務狀態需要隔離 — test A 在服務中建立的資料不應該影響 test B。</p>
<p>三種隔離策略：</p>
<p><strong>每 test 重啟服務</strong>：最強隔離，最慢。適合服務啟動快（&lt; 1 秒）的場景。</p>
<p><strong>每 test 重設狀態</strong>：服務持續運行，test 開始前清理狀態（truncate tables, flush cache）。適合服務啟動慢但重設快的場景。</p>
<p><strong>每 test 用獨立 namespace</strong>：服務持續運行，每個 test 使用獨立的 database schema / topic / channel。適合支援多租戶的服務。</p>
<p>app_tunnel 的 ttyd 是無狀態服務（每次連線是獨立的 terminal session），不需要狀態隔離。每個 test 建立新的 WebSocket 連線 = 新的 session。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>什麼時候值得建 protocol integration test 基礎設施 → <a href="/blog/testing/03-protocol-integration-test/cost-judgment/" data-link-title="成本判斷表" data-link-desc="什麼時候值得寫 protocol integration test、什麼時候用 contract test 或實機測試替代 — 根據服務啟動成本和協議複雜度判斷">成本判斷表</a></li>
<li>Protocol integration test 的定義 → <a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">Protocol integration test 定義</a></li>
<li>WebSocket 的 protocol test 實作 → <a href="/blog/testing/03-protocol-integration-test/websocket-protocol-test/" data-link-title="WebSocket 協議測試實作" data-link-desc="對真實 ttyd 驗證 frame type 和 auth handshake — 從 T.C1 和 T.C2 的教訓推導出的 protocol integration test 設計">WebSocket 協議測試實作</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>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>驗證導向的 CLI 工具文章：官方 docs 查核放過的落差類型</title><link>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</link><pubDate>Mon, 15 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/posts/%E9%A9%97%E8%AD%89%E5%B0%8E%E5%90%91%E7%9A%84-cli-%E5%B7%A5%E5%85%B7%E6%96%87%E7%AB%A0%E5%AE%98%E6%96%B9-docs-%E6%9F%A5%E6%A0%B8%E6%94%BE%E9%81%8E%E7%9A%84%E8%90%BD%E5%B7%AE%E9%A1%9E%E5%9E%8B/</guid><description>&lt;p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 &lt;code>.claude/skills/verification-driven-cli/&lt;/code>。&lt;/p>
&lt;h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差&lt;/h2>
&lt;p>&lt;code>content/cli/&lt;/code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：&lt;/p>
&lt;h3 id="1-旗標改名">1. 旗標改名&lt;/h3>
&lt;p>&lt;code>zellij web&lt;/code> 文件寫有 &lt;code>--bind&lt;/code>，實際 0.43.1 是分開的 &lt;code>--ip&lt;/code> 與 &lt;code>--port&lt;/code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。&lt;/p>
&lt;h3 id="2-設定鍵-migrate">2. 設定鍵 migrate&lt;/h3>
&lt;p>&lt;code>lazygit&lt;/code> 的 pager 設定文件寫 &lt;code>git.paging.pager&lt;/code>，新版 0.62.2 改成 &lt;code>git.pagers&lt;/code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。&lt;/p>
&lt;h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix&lt;/h3>
&lt;p>&lt;code>dblab&lt;/code> 的查詢編輯器要 schema 限定（&lt;code>SELECT * FROM public.products&lt;/code>），裸 &lt;code>products&lt;/code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。&lt;/p>
&lt;h3 id="4-平台特定-segfault">4. 平台特定 segfault&lt;/h3>
&lt;p>&lt;code>nvtop&lt;/code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。&lt;/p>
&lt;h3 id="5-driver-差異">5. Driver 差異&lt;/h3>
&lt;p>同一個 Postgres，&lt;code>lazysql&lt;/code>（Go pq driver）連無 SSL 的 DB 要 &lt;code>?sslmode=disable&lt;/code>，&lt;code>pgcli&lt;/code> / &lt;code>harlequin&lt;/code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。&lt;/p>
&lt;h2 id="共通模式">共通模式&lt;/h2>
&lt;p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。&lt;/p>
&lt;p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。&lt;/p>
&lt;h2 id="相關連結">相關連結&lt;/h2>
&lt;ul>
&lt;li>Verification-driven CLI skill（&lt;code>.claude/skills/verification-driven-cli/&lt;/code>）— 操作步驟&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽&lt;/a> — 用此流程生產的工具文章&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能&lt;/a> — 本文精簡的依據&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>本文記錄驗證導向生產流程背後的 evidence — 為什麼官方文件查核不夠、實機驗證抓到了什麼。操作步驟維護在 <code>.claude/skills/verification-driven-cli/</code>。</p>
<h2 id="官方文件查核放過的五類落差">官方文件查核放過的五類落差</h2>
<p><code>content/cli/</code> 五類終端機工具文章（監控 / 圖表 / 多工器 / 檔案管理 / SQL 客戶端）在實機驗證時抓到、純靠 docs 查核會放過的落差：</p>
<h3 id="1-旗標改名">1. 旗標改名</h3>
<p><code>zellij web</code> 文件寫有 <code>--bind</code>，實際 0.43.1 是分開的 <code>--ip</code> 與 <code>--port</code>。讀者照文件下指令會得到 unknown flag error、但不知道正確旗標是什麼。</p>
<h3 id="2-設定鍵-migrate">2. 設定鍵 migrate</h3>
<p><code>lazygit</code> 的 pager 設定文件寫 <code>git.paging.pager</code>，新版 0.62.2 改成 <code>git.pagers</code>（list）。舊鍵啟動時會被自動 migrate、改寫設定檔 — 讀者照舊文件設定後發現設定檔被工具自己改掉。</p>
<h3 id="3-隱含-schema-prefix">3. 隱含 schema prefix</h3>
<p><code>dblab</code> 的查詢編輯器要 schema 限定（<code>SELECT * FROM public.products</code>），裸 <code>products</code> 會報 relation 不存在。原因是編輯器連線的 search_path 不含 public — 文件沒提。</p>
<h3 id="4-平台特定-segfault">4. 平台特定 segfault</h3>
<p><code>nvtop</code> 在 Apple Silicon mac 裝得起來，但 snapshot 模式直接 segfault。GPU 後端不穩。裝成功不代表能用 — 文件只說「支援 macOS」。</p>
<h3 id="5-driver-差異">5. Driver 差異</h3>
<p>同一個 Postgres，<code>lazysql</code>（Go pq driver）連無 SSL 的 DB 要 <code>?sslmode=disable</code>，<code>pgcli</code> / <code>harlequin</code>（Python psycopg）不用。同樣的連線字串在不同工具會有不同行為、文件各自不提對方。</p>
<h2 id="共通模式">共通模式</h2>
<p>這五類落差有個共通點：讀者照文件走會撞牆、卻在文件裡找不到答案。實機跑一次就現形，而且現形的正是文章最該寫的內容 — gotcha 段落省下讀者各自撞一次的時間。</p>
<p>官方文件的 fact-check 只能驗證「文件說的是否正確」，驗不了「文件沒說的是否存在」。實機驗證補的是後者。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Verification-driven CLI skill（<code>.claude/skills/verification-driven-cli/</code>）— 操作步驟</li>
<li><a href="/blog/linux/tools/cli/cli-graphical-tools-overview/" data-link-title="終端機圖形化工具總覽：遠端操作下的 TUI、文字圖表與多工器" data-link-desc="在純文字終端機裡用 ASCII 與製圖字元做出監控儀表板、資料圖表與多視窗操作的工具總覽，並針對 SSH 伺服器、手機平板、低頻寬三種遠端情境給出選型判讀。">終端機圖形化工具總覽</a> — 用此流程生產的工具文章</li>
<li><a href="/blog/report/single-function-per-article-sop-vs-retrospective/" data-link-title="一篇文章只承擔一種功能：SOP 跟 retrospective 混寫兩邊都做不好" data-link-desc="文章同時塞操作步驟（SOP）和批次驗證紀錄（retrospective）時，機器讀者找不到可執行的步驟、人類讀者不知道哪段是給自己看的。">#199 一篇文章只承擔一種功能</a> — 本文精簡的依據</li>
</ul>
]]></content:encoded></item></channel></rss>