<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>NCSE Network</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://cdn-file.ncse.tw/image/Logo_NCSE_Network_Wing.svg</icon>
  <id>https://blog.ncse.tw/</id>
  <link href="https://blog.ncse.tw/" rel="alternate"/>
  <link href="https://blog.ncse.tw/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, NCSE Network</rights>
  <subtitle>雲端技術實戰知識庫</subtitle>
  <title>NCSE Network Blog</title>
  <updated>2026-06-13T08:06:37.111Z</updated>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Apache Doris" scheme="https://blog.ncse.tw/tags/Apache-Doris/"/>
    <category term="ClickHouse" scheme="https://blog.ncse.tw/tags/ClickHouse/"/>
    <category term="OLAP" scheme="https://blog.ncse.tw/tags/OLAP/"/>
    <category term="即時分析" scheme="https://blog.ncse.tw/tags/%E5%8D%B3%E6%99%82%E5%88%86%E6%9E%90/"/>
    <content>
      <![CDATA[<p>Apache Doris 在 2026 年 6 月剛發布 3.0.6 版，GitHub 累積接近一萬五千顆星。它把自己定位成「即時資料倉庫」，對外用 MySQL wire protocol、底層是 MPP 架構的 columnar engine——這個在 ClickHouse 跟 Snowflake 之間長期空缺的位置，這兩年慢慢被它跟 StarRocks 填了起來。</p><p>ClickHouse 過去幾年是自架 OLAP 的預設選項，但它有兩個會在實戰打到的限制。一個是 update 跟 delete 走 ReplacingMergeTree、CollapsingMergeTree 那條路，本質上是 eventual consistency，對於需要訂單狀態、使用者餘額即時刷新的場景並不友善。另一個是它的 SQL 方言跟 client driver 跟 MySQL 不相容，現有的 BI 工具、ORM、舊系統要全部換 connector。Doris 把這兩個痛點一次處理掉，這也是它在 2025 到 2026 年被點名的原因。</p><h2 id="update-寫不順是-ClickHouse-在實戰會打到的天花板"><a href="#update-寫不順是-ClickHouse-在實戰會打到的天花板" class="headerlink" title="update 寫不順是 ClickHouse 在實戰會打到的天花板"></a>update 寫不順是 ClickHouse 在實戰會打到的天花板</h2><p>ClickHouse 為了寫入吞吐犧牲了 update 跟 delete 的即時性。它的 MergeTree 本質是 append-only 設計，所謂的 update 其實是再寫一筆新版本進去，等下一次 merge 才會真的覆蓋舊資料。寫入吞吐被推到極致的代價，是任何需要「立刻讀到最新值」的場景都得繞——往往要在應用層加一層快取，或在查詢時自行加 FINAL 關鍵字付出更高的計算成本。</p><p>對於 log analytics、event tracking 這類 append-heavy 工作負載，這個 trade-off 很值得。但只要場景跨進電商訂單、SaaS 計量、IoT 設備狀態，事情就不一樣——這類資料天生就是 mutable 的，每分鐘可能有幾千筆 update 而不是 insert，ClickHouse 在這條路上就會卡住。</p><p>Apache Doris 的 Unique Key 模型直接針對這件事設計。它有一個 Merge-on-Write 模式，寫入當下就完成主鍵衝突的處理，之後的查詢直接讀最新版本而不需要 FINAL。官方在 ClickBench 與 SSB 的比對裡，update-intensive 工作負載上 Doris 比 ClickHouse 快了 18 到 34 倍，差距足以讓原本得在應用層自行包覆 dedup 邏輯的團隊把那層拿掉。</p><h2 id="MySQL-協議讓-OLAP-接回原本的客戶端"><a href="#MySQL-協議讓-OLAP-接回原本的客戶端" class="headerlink" title="MySQL 協議讓 OLAP 接回原本的客戶端"></a>MySQL 協議讓 OLAP 接回原本的客戶端</h2><p>這是 Doris 跟 ClickHouse、StarRocks 最不同的設計決定。它在 wire protocol 層面相容 MySQL——也就是說，任何能連 MySQL 的 driver、ORM、BI 工具，理論上都能直接連 Doris 而不需要換 connector。Tableau、Metabase、Superset、PowerBI 的 MySQL connector、PHP 的 PDO、Python 的 PyMySQL、Java 的 mysql-connector-j 全部都不需要改寫。</p><p>這件事的實際意義要放回團隊現實情境才看得出來。典型場景是公司本來把所有業務資料放在 MySQL 上，BI 工具直接接 read replica；做大之後 replica 開始追不上 OLAP query 的壓力，工程師會評估上 ClickHouse。但這時候會碰到三件事：所有的 dashboard query 要重寫成 ClickHouse 方言、BI 工具要重接 connector、ORM 那層要動。</p><p>換成 Doris，第一個動作就是把 read replica 換掉，BI 那邊感覺不到差別，只覺得 query 突然從 30 秒變成 200 毫秒。這對不想把整套技術棧大改一次的團隊是很關鍵的賣點。當然 Doris 並不是 100% MySQL 相容——沒有 unsigned 整數、沒有 BIT、沒有 BLOB，欄位定義也得指定 DUPLICATE &#x2F; AGGREGATE &#x2F; UNIQUE 三選一的 key model；但對於 read-mostly 的分析場景，這些差異多半不會踩到。</p><h2 id="v3-0-後的儲存運算分離把長期成本壓下來"><a href="#v3-0-後的儲存運算分離把長期成本壓下來" class="headerlink" title="v3.0 後的儲存運算分離把長期成本壓下來"></a>v3.0 後的儲存運算分離把長期成本壓下來</h2><p>3.0 版本帶來的儲存運算分離模式（Storage-Compute Decoupled）是 Doris 把成本曲線重新拉低的關鍵。在這個模式下，BE node 不再保留資料本體，全部 offload 到 S3 相容的物件儲存——可以是 AWS S3、MinIO、自家 Garage、阿里雲 OSS、GCS、Azure Blob。BE 只負責計算跟快取 hot data。</p><p>這個改動的實務意義是：冷資料的儲存成本從 NVMe SSD 直接掉到物件儲存的等級。一份 50 TB 的歷史報表資料原本要堆滿好幾顆企業級 SSD，現在丟進 MinIO 就行，BE 那邊只要備好夠跑 working set 的快取容量。對於資料保留期長、查詢只集中在最近一週的工作負載，這套架構幾乎是最佳解。</p><p>另一個附帶好處是 compute 群組可以多套並存。ETL 寫入用一組 BE、BI 查詢用另一組、AdHoc analysis 再開一組，三組共享同一份資料但彼此 CPU 隔離。這在過去要靠 ClickHouse 的 distributed table 加上一堆讀寫分離規則才能做到。</p><h2 id="VPS-自架的實際拆法"><a href="#VPS-自架的實際拆法" class="headerlink" title="VPS 自架的實際拆法"></a>VPS 自架的實際拆法</h2><p>把 Doris 真正塞進 VPS 環境最常見的做法是兩組節點：Frontend (FE) 跟 Backend (BE) 各自部署。FE 負責解析 SQL、規劃查詢、保管 metadata；BE 真的去算結果。正式環境通常會把 FE 跑成三台 Observer 加上一台 Leader 的小 quorum，但對於月查詢量不大的中型部署，單台 FE 加三台 BE 就夠用。</p><p>硬體規格上，BE 是吃 CPU 跟記憶體的主力。官方建議至少 8 核心 32 GB 記憶體配 NVMe SSD，但實務上 4 核 16 GB 也能跑得動展示流量。3.0 之後的儲存分離模式可以把 BE 規格進一步壓低——本地碟只放快取，主體跑 S3，BE 換成記憶體吃飽的機種就能撐住。</p><p>部署上唯一比較麻煩的是 BE 對於 swap、transparent huge page 跟 vm.overcommit_memory 有特定要求，這些在開機腳本裡先處理好之後維運就單純了。FE 跟 BE 之間靠 RPC 通訊，所以同機房內網延遲很重要——這也是為什麼 Doris 在臺灣自架時，把整個 cluster 都擺在同一個機房的同一個機櫃會比跨機房可靠得多。</p><h2 id="Doris、ClickHouse、StarRocks-該怎麼選"><a href="#Doris、ClickHouse、StarRocks-該怎麼選" class="headerlink" title="Doris、ClickHouse、StarRocks 該怎麼選"></a>Doris、ClickHouse、StarRocks 該怎麼選</h2><p>三套都是 MPP columnar database，但分工不太一樣。如果工作負載是 append-only 的 log 跟 event，每天 TB 級新資料但幾乎不 update，ClickHouse 的 raw scan throughput 仍然是頂規。如果需要大量的 multi-table join、面向終端使用者的低延遲 dashboard，StarRocks 的 cost-based optimizer 跟 colocated join 通常會勝出。</p><p>Apache Doris 的甜蜜點是「需要 update、需要 MySQL 客戶端相容、需要在 VPS 等中型自架環境上跑」這個交集。對於從 MySQL 主庫往上爬到需要 OLAP 的中型團隊，Doris 是門檻最低的下一步。對於已經有 ClickHouse 但卡在 update 場景的團隊，把那部分 workload 切到 Doris 通常比改 ClickHouse 表結構來得乾淨。</p><p>純就生態系成熟度，ClickHouse 仍然是書面資料、社群討論、第三方整合最多的選項。Doris 因為早期主要用戶集中在亞太區，英文文件偶爾會比中文文件落後幾個版本。要真正評估時，建議直接用自家 production 的 query pattern 在三套上各跑一遍，多花一個工程師週做基準，比導入後再換成本低得多。</p><h2 id="即時分析倉庫的位置已經被重排"><a href="#即時分析倉庫的位置已經被重排" class="headerlink" title="即時分析倉庫的位置已經被重排"></a>即時分析倉庫的位置已經被重排</h2><p>即時分析倉庫這個品類在 2026 年已經沒有什麼新概念，真正在拉開差距的是「跟現有技術棧的整合成本」。Doris 透過 MySQL 協議把這個成本壓到最低，這是它在 ClickHouse 跟 Snowflake 之間找到自己位置的方式——既不是 append-only 跑得最快的那個，也不是 SaaS 帳單最痛的那個，而是讓 BI 工具不用換 connector 的那個。</p><p>對於想在臺灣機房自架即時分析倉庫的團隊，硬體規格、機房線路品質、IPv6 出口與 IP Transit 頻寬都會直接影響整套系統的可用性。NCSE Network 提供位於是方電訊機房的 Intel Gold CPU 加 NVMe SSD VPS、原生雙堆疊 IPv6 與穩定的 IP Transit 線路，適合架設 FE 加 BE 多節點的 Doris cluster。需要進一步討論硬體配置或機房代管選項，可參考 <a href="https://ncse.tw/">ncse.tw</a> 上的 VPS 與 IP Transit 方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/apache-doris-mysql-realtime-analytics-warehouse/</id>
    <link href="https://blog.ncse.tw/apache-doris-mysql-realtime-analytics-warehouse/"/>
    <published>2026-06-13T03:00:00.000Z</published>
    <summary>ClickHouse 把 update 推成 eventual consistency、BI 工具又綁死 MySQL 協議。Apache Doris 用 MPP 加 MySQL wire protocol 補上中間那塊空缺，update 工作負載快 34 倍。本文解析定位、儲存運算分離與 VPS 自架實務。</summary>
    <title>ClickHouse 寫得快、改不動：Apache Doris 用 MySQL 協議把即時分析倉庫接回原本的客戶端</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="可觀測性" scheme="https://blog.ncse.tw/tags/%E5%8F%AF%E8%A7%80%E6%B8%AC%E6%80%A7/"/>
    <category term="日誌管理" scheme="https://blog.ncse.tw/tags/%E6%97%A5%E8%AA%8C%E7%AE%A1%E7%90%86/"/>
    <category term="VictoriaLogs" scheme="https://blog.ncse.tw/tags/VictoriaLogs/"/>
    <category term="Loki" scheme="https://blog.ncse.tw/tags/Loki/"/>
    <category term="Grafana" scheme="https://blog.ncse.tw/tags/Grafana/"/>
    <content>
      <![CDATA[<p>Grafana Loki 在小規模可觀測性場景確實夠用，但日誌量一爬到三位數 GB，Loki 的記憶體曲線就頂到上限、查詢從秒級退化到分鐘級、沒帶標籤的全文搜尋直接 timeout。VictoriaLogs 用 columnar 儲存加 per-token 索引把同樣工作量壓進三分之一的 RAM 跟磁碟，查詢延遲還順帶縮到 1&#x2F;12。這套日誌儲存方案在 2026 年初進入正式 GA，採集端對 Loki 協定相容，搬遷成本接近零。</p><p>VictoriaLogs 由 VictoriaMetrics 團隊在同一套 mergeset 引擎邏輯上延伸出來。設計目標寫得很直接：把日誌全文檢索的成本壓到接近指標儲存的水準。對照組是同樣 500GB &#x2F; 7 天的工作量，公開 benchmark 拿到的數字是 Loki 三分之一的記憶體、37% 更少的磁碟、12 倍的查詢速度。本文要拆的是這些數字背後的儲存設計，以及怎麼把它接上現有的 Vector、Fluent Bit、Grafana Alloy 採集鏈。</p><h2 id="Loki-的標籤索引貴在哪"><a href="#Loki-的標籤索引貴在哪" class="headerlink" title="Loki 的標籤索引貴在哪"></a>Loki 的標籤索引貴在哪</h2><p>Loki 對外的賣點一直是「只索引標籤，不索引內容」。聽起來合理：標籤少，索引就小，記憶體就省。實際走到大流量產環境之後就會看到代價。</p><p>第一個問題是 cardinality。一旦把 <code>pod_name</code>、<code>request_id</code>、<code>trace_id</code> 這種高基數欄位放進標籤，每個獨特組合都會在記憶體裡開一個 stream。Loki 沒有夠強的硬性限制機制，預設配置下幾百萬個 stream 就能把 ingester 的記憶體吃光，OOM kill 是日常。</p><p>第二個問題是查詢路徑。Loki 的查詢只在標籤層做剪枝，剩下的全文比對是逐 chunk 解壓縮、逐行 regex 掃過去。如果搜尋條件沒帶夠精準的標籤，整個時間範圍裡的 chunk 都得拉回 querier 解壓——500GB 規模的「大海撈針」查詢實測 12 秒、否定查詢直接 timeout，都是這個機制下的必然結果。</p><p>第三個問題是運維面。Loki 的單體模式只能撐到一定吞吐量，要繼續往上得拆成 distributor &#x2F; ingester &#x2F; querier 微服務，再加 Memcached、加 object storage、加 compactor。光把這套架構穩定跑起來就已經是一個全職工程師的工作量。</p><h2 id="per-token-索引換來的代價平衡"><a href="#per-token-索引換來的代價平衡" class="headerlink" title="per-token 索引換來的代價平衡"></a>per-token 索引換來的代價平衡</h2><p>VictoriaLogs 的儲存模型走的是反方向：所有欄位都建索引，但用 columnar 儲存把成本壓下來。</p><p>寫入時，每筆日誌被拆成若干欄位（<code>_msg</code>、<code>_time</code>、<code>_stream</code>、自訂結構化欄位），每個欄位獨立壓縮存成一欄。<code>_msg</code> 內文則再被切成 tokens 建反向索引。這套設計直接借自 VictoriaMetrics 的 mergeset 引擎，欄位之間的壓縮比通常落在 10 倍到 30 倍之間。</p><p>查詢時，per-token 索引讓「找出某行特定字串」這種需求不必再掃整個時間窗。實測同樣的 needle-in-haystack 查詢在 500GB 資料集上，Loki 跑 12 秒，VictoriaLogs 跑 900 毫秒。差距不是調整參數能補的，是儲存結構本身的差別。</p><p>partition 切分方式是按日分桶（<code>partitions/YYYYMMDD/</code>），這讓資料分層、備份、刪除舊資料的操作都能在不影響線上服務的前提下完成。要把三個月前的資料搬到便宜的 HDD 或 S3 做冷儲存，rsync 整個 partition 目錄就行，不需要走複雜的 API。</p><p>至於記憶體佔用，同樣的 500GB &#x2F; 7 天工作量，Loki 穩態下吃 6 到 7 GiB、爆量時還會被 OOM kill；VictoriaLogs 穩態 1.3 GiB、峰值也只到 2 GiB 左右。這個差距讓單台 4 GB RAM 的 VPS 從「勉強堪用」變成「綽綽有餘」。</p><h2 id="LogsQL-比-LogQL-多了什麼"><a href="#LogsQL-比-LogQL-多了什麼" class="headerlink" title="LogsQL 比 LogQL 多了什麼"></a>LogsQL 比 LogQL 多了什麼</h2><p>語法層面，LogsQL 抓住了一個 LogQL 一直沒處理好的點：把全文檢索當作第一公民。</p><p>最簡單的查詢就是一個字：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">error</span><br></pre></td></tr></table></figure><p>這會找到 <code>_msg</code> 欄位裡包含 <code>error</code> 的所有日誌。要找精確字串就加引號：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;connection refused&quot;</span><br></pre></td></tr></table></figure><p>組合條件時 AND 可以省略：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_time:1h log.level:error -app:buggy_service</span><br></pre></td></tr></table></figure><p>這條的意思是：過去一小時內，<code>log.level</code> 欄位為 <code>error</code>、但排除 <code>app</code> 為 <code>buggy_service</code> 的紀錄。負號代表排除，這在 LogQL 裡要寫成相對冗長的 <code>!=</code> 跟 <code>!~</code> 組合。</p><p>stream filter 用 <code>{}</code> 包起來，跟 Loki 的習慣對齊：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;app=&quot;nginx&quot;&#125; _time:5m 500</span><br></pre></td></tr></table></figure><p>聚合走 pipe 串接，這個設計比 LogQL 的函式包裹更接近 shell 的直覺：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_time:1h error | stats by (host) count() as cnt | sort by (cnt) desc</span><br></pre></td></tr></table></figure><p>對從 Splunk 或 Sumo Logic 轉過來的工程師，這套寫法的學習曲線會比 LogQL 那種 PromQL-style 的語法平緩許多。</p><h2 id="採集層相容現有的工具鏈"><a href="#採集層相容現有的工具鏈" class="headerlink" title="採集層相容現有的工具鏈"></a>採集層相容現有的工具鏈</h2><p>VictoriaLogs 對外暴露多套相容協定，採集端基本不用換：</p><ul><li>OpenTelemetry Collector 直接送 OTLP&#x2F;HTTP</li><li>Vector 走原生 sink</li><li>Fluent Bit、Fluentd、Logstash 用 HTTP output</li><li>Promtail 改個 URL 就能繼續用</li><li>Grafana Alloy 近期版本內建支援</li><li>Journald、syslog 直送也有對應的 endpoint</li></ul><p>這意味著從 Loki 切換到 VictoriaLogs 不必動採集端的設定。對已經部署 Promtail 或 Alloy 的環境，唯一要改的是把 <code>clients.url</code> 指到 VictoriaLogs 的 <code>/insert/loki/api/v1/push</code>——這個端點刻意做了 Loki 協定相容，讓搬遷成本接近零。</p><p>視覺化層也是。Grafana 裝個官方 plugin 就能用 LogsQL 查 VictoriaLogs，原本的 dashboard 改 datasource 就能繼續跑。對需要保留 Loki 邏輯不動的環境，VictoriaLogs 還支援 LogQL 查詢端的相容模式。</p><h2 id="單節點部署：一支-binary-加-Docker-Compose"><a href="#單節點部署：一支-binary-加-Docker-Compose" class="headerlink" title="單節點部署：一支 binary 加 Docker Compose"></a>單節點部署：一支 binary 加 Docker Compose</h2><p>VictoriaLogs 的單節點版本是一支 Go 編譯出來的 binary，沒有外部相依。最小可用的 Docker Compose 大致長這樣：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">victorialogs:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">victoriametrics/victoria-logs:v1.10.0</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9428:9428&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/victoria-logs-data</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">-storageDataPath=/victoria-logs-data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">-retentionPeriod=30d</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">-httpListenAddr=:9428</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><p><code>-retentionPeriod</code> 支援從 <code>1d</code> 到 <code>100y</code>，也可以改用 <code>-retention.maxDiskSpaceUsageBytes</code> 走磁碟容量上限。預設值只有 7 天，正式環境一定要改。</p><p>對外暴露的部分，建議在前面套一層 Caddy 或 Nginx 做 TLS 跟 basic auth。VictoriaLogs 本身也有 <code>-httpAuth.username</code> 跟 <code>-httpAuth.password</code> 參數，但走反代解決會更有彈性，也能順便處理跨服務的權限收斂。</p><p>多租戶用 <code>AccountID</code> 跟 <code>ProjectID</code> HTTP header 區分，寫入跟查詢都得帶上對應的 header 才會落到同一個邏輯空間。這個機制在 SaaS 場景或公司內部多團隊共用一台儲存實例時特別好用。</p><p>高可用方案官方推薦的是雙寫兩台單節點再用 vmauth 做查詢端 load balance，避開了 cluster 模式的部署複雜度。對絕大多數中小規模的場景，這個方案的容錯能力已經夠用，又不會引入額外的故障點。</p><h2 id="該選-Loki-還是-VictoriaLogs"><a href="#該選-Loki-還是-VictoriaLogs" class="headerlink" title="該選 Loki 還是 VictoriaLogs"></a>該選 Loki 還是 VictoriaLogs</h2><p>兩邊都不需要過度吹捧。場景對的時候各有勝場。</p><p>選 Loki 的情境：團隊已經跑了一兩年的 Loki，運維跟 dashboard 都成熟；查詢模式以標籤為主、全文檢索很少；對 Grafana 生態的整合深度有強要求；日均日誌量在 50GB 以內，記憶體跟磁碟成本不構成壓力。</p><p>選 VictoriaLogs 的情境：剛開始建立可觀測性技術棧、沒有歷史包袱；查詢需求以全文檢索或 ad-hoc 探索為主；資源密度有實質限制（VPS 預算固定、要在同一台機器上同時跑指標跟日誌）；不想花全職工程師力氣維護 Loki 微服務拓樸。</p><p>實務上對新建環境，VictoriaLogs 的綜合分數明顯高一些。少一個元件就少一個故障點，少幾 GiB 記憶體預算就能多塞別的服務。把 VictoriaMetrics + VictoriaLogs 跟 Grafana Alloy 湊在一起，會得到一套完全 columnar 的可觀測性堆疊，硬體成本只有傳統 ELK + Loki 組合的零頭。</p><h2 id="從-Loki-搬遷的實務順序"><a href="#從-Loki-搬遷的實務順序" class="headerlink" title="從 Loki 搬遷的實務順序"></a>從 Loki 搬遷的實務順序</h2><p>對已經跑 Loki 的環境，沒必要一次切換。建議的順序：</p><p>第一步是並行寫入。讓 Promtail 或 Alloy 同時送一份到 Loki 跟 VictoriaLogs，跑兩三週，確認新平台沒漏資料、查詢結果一致。VictoriaLogs 的吞吐能力撐得住雙寫，這段時間不會額外加重採集端壓力。</p><p>第二步是切查詢流量。Grafana datasource 加一條 VictoriaLogs，讓部分 dashboard 跟 alert 走新後端。觀察 query latency 跟錯誤率，確認 LogsQL 改寫過的查詢行為符合預期。</p><p>第三步是停寫 Loki。確認新後端穩定後關閉 Loki 寫入，Loki 那邊保留唯讀模式跑到資料過期或主動清理。</p><p>整個過程裡，採集端的設定變動最小。對運維面真正花時間的是查詢語法的改寫，但 LogsQL 的學習曲線本身就比 LogQL 平緩，這部分的成本通常被高估。</p><h2 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h2><p>Loki 在 2020 年解決了當時 ELK Stack 的成本問題，但 2026 年看回去，它對標籤索引的依賴反而成了新的瓶頸。VictoriaLogs 用 columnar 加 per-token 索引把同樣的工作量壓進三分之一的資源，採集端跟視覺化端又都向後相容，搬遷成本比預期低很多。</p><p>部署 VictoriaLogs 不需要叢集、不需要 object storage、不需要 Memcached，一台中階 VPS 就能撐到日均百 GB 的日誌吞吐。columnar 儲存對 I&#x2F;O 隨機讀寫跟記憶體頻寬都敏感，搭配 NVMe SSD 跟現代 CPU 才能把效能跑滿。NCSE Network 提供位於臺灣是方電信機房的 VPS 服務，採用 Intel Gold CPU 與 NVMe SSD，適合作為自架可觀測性技術棧的儲存節點，詳細規格與報價可以到 <a href="https://ncse.tw/">ncse.tw</a> 查看。</p>]]>
    </content>
    <id>https://blog.ncse.tw/victorialogs-columnar-log-storage-loki-alternative/</id>
    <link href="https://blog.ncse.tw/victorialogs-columnar-log-storage-loki-alternative/"/>
    <published>2026-06-12T06:30:00.000Z</published>
    <summary>Grafana Loki 在日誌量上 500GB 之後記憶體吃滿、全文檢索動輒分鐘級 timeout。VictoriaLogs 用 columnar 儲存加 per-token 索引把同樣的工作量壓進三分之一 RAM 跟磁碟，查詢延遲還順帶縮到 1/12。本文拆解儲存設計、LogsQL 語法與單節點自架流程。</summary>
    <title>Loki 跑到 500GB 就爆 RAM：VictoriaLogs 用 columnar 把同樣工作量壓進三分之一資源</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Coroot" scheme="https://blog.ncse.tw/tags/Coroot/"/>
    <category term="eBPF" scheme="https://blog.ncse.tw/tags/eBPF/"/>
    <category term="APM" scheme="https://blog.ncse.tw/tags/APM/"/>
    <category term="可觀測性" scheme="https://blog.ncse.tw/tags/%E5%8F%AF%E8%A7%80%E6%B8%AC%E6%80%A7/"/>
    <content>
      <![CDATA[<p>過去兩年最常被點名退訂的雲端服務，第一名是 Datadog。月底帳單動輒新臺幣幾十萬、log ingest 跟 indexed span 各算一份錢、APM host 又另外計算——對於只有幾十台機器、月營收還沒爬到能養得起一個 SRE 團隊的公司來說，這是一筆很尷尬的固定支出。但要自架 APM 又卡在一個老問題：所有開源方案都要先在程式碼裡插 OpenTelemetry SDK，PHP、Ruby、舊版 Node、內部小工具往往沒人想去碰。</p><p>Coroot 是這幾年裡少數把 eBPF 真的拿來做完整 APM 的開源方案。寫在 Go 與 Rust 之上、Apache 2.0 授權、2026 年 6 月發到 1.22 版，GitHub 上累積近 8000 顆星。它能在不改任何一行程式碼的前提下抓出 trace、CPU profile 與 service map，這是 SigNoz 跟 Grafana Stack 都沒有直接提供的能力。</p><h2 id="SDK-插樁這件事正在變成-APM-的瓶頸"><a href="#SDK-插樁這件事正在變成-APM-的瓶頸" class="headerlink" title="SDK 插樁這件事正在變成 APM 的瓶頸"></a>SDK 插樁這件事正在變成 APM 的瓶頸</h2><p>OpenTelemetry 已經是事實標準，但實際導入時的痛點從來不是規格本身，而是「誰要去幫每個服務接 SDK」。後端用 Spring Boot 的還好，auto-instrumentation agent 解決一半問題；換成 Express 加一堆內部 npm 套件就會卡在某些函式抓不到 span。再加上 PHP 老站、CRM 廠商打包好的封閉容器、用 Bash 與 systemd timer 串起來的 cron job，每一塊都得另外排工時去處理。</p><p>落到實作層面，這代表的是兩到三個月的工程工時，外加跨團隊溝通成本。中小型團隊根本不會把這件事排進當季 OKR——結果就是 APM 只覆蓋到核心 API，邊緣服務出問題依然得靠 SSH 進去翻 log。</p><p>把同樣的事丟給商用 SaaS 也不會更輕鬆，只是把「插樁工時」換成「月費」。Datadog 與 New Relic 的 agent 通常會自動掛上常見執行環境，但對於非主流語言或自製框架還是要寫客製 instrumentation。真正不一樣的是 eBPF。</p><h2 id="eBPF-把觀測點從應用層搬進核心"><a href="#eBPF-把觀測點從應用層搬進核心" class="headerlink" title="eBPF 把觀測點從應用層搬進核心"></a>eBPF 把觀測點從應用層搬進核心</h2><p>eBPF 不是新東西，Cilium、Tetragon、Falco 都已經把它用在網路與安全層。Coroot 走的是另一條路：把 eBPF 程式掛在 Linux 的 socket 系統呼叫、TCP 連線事件與排程點上，當任何程序送出 HTTP 請求、開 PostgreSQL 連線、把資料推進 Kafka，核心就會把這些事件回報給 node agent。</p><p>這代表的不只是抓網路流量，而是抓「應用層協定」的呼叫。Coroot node agent 內建了 HTTP、HTTP&#x2F;2、PostgreSQL、MySQL、Redis、MongoDB、Kafka、AMQP 的解析器，連 TLS 加密過的流量都能透過 uprobe 掛到 OpenSSL、GnuTLS、Go 的 crypto&#x2F;tls 上抓 plaintext。從程式的角度看，什麼都沒發生；從觀測平台的角度看，每次資料庫查詢的 SQL、每次外部 API 呼叫的路徑、每次 cache 命中與否，都已經被記下來。</p><p>CPU profile 走的是另一條 eBPF 路線：透過 <code>perf_event_open</code> 取得堆疊樣本，再對齊容器 metadata 寫進 Pyroscope 後端。對於想知道「上週四下午那波 CPU 飆高到底是哪段程式碼在燒」的場景，這比事後翻 metric 直觀得多。</p><p>零插樁的代價當然存在：eBPF 程式只能看到核心層可見的東西，例如業務語意上的 trace_id 它不會自動產生。Coroot 處理的方式是針對標準 HTTP header（W3C Trace Context、B3）做 in-kernel 解析，能跨進程接上就接，接不上的退回成獨立的 span。對絕大多數 RESTful 微服務架構，這個機制夠用；如果整套系統大量用 gRPC 加客製 metadata 傳 context，效果會打折扣。</p><h2 id="從-service-map-倒推系統健康狀態"><a href="#從-service-map-倒推系統健康狀態" class="headerlink" title="從 service map 倒推系統健康狀態"></a>從 service map 倒推系統健康狀態</h2><p>Coroot 的核心 UI 不是傳統的 metric dashboard，而是一張自動產生的 service map。每個節點代表一個服務或外部資源，邊代表它們之間的呼叫關係，顏色代表 SLO 健康度。從這張圖上點下去，可以直接跳到該服務的 latency 分位數、錯誤率、相關 log、相關 trace。</p><p>這個設計的價值要實際出過事才看得出來。傳統 APM 的工作流是「拿著 alert 上的 metric 名稱去 dashboard 翻」，找關聯服務還要記得它叫什麼名字。service map 直接把拓樸畫出來，當下游的 PostgreSQL 連線數爆掉時，上游的哪幾個 service 在打它一目了然，不需要先去查服務目錄。</p><p>Coroot 還把這套 service map 跟 SLO 綁在一起。每個服務預設都會有 latency 與 availability 的 SLO，達不到就在地圖上標成紅色；點進紅色節點會直接列出最有可能的根因（連線飽和、磁碟 I&#x2F;O、特定 query 慢），這就是它官網一直在講的「AI Root Cause Analysis」。實務上這個 AI 不是大型語言模型，而是規則引擎加上時序資料關聯——夠用，但別期待它能解出複雜的分散式 race condition。</p><h2 id="跟-SigNoz、Grafana-Stack-的定位差異"><a href="#跟-SigNoz、Grafana-Stack-的定位差異" class="headerlink" title="跟 SigNoz、Grafana Stack 的定位差異"></a>跟 SigNoz、Grafana Stack 的定位差異</h2><p>SigNoz 與 Coroot 表面看起來都是「自架的 OTel observability 平台」，但實際工作流幾乎是反過來的。SigNoz 預設假設你已經接好 OTel SDK，它負責把 trace、metric、log 收進 ClickHouse 然後給漂亮的 UI；如果還沒接 SDK，SigNoz 等於少了大半功能。Coroot 預設假設你「不想接 SDK」，它先用 eBPF 把可觀測性的最低保證打起來，OTel SDK 是 nice to have。</p><p>Grafana Stack（Prometheus、Loki、Tempo、Pyroscope）走的是「拼裝」路線：每個元件都很強，但要自己接 Alloy、寫 PromQL、設 alerting rule、串 dashboard。對工程團隊已經養出 Grafana 文化的公司，這條路會繼續是首選。對「需要 APM 但不想再花三個月接元件」的團隊，Coroot 的單一安裝包會省掉很多事。</p><p>務實的選法：團隊規模 10 人以內、沒有專職可觀測性工程師，直接上 Coroot；已經有 Prometheus 加 Grafana 跑了兩年、要加進階 trace 與 profile，疊一層 SigNoz 或 Tempo；Kubernetes 規模上百節點、SRE 團隊願意自己拼，Grafana Stack 還是天花板最高的選項。</p><h2 id="單機-VPS-跟-Kubernetes-的部署實際樣貌"><a href="#單機-VPS-跟-Kubernetes-的部署實際樣貌" class="headerlink" title="單機 VPS 跟 Kubernetes 的部署實際樣貌"></a>單機 VPS 跟 Kubernetes 的部署實際樣貌</h2><p>Coroot 在 Kubernetes 上有官方 Helm chart，裝完會跑出一組 deployment：coroot server（UI 與 API）、ClickHouse（儲存 trace、log、profile）、Prometheus（儲存 metric）、Pyroscope（profile 後端），加上以 DaemonSet 形式部署的 coroot-node-agent。對既有 K8s 叢集這是最順的安裝路徑。</p><p>VPS 單機部署也支援。官方提供的 docker-compose 大致長這樣：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">coroot:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/coroot/coroot:1.22.0</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:8080:8080&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">BOOTSTRAP_PROMETHEUS_URL:</span> <span class="string">&quot;http://prometheus:9090&quot;</span></span><br><span class="line">      <span class="attr">BOOTSTRAP_CLICKHOUSE_ADDRESS:</span> <span class="string">&quot;clickhouse:9000&quot;</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">prometheus</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">clickhouse</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">node-agent:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/coroot/coroot-node-agent:1.22.0</span></span><br><span class="line">    <span class="attr">privileged:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">pid:</span> <span class="string">host</span></span><br><span class="line">    <span class="attr">network_mode:</span> <span class="string">host</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/sys/kernel/debug:/sys/kernel/debug:rw</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/sys/fs/cgroup:/sys/fs/cgroup:ro</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--collector-endpoint=http://127.0.0.1:8080</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--cgroupfs-root=/sys/fs/cgroup</span></span><br></pre></td></tr></table></figure><p><code>privileged: true</code>、<code>pid: host</code> 與 debugfs mount 都是 eBPF 載入所需，避不掉。也因此 node agent 必須跑在每一台要被觀測的主機上，無法把它放在另一台機器遠端抓。Linux kernel 需要 4.16 以上（推薦 5.10 以上），CentOS 7 那種還停在 3.10 的環境裝不上。</p><p>資源開銷實測：node agent 大約吃 100 到 200 MB 記憶體加上 1% 到 3% CPU，視服務數量與流量而定。Coroot server 加 ClickHouse 在中等規模（20 到 50 個服務）下大約需要 4 GB 記憶體與 50 GB 磁碟，跑在 4 vCPU 的 VPS 上綽綽有餘。</p><h2 id="哪些團隊裝了會有感、哪些不會"><a href="#哪些團隊裝了會有感、哪些不會" class="headerlink" title="哪些團隊裝了會有感、哪些不會"></a>哪些團隊裝了會有感、哪些不會</h2><p>最有感的場景是「服務數量不少、語言混雜、沒人有時間去接 OTel」的中小型團隊。電商後端有 Node 加 PHP 加 Python 三種語言、財報系統綁了一堆內部套件、資料平台有 Airflow 拋的 Python job，這種環境裝完 Coroot 大約 30 分鐘就能看到完整的 service map。同樣的工作如果走傳統路線，光是排程跨團隊改程式碼就要一季。</p><p>不太適合的是兩種：一是純單體應用、只有一個 service，service map 沒什麼意義，直接用 Prometheus 加 Pyroscope 就好；二是已經完整接好 OpenTelemetry SDK 的成熟系統，Coroot 的 eBPF 資料反而會跟現有 trace 重複，這時 SigNoz 或 Grafana Stack 更能利用既有插樁。</p><p>還有一個常被忽略的限制：Coroot 看不到 serverless 與 managed runtime。AWS Lambda、Cloudflare Workers、託管的 Kubernetes 控制面，這些沒有 root 權限的環境裝不了 node agent。混合架構下，Coroot 適合罩住自家機房與 VPS 那部分，雲端 managed 服務還是得回頭走 vendor 自己的 metric。</p><h2 id="把可觀測性的入場費降下來"><a href="#把可觀測性的入場費降下來" class="headerlink" title="把可觀測性的入場費降下來"></a>把可觀測性的入場費降下來</h2><p>eBPF 之所以重要，不是因為它新潮，而是因為它真的把可觀測性的入場成本降到「裝一個 agent 就有」的等級。Coroot 不會取代所有 APM 方案，但對於那些一直被「先去插樁」卡住的團隊，它把第一步從三個月縮短到一個下午。</p><p>如果手上的服務還在靠 SSH 進去翻 log 抓問題，或是雲端 APM 帳單已經佔到月支出的兩成以上，自架一套 eBPF observability 的投資報酬率通常很可觀。NCSE Network 提供臺灣本地的高效能 VPS 主機（Intel Gold CPU、NVMe SSD、是方電訊機房），核心規格與較新的 Linux kernel 都符合 eBPF observability 的部署需求，需要評估自架 APM 的硬體配置時可以參考。</p>]]>
    </content>
    <id>https://blog.ncse.tw/coroot-ebpf-apm-self-hosted-datadog-alternative/</id>
    <link href="https://blog.ncse.tw/coroot-ebpf-apm-self-hosted-datadog-alternative/"/>
    <published>2026-06-11T03:00:00.000Z</published>
    <summary>SigNoz、Grafana Stack 都得先改程式碼接 OpenTelemetry SDK，Datadog 帳單又貴得嚇人。本文解析 Coroot 用 eBPF 抓 trace、profile、service map 的做法、跟 SigNoz 的定位差異，以及單機 VPS 與 Kubernetes 的實際部署選擇。</summary>
    <title>程式碼還沒插樁、Datadog 帳單就已經到了：Coroot 用 eBPF 把 APM 整套搬回自家機房</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="可觀測性" scheme="https://blog.ncse.tw/tags/%E5%8F%AF%E8%A7%80%E6%B8%AC%E6%80%A7/"/>
    <category term="監控" scheme="https://blog.ncse.tw/tags/%E7%9B%A3%E6%8E%A7/"/>
    <category term="Grafana Alloy" scheme="https://blog.ncse.tw/tags/Grafana-Alloy/"/>
    <category term="OpenTelemetry" scheme="https://blog.ncse.tw/tags/OpenTelemetry/"/>
    <category term="Promtail" scheme="https://blog.ncse.tw/tags/Promtail/"/>
    <content>
      <![CDATA[<p>2026 年 3 月 2 日，Loki 官方正式宣布 Promtail 結束生命週期。Grafana Agent 更早，在 2025 年就被合併進 Alloy，文件頁上只剩一行 redirect。對於還在跑「Promtail 拉 log、node_exporter 暴露系統指標、OpenTelemetry Collector 處理 trace」這套組合的團隊，這已經不是選配題目——Loki 官方目前推的採集器只剩 Alloy 一個。</p><p>Grafana Alloy 是這次整合的最終形態。它把 metrics、logs、traces、profiles 四種訊號的採集收進同一支 binary，並以 OpenTelemetry Collector 發行版的形式對外發布。本文拆解它的定位、實際設定方式，以及取代既有採集 agent 的路徑。</p><h2 id="為什麼-Grafana-把這四隻-agent-全收成一隻"><a href="#為什麼-Grafana-把這四隻-agent-全收成一隻" class="headerlink" title="為什麼 Grafana 把這四隻 agent 全收成一隻"></a>為什麼 Grafana 把這四隻 agent 全收成一隻</h2><p>過去三年，一臺中等規模的 Linux 伺服器同時跑四隻採集器是常態：Promtail 拉 log 進 Loki、node_exporter 暴露系統指標讓 Prometheus 抓、OpenTelemetry Collector 處理 trace、再加上 Grafana Agent 想把這些事做一遍但只成功了一半。每隻 agent 各自吃資源、各自要維護設定檔、各自要被監控，光是處理「監控本身」的成本就比想像中重。</p><p>Alloy 的核心主張是：這四件事在資料管線層面其實是同一件事——收集、轉換、轉發。把它們合進同一個 OpenTelemetry Collector 發行版裡，比維護四個獨立 agent 合理得多。對 Grafana 而言這也是必然選擇，CNCF 標準正在統一所有可觀測訊號的傳輸協定，繼續維護獨立 agent 等於浪費工程資源。</p><h2 id="Alloy-不是新東西，是-OpenTelemetry-Collector-的發行版"><a href="#Alloy-不是新東西，是-OpenTelemetry-Collector-的發行版" class="headerlink" title="Alloy 不是新東西，是 OpenTelemetry Collector 的發行版"></a>Alloy 不是新東西，是 OpenTelemetry Collector 的發行版</h2><p>這點常被誤解。Alloy 並不是 Grafana 重新寫的另一個採集器，而是基於 OpenTelemetry Collector 上游的 distribution，類似 Splunk OTel Collector、AWS Distro for OpenTelemetry 的角色。差別是 Grafana 在標準 OTel pipeline 之外，把 Prometheus 生態的 pull-based scrape、各種 exporter，跟 Loki 的 log source 整進來。</p><p>2026 年 GrafanaCON 公布的版本進一步加上了 OpenTelemetry Engine mode，可以直接吃標準 OTel Collector 的 YAML 設定檔，不用學新語法。原本擔心 vendor lock-in 的團隊，這條退路至少留好了。</p><h2 id="alloy-語法長這樣"><a href="#alloy-語法長這樣" class="headerlink" title=".alloy 語法長這樣"></a>.alloy 語法長這樣</h2><p>Alloy 用一套叫做 River 的設定語法，檔名是 .alloy。每個元件叫做 component，類似函式呼叫，元件之間用 reference 串成 pipeline：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">prometheus.exporter.unix &quot;default&quot; &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">prometheus.scrape &quot;system&quot; &#123;</span><br><span class="line">  targets    = prometheus.exporter.unix.default.targets</span><br><span class="line">  forward_to = [prometheus.remote_write.local.receiver]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">prometheus.remote_write &quot;local&quot; &#123;</span><br><span class="line">  endpoint &#123;</span><br><span class="line">    url = &quot;http://prometheus:9090/api/v1/write&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">loki.source.docker &quot;containers&quot; &#123;</span><br><span class="line">  host       = &quot;unix:///var/run/docker.sock&quot;</span><br><span class="line">  targets    = discovery.docker.containers.targets</span><br><span class="line">  forward_to = [loki.write.local.receiver]</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">loki.write &quot;local&quot; &#123;</span><br><span class="line">  endpoint &#123;</span><br><span class="line">    url = &quot;http://loki:3100/loki/api/v1/push&quot;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>跟傳統 YAML 比起來，這套語法的優勢是元件之間用 reference 而不是字串對應名稱串聯。語法錯誤在 alloy fmt 階段就會被抓到，不會等到 runtime 才爆。IDE 也能做出像樣的補全跟 type check，這在動輒上千行的設定檔上是實質生產力差別。</p><h2 id="Promtail-到-Alloy-的遷移成本"><a href="#Promtail-到-Alloy-的遷移成本" class="headerlink" title="Promtail 到 Alloy 的遷移成本"></a>Promtail 到 Alloy 的遷移成本</h2><p>Promtail 的 YAML 可以直接轉換。Alloy 內建 convert 指令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">alloy convert --source-format=promtail \</span><br><span class="line">  --output=config.alloy promtail.yaml</span><br></pre></td></tr></table></figure><p>實測下來，多數的 promtail.yaml 都能完整轉成 .alloy，但有兩個地方務必檢查。pipeline_stages 裡的 multiline 處理在 Alloy 變成 loki.process 的 stage.multiline block，欄位名跟舊版不完全一致。relabel_configs 則由獨立的 discovery.relabel 元件處理，邏輯更清楚但要重新組裝。</p><p>至於 node_exporter，Alloy 沒有提供轉換器，因為 node_exporter 本身沒有設定檔，原本就是 CLI flag 控制。重點是把對應的 collector 在 prometheus.exporter.unix 裡開好。預設啟用的 collector list 跟 node_exporter 不完全一樣——例如 textfile collector 預設沒開，這是一個常被忽略的差異，從 node_exporter 切過去後會發現某些自訂指標突然不見了。</p><h2 id="跟原生-OpenTelemetry-Collector-該怎麼選"><a href="#跟原生-OpenTelemetry-Collector-該怎麼選" class="headerlink" title="跟原生 OpenTelemetry Collector 該怎麼選"></a>跟原生 OpenTelemetry Collector 該怎麼選</h2><p>這是 2026 年實際在 deploy 時最常遇到的問題，建議分兩種情境給答案。</p><p><strong>已經跑 Loki、Prometheus 自架 stack 的團隊</strong>：直接上 Alloy。把舊有 Promtail、node_exporter、Grafana Agent 全收成 Alloy 是最省事的路徑，且 Alloy 對 Loki 跟 Prometheus 的整合明顯比 vanilla OTel Collector 完整，許多 exporter 不必另外接 receiver 就能直接用。</p><p><strong>走純 OpenTelemetry、後端不是 Grafana stack 的團隊</strong>：留在 vanilla OpenTelemetry Collector。Alloy 雖然支援 OTel YAML mode，但社群 component 跟 receiver 還是以 OTel Collector contrib 的更新最快，CNCF 那一側的相容性也更直接。</p><p>值得避免的反模式是：把 Alloy 當成「Grafana 版本的 OTel Collector」然後沿用 OTel 寫法。如果根本不打算用到 Prometheus pull-based scrape 跟 Loki source，純粹跑 OTel pipeline，vanilla collector 反而更輕、更新更快。</p><h2 id="單一-binary-在資源佔用上的真實表現"><a href="#單一-binary-在資源佔用上的真實表現" class="headerlink" title="單一 binary 在資源佔用上的真實表現"></a>單一 binary 在資源佔用上的真實表現</h2><p>合併四隻 agent 不代表資源用量直接除以四。Alloy 處於空閒（只收 host metrics）狀態大概吃 80MB RAM。同時開啟 Loki source、Prometheus scrape、OTel receiver、tail-sampling 等四個 pipeline 後，記憶體會穩定在 200 到 300MB 之間。</p><p>跟分開跑四隻 agent 加總的 400 到 500MB 比起來，省掉 30 到 50% 的記憶體佔用。但更大的價值不是省記憶體，而是除錯成本下降。當所有採集都走同一個 process，內建 web UI 可以看每個 component 的健康狀態跟 sample 資料流，遠比同時看四份 log 容易找出問題。「為什麼這條 log 沒進 Loki」這種問題，原本要查 Promtail log、Loki ingester、relabel 規則，現在可以在 Alloy UI 裡直接看 loki.source.docker 那個元件吐出了什麼。</p><h2 id="容器化部署最容易踩的-WAL-問題"><a href="#容器化部署最容易踩的-WAL-問題" class="headerlink" title="容器化部署最容易踩的 WAL 問題"></a>容器化部署最容易踩的 WAL 問題</h2><p>Alloy 在 Docker 部署時，預設會把資料存進 container 內的 &#x2F;var&#x2F;lib&#x2F;alloy 目錄。這個目錄存的不只是 cache，還有 write-ahead log（WAL）。如果沒做 volume mount，容器重啟時所有還沒送出的指標、log 都會掉，重啟頻繁的環境特別容易遺失資料。</p><p>正確做法是把 WAL 目錄掛到 named volume，並且把 graceful shutdown 時間從預設 30 秒拉到 60 到 90 秒，確保 WAL 的資料能完整 flush：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">alloy:</span></span><br><span class="line">    <span class="comment"># 正式環境請釘定版本，不要用 latest</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">grafana/alloy:v1.5.1</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;run&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;--server.http.listen-addr=0.0.0.0:12345&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;--storage.path=/var/lib/alloy/data&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;/etc/alloy/config.alloy&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./config.alloy:/etc/alloy/config.alloy:ro</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">alloy-data:/var/lib/alloy/data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/var/run/docker.sock:/var/run/docker.sock:ro</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;12345:12345&quot;</span></span><br><span class="line">    <span class="attr">stop_grace_period:</span> <span class="string">90s</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">alloy-data:</span></span><br></pre></td></tr></table></figure><p>12345 那個 port 是 Alloy 內建的 HTTP server 與 web UI，可以連進去看每個 component 的狀態跟 live debug。要接收 OTLP 流量需要另外在 .alloy 設定檔加上 otelcol.receiver.otlp 並暴露對應 port（gRPC 慣例 4317、HTTP 4318），不是打開 12345 就會自動收。正式環境記得用反向代理鎖權限，這個介面對所有採集的內容都看得到，沒做存取控制等於把整套系統指標公開。</p><h2 id="把採集層收齊之後值得想清楚的事"><a href="#把採集層收齊之後值得想清楚的事" class="headerlink" title="把採集層收齊之後值得想清楚的事"></a>把採集層收齊之後值得想清楚的事</h2><p>Alloy 的真正價值不在「省一隻 agent」，而在把採集這層的維運責任收斂到單一介面。原本同時要熟悉 Promtail YAML、node_exporter flag、OTel Collector pipeline 三套配置的工程師，現在只要熟 .alloy 一套語法。在團隊規模有限的情境下，這是非常實際的效益。</p><p>但別期待 Alloy 解決所有問題。它依然只是採集層，後端的 Loki、Prometheus、Tempo 還是要自己維運；資料儲存、retention 策略、查詢效能這些都跟 Alloy 沒關係。決定要不要採用，看的是「採集層的維運成本」對團隊現階段的痛點程度，以及未來是否會繼續使用 Grafana 系的後端。</p><p>要在臺灣機房內把這套採集管線跟 Loki、Prometheus 後端完整跑起來，VPS 規格、網路品質與磁碟 I&#x2F;O 都會直接影響整體的可觀測性效能。NCSE Network 提供臺灣是方電訊機房的 VPS 主機，Intel Gold CPU 與 NVMe SSD 的組合適合承擔多 pipeline 採集與時序資料庫高寫入量的負載，有相關自架監控需求歡迎參考 <a href="https://ncse.tw/">ncse.tw</a>。</p>]]>
    </content>
    <id>https://blog.ncse.tw/grafana-alloy-unified-telemetry-collector/</id>
    <link href="https://blog.ncse.tw/grafana-alloy-unified-telemetry-collector/"/>
    <published>2026-06-10T06:30:00.000Z</published>
    <summary>Promtail 已於 2026 年 3 月正式 EOL，Grafana Agent 早一年就被合併進 Alloy。本文拆解 Alloy 作為 OpenTelemetry Collector 發行版的設計、River 語法、Promtail 與 node_exporter 的遷移路線、跟原生 OTel Collector 的取捨，以及容器化部署時最常被踩的 WAL volume 問題。</summary>
    <title>裝了 Promtail 加 node_exporter 加 OTel Collector？Alloy 用一支 binary 把採集層收齊</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="OpenWorkers" scheme="https://blog.ncse.tw/tags/OpenWorkers/"/>
    <category term="Cloudflare Workers" scheme="https://blog.ncse.tw/tags/Cloudflare-Workers/"/>
    <category term="V8 isolate" scheme="https://blog.ncse.tw/tags/V8-isolate/"/>
    <category term="Rust" scheme="https://blog.ncse.tw/tags/Rust/"/>
    <category term="邊緣運算" scheme="https://blog.ncse.tw/tags/%E9%82%8A%E7%B7%A3%E9%81%8B%E7%AE%97/"/>
    <content>
      <![CDATA[<p>Cloudflare Workers 的開發體驗在無伺服器這個賽道幾乎沒有對手——一支 <code>wrangler deploy</code> 把 JavaScript 推上去，請求進來幾毫秒內冷啟、KV 跟 R2 binding 直接拿來用、Cron Trigger 排程內建。但這個體驗的代價是廠商鎖定到骨子裡：KV、R2、Durable Objects、Service Binding 全是 Cloudflare 的專有介面，搬離平台等同於整套程式碼重寫。</p><p>OpenWorkers 是 2026 年 1 月在 Hacker News 上冒出來的專案，用 Rust 直接綁 rusty_v8 重寫了一套相容於 Cloudflare Workers 程式設計模型的執行環境。和過去那些「Workers 替代品」最大的差別在於：它不是把 Node.js 或 Deno 套上 Workers 風格的 wrapper，而是真的下到 V8 isolate 這一層，把整套執行環境跟 binding 系統重新實作一次。對於想要 Workers 開發體驗但拒絕被綁在 Cloudflare 帳單上的團隊，這是 2026 年目前最值得追蹤的選項。</p><h2 id="V8-isolate-為什麼跟容器化函式不是同一回事"><a href="#V8-isolate-為什麼跟容器化函式不是同一回事" class="headerlink" title="V8 isolate 為什麼跟容器化函式不是同一回事"></a>V8 isolate 為什麼跟容器化函式不是同一回事</h2><p>把 OpenWorkers 跟「Lambda、Cloud Run、Knative 之類的容器化函式」放在一起比較會看不出差異——表面上都是「事件進來、跑一段程式碼、回應出去」。差別在隔離單位的層級。</p><p>Lambda 把每個函式打包成一個 container image，每次冷啟動要拉起一個 microVM（Firecracker），記憶體跟 CPU 都是獨立分配的。即使最快的冷啟也要幾十毫秒到幾百毫秒，常駐記憶體最少 128MB 起跳，閒置實例要錢。</p><p>V8 isolate 是另一種模型。一個作業系統 process 裡可以同時存在幾千個 isolate，每個 isolate 有獨立的 heap、獨立的 global 物件、彼此完全看不到對方的記憶體，但共用同一個 V8 引擎實例。冷啟動只是 instantiate 一個新的 isolate context，通常 5 毫秒以下完成；每個 isolate 的記憶體基線可以低到幾 MB。</p><p>這個架構在 Cloudflare 全球三百個 PoP 的規模下意義很大——每個 PoP 機房不必為了某個冷門 worker 預留一整顆 CPU。在自架單一機房的場景，意義會打折扣，但對於「同一臺機器要跑幾百個小型 JS 服務」的場景，isolate 的密度還是壓倒性贏過容器。</p><h2 id="為什麼是-rusty-v8-而不是-vm2、deno-core"><a href="#為什麼是-rusty-v8-而不是-vm2、deno-core" class="headerlink" title="為什麼是 rusty_v8 而不是 vm2、deno-core"></a>為什麼是 rusty_v8 而不是 vm2、deno-core</h2><p>OpenWorkers 一開始選用過 vm2 跟 deno-core，後來都換掉。這段技術選型的演進值得細看，因為它反映了 JavaScript 沙箱這條路線的現實。</p><p>vm2 是 Node.js 生態裡早期最常被推薦的 sandbox 套件，2023 年因為 CVE-2023-29017 與後續一連串無法修補的 escape 漏洞，原作者直接宣告 vm2 不再維護、不應該被當作安全邊界使用。任何把 vm2 當沙箱跑使用者程式碼的服務，本質上都是裸奔。</p><p>deno-core 是 Deno runtime 的核心模組，把 V8 跟 Tokio 整合在一起。技術上沒問題，但它把整個 Deno 的 Web API（包含 Deno.* 命名空間下的檔案系統存取、子程序、網路 syscall）都帶進來。如果只想要一個乾淨的 JavaScript 執行環境、自己決定要暴露什麼 binding，deno-core 等於先把不需要的東西全塞進來再想辦法關掉。</p><p>rusty_v8 是 Deno 團隊維護的另一個套件——純粹是 V8 的 Rust 綁定，沒有任何 runtime 概念。要建立 isolate、要暴露 host function、要從 Rust 端 throw exception 進 JavaScript，全部都得自己寫。但好處也很直接：執行環境裡有什麼，全是 OpenWorkers 自己決定的，沒有藏起來的 syscall、沒有預設啟用的 Web API。</p><p>對於要實作 Cloudflare Workers 相容介面的場景，這個選擇近乎必要。Cloudflare 對 worker 能存取什麼定義得很嚴格——只有 fetch、KV、R2、Durable Objects、Service Binding 這些明確的 binding，沒有 <code>process</code>、沒有 <code>fs</code>、沒有任意 socket。要做出語意一致的相容層，從 rusty_v8 起手反而比從 deno-core 砍 API 來得乾淨。</p><h2 id="binding-系統的相容深度"><a href="#binding-系統的相容深度" class="headerlink" title="binding 系統的相容深度"></a>binding 系統的相容深度</h2><p>OpenWorkers 目前實作的 binding 涵蓋了 Cloudflare Workers 八成的常用情境：</p><ul><li><strong>KV namespace</strong>：<code>env.MY_KV.get(key)</code> 這套介面直接相容，背後接 PostgreSQL 的 key-value table</li><li><strong>R2 bucket</strong>：對應到任何 S3 相容的物件儲存，可以指向 MinIO、Garage、Backblaze B2 或 AWS S3</li><li><strong>Service Binding</strong>：worker 之間互相呼叫，語意跟 Cloudflare 的 <code>env.OTHER_WORKER.fetch()</code> 一致</li><li><strong>環境變數與 Secrets</strong>：透過儀表板或 API 設定，注入到 <code>env</code> 物件</li><li><strong>Cron Trigger</strong>：支援 5 欄位跟 6 欄位 cron 語法，排程器是獨立元件</li></ul><p>Durable Objects 跟 WebSocket Hibernation 還在 roadmap 上，這兩個是 Cloudflare Workers 真正難複製的部分——Durable Objects 要解決全球一致性、WebSocket Hibernation 牽涉到把長連線從 isolate 解綁。在 OpenWorkers 補上這兩個之前，依賴 Durable Objects 做狀態管理的程式碼遷移過去會卡關。</p><p>Workers AI 那條線完全沒有對應——本來就是綁定 Cloudflare 自家的推論基礎設施，要替代得自己接 Ollama 或 vLLM。但這在自架場景反而是好事，可以選用本地模型而不是被綁在某家服務的 token 計費。</p><h2 id="部署架構與一份-docker-compose"><a href="#部署架構與一份-docker-compose" class="headerlink" title="部署架構與一份 docker-compose"></a>部署架構與一份 docker-compose</h2><p>OpenWorkers 把整套執行環境拆成幾個獨立服務：</p><ul><li><strong>nginx</strong>：前端代理，處理 TLS termination 跟 routing</li><li><strong>dashboard</strong>：Web UI，管理 worker、binding、log</li><li><strong>api</strong>：控制平面 API，提供部署、設定、metrics</li><li><strong>runners</strong>：實際執行 isolate 的工作節點，可以水平擴展</li><li><strong>scheduler</strong>：cron trigger 的排程器</li><li><strong>logs</strong>：執行日誌收集與查詢</li><li><strong>postgate</strong>：PostgreSQL 連線池與 binding 後端</li><li><strong>NATS</strong>：服務間訊息通訊</li></ul><p>聽起來很複雜，實際部署只需要一份 <code>docker-compose.yml</code> 跟一個 PostgreSQL 實例。官方倉庫把所有元件包成同一份 Compose，clone 下來改幾個環境變數就能跑：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/openworkers/openworkers-infra</span><br><span class="line"><span class="built_in">cd</span> openworkers-infra</span><br><span class="line"><span class="built_in">cp</span> .env.example .<span class="built_in">env</span></span><br><span class="line"><span class="comment"># 編輯 .env 設定 OPENWORKERS_DOMAIN、POSTGRES_PASSWORD</span></span><br><span class="line">docker compose up -d</span><br></pre></td></tr></table></figure><p>最小化部署的資源需求是 2 vCPU、4GB RAM、20GB SSD。runner 數量根據預期負載調整，每個 runner 預設能撐 50 到 100 個併發 isolate，視 worker 的記憶體使用而定。</p><p>跑起來之後從 dashboard 用 <code>wrangler</code> 相容的 CLI 部署：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npx @openworkers/cli login https://workers.example.com</span><br><span class="line">npx @openworkers/cli deploy</span><br></pre></td></tr></table></figure><p><code>wrangler.toml</code> 的格式基本沿用，只是 binding 的 namespace ID 改成 OpenWorkers 自己的識別碼。已經有 Cloudflare Workers 程式碼的團隊，遷移成本主要落在「重新建立 KV namespace、重新上傳 R2 bucket 內容」這塊，程式碼本身改動很少。</p><h2 id="沙箱不是安全邊界，這件事要先講清楚"><a href="#沙箱不是安全邊界，這件事要先講清楚" class="headerlink" title="沙箱不是安全邊界，這件事要先講清楚"></a>沙箱不是安全邊界，這件事要先講清楚</h2><p>OpenWorkers 在 Hacker News 上被問到最多的問題就是：V8 isolate 算不算 security boundary？開發者的回應很誠實——「the sandboxing is more about resource isolation than security-grade multi-tenancy」，要跑真正不可信的程式碼，請在 isolate 外面再包一層 container 或 VM。</p><p>這個立場其實是對的。V8 本身雖然有 sandbox，但 V8 的 CVE 歷史證明 isolate 之間的隔離不是設計來抵擋對抗性攻擊的——Cloudflare 之所以敢拿 isolate 跑多租戶，是因為他們有完整的內部威脅模型、加上 isolate 之外的多層次防禦（網路隔離、行為偵測、shadow execution）。</p><p>對自架使用者來說這意味著兩種使用模式：</p><p><strong>單租戶或受信任程式碼</strong>：自己團隊寫的 worker、內部服務、合作夥伴的程式碼，isolate 提供的資源隔離（CPU 100ms、記憶體 128MB）就夠了。</p><p><strong>多租戶或不可信程式碼</strong>：把每個租戶的 runner 跑在獨立的 container 裡，或更激進一點放進獨立的 VM。OpenWorkers 的 runner 設計成可以水平擴展，多開幾個 runner 把不同租戶綁到不同 runner 是合理的做法。</p><p>對於只想替自家業務找 Workers 自架方案的團隊，第一種模式直接適用，不必過度焦慮。</p><h2 id="自架邊緣這個說法本身要重新校準"><a href="#自架邊緣這個說法本身要重新校準" class="headerlink" title="自架邊緣這個說法本身要重新校準"></a>自架邊緣這個說法本身要重新校準</h2><p>社群討論裡有一派質疑：自架 Workers 失去了邊緣運算的意義，因為自家機房不可能有三百個 PoP。這個質疑技術上對，但搞錯了使用情境。</p><p>邊緣運算的核心好處有兩個：低延遲（地理上靠近使用者）、高吞吐（請求不需要回源）。Cloudflare Workers 之所以強，是因為這兩個好處同時有。自架到單一機房只能拿到第二個——請求不需要再回源到另一臺 origin server——對於 API gateway、邊緣鑑權、靜態內容前置處理這些場景，第二個好處的價值就夠了。</p><p>更具體的場景：把原本跑在後端的某些輕量邏輯（A&#x2F;B 測試分流、IP 黑白名單、bot 偵測、redirect 規則）從 application server 拆出來放進 worker，application server 就能專注處理真正的業務邏輯。這套架構在自架單機房下完全成立。</p><p>如果用戶分佈全球、低延遲是硬需求，那 Cloudflare 那種 anycast 邊緣本來就無法自架——這時要的是 Workers 那套，不是 OpenWorkers。</p><h2 id="該選-OpenWorkers-還是直接用-Cloudflare-Workers"><a href="#該選-OpenWorkers-還是直接用-Cloudflare-Workers" class="headerlink" title="該選 OpenWorkers 還是直接用 Cloudflare Workers"></a>該選 OpenWorkers 還是直接用 Cloudflare Workers</h2><p>放下意識形態的話，這個決策其實有清楚的判斷點。</p><p><strong>該選 OpenWorkers 的情境</strong>：</p><ul><li>已經被 Cloudflare 帳單嚇到——AI 生成的程式碼產生意外的高請求量這件事在 2025 到 2026 年發生過好幾次</li><li>監管或合規要求資料不能離境，臺灣金融業、政府專案常見</li><li>想把 Workers 當作 microservice 平台用，部署在自家機房裡</li><li>已經有 Postgres 跟物件儲存的基礎設施，希望把 worker 接到既有資料層</li></ul><p><strong>該繼續用 Cloudflare Workers 的情境</strong>：</p><ul><li>真的需要全球邊緣 PoP 的低延遲分佈</li><li>依賴 Durable Objects 做全球一致性狀態管理</li><li>流量規模還小，免費或 $5 方案就夠用，自架的維運成本不划算</li><li>重度使用 Workers AI、Vectorize、Hyperdrive 這些 Cloudflare 專屬服務</li></ul><p>中間有一個值得考慮的混合模式：把對延遲敏感、對外公開的入口放在 Cloudflare Workers，把對成本敏感、邏輯較重的後端 worker 放在 OpenWorkers，前後透過 fetch 或 service binding 串起來。這樣既享受到全球邊緣的低延遲，又把運算密集的部分搬回自家機房控制成本。</p><h2 id="把-Workers-那套搬回自家機房之後"><a href="#把-Workers-那套搬回自家機房之後" class="headerlink" title="把 Workers 那套搬回自家機房之後"></a>把 Workers 那套搬回自家機房之後</h2><p>無伺服器 JavaScript runtime 這條路線過去幾年被 Cloudflare、Vercel、Netlify、Deno Deploy 佔滿，「自架」幾乎不在這個賽道的選項裡。OpenWorkers 用 rusty_v8 直接從 V8 引擎這層重做，把 Cloudflare 那套開發體驗從「只能在他們的雲跑」拉到「可以在任何一臺 Linux 機器跑」，這在心智模型上是個明顯的改變。</p><p>要在自家機房或 VPS 上跑 OpenWorkers 平穩運轉，幾個基礎設施條件要先到位：穩定的 PostgreSQL（KV 跟 metadata 都靠它）、可靠的物件儲存（R2 binding 的替代）、有規模一點的話再加上獨立的 NATS 集群。CPU 要選單核心效能好的——V8 isolate 的 JIT 編譯對單核效能敏感，多核心數量遠不如時脈高來得有感。</p><p>NCSE Network 在臺灣的節點位於是方電訊機房，並提供採用 Intel Xeon Gold CPU 的 VPS 主機與 NVMe SSD 儲存；對於跑 OpenWorkers 這類重視單核效能與低延遲 I&#x2F;O 的 JavaScript runtime，是合適的底層配置。搭配臺灣 IP Transit 的線路品質，自架邊緣運算服務的本地延遲體驗會比放在海外節點明顯好。前往 <a href="https://ncse.tw/">ncse.tw</a> 了解規格與方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/openworkers-self-hosted-cloudflare-workers-rust-v8/</id>
    <link href="https://blog.ncse.tw/openworkers-self-hosted-cloudflare-workers-rust-v8/"/>
    <published>2026-06-09T06:00:00.000Z</published>
    <summary>OpenWorkers 在 2026 年 1 月釋出，用 Rust 直接綁 rusty_v8 重寫 JavaScript runtime，提供與 Cloudflare Workers 相容的 KV、R2、Service Binding 介面。本文拆解架構選擇、V8 isolate 對比容器化函式的優勢、單一 PoP 自架邊緣的真實取捨，以及在 VPS 上的部署細節。</summary>
    <title>vm2 死透、Deno Deploy 是 SaaS：OpenWorkers 用 rusty_v8 把 Cloudflare Workers 那套搬回自家機房</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Atuin" scheme="https://blog.ncse.tw/tags/Atuin/"/>
    <category term="Shell" scheme="https://blog.ncse.tw/tags/Shell/"/>
    <category term="Linux" scheme="https://blog.ncse.tw/tags/Linux/"/>
    <category term="開發工具" scheme="https://blog.ncse.tw/tags/%E9%96%8B%E7%99%BC%E5%B7%A5%E5%85%B7/"/>
    <category term="SQLite" scheme="https://blog.ncse.tw/tags/SQLite/"/>
    <content>
      <![CDATA[<p><code>~/.bash_history</code> 跟 <code>~/.zsh_history</code> 是 Unix 工具鏈裡少數三十年沒進步過的東西。同時開兩個終端，後關的那個會覆蓋掉先關的歷史；不記時間、不記工作目錄、不記退出碼；搜尋只能靠 <code>Ctrl+R</code> 一條條往回翻，跨機器同步基本上是天方夜譚。多數人選擇加上 <code>HISTSIZE=100000</code>、<code>shopt -s histappend</code> 然後假裝問題解決了。</p><p>Atuin 從這個前提重寫了整個流程。本地用 SQLite 儲存，每筆紀錄帶上 cwd、執行時間、耗時、退出碼、session ID 和主機名；搜尋是 fuzzy match 加上多欄位過濾；同步是選擇性開啟的端對端加密，伺服器可以自架。整個工具用 Rust 寫，單一 binary，跨 bash、zsh、fish、nushell、xonsh、PowerShell。</p><h2 id="預設-shell-history-到底缺什麼"><a href="#預設-shell-history-到底缺什麼" class="headerlink" title="預設 shell history 到底缺什麼"></a>預設 shell history 到底缺什麼</h2><p>舉個情境：上週在某台伺服器上跑了一條長到不像話的 <code>ffmpeg</code> 指令解 codec 問題，這週另一台機器又碰到，但記不得參數。打開 <code>.bash_history</code>，看到的只是一行純文字，沒有時間、沒有目錄、沒有結果。如果中間還曾把 shell 卡死強制重開，這行甚至可能根本沒寫進檔案。</p><p>Atuin 改成事件式記錄，每條指令在開始執行時就先寫進 SQLite，執行完成後再回頭補上耗時和退出碼。資料結構大致是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">id, timestamp, duration, exit, command,</span><br><span class="line">cwd, hostname, session, deleted_at</span><br></pre></td></tr></table></figure><p>這意味著可以下這種查詢：「上個月在 prod1 主機的 <code>/etc/nginx/</code> 底下執行過、退出碼非 0 的指令」。Atuin CLI 直接提供 <code>atuin search --cwd /etc/nginx --exit 1 --before &quot;1 month ago&quot;</code> 這種組合條件。</p><p>對團隊更實際的價值在於失敗指令的可追溯性。退出碼存下來之後，事後追查「那次部署到底哪一步噴錯」不必再翻 tmux 的卷軸緩衝區。</p><h2 id="E2E-加密同步：伺服器看不到明文"><a href="#E2E-加密同步：伺服器看不到明文" class="headerlink" title="E2E 加密同步：伺服器看不到明文"></a>E2E 加密同步：伺服器看不到明文</h2><p>Atuin 的同步協定值得單獨講。每個帳號註冊時會在客戶端生成一組對稱金鑰，這把金鑰用密碼派生的另一把金鑰加密後，存在本地的 <code>~/.local/share/atuin/key</code>。所有要上傳的歷史紀錄都先以這把金鑰做 XChaCha20-Poly1305 加密，伺服器拿到的只是密文跟一個用來去重的 hash。</p><p>換句話說，自架伺服器看到的歷史紀錄全是亂碼，連伺服器管理員（也就是使用者本人）都無法從資料庫倒推內容——除非同一把金鑰還在某台機器上。這對含有密碼、token、API key 的指令歷史是必要的設計，畢竟很多人會把 secret 直接打在 command line 上。</p><p>副作用是換機器時需要做 <code>atuin key</code> 把現有金鑰印出來，到新機器跑 <code>atuin login</code> 時貼進去。忘記備份金鑰等於整份雲端歷史變垃圾資料，這點在第一次設定時就要警覺。</p><h2 id="自架-sync-伺服器：PostgreSQL-加一支-binary"><a href="#自架-sync-伺服器：PostgreSQL-加一支-binary" class="headerlink" title="自架 sync 伺服器：PostgreSQL 加一支 binary"></a>自架 sync 伺服器：PostgreSQL 加一支 binary</h2><p>官方雲端服務免費，但要有完整控制權還是建議自架。Atuin server 需要 PostgreSQL 14 以上，本身就是一支 Rust binary 加一個薄薄的 HTTP API。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">atuin:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/atuinsh/atuin:latest</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">server</span> <span class="string">start</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:8888:8888&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">ATUIN_HOST:</span> <span class="string">&quot;0.0.0.0&quot;</span></span><br><span class="line">      <span class="attr">ATUIN_PORT:</span> <span class="number">8888</span></span><br><span class="line">      <span class="attr">ATUIN_OPEN_REGISTRATION:</span> <span class="string">&quot;false&quot;</span></span><br><span class="line">      <span class="attr">ATUIN_DB_URI:</span> <span class="string">postgres://atuin:$&#123;DB_PASSWORD&#125;@db/atuin</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">db:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:16.3-alpine</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">POSTGRES_USER:</span> <span class="string">atuin</span></span><br><span class="line">      <span class="attr">POSTGRES_PASSWORD:</span> <span class="string">$&#123;DB_PASSWORD&#125;</span></span><br><span class="line">      <span class="attr">POSTGRES_DB:</span> <span class="string">atuin</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./pgdata:/var/lib/postgresql/data</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;pg_isready&quot;</span>, <span class="string">&quot;-U&quot;</span>, <span class="string">&quot;atuin&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">5</span></span><br></pre></td></tr></table></figure><p>兩個細節容易踩到。第一，<code>ATUIN_OPEN_REGISTRATION</code> 預設是 <code>true</code>，意思是任何人知道網址就能註冊帳號開始用你的 PostgreSQL。除非有意對外開放，否則第一件事就是改成 <code>false</code>，註冊完自己的帳號再關掉。</p><p>第二，伺服器不終結 TLS，前面一定要放反向代理。Caddy 或 Nginx 都行，把 <code>8888</code> 綁在 loopback、上層接 HTTPS 才是安全配置。直接把 <code>8888</code> 暴露在公網是常見錯誤。</p><p>客戶端設定走 <code>~/.config/atuin/config.toml</code>：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">sync_address</span> = <span class="string">&quot;https://atuin.example.com&quot;</span></span><br><span class="line"><span class="attr">sync_frequency</span> = <span class="string">&quot;5m&quot;</span></span><br><span class="line"><span class="attr">auto_sync</span> = <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>註冊跟登入：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">atuin register -u user -e me@example.com</span><br><span class="line">atuin import auto      <span class="comment"># 從現有 shell history 一次性匯入</span></span><br><span class="line">atuin <span class="built_in">sync</span></span><br></pre></td></tr></table></figure><h2 id="v18-13-加進來的搜尋功能"><a href="#v18-13-加進來的搜尋功能" class="headerlink" title="v18.13 加進來的搜尋功能"></a>v18.13 加進來的搜尋功能</h2><p>2026 年的 v18.13 是把搜尋體驗推上另一個層級的版本。最值得提的兩點：</p><p><strong>自訂鍵位</strong>：TUI 模式的鍵位過去是寫死的，這次終於允許在 config 裡完全自訂。對習慣 Vim 風格綁定的使用者來說是大解放。</p><p><strong>Smart sort 改善</strong>：搜尋結果的排序權重重新調校，加上「在同樣的 cwd 用過幾次」的因素。也就是說在 <code>/var/log</code> 底下搜 <code>grep</code> 時，跟在 <code>~/project</code> 底下搜 <code>grep</code> 會看到完全不同的順序，命中率比之前高出一截。</p><p>更值得期待的是 PTY proxy 功能進入 beta。Atuin 開始能直接記錄整個 PTY session 的輸出，而不只是指令本身，未來會成為 Atuin Desktop 的可執行 runbook 基礎建設。</p><h2 id="Atuin-Desktop-與-runbook：方向轉變的訊號"><a href="#Atuin-Desktop-與-runbook：方向轉變的訊號" class="headerlink" title="Atuin Desktop 與 runbook：方向轉變的訊號"></a>Atuin Desktop 與 runbook：方向轉變的訊號</h2><p>2026 年 Atuin 把 Desktop 應用程式開源化，並推出重新設計的 runbook 執行引擎。Runbook 在這裡指的是一份可執行的文件——把指令、變數、輸出全部嵌進 Markdown，可以一格一格執行，狀態跨重啟保留。</p><p>這條產品線目前比較適合運維場景：用來把「機器壞掉時要跑的處置流程」寫成可重複執行的文件。Workspace 可以選擇雲端同步或純本地由 Git 管理，後者對需要納入版本控制的 SRE 團隊比較合適。</p><p>對只想要好用 shell history 的使用者來說，Desktop 是錦上添花，不必非裝不可。CLI 本身已經足夠完整。</p><h2 id="適合自架的場景與不適合的情況"><a href="#適合自架的場景與不適合的情況" class="headerlink" title="適合自架的場景與不適合的情況"></a>適合自架的場景與不適合的情況</h2><p>自架 Atuin 的甜蜜點是個人或小團隊在管理數台到數十台 VPS。每天在多台機器之間切換，歷史紀錄分散是真實痛點，自架可以把這份資料完全留在自己掌控的伺服器上。</p><p>不太適合的情況有兩種。一是純粹只用一台機器，那 SQLite 本地版就夠了，連帳號都不必註冊。二是組織有嚴格的合規限制要求每條指令必須有稽核軌跡——Atuin 不是為了稽核設計的，使用者隨時可以刪除自己的歷史紀錄，這是個人工具的定位，不是 SIEM。</p><p>備份策略上，PostgreSQL 那邊用 <code>pg_dump</code> 加異地儲存即可。但更關鍵的是金鑰備份：每個用戶端的 <code>~/.local/share/atuin/key</code> 應該離線存一份，密碼管理器或實體紙本都行。伺服器資料庫掛了可以從 dump 還原，金鑰沒了就真的沒了。</p><hr><p>shell history 看起來小，每天用 shell 工作的人卻會被它的爛設計拖累一輩子。把 Atuin 跟自架同步服務搭起來，等於把過去散落在各台機器、各個檔案的指令歷史，整合成一份可搜尋、可分析、加密留存的個人知識庫。</p><p>NCSE Network 的 VPS 採 Intel Gold CPU 與 NVMe SSD，位於臺灣是方電訊機房，跑 Atuin server 加 PostgreSQL 的資源需求極低，最小規格方案就綽綽有餘，延遲低、頻寬穩，適合作為個人開發工具鏈的長期落腳處。詳情可到 <a href="https://ncse.tw/">ncse.tw</a> 查看方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/atuin-shell-history-self-hosted-sync-e2ee/</id>
    <link href="https://blog.ncse.tw/atuin-shell-history-self-hosted-sync-e2ee/"/>
    <published>2026-06-08T02:30:00.000Z</published>
    <summary>bash 預設的 .bash_history 是個壞了三十年的功能——只記指令、會被覆蓋、跨機器無法同步。Atuin 用 SQLite 接管本地搜尋，再以端對端加密同步到自架伺服器，本文拆解儲存格式、加密模型與自架部署。</summary>
    <title>shell history 該有的樣子：Atuin 把每條指令連同 cwd、退出碼、耗時全收進 SQLite</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="OpenCloud" scheme="https://blog.ncse.tw/tags/OpenCloud/"/>
    <category term="自架雲碟" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E9%9B%B2%E7%A2%9F/"/>
    <category term="Nextcloud 替代" scheme="https://blog.ncse.tw/tags/Nextcloud-%E6%9B%BF%E4%BB%A3/"/>
    <category term="Go" scheme="https://blog.ncse.tw/tags/Go/"/>
    <category term="檔案同步" scheme="https://blog.ncse.tw/tags/%E6%AA%94%E6%A1%88%E5%90%8C%E6%AD%A5/"/>
    <content>
      <![CDATA[<p>自架雲端硬碟這件事，過去十年的答案幾乎只有一個：Nextcloud。它能跑、社群活躍、app store 裡什麼都有，但代價也很實在——一支 PHP 巨石應用、外加 MariaDB 或 PostgreSQL、外加 Redis 快取，閒置狀態就把一臺 2GB VPS 吃掉一半。對只想找個地方同步家裡那臺 NAS 跟手機照片的使用者來說，這個門檻不低。</p><p>OpenCloud 是 2025 年才出現的另一個選項。它從 ownCloud Infinite Scale（OCIS）分叉而來，用 Go 寫、不需要資料庫、把整個雲碟拆成幾十個 gRPC 微服務。背後的故事比技術細節更值得先講：寫出 OCIS 那批工程師，2025 年 1 月整批離開了 ownCloud 的母公司 Kiteworks。</p><h2 id="為什麼-OCIS-的人會走"><a href="#為什麼-OCIS-的人會走" class="headerlink" title="為什麼 OCIS 的人會走"></a>為什麼 OCIS 的人會走</h2><p>ownCloud 在 2023 年被資安公司 Kiteworks 併購。對外的說法是「強化企業安全產品線」，對內的氣氛則完全不同——根據 heise 報導，原 ownCloud 的員工事後匿名表示，併購之後管理風格轉向，Kiteworks 沒有對 OCIS 的長期投入做出具體承諾，而 OCIS 是這群工程師花了好幾年才從 PHP 重寫到 Go 的核心產品。</p><p>2025 年 1 月 22 日，OpenCloud GmbH 正式營運，創辦團隊大約十五位前 OCIS 工程師，全數加入柏林老牌開源服務商 Heinlein Group 旗下。這群人原本就是 OCIS 程式碼最熟的人，分叉出來之後等於把整條 Go 重寫的核心知識帶離原公司。Kiteworks 隨即放話要採取法律行動，但到 2026 年中，威脅並沒有實質落地，反而是 Kiteworks 自己在 5 月成立了 OSPO，把 ownCloud 旗下超過一百個專案改授權成 Apache 2.0，看起來像是被社群輿論逼著轉向。</p><p>對使用者要評估的事情很單純：OCIS 與 OpenCloud 在程式碼層面有共同起點，但接下來的方向會分開走。從 commit 活躍度與 release 節奏來看，OpenCloud 那一側的迭代速度明顯更快。</p><h2 id="沒有資料庫的雲端硬碟"><a href="#沒有資料庫的雲端硬碟" class="headerlink" title="沒有資料庫的雲端硬碟"></a>沒有資料庫的雲端硬碟</h2><p>OpenCloud 在架構選擇上最違反直覺的一點是：沒有資料庫。</p><p>Nextcloud 的所有 metadata——誰擁有哪個檔案、誰把哪個資料夾分享給誰、版本歷史、活動紀錄——都塞在 MariaDB 或 PostgreSQL 裡。檔案內容放在磁碟、metadata 放在資料庫，兩邊要持續同步，備份時兩邊都要備、還原時也得確保時間點一致。檔案數量上去之後，光是 <code>oc_filecache</code> 這張表就能膨脹到幾十 GB，資料庫的查詢效能變成系統瓶頸。</p><p>OpenCloud 用兩個叫做 PosixFS 與 DecomposedFS 的儲存驅動把這件事拆掉。PosixFS 直接把檔案以使用者在介面上看到的目錄結構寫到磁碟，連 metadata 也是用副檔案的形式存在同一個資料夾——<code>.foo.txt.metadata</code> 之類的伴生檔。DecomposedFS 則是另一種路線：用內容定址的方式拆解儲存，blob 跟 metadata 分離，可以把 blob 部分接到 S3 後端（也就是 DecomposedS3 驅動），讓真正的檔案內容放在物件儲存上，metadata 留在本地。</p><p>實務上的差別是備份這件事突然變簡單。<code>tar</code> 整個資料目錄、<code>restic</code> 增量備份、<code>rsync</code> 到異地——任何一種檔案層級的備份方案都能直接用，不用先 <code>pg_dump</code> 再煩惱資料庫快照與檔案系統快照的時間點是否一致。對自架在 VPS 上、只有一個人在維運的場景，這個改變很有感。</p><h2 id="Go-微服務不只是宣傳詞"><a href="#Go-微服務不只是宣傳詞" class="headerlink" title="Go 微服務不只是宣傳詞"></a>Go 微服務不只是宣傳詞</h2><p>OpenCloud 把整個服務拆成大約三十個獨立的微服務：身分驗證、權限檢查、Web 介面、WebDAV、搜尋索引、縮圖產生、防毒掃描，每個都是獨立的 Go 程序，內部用 gRPC 通訊。預設模式下這些微服務全跑在同一支 <code>opencloud</code> binary 裡，由內建的 supervisor 啟動成不同的 goroutine。</p><p>這個設計的好處是延展性。當使用者數量大到單機撐不住的時候，可以把吃資源的服務——例如 thumbnails、search 或 antivirus——拆到另一臺機器跑，前面的服務發現會自動把流量導過去。Nextcloud 在這個層面只能靠橫向擴展整支 PHP 應用，配合 Redis cluster 跟外接資料庫複本，整個拓樸複雜得多。</p><p>對只想跑個十幾人團隊的場景，分散式部署的能力用不上，但 Go 的記憶體效率還是體現得出來。OpenCloud 在小型部署的閒置 RAM 大約落在 400 到 600MB，Nextcloud 加 PHP-FPM 加 MariaDB 加 Redis 通常要 1.5GB 起跳。</p><p>底層的 gRPC 通訊框架沿用了 CERN 主導的 REVA 專案——這是 CS3（Cloud Storage Services for Sync and Sharing）這套 API 規範的參考實作。OCIS 當年選 Go 重寫的時候就用了 REVA，OpenCloud 把它一起帶過來並繼續維護。這意味著 OpenCloud 跟 Nextcloud 在 EU 學研機構主導的開源檔案儲存生態圈裡是同一條船上的，不是孤立分叉。</p><h2 id="該選-OpenCloud-還是-Nextcloud"><a href="#該選-OpenCloud-還是-Nextcloud" class="headerlink" title="該選 OpenCloud 還是 Nextcloud"></a>該選 OpenCloud 還是 Nextcloud</h2><p>Nextcloud 的優勢非常清楚：app store 裡有幾百個外掛、能直接當作 Google Workspace 的替代品、行事曆與聯絡人與 Talk 視訊與筆記與 Deck 看板全都整合好。對一個想徹底擺脫 Microsoft 365 或 Google Workspace 的小型團隊，Nextcloud 還是目前唯一能撐住整個工作流程的方案。</p><p>OpenCloud 不打算往這個方向走。它的定位回到「把檔案管理跟分享這件事做好」，行事曆跟聯絡人用獨立的 Radicale 容器處理、文件編輯接 Collabora Online、全文搜尋接 Apache Tika，這些功能都不在主程式裡。對單純想要一個「家庭照片同步 + 跟外部分享連結 + 偶爾在瀏覽器裡編個文件」的使用者，這個取捨剛好對：少裝一堆用不到的東西，主程式更輕、升級更快、出問題更好除錯。</p><p>爭議點落在文件協作上。Nextcloud 內建 Office 整合（OnlyOffice 或 Collabora），文件直接在介面裡開、即時多人協作就能用。OpenCloud 也支援 Collabora，但需要自己另起一個容器、設好 WOPI 路由，部署時得多一些設定。對團隊協作場景，Nextcloud 還是省事；對個人或家庭使用，OpenCloud 反而比較適合 VPS 的記憶體預算。</p><p>那種「兩個都試試看再決定」的建議在這裡不適用。要做的判斷其實只有一個：「需不需要 app store 裡那一堆功能」。答案是的話選 Nextcloud，不是的話 OpenCloud 在資源、效能、備份維運上都明顯領先。</p><h2 id="部署到-VPS-上的實務細節"><a href="#部署到-VPS-上的實務細節" class="headerlink" title="部署到 VPS 上的實務細節"></a>部署到 VPS 上的實務細節</h2><p>官方提供的 <code>opencloud-compose</code> 倉庫把整套 stack 包成 Docker Compose，包括 Traefik、Keycloak、Collabora、Tika、ClamAV、Radicale。對中小型部署，建議只挑必要的元件起來——Traefik 跟 OpenCloud 本體幾乎不能少，Keycloak 可以用內建的 LibreGraph Connect 取代，Collabora 跟 ClamAV 如果用不到先關掉。</p><p>記憶體規劃上，2GB VPS 跑得起來但會有點吃緊，建議從 4GB 起跳；如果同時要開 Collabora Online，記憶體至少 6GB——Collabora 自己一個容器空閒就 800MB 到 1GB。</p><p>儲存層的 IOPS 比容量更重要。OpenCloud 在 PosixFS 模式下會頻繁讀寫小型 metadata 檔，傳統機械硬碟的 OLTP 性質負載撐不住，建議只跑在 NVMe SSD 上。如果是要存大量照片或影片但磁碟不夠，DecomposedS3 把 blob 接到 S3 後端是合理的設計——前面接 Garage 或 MinIO 替代品都行，metadata 留本地、blob 推到物件儲存。</p><p>外部反向代理建議直接用 Caddy 或 Traefik 自動處理 TLS 憑證。Nginx 也可以，但 OpenCloud 的 WebSocket 跟 gRPC 流量得手動調 timeout 跟 buffer，比較囉嗦。</p><h2 id="什麼情況不該選-OpenCloud"><a href="#什麼情況不該選-OpenCloud" class="headerlink" title="什麼情況不該選 OpenCloud"></a>什麼情況不該選 OpenCloud</h2><p>2026 年中的時間點，OpenCloud 仍然是一個年輕專案，不適合幾種場景。</p><p>需要既有 Nextcloud 外掛的——例如 Nextcloud Talk、Deck、Notes、Forms——OpenCloud 沒有對應品。要等社群把這些功能補齊，短期內看不到。</p><p>組織規模超過幾百人、需要正式 SLA 跟商業支援的——Heinlein Group 雖然有提供商業合約，但相較 Kiteworks 旗下的 ownCloud 與 Nextcloud GmbH，市佔率跟服務網路規模都還小。法務團隊要審第三方廠商風險時，這個維度通常過不去。</p><p>希望大量重用既有 LDAP&#x2F;AD 整合設定的——OpenCloud 用 OpenID Connect 為主，雖然可以透過 Keycloak 接 LDAP，多了一層元件要維護。原本就跑 Nextcloud 加 LDAP 的環境直接遷移意義不大。</p><p>剩下的場景——個人雲端、家庭使用、十人以下小團隊、把 S3 後端接到自架物件儲存的中等規模部署——OpenCloud 在資源效率與架構乾淨度上的優勢足以讓它成為新建置時的首選。</p><h2 id="自架雲碟的選擇終於不只一條"><a href="#自架雲碟的選擇終於不只一條" class="headerlink" title="自架雲碟的選擇終於不只一條"></a>自架雲碟的選擇終於不只一條</h2><p>自架雲端硬碟這個賽道過去十年沒怎麼變過：Nextcloud 一家獨大、Seafile 守著「同步速度快」的差異點、ownCloud Infinite Scale 的 Go 重寫雖然進度不錯但長期方向不明。2025 年 OCIS 原班人馬整批跳到 OpenCloud，把這條路線重新清出來，等於替自架族群多了一個架構選擇——不再只是「PHP 還是 PHP」的二選一。</p><p>要把 OpenCloud 跑得順，底層 VPS 的 NVMe 效能跟記憶體預算都得算到位。NCSE Network 在臺灣是方電訊機房提供 Intel Gold CPU 與 NVMe SSD 的 VPS 方案，對這類自架雲碟負載的 IOPS 跟低延遲需求相對合適，需要 6GB 以上記憶體跑完整 OpenCloud + Collabora 組合的使用者，可以參考 ncse.tw 上的 VPS 規格選擇。</p>]]>
    </content>
    <id>https://blog.ncse.tw/opencloud-self-hosted-file-sync-nextcloud-alternative-go/</id>
    <link href="https://blog.ncse.tw/opencloud-self-hosted-file-sync-nextcloud-alternative-go/"/>
    <published>2026-06-07T02:00:00.000Z</published>
    <summary>2025 年 1 月，寫出 ownCloud Infinite Scale 的工程師整批離開 Kiteworks，在柏林 Heinlein 旗下成立 OpenCloud。本文解析它的 Go 微服務架構、無資料庫設計、與 Nextcloud 的取捨，以及在 VPS 上部署的實務考量。</summary>
    <title>OCIS 原班工程師整批跳到 OpenCloud：自架雲碟這條路線，這次用 Go 重新走一遍</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="AI Agent" scheme="https://blog.ncse.tw/tags/AI-Agent/"/>
    <category term="Steel Browser" scheme="https://blog.ncse.tw/tags/Steel-Browser/"/>
    <category term="Browserbase" scheme="https://blog.ncse.tw/tags/Browserbase/"/>
    <category term="Playwright" scheme="https://blog.ncse.tw/tags/Playwright/"/>
    <content>
      <![CDATA[<p>AI Agent 在 2025 年從「會聊天」進化到「會點按鈕」之後，背後跑的是哪一台 Chrome 開始變成正式的工程問題。Anthropic 的 Computer Use、OpenAI 的 Operator、開源的 browser-use 與 Stagehand，這些 framework 共同的前提都是有一個能被 LLM 控制、能維持 session 狀態、能應付網站反爬的瀏覽器執行環境。本機跑 Playwright 寫範例還行，正式服務要服務多個 Agent 並發、保留 cookie、過 Cloudflare 的 challenge，這層基礎建設就得獨立出來。</p><p>Browserbase 在 2024 年用 SaaS 模式接管了這個位置，每月訂閱費用按並發 session 計算，定價對個人開發者偏貴。Steel Browser 在 2024 年底由 steel-dev 團隊以 Apache 2.0 釋出，把同一套功能——session 管理、stealth、proxy chain、CDP 控制——做成可以一支 <code>docker run</code> 拉起來的服務。2026 年 3 月的 v0.5.2 把請求追蹤搬到 Chrome Network domain、4 月的 v0.5.3 補上 session lifecycle hook 與 fullscreen mode，自架版本已經穩定到可以放進正式 Agent 工作流。</p><h2 id="AI-Agent-為什麼不能直接用-Playwright-就好"><a href="#AI-Agent-為什麼不能直接用-Playwright-就好" class="headerlink" title="AI Agent 為什麼不能直接用 Playwright 就好"></a>AI Agent 為什麼不能直接用 Playwright 就好</h2><p>Playwright 跟 Puppeteer 在 CI&#x2F;CD 跑 E2E 測試的世界已經是預設選項，但把它直接搬去支撐 Agent 工作流會撞到幾個本質上的不匹配。</p><p>第一個是 session 的生命週期。E2E 測試一輪跑完瀏覽器就關，狀態不需要保留；Agent 的場景剛好反過來——使用者半小時前登入了某個 SaaS，現在要求 Agent 接著操作，cookie、localStorage、登入態都得跨多次請求保留。Playwright 的 <code>BrowserContext</code> 是設計給單一進程內用的物件，要做到「Agent 第一次呼叫建立 session，半小時後第二次呼叫接續同一個瀏覽器」就得自己做 session pool、context 持久化、超時回收，這套東西寫起來不複雜，但每個團隊都得從頭刻一遍。</p><p>第二個是反偵測。Cloudflare、PerimeterX、DataDome 在 2024 年之後對 headless Chrome 的識別率高得很——<code>navigator.webdriver</code>、CDP runtime detection、字型清單、WebGL renderer 一堆指紋，原生 Playwright 走過 Cloudflare Turnstile 的成功率大約只有兩到三成。社群有 <code>playwright-extra</code> 加 stealth plugin 可以打補丁，但這個生態系本來就有點半維護狀態，新版本的 Chrome 出來後 stealth 規則跟不上是常態。</p><p>第三個是 IP 跟 proxy。Agent 工作流常常要訪問跟使用者地理位置一致的網站，住宅代理或 ISP 代理的整合得自己接 API、做 IP 健康度判斷、按請求輪換。這部分沒有開源套件能直接吃下來，多數團隊的做法是寫一坨 middleware 然後祈禱代理供應商的 API 不要改。</p><p>把這三件事疊在一起，就會得出「需要一層介於 Playwright 跟 Agent 之間的服務」的結論。Browserbase 看準的是這個位置，Steel 補上的是同一個位置的開源版本。</p><h2 id="Steel-跟-Browserbase-拿的是同一塊地"><a href="#Steel-跟-Browserbase-拿的是同一塊地" class="headerlink" title="Steel 跟 Browserbase 拿的是同一塊地"></a>Steel 跟 Browserbase 拿的是同一塊地</h2><p>兩個專案的功能對應幾乎是一比一。Browserbase 提供 hosted browser session、stealth、residential proxy、extension 載入、recording；Steel 提供同一組功能，差別在於前者按 session-minute 收費，後者讓使用者自己跑在 VPS 上。</p><p>API 設計上 Steel 對齊了業界已經形成的習慣：<code>POST /v1/sessions</code> 建立 session 回傳 connection URL，client 拿這個 URL 用 Playwright 或 Puppeteer 的 <code>connect</code> 模式接上去；session 在伺服器端維持，TTL 預設一小時，可以隨時用 <code>DELETE /v1/sessions/:id</code> 收掉。對於已經寫好 Playwright 程式碼的團隊，遷移成本只是把 <code>chromium.launch()</code> 換成 <code>chromium.connectOverCDP(steelConnectionUrl)</code>，腳本內部的 selector、wait、action 完全不用改。</p><p>stealth 是 Steel 比較有差異化的部分。它預設整合 <code>puppeteer-extra-plugin-stealth</code> 的維護分支，把 <code>navigator.webdriver</code>、Chrome runtime、permissions API、WebGL vendor 這些常見指紋打掉；CAPTCHA 的部分內建可以串第三方 solver 服務（2captcha、CapMonster），對於 reCAPTCHA v2 跟 hCaptcha 大致夠用。Cloudflare Turnstile 沒有任何瀏覽器自動化能 100% 過——這是設計上的對抗——但 Steel 的內建配置實測通過率比裸 Playwright 高出一截。</p><p>proxy 這層 Steel 做了 BYOP（Bring Your Own Proxy）跟 managed 兩種模式。BYOP 是給 session 設定 proxy URL 就好，managed 則是直接用 Steel 官方的 residential proxy（這部分是雲端版才有的 add-on）。自架的場景大多走 BYOP，把 Bright Data、Oxylabs 或 Soax 的 endpoint 餵進去，IP 輪換交給上游處理。</p><h2 id="架構上-Steel-是怎麼運作的"><a href="#架構上-Steel-是怎麼運作的" class="headerlink" title="架構上 Steel 是怎麼運作的"></a>架構上 Steel 是怎麼運作的</h2><p>Steel 自架版本拆成兩個容器：API server 跟 UI dashboard，分別是 <code>ghcr.io/steel-dev/steel-browser-api</code> 跟 <code>ghcr.io/steel-dev/steel-browser-ui</code>。Chrome 是內建在 API 容器裡的，不需要另外裝。整套服務暴露三個 port——API 在 3000、UI 在 5173、CDP debugging 在 9223——9223 是 client 拿來連 Chrome DevTools Protocol 的，是整個自架部署最容易設定錯的地方。</p><p>session 的生命週期由幾個 hook 控制。v0.5.3 補上的 <code>onSessionStart</code>、<code>onBeforeSessionEnd</code>、<code>onAfterSessionEnd</code> 讓使用者可以在 session 建立時注入 cookie 或 localStorage、在 session 結束前 dump 出狀態做下次續用、在 session 完全收掉後清理外部資源。這套設計把「session 持久化」這件事從應用層搬進服務層，Agent 框架不需要自己管 BrowserContext 的儲存。</p><p>請求追蹤從 page event 換成 Chrome 的 Network domain，這是 v0.5.2 的核心變動。差別是後者能完整捕捉到 fetch、XHR、WebSocket、redirect 鏈，前者會漏掉一些非主 frame 的請求。對於需要 inspection 跟 replay 的 Agent debugging 場景，Network domain 是必要的。</p><p>資源消耗的數字大致是這樣：閒置一個 Steel API 容器約佔 800 MB 記憶體，每個活躍 session 額外吃 250 到 400 MB；CPU 在 session 沒做密集渲染時幾乎可忽略，碰到複雜 SPA 全力渲染瞬間會把單核打滿幾秒。實務上一台 4 vCPU、8 GB RAM 的 VPS 可以同時跑 8 到 12 個 session，再多就會碰到記憶體跟 swap 的瓶頸。</p><h2 id="自架部署的實務細節"><a href="#自架部署的實務細節" class="headerlink" title="自架部署的實務細節"></a>自架部署的實務細節</h2><p>最直接的部署是 docker-compose：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">api:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/steel-dev/steel-browser-api:latest</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:3000:3000&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:9223:9223&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">HOST=0.0.0.0</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">NODE_ENV=production</span></span><br><span class="line">    <span class="attr">shm_size:</span> <span class="string">2gb</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">ui:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/steel-dev/steel-browser-ui:latest</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:5173:80&quot;</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">API_URL=http://api:3000</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">api</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><p>幾個重點。一是 port 一律綁到 <code>127.0.0.1</code>，再透過 SSH tunnel 或反向代理對外，因為 Steel 的 CDP debug port 一旦對公網開啟，等同於讓任何人遠端控制這台機器上的瀏覽器，做什麼都有可能。</p><p>二是 <code>shm_size</code> 一定要拉到 2 GB 以上。Chrome 在 <code>/dev/shm</code> 放共享記憶體，Docker 預設只給 64 MB，session 一開分頁就會看到 <code>Tab crashed</code> 或 <code>Cannot allocate memory</code>。</p><p>三是 Cloudflare Tunnel、Caddy、Tailscale 任選一個做對外接入點。前面再擺一層 OAuth proxy（oauth2-proxy 或 Pomerium）擋掉未授權的 API 呼叫，避免 API key 一旦外洩等同送人一台無限制的雲端 Chrome。</p><p>四是 session 數量上限的調整。Steel 預設沒有硬限制，但實務上得在 application 層加一個 semaphore 控制併發，否則 LLM Agent 偶爾失控狂建 session，記憶體很快就會掛。</p><p>對於要打外網的 Agent 工作流，proxy 用 Steel 的 BYOP API 在每個 session 建立時傳入，不要寫死在 Steel 設定檔。原因是不同 Agent 場景需要的 IP 地理位置不同——抓臺灣本地網站要用臺灣 IP、抓美國 SaaS 要用美國住宅 IP——按 session 傳 proxy URL 才能做到動態切換。</p><h2 id="跟-browser-use、Stagehand-接起來"><a href="#跟-browser-use、Stagehand-接起來" class="headerlink" title="跟 browser-use、Stagehand 接起來"></a>跟 browser-use、Stagehand 接起來</h2><p>Steel 把自己定位成基礎建設層，上面接的 Agent framework 才是處理 LLM 邏輯的地方。最常見的兩個組合是 browser-use 跟 Stagehand。</p><p>browser-use 是 Python 生態的開源 Agent framework，58K 星，給 LLM 一段任務描述，它會把 DOM 解析成可被模型理解的格式，輪詢決策、執行 action。預設它會在本機開 Playwright；要接 Steel 只需要把 browser 物件改成 <code>connect_over_cdp</code> 模式指向 Steel 的 connection URL，其他程式碼一行不改。</p><p>Stagehand 是 Browserbase 自家推的 TypeScript framework，原生支援 Browserbase 跟 local Playwright 兩種模式，社群有人寫了 connector 讓它接 Steel，但官方支援不直接——這部分要評估是不是值得投入。對於從零開始的專案，直接用 browser-use 加 Steel 更乾淨。</p><p>實務上一個典型的部署拓樸是這樣的：應用伺服器跑 Agent framework（Python 或 Node）、Steel API 跟 Chrome 跑在獨立的 VPS、Redis 做 session metadata 跟任務佇列、PostgreSQL 存 Agent 執行紀錄。Agent server 跟 Steel server 之間走內網或 WireGuard，Steel 不對公網直接暴露。這套拓樸在臺灣本地部署的好處是 Agent 跟瀏覽器之間的網路延遲控制在毫秒級，比起 Steel 跑在海外雲再讓 Agent 控制要省下 100 到 200 毫秒的每步往返。</p><h2 id="什麼時候-Steel-不是對的選擇"><a href="#什麼時候-Steel-不是對的選擇" class="headerlink" title="什麼時候 Steel 不是對的選擇"></a>什麼時候 Steel 不是對的選擇</h2><p>Steel 不是萬靈丹，幾個情境用它反而是過度設計。</p><p>純粹的爬蟲——抓固定網站的結構化資料、不需要登入、不需要 stealth——直接用 <code>requests</code> 加 <code>selectolax</code> 就夠快，硬塞 headless Chrome 進來只是讓事情慢十倍。Steel 的價值在於對抗網站防禦跟維持 session 狀態，這兩件事都不需要的場景不該用它。</p><p>E2E 測試也不適合。CI 跑測試的本質是隔離、可重複、快速啟動結束，Steel 的 session 模型反而會引入不必要的狀態。Playwright 直接在 CI runner 裡跑就好。</p><p>大規模並發（每秒幾百個 session）的場景，Steel 目前的單機架構會撐不住。橫向擴展需要在前面架 load balancer、後面做 session 親和性，這部分官方還沒做出對應的 cluster mode。需要大規模並發的團隊現階段還是會回去買 Browserbase 的 enterprise plan，或自己花時間刻 Kubernetes 編排。</p><h2 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h2><p>AI Agent 從操作 chat 進化到操作瀏覽器之後，「跑在哪一台 Chrome」會變成 Agent 工作流裡跟 LLM 同等重要的基礎設施問題。Browserbase 把這個位置變成 SaaS 標準答案、Steel 用 Apache 2.0 把同樣的功能拉回自架的選項。對於中型 Agent 專案，自架 Steel 在成本、資料主權、定制空間上都比 SaaS 方案有優勢，前提是團隊願意投入維運。</p><p>把 Steel 跟 Agent 服務一起放在臺灣本地有兩個務實的好處：Agent 控制每一步動作的往返延遲壓在毫秒級，配合住宅代理還能讓對外請求看起來來自任意地理位置，兩者之間的內網流量則完全不出國。NCSE Network 在臺灣是方電訊機房提供配備 Intel Gold CPU 與 NVMe SSD 的 VPS 主機，足夠應付 Steel 自架所需的記憶體與 CPU 突發負載，適合作為 Agent 工作流的瀏覽器執行層落腳點。需要把 AI Agent 的瀏覽器基礎建設收回自家手裡的團隊，可以參考 NCSE Network 的 VPS 方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/steel-browser-ai-agent-self-hosted-browserbase-alternative/</id>
    <link href="https://blog.ncse.tw/steel-browser-ai-agent-self-hosted-browserbase-alternative/"/>
    <published>2026-06-05T03:00:00.000Z</published>
    <summary>AI Agent 從 chat 進化到能操作瀏覽器之後，背後跑的是哪台 Chrome 變成新問題。Browserbase 用 SaaS 模式吃下這個市場，Playwright 自架在 stealth、session 管理、反偵測上又拼湊感太重。Steel 在 2024 年底把這層基礎建設用 Apache 2.0 開源出來，2026 年 4 月的 v0.5.3 把 CDP 控制、stealth plugin、proxy chain、CAPTCHA solver 打包成一支 Docker image。</summary>
    <title>AI Agent 需要瀏覽器這件事，Browserbase 收訂閱費——Steel 用 Apache 2.0 把這層沙箱補上</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="WireGuard" scheme="https://blog.ncse.tw/tags/WireGuard/"/>
    <category term="NetBird" scheme="https://blog.ncse.tw/tags/NetBird/"/>
    <category term="Zero Trust" scheme="https://blog.ncse.tw/tags/Zero-Trust/"/>
    <category term="mesh VPN" scheme="https://blog.ncse.tw/tags/mesh-VPN/"/>
    <category term="IdP" scheme="https://blog.ncse.tw/tags/IdP/"/>
    <content>
      <![CDATA[<p>NetBird 在 2026 年 2 月釋出的 v0.65 把整套自架 mesh VPN 從原本五六個容器收斂到一支 binary，並在管理服務裡內建 IdP 跟反向代理。對於正在用 Headscale 撐 Tailscale 替代方案、卻一直繞不開「身分驗證要另外接一套」這個痛點的團隊，NetBird 提供的不是同類產品，而是更高一階的 Zero Trust 網路平台。</p><p>WireGuard 把端對端加密這件事壓到核心模組裡，效能跟簡潔度都遠勝過 OpenVPN 或 IPsec。但 WireGuard 本身只負責「兩個 peer 之間怎麼加密通訊」——peer 怎麼找到對方、誰能加入網路、ACL 怎麼設、SSO 怎麼接，全是上層問題。Headscale 把 Tailscale 的控制平面開源化，已經解決了很大一塊；NetBird 則把問題拉到「整個企業 Zero Trust」這個層級。</p><h2 id="Headscale-是控制平面，NetBird-是完整網路平台"><a href="#Headscale-是控制平面，NetBird-是完整網路平台" class="headerlink" title="Headscale 是控制平面，NetBird 是完整網路平台"></a>Headscale 是控制平面，NetBird 是完整網路平台</h2><p>把兩個專案放在一起比較其實有點失焦——Headscale 的設計目標就是「給 Tailscale 客戶端一個自架的協調節點」，所以它接受了 Tailscale 客戶端的全部限制：閘道靠 DERP relay、ACL 用 Tailscale 那套 JSON 描述、SSO 走 Tailscale Connector 邏輯。優點是體積極小、跑得起來幾乎零維運；缺點是想擴充功能就會撞到 Tailscale 客戶端不開源的牆。</p><p>NetBird 從第一天就是自己一套體系。它有自己的客戶端（Go 寫的，BSD-3 授權）、自己的管理服務、自己的 signal server 跟 relay。完整的 Zero Trust 功能堆疊長這樣：</p><ul><li><strong>管理服務</strong>：peer 註冊、ACL、user&#x2F;group 規則、稽核日誌</li><li><strong>Signal Server</strong>：peer 間 NAT traversal 的協調訊號，不轉發實際流量</li><li><strong>TURN&#x2F;Relay</strong>：peer 直連失敗時的中繼，用 coturn 或 NetBird 自家 relay</li><li><strong>內建 IdP</strong>：v0.62 之後直接內建，不必再架 Keycloak</li><li><strong>反向代理</strong>：v0.65 加進來的，等同於 Tailscale Funnel</li></ul><p>換句話說 Headscale 解的是「Tailscale 控制平面屬於別人」這個問題，NetBird 解的是「整套企業遠端存取要怎麼自家化」這個更大的問題。</p><h2 id="v0-65-把五個服務收進一個容器的工程取捨"><a href="#v0-65-把五個服務收進一個容器的工程取捨" class="headerlink" title="v0.65 把五個服務收進一個容器的工程取捨"></a>v0.65 把五個服務收進一個容器的工程取捨</h2><p>NetBird 早期的 Docker Compose 配置長得很「微服務」：dashboard 一個容器、管理服務一個、signal 一個、coturn 一個、外加 Caddy 做 reverse proxy。實際運行五六個容器、每個都要顧 health check 跟 volume，部署門檻直接讓不少團隊放棄。</p><p>v0.65 釋出的 Unified Server Binary 是有意識的去微服務化決策。對於自架場景來說，management、signal、relay 三個服務通常都跑在同一台機器上，拆成三個容器只是增加部署複雜度，沒有實質的擴展性好處。把它們合進同一支 Go binary 的好處很直接：</p><ul><li>只剩一個容器要管，重啟、升級、看 log 都簡單</li><li>內部通訊不必走 TCP socket，直接 in-process function call</li><li>設定檔合併成單一 YAML，不必對齊三份 config 的版本</li><li>記憶體基線從原本 500 MB 左右降到 200 MB 上下</li></ul><p>代價是大型部署要橫向擴展時要把容器拆回去——但對絕大多數的自架使用者來說，這個取捨完全合理。</p><h2 id="內建-IdP-把自架的心理門檻砍掉一大半"><a href="#內建-IdP-把自架的心理門檻砍掉一大半" class="headerlink" title="內建 IdP 把自架的心理門檻砍掉一大半"></a>內建 IdP 把自架的心理門檻砍掉一大半</h2><p>舊版 NetBird 強制依賴外部 OIDC 提供者，意味著想自架的人得先去學 Keycloak 或 Authentik。光是 Keycloak 一個 realm 設下來，對非身分驗證背景的工程師就是半天的學習曲線。</p><p>v0.62 引進內建 IdP 之後，整個流程變成：跑自架腳本、腳本自動建立第一個 admin user、從 dashboard 邀請其他成員、他們收到 email 就能登入。對於 30 人以下的小型團隊這就夠了。內建 IdP 支援本地帳號、密碼、TOTP 二階段驗證，完全是合理的最小集合。</p><p>需要走 SSO 的，再把外部 IdP 接上來——NetBird 支援 Authentik、Keycloak、Auth0、Google Workspace、Microsoft Entra ID，配置時切到外部 IdP 模式即可，使用者資料在遷移時可以保留。</p><p>這個設計取捨值得稱讚。Headscale 跟 Tailscale 的 SSO 都是「沒它就動不了」，內建 IdP 把這個前置條件拿掉，自架的心理門檻降了一大截。</p><h2 id="反向代理補上-Funnel-那塊"><a href="#反向代理補上-Funnel-那塊" class="headerlink" title="反向代理補上 Funnel 那塊"></a>反向代理補上 Funnel 那塊</h2><p>Tailscale Funnel 讓 tailnet 裡的服務能對公網開放——把 <code>service.tailnet.ts.net</code> 變成可以從外部存取的網址，而且自動配 HTTPS。Headscale 一直沒有這個功能，因為它本質上就是個 control plane，不負責資料平面。</p><p>NetBird v0.65 的內建反向代理直接解決這件事。在管理介面上點幾下就能把某個 peer 上的服務（例如 <code>internal-app:3000</code>）對應到一個 <code>*.netbird.example.com</code> 子網域，自動申請 Let’s Encrypt 憑證、自動加上認證閘道、只允許網內成員存取。</p><p>底層的實作是把 Caddy 內嵌到管理服務裡——這也是為什麼 v0.65 之前需要在 Compose 旁邊另外跑一個 Caddy。內嵌之後一切都包在同一支 binary 裡，憑證、規則、log 集中管理。</p><p>對於 dev 環境想暫時開放某個服務給合作夥伴看、或是 staging 環境要讓 QA 從家裡連進來，這個功能比起「先架 Nginx 再設 SSO 再開防火牆規則」省掉太多步驟。</p><h2 id="自架硬體配置與容量規劃"><a href="#自架硬體配置與容量規劃" class="headerlink" title="自架硬體配置與容量規劃"></a>自架硬體配置與容量規劃</h2><p>NetBird 自架管理服務的資源需求其實不大，因為 peer 之間是直連，管理服務只負責協調跟 ACL 評估。實務參考點：</p><ul><li><strong>小型團隊（&lt; 50 peer）</strong>：1 vCPU、2 GB RAM、20 GB SSD 就夠了</li><li><strong>中型部署（50–500 peer）</strong>：2 vCPU、4 GB RAM、50 GB SSD，relay 流量是主要變數</li><li><strong>大型部署（500+ peer）</strong>：建議把 relay 拆出去獨立部署，避免 NAT traversal 失敗時 relay 流量擠爆管理節點</li></ul><p>頻寬規劃要注意 relay 那一塊。理論上 peer 直連時 relay 不耗流量，但實際上有 NAT 環境（特別是雙重 NAT、企業防火牆封 UDP）會強制走 relay。觀察 NetBird 統計儀表板上的 relay 流量比例，超過 30% 就要考慮升級頻寬或部署多個地理位置的 relay。</p><p>控制節點不一定要放臺灣——它只處理控制訊號，延遲幾十毫秒對使用體驗沒有實際影響。但 relay 節點建議放在 peer 集中的區域，因為 relay 流量等於完整的應用流量。臺灣團隊的 relay 放臺灣本地會比走海外回程明顯順暢。</p><h2 id="Docker-Compose-五分鐘部署"><a href="#Docker-Compose-五分鐘部署" class="headerlink" title="Docker Compose 五分鐘部署"></a>Docker Compose 五分鐘部署</h2><p>從 v0.65 開始，官方 quickstart 腳本就是抓 unified binary 的 Compose 範本，問幾個問題然後跑起來：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> NETBIRD_DOMAIN=netbird.example.com</span><br><span class="line">curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh \</span><br><span class="line">  | bash</span><br></pre></td></tr></table></figure><p>腳本會問對外網域（DNS 要事先指過來）、管理員帳號 email、是否啟用內建反向代理。跑完會吐出 admin 密碼，打開 dashboard 登入之後，安裝指令會直接顯示在介面上，複製到目標機器執行：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://pkgs.netbird.io/install.sh | sh</span><br><span class="line">netbird up --management-url https://netbird.example.com:33073</span><br></pre></td></tr></table></figure><p>第二行會跳出瀏覽器登入流程，或者顯示一個 setup key 讓你貼。完成之後這台機器就加進 mesh，可以從其他 peer 直接 ping 它的 NetBird IP。</p><p>正式上線前要調的幾個地方：</p><p><strong>反向代理憑證</strong>：預設用 Let’s Encrypt staging，要切到 production 把 config 裡的 <code>ca</code> 設成 <code>letsencrypt</code>。</p><p><strong>Setup Key 過期</strong>：批次部署用的 setup key 預設一個月過期，CI&#x2F;CD 跑容器時要避開這個陷阱，要嘛用長期 key、要嘛在 pipeline 裡動態取得。</p><p><strong>Relay 認證</strong>：開放公網的 relay 一定要打開 hmac secret，否則任何人都能拿來當免費 TURN 中繼。</p><h2 id="什麼情況下還是該選-Headscale"><a href="#什麼情況下還是該選-Headscale" class="headerlink" title="什麼情況下還是該選 Headscale"></a>什麼情況下還是該選 Headscale</h2><p>NetBird 不是 Headscale 的全面替代品。幾個明顯該留在 Headscale 的場景：</p><p><strong>已經深度使用 Tailscale 客戶端</strong>：MagicDNS、Tailscale SSH、Funnel 客戶端側的整合在 Tailscale 原生客戶端上更成熟。Headscale 讓既有 Tailscale 部署能無痛搬控制平面，NetBird 則要全套換掉客戶端。</p><p><strong>手上的設備跑著 Tailscale-only 的整合</strong>：某些 IoT 設備、NAS 內建 Tailscale 客戶端，這些東西沒有 NetBird 版本，硬上會卡住。</p><p><strong>只要最小化的 mesh VPN</strong>：如果只想串幾台 VPS 不要被公網掃到、不在乎 SSO 跟 ACL，Headscale 五十 MB 的 binary 就是最輕的方案。NetBird 那些功能會變成沒在用的程式碼。</p><h2 id="把企業遠端存取搬回自家機房"><a href="#把企業遠端存取搬回自家機房" class="headerlink" title="把企業遠端存取搬回自家機房"></a>把企業遠端存取搬回自家機房</h2><p>Zero Trust 網路在 2026 年已經是企業合規的硬性需求，Zscaler、Cloudflare Access 這類 SaaS 是預設選項。但成本曲線跟 Datadog 是同一回事——使用者規模上去之後價錢爆炸，而且員工存取資料的流量全部要送到第三方廠商，敏感產業（金融、醫療、政府）光是稽核就過不去。</p><p>NetBird 把 SaaS Zero Trust 平台的功能完整自家化，授權是 BSD-3-Clause，沒有「企業版才有的功能」這種陷阱。對於想把遠端辦公、CI&#x2F;CD agent、伺服器運維通道整合在同一張網的團隊，這套東西是少數真正把所有元件都開源的方案。</p><p>要在臺灣本地架設 NetBird 控制平面與 relay 節點，網路延遲跟跨境穩定度是決定使用體驗的關鍵。NCSE Network 提供位於是方電訊機房的 VPS 主機與 IP Transit 服務，臺灣骨幹直連對於 UDP-heavy 的 mesh VPN 流量特別友善，relay 節點放在這裡能明顯改善 NAT 穿透失敗時的回退體驗。前往 <a href="https://ncse.tw/">ncse.tw</a> 了解規格與方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/netbird-self-hosted-zero-trust-mesh-vpn/</id>
    <link href="https://blog.ncse.tw/netbird-self-hosted-zero-trust-mesh-vpn/"/>
    <published>2026-06-04T06:00:00.000Z</published>
    <summary>NetBird 在 2026 年 2 月釋出的 v0.65 把整套自架 mesh VPN 從原本五六個容器收斂到一支 binary，並內建 IdP 跟反向代理。本文拆解它跟 Headscale 的定位差異、unified binary 的工程取捨、內建 IdP 對自架門檻的影響，以及 Docker Compose 部署細節。</summary>
    <title>WireGuard 解了加密、Headscale 解了路由，NetBird 用一支 binary 把 Zero Trust 的最後一塊拼起來</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Docker" scheme="https://blog.ncse.tw/tags/Docker/"/>
    <category term="SSH" scheme="https://blog.ncse.tw/tags/SSH/"/>
    <category term="Unregistry" scheme="https://blog.ncse.tw/tags/Unregistry/"/>
    <category term="部署" scheme="https://blog.ncse.tw/tags/%E9%83%A8%E7%BD%B2/"/>
    <content>
      <![CDATA[<p>Docker 把應用程式包成 image 容易，把 image 送到伺服器卻常常是整個部署流程裡最尷尬的一段。CI build 完一個 200 MB 的映像檔，要先推到 Docker Hub、ghcr.io 或者自架的 Harbor，再到目標機器 docker pull 下來。為了一台跑五個容器的 VPS 而開一座 Registry 服務，這個比例在實務上一直被質疑。</p><p>Unregistry 是 2025 年中由 Pavlo Sviderski 開源、2026 年初開始被廣泛討論的工具。設計上它不是另一套 Registry 替代品，而是徹底跳過中間那層——把映像檔以 SSH 直送到目標伺服器，過程中只傳缺少的 layer。這篇拆解它為什麼被需要、運作機制，以及在自架 VPS 部署流程裡的真實定位。</p><h2 id="中小型部署被-Registry-卡住的場景"><a href="#中小型部署被-Registry-卡住的場景" class="headerlink" title="中小型部署被 Registry 卡住的場景"></a>中小型部署被 Registry 卡住的場景</h2><p>問題出在 Docker 預設工作流的不對稱性。本地 build 出來的 image 已經完整存在開發者或 CI 機器的 Docker daemon 裡，目標伺服器需要的也只是同一份檔案，但兩者中間多了一個 Registry。</p><p>走 Registry 的代價包含三件事。其一是部署環節多一個必須維運的服務：Harbor 要外掛 PostgreSQL、Redis、TLS 憑證，自架 Distribution 雖然輕量但仍需要儲存清理與授權設定。其二是頻寬重複：image 先從 CI 推上 Registry，再從 Registry 拉下伺服器，等於同一份 layer 在公開網路上多走一趟。其三是攻擊面變大：對外提供 Registry 端點意味著得處理使用者認證、金鑰輪替、log 稽核。</p><p>繞過 Registry 的傳統做法是 docker save 後 scp 過去再 docker load。但這條路的缺陷很明顯——save 出來的是完整 tar，每次部署都會把 200 MB 整包重新傳送，即使應用層只動了 5 MB。對於每天部署多次的小型服務，這個延遲跟頻寬浪費累積得很快。</p><h2 id="Unregistry-的設計把-Registry-收進伺服器的-Docker-daemon"><a href="#Unregistry-的設計把-Registry-收進伺服器的-Docker-daemon" class="headerlink" title="Unregistry 的設計把 Registry 收進伺服器的 Docker daemon"></a>Unregistry 的設計把 Registry 收進伺服器的 Docker daemon</h2><p>Unregistry 的核心想法是：與其架一座中央 Registry，不如讓目標伺服器自身的 Docker daemon「臨時扮演」一座 Registry。</p><p>具體實作上，Unregistry 是一個容器化的 Registry 服務，但儲存後端不是傳統檔案系統，而是直接讀寫 containerd 的 image store。這代表當映像檔被推進 Unregistry，它就已經是 Docker daemon 認得的 image，不需要額外的「從 Registry 同步到 Docker」步驟。</p><p>搭配 Unregistry 一起發佈的 <code>docker pussh</code> 指令——刻意多一個 s 提醒這是走 SSH 推送——是一個 Docker CLI plugin。它編排了整套流程：建立 SSH 連線、在遠端啟動暫時的 Unregistry 容器、把本地的某個 port 轉發到容器內的 Registry port、執行標準的 docker push、結束後關閉容器與通道。</p><p>對使用者而言，整個指令長這樣：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pussh myapp:latest user@server.example.com</span><br></pre></td></tr></table></figure><p>對遠端伺服器而言，整個過程只在記憶體裡多了一個短暫的容器，沒有任何長駐服務、沒有對外暴露的 port、沒有額外的身分驗證系統。</p><h2 id="docker-pussh-的執行細節"><a href="#docker-pussh-的執行細節" class="headerlink" title="docker pussh 的執行細節"></a>docker pussh 的執行細節</h2><p>把整段流程攤開來看，每次推送會發生這些事：</p><p>本地的 docker-pussh plugin 解析參數，確認目標 host 與映像檔。SSH 連到目標伺服器後，下達指令啟動 <code>ghcr.io/psviderski/unregistry</code> 容器，這個容器掛載目標伺服器的 containerd socket。SSH 通道把本地的隨機 port 轉發到容器內的 5000 port。</p><p>接著 plugin 在本地呼叫標準的 docker push，目的地是 <code>localhost:&lt;隨機 port&gt;</code>。Docker daemon 走標準 Registry V2 API，把每一層 blob 上傳——但因為目標 Registry 直接讀 containerd 的儲存，它能立刻回應「此 layer 已存在，跳過」，於是只有缺少的 layer 才會實際走過 SSH 通道。</p><p>推送完成後，plugin 關閉遠端容器、收回 SSH 通道。整個過程在 layer 變動小的情況下，通常比 <code>docker save | ssh &quot;docker load&quot;</code> 快上一個數量級。</p><h2 id="跟-docker-save-load-和-Harbor-的真正差距"><a href="#跟-docker-save-load-和-Harbor-的真正差距" class="headerlink" title="跟 docker save&#x2F;load 和 Harbor 的真正差距"></a>跟 docker save&#x2F;load 和 Harbor 的真正差距</h2><p>實務上會拿來比較的方案大概是三種：自架 Harbor、純 docker save&#x2F;load、Unregistry。各自的成本曲線並不相同。</p><p>Harbor 的長處在多人協作與映像檔治理。它有 RBAC、漏洞掃描、簽章驗證、垃圾回收，適合維護幾十個專案、幾十個開發者共享 image 的場景。但對一個只有兩三個應用、一兩個維運人員的小型 VPS，這套機制顯得過重。Harbor 自身吃的記憶體跟 PostgreSQL 加起來通常超過 1 GB。</p><p>docker save&#x2F;load 的長處在零相依——只需要 Docker、ssh、tar 就能完成。缺點是每次都傳完整 image。對於只動 0.5% 內容的部署，等於浪費 99.5% 的頻寬與傳輸時間。</p><p>Unregistry 卡在中間：沒有治理功能、沒有掃描、沒有保留歷史版本，但部署效率接近 Harbor、維運成本接近 save&#x2F;load。它的甜蜜點是「一個人或小團隊維護幾個應用、每天部署很多次」的場景。對於這種情境，建議直接放棄 Harbor 的念頭，Unregistry 加上 CI 端的 tag 命名規則已經夠用。</p><h2 id="與-Kamal、Coolify、Dokploy-的整合位置"><a href="#與-Kamal、Coolify、Dokploy-的整合位置" class="headerlink" title="與 Kamal、Coolify、Dokploy 的整合位置"></a>與 Kamal、Coolify、Dokploy 的整合位置</h2><p>Kamal 從 2.0 開始把外部 Registry 視為部署的硬性依賴，這在自架 VPS 場景下偶爾會讓人遲疑。社群在 Kamal 的 GitHub issue 裡長期討論能不能整合 Unregistry，目前可行的做法是在 Kamal 設定裡把 builder 與 Registry 都指向本地，再用 Unregistry 替代實際的推送步驟。Kamal 3 是否正式納入這套模式還在觀察。</p><p>Coolify 與 Dokploy 走的是另一種思路：build 直接在伺服器上進行，image 從本地 Docker daemon 拉出來部署。這套模型不需要 Registry，但也犧牲了「跨機器分發同一份 image」的能力。Unregistry 補的正是這個縫——build 在 CI 或開發機，部署到多台伺服器時不必架 Registry，但每台都拿到一致的 image。</p><p>對於正在從「手動 ssh 進伺服器 git pull 再 docker compose up」轉向標準化部署的團隊，Unregistry 通常是比 Harbor 更務實的第一步。</p><h2 id="該注意的限制"><a href="#該注意的限制" class="headerlink" title="該注意的限制"></a>該注意的限制</h2><p>Unregistry 不是萬用工具。實際導入前有幾個點需要先確認。</p><p>Windows 平台需要 WSL 2 才能跑 docker-pussh plugin。多平台映像檔（multi-arch）要求本地 Docker 啟用 containerd image store，這在 Docker Desktop 預設配置下需要手動切換。SSH 使用者必須有 docker 群組權限，或者能 sudo 免密碼跑 docker 指令——這在強化過的伺服器上會跟最小權限原則衝突，需要另外設計。</p><p>容錯方面，Unregistry 沒有保留版本歷史，推送過去的 image 就是該 tag 的當下版本，無法像正規 Registry 那樣回滾到「三天前那個版本」。在生產環境裡，這代表 image 的版本管理責任全部回到 CI 端的 tag 命名策略上，每次 build 用 git commit hash 當 tag 是比 latest 安全得多的選擇。</p><p>跨資料中心同步也不是 Unregistry 的長處。Harbor 有 replication policy 可以把同一份 image 自動同步到多個區域，Unregistry 的設計是點對點，要分發到十台機器就是十次 docker pussh。如果機器數量已經到了需要 fan-out 的規模，Registry 仍然是更合理的架構。</p><h2 id="部署這條路該怎麼選"><a href="#部署這條路該怎麼選" class="headerlink" title="部署這條路該怎麼選"></a>部署這條路該怎麼選</h2><p>Unregistry 把 Docker 部署裡最不直覺的那段——為了搬一份檔案而架一座服務——壓縮到最低成本。對於跑在臺灣 VPS 上的中小型應用，這個工具能直接拿掉 Harbor 那塊維運負擔，把部署流程簡化成「build 完 → 一行指令 → 上線」。</p><p>需要的條件其實只有兩個：穩定的 SSH 連線跟一台跑得動 Docker 的伺服器。NCSE Network 在臺灣是方電訊機房提供搭載 Intel Gold CPU 與 NVMe SSD 的 VPS 主機，搭配低延遲的本地對外連線，是把 Unregistry 接進部署流程的合理基礎。從 build 端到生產機之間少掉一座 Registry，整體部署管線會直接俐落不少。</p>]]>
    </content>
    <id>https://blog.ncse.tw/unregistry-docker-push-ssh-no-registry/</id>
    <link href="https://blog.ncse.tw/unregistry-docker-push-ssh-no-registry/"/>
    <published>2026-06-04T02:00:00.000Z</published>
    <summary>Docker 部署到自家 VPS 卻被 Registry 卡住的場景並不罕見。Unregistry 把 Registry 收進目標伺服器自身的 Docker daemon，搭配 docker pussh 指令以 SSH 直送映像檔，只傳輸缺少的 layer。本文拆解它的設計、效率、限制，以及跟 Kamal、Coolify 整合的位置。</summary>
    <title>Unregistry：把 docker push 走 SSH 直送伺服器，不再需要架 Registry</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="eBPF" scheme="https://blog.ncse.tw/tags/eBPF/"/>
    <category term="Tetragon" scheme="https://blog.ncse.tw/tags/Tetragon/"/>
    <category term="容器安全" scheme="https://blog.ncse.tw/tags/%E5%AE%B9%E5%99%A8%E5%AE%89%E5%85%A8/"/>
    <category term="執行期安全" scheme="https://blog.ncse.tw/tags/%E5%9F%B7%E8%A1%8C%E6%9C%9F%E5%AE%89%E5%85%A8/"/>
    <category term="Cilium" scheme="https://blog.ncse.tw/tags/Cilium/"/>
    <content>
      <![CDATA[<p>防火牆把外面擋住、TLS 把連線加密、image scanner 把已知漏洞抓出來，這些都是容器安全的標準動作。真正難處理的是另一條問題：容器跑起來之後，裡面那支 process 到底做了什麼？讀了哪些檔案、開了哪些 socket、有沒有去動 <code>/etc/shadow</code> 或 <code>nsenter</code> 跳出 namespace？這層可見性在多數 VPS 上仍是黑盒。Tetragon 用 eBPF 把這層補起來，把偵測與阻擋都搬進 Linux 核心，1.7 版在 2026 年 4 月底釋出之後，已經成熟到能單機跑在一臺 VPS 上而不必先架 Kubernetes 叢集。</p><p>Tetragon 是 Cilium 旗下的子專案，由 Isovalent 起家、捐進 CNCF，定位是 eBPF-based Security Observability 與 Runtime Enforcement。它跟 Cilium 共用底層機制但完全可以獨立部署，跑在 Debian、Ubuntu 或任何 5.10 以上核心的 Linux 上都行。核心賣點是：傳統 auditd 是事件水龍頭，需要在 user space 大量過濾才看得出名堂；Falco 把規則引擎拉到 user space 處理；Tetragon 把過濾、enrichment、甚至 enforcement 動作全部編譯成 eBPF 程式，直接掛在核心的 kprobe 與 tracepoint 上。</p><h2 id="容器內部到底發生什麼，是多數-VPS-的可見性盲區"><a href="#容器內部到底發生什麼，是多數-VPS-的可見性盲區" class="headerlink" title="容器內部到底發生什麼，是多數 VPS 的可見性盲區"></a>容器內部到底發生什麼，是多數 VPS 的可見性盲區</h2><p>跑 Docker 的 VPS 管理員多半遇過這種狀況：某天伺服器 CPU 莫名升高，<code>docker stats</code> 顯示某個容器吃滿一顆核心，但裡面到底在做什麼說不清楚。<code>docker logs</code> 只看得到應用程式自己印出的東西，惡意 process 通常不會貼心地寫一行「正在挖礦」。要更深一層，傳統選項只有兩個：在容器裡面裝 strace 或 auditctl 規則。前者效能崩潰，後者吞掉大量 disk I&#x2F;O 還難解讀。</p><p>問題本質是 Linux 把「容器」看成一組 namespace 加 cgroup 的組合，並沒有一個原生的「容器內事件 audit」介面。從主機角度看，容器裡的 process 跟主機上其他 process 沒兩樣，PID 在主機 namespace 裡是另一個數字，syscall 來了就服務。要把「主機 PID 12345 屬於 nginx 容器」這個對應關係加回事件流裡，就需要一個既懂 Linux 又懂容器 runtime 的代理層。</p><p>過去這個位置長期被 Falco 佔著。Falco 在 2016 年從 Sysdig 分支出來，2024 年成為 CNCF 畢業專案，產品成熟、規則豐富、社群活躍。它撐起了容器 runtime security 這個類別，沒有理由貶低它。Tetragon 之所以還能切進這個市場，是因為它對「攔截點該放在哪一層」的判斷不一樣。</p><h2 id="Falco-不是不夠好，是它把判斷工作交給了-user-space"><a href="#Falco-不是不夠好，是它把判斷工作交給了-user-space" class="headerlink" title="Falco 不是不夠好，是它把判斷工作交給了 user space"></a>Falco 不是不夠好，是它把判斷工作交給了 user space</h2><p>Falco 的架構是：在核心用 eBPF probe（或舊版的 kernel module）抓 syscall 事件，把原始事件透過 ring buffer 送到 user space 的 Falco daemon，daemon 在 user space 跑 rule engine 比對 YAML 規則，命中就觸發 alert。這套設計的好處是規則語言彈性高、寫起來像在寫 SIEM 條件式；代價是高吞吐情境下 user space 要消化大量事件，做不完就丟、或者把 CPU 吃掉一塊。</p><p>公開壓測結果差異不小。Falco 自己的 benchmark 在中等工作負載下大約是 1–2% CPU 開銷；獨立第三方測試在高 syscall 吞吐情境下測到 5–10%，主要花在 user space 的解析與規則比對。Tetragon 把這部分搬回核心：用 eBPF map 在核心裡完成 binary 比對、引數過濾、cgroup 限定，事件根本不需要送到 user space 就先被過濾掉。多數公開測試把 Tetragon 的長期 CPU 開銷壓在 1% 以下，2025 年底 SciTePress 的一份比較研究量到 Tetragon 在 CPU 上勝 Falco、在記憶體上略遜於 Falco。</p><p>更關鍵的差別在 enforcement。Falco 自身只負責偵測，要做阻擋得搭 Falco Talon 或自己接 webhook 觸發外部動作，反應週期是「偵測 → user space → 外部 process → 做出反應」，整段大概數十毫秒到秒級。Tetragon 可以在 eBPF program 命中規則的當下直接 send SIGKILL 給該 process，或在 syscall 還沒回來之前 override return value 讓那次 syscall 失敗，整個閉環在核心裡完成，延遲在微秒等級。對於攔截「容器裡被植入的程式去讀 <code>/etc/shadow</code>」這類動作，這個差別是「擋下來」跟「事後才知道」的差別。</p><h2 id="TracingPolicy-是-Tetragon-學習曲線的重心"><a href="#TracingPolicy-是-Tetragon-學習曲線的重心" class="headerlink" title="TracingPolicy 是 Tetragon 學習曲線的重心"></a>TracingPolicy 是 Tetragon 學習曲線的重心</h2><p>Tetragon 暴露給使用者的主要介面叫 TracingPolicy，是一份 YAML，描述要在核心哪個 hook 點掛 eBPF program、要看什麼引數、命中什麼條件、要做什麼動作。一份最小化的 policy 大概長這樣：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">cilium.io/v1alpha1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">TracingPolicy</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">&quot;watch-sensitive-files&quot;</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">kprobes:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">call:</span> <span class="string">&quot;security_file_open&quot;</span></span><br><span class="line">    <span class="attr">syscall:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">args:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">index:</span> <span class="number">0</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">&quot;file&quot;</span></span><br><span class="line">    <span class="attr">selectors:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">matchArgs:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">index:</span> <span class="number">0</span></span><br><span class="line">        <span class="attr">operator:</span> <span class="string">&quot;Prefix&quot;</span></span><br><span class="line">        <span class="attr">values:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;/etc/shadow&quot;</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&quot;/root/.ssh/&quot;</span></span><br><span class="line">      <span class="attr">matchActions:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">action:</span> <span class="string">Sigkill</span></span><br></pre></td></tr></table></figure><p>這份 policy 的意思是：在 <code>security_file_open</code> 這個 LSM hook 上掛 probe，凡是路徑開頭是 <code>/etc/shadow</code> 或 <code>/root/.ssh/</code> 的開檔行為，直接送 SIGKILL 給發起者。注意這裡選的是 <code>security_file_open</code> 而不是 <code>openat</code> syscall——後者每秒會被叫上萬次、前者只在真正進到 VFS 開檔流程時觸發，filter 成本低很多。</p><p><code>matchBinaries</code> 可以把這個規則進一步限縮到只對特定執行檔生效；<code>matchNamespaces</code> 可以限定只看某個容器的 PID&#x2F;UTS namespace；<code>matchCapabilities</code> 可以鎖定只有有 <code>CAP_SYS_ADMIN</code> 的 process 才被攔截。這些 selector 全部編譯進 eBPF program 裡跑，意味著「找出特權容器存取敏感檔的瞬間」可以做到接近零誤報的精準偵測。</p><p>實務上的建議是不要從零自己寫 policy。Tetragon 官方倉庫有 <code>examples/tracingpolicy/</code> 目錄，cilium&#x2F;tetragon 在 GitHub 上的 <code>policy-library</code> 目錄收錄了監控 sudo 提權、偵測 reverse shell、攔截 <code>kubectl exec</code> 進容器、防止寫入 <code>/proc/sys</code> 等等的範本。直接拿來改、不要自己土法煉鋼是最省時的路。</p><h2 id="不靠-Kubernetes-也能跑：單機-VPS-的部署方式"><a href="#不靠-Kubernetes-也能跑：單機-VPS-的部署方式" class="headerlink" title="不靠 Kubernetes 也能跑：單機 VPS 的部署方式"></a>不靠 Kubernetes 也能跑：單機 VPS 的部署方式</h2><p>Tetragon 文件大半在講 Kubernetes 安裝，但實際上它可以當成一支獨立 daemon 跑。在一臺裝有 Docker 與 5.10 以上核心的 VPS 上，最低限度的指令是：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">docker run --name tetragon --<span class="built_in">rm</span> -d \</span><br><span class="line">  --pid=host --cgroupns=host --privileged \</span><br><span class="line">  -v /sys/kernel/btf/vmlinux:/var/lib/tetragon/btf \</span><br><span class="line">  -v /var/log/tetragon:/var/log/tetragon \</span><br><span class="line">  -v /etc/tetragon/tracing-policies:/etc/tetragon/tetragon.tp.d \</span><br><span class="line">  --<span class="built_in">env</span> <span class="string">&quot;TETRAGON_EXPORT_FILENAME=/var/log/tetragon/tetragon.log&quot;</span> \</span><br><span class="line">  quay.io/cilium/tetragon:v1.7.0</span><br></pre></td></tr></table></figure><p><code>--privileged</code> 是必要的，因為要 load eBPF program 並掛上 kprobe。<code>--pid=host</code> 與 <code>--cgroupns=host</code> 讓 Tetragon 看得到主機完整的 process 與 cgroup 拓撲，這樣它才能把事件回填上「這個 PID 屬於哪個容器」的資訊。<code>/sys/kernel/btf/vmlinux</code> 這個 mount 是給 BTF 用的，eBPF program 靠它做 CO-RE（Compile Once, Run Everywhere），不必依賴開發機跟正式機核心完全一致。</p><p>policy 直接丟進 <code>/etc/tetragon/tracing-policies/</code>，daemon 啟動時會掃整個目錄載入。要在運行中加新 policy 也可以用 <code>tetra</code> CLI 從 grpc 動態載入。事件預設輸出成 JSON Lines 寫進 <code>/var/log/tetragon/tetragon.log</code>，要轉給 SIEM 直接接 Vector 或 Fluent Bit 把它推到下游就好。</p><p>需要留意的是 BPF LSM 相關的 enforcement 動作（例如 file open 攔截）需要核心開啟 <code>CONFIG_BPF_LSM</code> 並在 <code>/etc/default/grub</code> 加 <code>lsm=bpf</code>。Debian 13、Ubuntu 24.04 預設都符合，CentOS Stream 9 系列需要自己啟用。沒有開 BPF LSM 時 Tetragon 仍能做偵測，只是 SIGKILL 之外的精細 enforcement 動作會降級。</p><h2 id="適合放進哪一臺機器、不適合放進哪一臺"><a href="#適合放進哪一臺機器、不適合放進哪一臺" class="headerlink" title="適合放進哪一臺機器、不適合放進哪一臺"></a>適合放進哪一臺機器、不適合放進哪一臺</h2><p>Tetragon 的甜蜜點是「跑著面向公網應用、又無法完全信任應用程式碼安全的 VPS」。電商後端、Webhook 接收器、表單轉發、社群媒體機器人這類場景一旦被植入 webshell，傳統監控的反應點通常是「奇怪流量被注意到」，而那時已經晚了。把監控 <code>connect()</code> 到非預期 IP、寫入 <code>/tmp/*.sh</code> 後立刻 execve 之類的行為策略掛上去，能在被植入的下一秒就觸發告警甚至直接 KILL。</p><p>不適合的是「規模小到不會被當目標」的個人實驗 VPS。Tetragon 本身輕量，但 policy 設計與事件處理需要時間經營，沒有事件需要應對時這套機制是純成本。同樣道理，跑 LAMP + WordPress 又只開 80&#x2F;443 的個人站，先做好 Fail2ban、CrowdSec、自動更新可能比導入 Tetragon 更划算。</p><p>把 Tetragon 想成「容器內的 IDS&#x2F;IPS」是正確的心智模型。它不取代防火牆、不取代 image scanner、不取代 WAF——它補的是這些工具都看不見的那一層：應用程式自己開始做奇怪事的瞬間。對於把多個容器塞在同一臺 VPS、又跑開放給外部使用者的服務的場景，這個觀察點過去只能靠商業 EDR 取得，現在用一支 open source binary 加幾份 YAML 就能架起來。</p><p>正在為一批面向公網的應用挑伺服器、又希望從第零天就把執行期安全納入規劃，可以參考 NCSE Network 的臺灣 VPS 服務——使用 Intel Gold CPU 與 NVMe SSD、機房設在是方電訊，預設核心版本支援 eBPF 完整功能，跑 Tetragon、Cilium 之類的 kernel-level 工具不需要額外調整。</p>]]>
    </content>
    <id>https://blog.ncse.tw/tetragon-ebpf-runtime-security-container-vps/</id>
    <link href="https://blog.ncse.tw/tetragon-ebpf-runtime-security-container-vps/"/>
    <published>2026-06-03T03:00:00.000Z</published>
    <summary>容器跑起來之後內部發生什麼事，傳統工具看不到，看到也來不及。Tetragon 用 eBPF 把可疑系統呼叫的偵測與阻擋都搬進 Linux 核心，1.7 版在 4 月底釋出之後已經成熟到能單機跑在 VPS 上。本文拆解它與 Falco 的差別、TracingPolicy 的寫法，以及不靠 Kubernetes 怎麼部署。</summary>
    <title>auditd 太吵、Falco 太重：Tetragon 用 eBPF 把容器執行期安全的攔截點搬進核心</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="OpenBao" scheme="https://blog.ncse.tw/tags/OpenBao/"/>
    <category term="HashiCorp Vault" scheme="https://blog.ncse.tw/tags/HashiCorp-Vault/"/>
    <category term="Secrets Management" scheme="https://blog.ncse.tw/tags/Secrets-Management/"/>
    <category term="開源授權" scheme="https://blog.ncse.tw/tags/%E9%96%8B%E6%BA%90%E6%8E%88%E6%AC%8A/"/>
    <content>
      <![CDATA[<p>HashiCorp 在 2023 年 8 月把整套產品線從 MPL 2.0 改成 BUSL 1.1，Vault 1.14 是最後一個開源版本。當時社群第一時間的反應是不安，但能拿來填補位置的開源 fork 並沒有立刻成形——Terraform 那一側有 OpenTofu 火速接管，Vault 這邊則拖了將近一年才有正式可用的東西。OpenBao 在 2026 年 2 月 4 日推出 2.5.0，這是它從一個「IBM 工程師私下維護的 fork」走到「Linux Foundation 旗下、PostgreSQL 後端可用、橫向讀取可擴展」的轉捩點。對於還在用 Vault 1.14 LTS 拖時間、或者根本沒導入 secrets management 的團隊，這個版本是重新評估的時機。</p><h2 id="BUSL-對自架使用者真正麻煩的地方"><a href="#BUSL-對自架使用者真正麻煩的地方" class="headerlink" title="BUSL 對自架使用者真正麻煩的地方"></a>BUSL 對自架使用者真正麻煩的地方</h2><p>Business Source License 表面上看起來跟 MPL 沒差太多——可以讀原始碼、可以改、可以自架——但條款裡塞了一句「不得用於與授權方競爭的產品或服務」。問題在於「競爭」這兩個字的解釋權在 HashiCorp 手上。一家臺灣的雲端服務商如果想把 Vault 包進自家的代管產品提供給客戶，理論上就踩到了紅線；即使是內部使用，只要公司有任何對外的 SaaS 服務，法務檢查時都會把這條標出來。</p><p>第二個現實問題是版本支援。HashiCorp 把 1.14 之後的 patch 全部關進 BUSL，安全修補只回流到企業版客戶。社群版繼續用 1.14 等於是公開地停在一個越來越多 CVE 的版本上，每一次 audit 都會被指出來。</p><p>對於業務上不需要 Vault 企業版功能（HSM、performance replication、Sentinel policy）的中小團隊，這兩個問題加起來等於一個明確訊號：要嘛升級成付費客戶，要嘛找替代品。前者通常超出預算，後者過去一年半都沒有夠成熟的選項。OpenBao 把這個缺口補上。</p><h2 id="從-Vault-1-14-fork-出來的兩年半"><a href="#從-Vault-1-14-fork-出來的兩年半" class="headerlink" title="從 Vault 1.14 fork 出來的兩年半"></a>從 Vault 1.14 fork 出來的兩年半</h2><p>OpenBao 不是新寫的專案，它的程式碼基底就是 Vault 1.14.0。IBM 的工程師 Nathan Phelps 跟 Joe Pearson 在 2023 年底發起，2024 年 4 月進入 LF Edge 旗下成為正式專案，binary 名稱從 <code>vault</code> 改成 <code>bao</code>。值得一提的是，雖然 IBM 後來在 2025 年初完成併購 HashiCorp，OpenBao 在治理上維持獨立——專案路線由 TSC 決定，不受 IBM 的產品策略左右。</p><p>API、CLI 指令、secret engine、auth method、storage backend、policy 語法全部跟 Vault 相容。已經寫好的 Terraform provider、Ansible playbook、應用程式裡的 Vault SDK 客戶端，幾乎都能不改一行直接指向 OpenBao。這是 fork 策略選對方向的結果：先保證遷移成本接近零，再慢慢累積差異化功能。</p><p>兩年半下來，OpenBao 走了三個重要里程碑。2.1 加入 PostgreSQL storage backend，這對只想用一台 VPS 跑 secrets management 的團隊意義重大——過去 Vault 在 HA 模式下強制要 Consul 或 Integrated Storage（Raft），前者要額外維護整套 Consul 叢集，後者在 Vault 階段就已經穩定，但 OpenBao 把 PostgreSQL 也納進候選名單，等於讓既有的 Postgres 實例多承擔一個角色，少開一台機器。2.3 補上 transactional storage 與更乾淨的 plugin SDK。2.5 則是把 horizontal read scalability 跟 OCI plugin 分發兩個比較困難的功能做完。</p><h2 id="2-5-真正改變使用情境的兩件事"><a href="#2-5-真正改變使用情境的兩件事" class="headerlink" title="2.5 真正改變使用情境的兩件事"></a>2.5 真正改變使用情境的兩件事</h2><p>2 月 4 日的 release notes 列了一長串改動，多數是 patch 與 enhancement，但有兩項對日常使用影響特別大。</p><p><strong>Standby 節點可以直接服務讀取請求。</strong> 在 Vault 跟先前版本的 OpenBao 中，HA 架構裡的 standby 節點純粹是備援，所有讀寫都要轉發給 active leader。應用程式拿 secret 是壓倒性的讀取操作——CI&#x2F;CD 啟動時拉資料庫密碼、應用程式啟動時拉 API key、cert-manager 簽憑證——尖峰時這些請求全擠到 leader 一台機器上。2.5 開始 standby 節點可以本地處理唯讀請求，寫入仍轉發給 leader 維持強一致性。對於同一個 OpenBao 叢集要服務上百個 microservice 的場景，這項變動可以把讀取吞吐量直接翻三到五倍，而且不用改客戶端設定，現有 sidecar 跟 SDK 自動受惠。</p><p><strong>Plugin 可以宣告式地從 OCI registry 拉下來。</strong> 過去要在 Vault 上裝第三方 plugin 是一段繁瑣流程：把 binary 上傳到 server、註冊 SHA256、啟用 plugin、設定 mount。2.5 引入 declarative plugin distribution，可以在 server 設定檔裡直接寫 <code>image = &quot;ghcr.io/example/bao-plugin-foo:v1.2.0&quot;</code>，OpenBao 啟動時自動拉、驗證簽章、註冊。把 plugin 管理納入 IaC 流程之後，重建叢集不再需要手動補 plugin 這個步驟。對於需要客製化 secret engine 或 audit device 的團隊特別有用。</p><p>附帶的還有 OIDC Provider 補上 Client Credentials flow（讓 OpenBao 直接當 machine-to-machine OIDC IdP）、audit 多了 HTTP webhook device（低流量場景可以直接推到 Slack 或 Discord）、stricter path validation 把幾個歷史遺留的權限繞過漏洞收掉。</p><h2 id="從-Vault-搬過來會在哪些地方卡住"><a href="#從-Vault-搬過來會在哪些地方卡住" class="headerlink" title="從 Vault 搬過來會在哪些地方卡住"></a>從 Vault 搬過來會在哪些地方卡住</h2><p>完全相容是 OpenBao 的設計目標，但實務上有幾個邊角要先確認。</p><p>企業版功能是第一個。如果環境有用到 HSM 整合（Vault Enterprise 的 Auto-unseal HSM）、performance replication、Sentinel policy、DR replication、MFA via Duo&#x2F;PingID 這些只在企業版的東西，OpenBao 沒有對等實作。Auto-unseal 用 cloud KMS 那一支倒是社群版就有，沒受影響。</p><p>第二個是 plugin 生態。Vault 的官方 plugin 走 HashiCorp 的 registry，部分 plugin 在 BUSL 之後也跟著鎖授權。OpenBao 自己維護的 plugin registry 還在補齊，常見的 database plugin、SSH plugin、AWS plugin 都已經 fork 過來且持續更新，但比較冷門的第三方 plugin（某些雲廠商的 dynamic credentials）可能要自己編。</p><p>第三個是 Terraform provider。HashiCorp 的 <code>hashicorp/vault</code> provider 仍可使用，因為它走的是 HTTP API；社群另外維護了 <code>openbao/openbao</code> provider 提供原生介面，差異主要在於後者跟著 OpenBao 的新功能更新更快。兩個都能用，但建議新專案直接走 <code>openbao/openbao</code> 避免將來踩到 HashiCorp 的授權邊界。</p><p>實際搬遷流程是這樣：先用 <code>vault operator raft snapshot save</code> 匯出現有資料；架一個 OpenBao 叢集，初始化但不 unseal；把 snapshot 用 <code>bao operator raft snapshot restore</code> 灌進去；驗證 secret engine、policy、token 都能讀；切換客戶端的 endpoint。整個過程通常一個下午可以做完，前提是事先把 Vault 跟 OpenBao 版本對齊（OpenBao 2.5 對應 Vault 1.14 為基底，但已經往前疊了兩年的修補）。</p><h2 id="在臺灣的-VPS-上跑-OpenBao-的最小可用設定"><a href="#在臺灣的-VPS-上跑-OpenBao-的最小可用設定" class="headerlink" title="在臺灣的 VPS 上跑 OpenBao 的最小可用設定"></a>在臺灣的 VPS 上跑 OpenBao 的最小可用設定</h2><p>KVM-based VPS 都能跑，記憶體 1 GB 起跳；2 GB 比較舒服，留空間給 PostgreSQL。儲存後端有三個合理選擇：</p><ul><li>單機測試用 file backend 就夠，設定一行 <code>storage &quot;file&quot; { path = &quot;/opt/bao/data&quot; }</code>。</li><li>正式環境的單機部署建議 PostgreSQL backend，跟既有的 Postgres 共用，省一台 VM。</li><li>真的需要 HA 才上 Integrated Storage（Raft），三節點起跳，每節點獨立的 disk。</li></ul><p>TLS 千萬不要省。內部 service mesh 已經 mTLS 的情境可以用 PROXY protocol 把 TLS 終止在前面的 reverse proxy，否則直接讓 OpenBao 自己拿 Let’s Encrypt 憑證，或者用 Caddy 做 reverse proxy 自動處理 ACME。Audit log 一定要開——OpenBao 預設不開 audit，但只要任何 request 失敗它就會自我關閉，所以這項要在初始化後馬上啟用，否則第一次 storage write 失敗整個服務就停掉。</p><p>Auto-unseal 是上正式環境的另一個關鍵。如果靠人手 unseal，每次重啟服務都得有人在線輸入 unseal key，這在自動化部署裡完全不可行。把 unseal 委派給 AWS KMS、Google Cloud KMS、Azure Key Vault 是常見做法，臺灣本地沒有對等服務，務實做法是用海外雲廠商的 KMS 加上嚴格的 IAM policy，或者用另一個 OpenBao 實例的 Transit secret engine 來達成同樣效果。</p><p>備份這件事容易被忽略但代價最高。OpenBao 的資料一旦掉了沒有救援機制——加密 key 在 master key 裡、master key 用 unseal key 解、unseal key 五份分散。建議搭配 <a href="/restic-vps-backup-encryption-deduplication/">Restic 把整個 storage backend 加密增量備份</a> 到異地物件儲存，這比 snapshot 單檔複製安全得多。</p><hr><p>對於還在猶豫要不要把 secrets 從 <code>.env</code> 檔搬出來的團隊，OpenBao 2.5 把過去 Vault 在自架場景的痛點降到最低：相容、可擴展、明確的開源授權、不用支付企業版授權費。在臺灣選擇穩定的 VPS 環境跑 OpenBao 並做好異地備份，是兼顧成本與資料主權的務實組合。NCSE Network 提供位於是方電訊機房、Intel Gold CPU 與 NVMe SSD 的 VPS 方案，適合用來承載 OpenBao 這類需要低延遲存取的核心基礎設施；如果規模擴大需要多機 HA，也可以延伸到機房代管。詳情請至 <a href="https://ncse.tw/">NCSE Network</a> 了解。</p>]]>
    </content>
    <id>https://blog.ncse.tw/openbao-vault-fork-secrets-management-self-hosted/</id>
    <link href="https://blog.ncse.tw/openbao-vault-fork-secrets-management-self-hosted/"/>
    <published>2026-06-02T03:00:00.000Z</published>
    <summary>HashiCorp 2023 年改授權之後，Vault 的開源位置一直沒人接手。OpenBao 從 Vault 1.14 fork 出來，2026 年 2 月推出的 2.5 把橫向讀取擴展與 OCI 外掛分發補上，正式變成 self-hosted secrets management 的可行替代方案。本文解析 fork 的來龍去脈、2.5 真正值得停下來看的變動，以及搬遷時會踩到的地方。</summary>
    <title>HashiCorp 把 Vault 鎖進 BUSL，OpenBao 用 MPL 把秘密管理留在開源這一側</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="LLM" scheme="https://blog.ncse.tw/tags/LLM/"/>
    <category term="AI Gateway" scheme="https://blog.ncse.tw/tags/AI-Gateway/"/>
    <category term="LiteLLM" scheme="https://blog.ncse.tw/tags/LiteLLM/"/>
    <category term="Bifrost" scheme="https://blog.ncse.tw/tags/Bifrost/"/>
    <category term="MCP" scheme="https://blog.ncse.tw/tags/MCP/"/>
    <content>
      <![CDATA[<p>LLM API 串接在 2024 年還是一段 fetch 就能解決的事。到了 2026 年中，多數正式服務已經面對截然不同的問題：哪個應用打哪家模型、誰花了多少額度、Anthropic 限流時要不要切到 Bedrock 的 Claude、新進工程師的 OpenAI 金鑰要不要直接配發。這些問題堆在一起會逼出一個明確結論——應用程式不應該再直接呼叫廠商端點，中間需要一層 AI Gateway。</p><p>LiteLLM 把這個類別從概念推成預設選項。從 2023 年一個 Python SDK 出發，到 2026 年已經支援超過一百家供應商，自架版本接管虛擬金鑰、成本追蹤、速率限制與多供應商容錯。但 Python 的 GIL 在高吞吐情境下會撞到上限。Bifrost 在 2025 年由 Maxim 團隊以 Apache 2.0 釋出，用 Go 重寫整套閘道，在 5K RPS 持續壓測下只多 11 微秒延遲，宣稱比 LiteLLM 快上五十倍。</p><h2 id="AI-Gateway-已經不是選配，而是基礎建設的一層"><a href="#AI-Gateway-已經不是選配，而是基礎建設的一層" class="headerlink" title="AI Gateway 已經不是選配，而是基礎建設的一層"></a>AI Gateway 已經不是選配，而是基礎建設的一層</h2><p>把 LLM 呼叫直接散落在各個服務裡會在三件事上出問題。第一是成本歸屬：CFO 拿到 Anthropic 月帳問哪個產品線吃了八成額度，沒有閘道就只能靠埋 log 拼湊。第二是供應商容錯：OpenAI 半夜當機，希望業務流程自動切到 Vertex AI 的 Gemini，沒有閘道就得讓每個應用各自實作 fallback 邏輯。第三是治理：給合約商一把 key、給內部工程師另一把、不同團隊的預算上限不同，原生 API 沒有這個概念。</p><p>這三個問題單獨解都不難，但要做到統一就需要一個中介層。AI Gateway 接管的就是這層——它對外提供 OpenAI 相容的 endpoint，對內把流量分派、計費、稽核、容錯抽象成可組態的策略。當 2025 年中型團隊普遍導入這套設計之後，這個類別就從「可選工具」變成「跟 reverse proxy 同等地位的基礎元件」。</p><h2 id="LiteLLM-把生態系拉起來，也撞到了-Python-的天花板"><a href="#LiteLLM-把生態系拉起來，也撞到了-Python-的天花板" class="headerlink" title="LiteLLM 把生態系拉起來，也撞到了 Python 的天花板"></a>LiteLLM 把生態系拉起來，也撞到了 Python 的天花板</h2><p>LiteLLM 之所以變成主流，是因為它把所有廠商的請求格式統一成 OpenAI Chat Completions。一個 Anthropic、一個 Bedrock、一個 Vertex AI，原生 SDK 的參數、串流格式、錯誤碼全部不同；LiteLLM 在中間做翻譯，下游應用只需要假設自己在打 OpenAI。Proxy 模式進一步把這層搬進獨立程序，搭配 Postgres 做虛擬金鑰、預算、稽核日誌，自架版本可以撐起整個公司的 LLM 流量分發。</p><p>問題出在它是 Python。GIL 讓單一程序的請求處理被序列化，正式環境想吃滿一台 8 核心機器就得開八個 process 在 load balancer 後面排隊。社群實測在 1K RPS 以上的壓力下，LiteLLM 大約會額外加上 40 毫秒的閘道延遲——對於需要等模型回應的同步請求或許不痛，但對於 agent 場景一次工作流要打十幾次 LLM 的情境，這個延遲會累加成可觀的尾延遲。</p><p>更實際的限制是部署複雜度。LiteLLM 在正式環境通常需要 Postgres 存設定、Redis 做快取、Prometheus 收 metric，外加 N 個 worker process。對於只想跑一台 VPS 處理幾百 QPS 的中型團隊，這套堆疊偏厚。</p><h2 id="Bifrost-換了一條路徑：Go、零外部依賴、Plugin-化"><a href="#Bifrost-換了一條路徑：Go、零外部依賴、Plugin-化" class="headerlink" title="Bifrost 換了一條路徑：Go、零外部依賴、Plugin 化"></a>Bifrost 換了一條路徑：Go、零外部依賴、Plugin 化</h2><p>Bifrost 沒有試圖比 LiteLLM 多支援哪一家廠商，而是把 Gateway 這層重新設計。整個服務用 Go 寫成單一 binary，預設不需要外接資料庫，啟動之後直接吃進設定檔或透過 Web UI 動態調整。把 LiteLLM 那套「Postgres + Redis + 多 worker」的編排簡化成一支執行檔，是它最直接的賣點。</p><p>架構分成幾個明確的層：核心提供供應商抽象與請求&#x2F;回應 schema、Transport 層處理 HTTP 與 WebSocket、Plugin 系統用來掛載治理、快取、稽核、可觀測性。語義快取、MCP Gateway、預算控制、OIDC 登入全部都是 first-class plugin，可以選擇開啟或拔掉。</p><p>效能數字是它最被引用的點。在 5,000 RPS 的持續壓測下，Bifrost 的閘道層平均只額外增加 11 微秒延遲，記憶體佔用比 LiteLLM 對等配置少約六成八。這不是寫死的 mock，而是把請求實際轉發到 OpenAI、Anthropic 之後扣掉上游時間量出來的結果。對應的具體場景是：一個 agent 工作流要連續打八次 LLM，閘道本身只貢獻不到一毫秒的累積延遲。</p><p>不過效能數字向來要保留懷疑空間。50x 是廠商自己提出的對比，社群獨立實測差距更接近 30 到 40 倍，且只有在高並發下才會明顯。如果服務全天 QPS 不到一百，Bifrost 對比 LiteLLM 的差異主要不在速度，而在維運的單純程度。</p><h2 id="語義快取與-MCP-Gateway-才是真正省錢的兩個閥門"><a href="#語義快取與-MCP-Gateway-才是真正省錢的兩個閥門" class="headerlink" title="語義快取與 MCP Gateway 才是真正省錢的兩個閥門"></a>語義快取與 MCP Gateway 才是真正省錢的兩個閥門</h2><p>LLM 帳單最大的洩漏點不是延遲，是「同樣意思的問題被問了第二次」。語義快取的概念是用向量相似度去判斷新請求跟某個快取項目語意接近，直接回傳之前的回應。Bifrost 內建這套機制，支援 Valkey、Qdrant、Weaviate、Pinecone 作為向量儲存後端。實測 cache hit 大約在 5 毫秒回應，cache miss 含 embedding 計算約 60 毫秒。</p><p>這套機制在文件問答、客服 bot、agent 工具呼叫情境下命中率特別高。一個典型的內部知識庫機器人，員工 70% 的問題會集中在前 50 種意圖上，啟用語義快取之後 token 用量可以直接腰斬。LiteLLM 也有快取，但只做精確雜湊比對，要求字串完全一樣才命中，實務上能省下的 token 有限。</p><p>MCP Gateway 則是另一個 2025 下半年才浮上來的需求。當 agent 開始大量呼叫外部工具，每個請求都要把所有可用工具的 schema 塞進 prompt，光是「告訴模型有哪些 tool 可以用」就吃掉幾千個 token。Bifrost 的 MCP Gateway 接管 STDIO、HTTP、SSE 三種傳輸協定的 MCP server，做 tool filtering，每次請求只把當下相關的工具 schema 傳給模型。社群測得在多 MCP server 的 agent 工作流下，token 消耗可以再砍掉約一半。</p><p>語義快取省的是重複問題的錢，MCP Gateway 省的是工具描述的錢。兩者疊加是中型團隊把 LLM 月帳壓進預算的主要手段。</p><h2 id="在-VPS-上自架的實務細節"><a href="#在-VPS-上自架的實務細節" class="headerlink" title="在 VPS 上自架的實務細節"></a>在 VPS 上自架的實務細節</h2><p>Bifrost 的部署在小規模情境簡單到接近 trivial。一台 2 vCPU、4 GB RAM 的 VPS 就能撐住每秒一兩百個請求。最直接的方式是 Docker：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker run -d --name bifrost \</span><br><span class="line">  -p 127.0.0.1:8080:8080 \</span><br><span class="line">  -v $(<span class="built_in">pwd</span>)/data:/app/data \</span><br><span class="line">  -e OPENAI_API_KEY=<span class="variable">$OPENAI_API_KEY</span> \</span><br><span class="line">  -e ANTHROPIC_API_KEY=<span class="variable">$ANTHROPIC_API_KEY</span> \</span><br><span class="line">  maximhq/bifrost:latest</span><br></pre></td></tr></table></figure><p>這裡刻意只把 port 綁到 <code>127.0.0.1</code>，不要直接寫 <code>-p 8080:8080</code>——Docker 會繞過 UFW 直接打開公網的 8080，等於把含金鑰的管理 UI 裸奔在 Internet 上。啟動之後透過 SSH tunnel 或反向代理連到 <code>127.0.0.1:8080</code> 就會看到管理 UI，廠商金鑰、虛擬金鑰、預算、限流規則都從 Web UI 設定，落地存進 SQLite，重啟不會掉。要做語義快取的場景再多開一個 Valkey 或 Qdrant 容器即可。</p><p>正式環境前面再擺一層 Caddy 或 Nginx 接 TLS，公開的部分只暴露 <code>/v1/chat/completions</code> 這類 OpenAI 相容路徑，管理 UI 用 Basic Auth 或 OIDC 擋掉，或乾脆只開放從內網存取。日誌量在高流量下會迅速膨脹，內建的 Plugin 可以把詳細請求&#x2F;回應內容轉發到外部物件儲存或 ClickHouse，避免本機磁碟塞爆。</p><p>如果整個機房在臺灣、目標讀者也在亞太，Gateway 跟使用者放同區可以省下大約 100 到 200 毫秒的往返時間，這段延遲在 agent 工作流的尾延遲上會被放大很多倍。</p><h2 id="什麼時候不該換到-Bifrost"><a href="#什麼時候不該換到-Bifrost" class="headerlink" title="什麼時候不該換到 Bifrost"></a>什麼時候不該換到 Bifrost</h2><p>LiteLLM 在兩個情境下仍然是更務實的選擇。一是已經在用 LiteLLM 的 Python SDK 直接 import 進應用程式碼當函式庫使用——那個場景不需要獨立閘道，Bifrost 也沒打算提供等價的 Python SDK。二是依賴 LiteLLM 某個特定 plugin 或廠商整合，例如某個冷門地區的本地 LLM 服務商，LiteLLM 多年累積的廠商覆蓋目前仍領先。</p><p>效能數字也要看真實負載決定價值。QPS 在 50 以下的小團隊，Python 的天花板根本碰不到，這時候挑 LiteLLM 還是 Bifrost 比的是維運偏好，不是速度。Bifrost 的優勢真正會體現在 agent 工作流、批次推論、企業內部多應用共享閘道的中高並發場景。</p><h2 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h2><p>AI Gateway 從可選工具變成中型團隊基礎建設的一層，已經是 2026 年的既定事實。LiteLLM 撐起了這個類別的早期，並把 OpenAI 相容介面變成行業預設；Bifrost 把效能瓶頸打開、用 Go 簡化部署、把語義快取與 MCP Gateway 做成內建。對於正在規劃自架 AI Gateway、或從 LiteLLM 評估升級的團隊，Bifrost 在 2026 年的版本已經穩定到可以投入正式環境。</p><p>把 AI Gateway 放在臺灣本地有兩個現實好處：使用者到閘道之間少 100 到 200 毫秒的延遲、流量不必繞出國跨海。NCSE Network 在臺灣是方電訊機房提供配備 Intel Gold CPU 與 NVMe SSD 的 VPS 主機，搭配充足的對外頻寬，適合作為自架 AI Gateway 的落腳點。需要把 LLM 流量收回自己手裡的團隊，可以參考 NCSE Network 的 VPS 方案。</p>]]>
    </content>
    <id>https://blog.ncse.tw/bifrost-llm-gateway-go-litellm-alternative/</id>
    <link href="https://blog.ncse.tw/bifrost-llm-gateway-go-litellm-alternative/"/>
    <published>2026-06-01T03:00:00.000Z</published>
    <summary>應用程式直接打 OpenAI、Anthropic 端點的時代結束了，多數正式服務已經把 AI Gateway 當成基礎建設的一層。LiteLLM 撐起了大半個自架生態系，但 Python GIL 讓它撞到天花板。Bifrost 用 Go 重寫整套閘道，5K RPS 下只多 11 微秒延遲，並把語義快取與 MCP Gateway 做成內建模組。</summary>
    <title>LiteLLM 養大了 AI Gateway 這個類別，2026 年 Bifrost 用 Go 把效能上限再推 50 倍</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Rust" scheme="https://blog.ncse.tw/tags/Rust/"/>
    <category term="Stalwart" scheme="https://blog.ncse.tw/tags/Stalwart/"/>
    <category term="信箱伺服器" scheme="https://blog.ncse.tw/tags/%E4%BF%A1%E7%AE%B1%E4%BC%BA%E6%9C%8D%E5%99%A8/"/>
    <category term="SMTP" scheme="https://blog.ncse.tw/tags/SMTP/"/>
    <category term="JMAP" scheme="https://blog.ncse.tw/tags/JMAP/"/>
    <content>
      <![CDATA[<p>自架信箱伺服器在過去十年幾乎是一道沒人想再走一次的關卡。Postfix 負責 SMTP、Dovecot 負責 IMAP、Rspamd 負責反垃圾、OpenDKIM 處理簽章、ClamAV 掃毒、Roundcube 提供 Webmail——這還沒算上 LDAP、Redis、MySQL 這些底層。Mailcow 把這些東西打包成 docker-compose，一鍵起來看似輕鬆，實際上開機後要協調十五個以上的容器，記憶體至少吃 2GB，重啟一次要等好幾分鐘。</p><p>Stalwart Mail Server 從另一個方向解這個問題。它用 Rust 從頭實作了一支完整的信箱伺服器，把 SMTP、IMAP4rev2、POP3、JMAP、CalDAV、CardDAV、WebDAV、反垃圾、DKIM 簽章、ManageSieve 全部塞進同一支 binary。記憶體下限 512MB，啟動時間以秒計算。在臺灣自架信箱、又不想被 Mailcow 那一堆容器壓垮的人，現在多了一個值得認真評估的選項。</p><h2 id="為什麼信箱伺服器要這麼多元件"><a href="#為什麼信箱伺服器要這麼多元件" class="headerlink" title="為什麼信箱伺服器要這麼多元件"></a>為什麼信箱伺服器要這麼多元件</h2><p>傳統信箱架構的複雜不是設計者愛找麻煩，而是歷史包袱。SMTP、IMAP、POP3 是 1980 到 1990 年代分開長出來的協定，當時各自的 reference implementation 也分開維護。Postfix 寫 SMTP、Dovecot 寫 IMAP，兩邊都已經是十幾年的成熟專案，沒有人想合併。反垃圾這層更晚才獨立出來，Rspamd、SpamAssassin 都是後來的事。每多一個元件，就多一份設定檔、一條 Unix socket、一個失敗點。</p><p>這套組合在大型郵件營運商那邊運作得很好，因為他們有專職的郵件工程師，每個元件單獨擴展更有彈性。對自架用戶來說剛好相反——大部分人只想要一支 binary 收信、寄信、不要被當垃圾，三件事完成就好。</p><p>Mailcow、Mail-in-a-Box、Docker-Mailserver 都是試圖把這套組合「打包好」的方案，但本質上還是同一個架構，只是把痛苦藏在 compose 檔後面。Stalwart 是少數真的從零開始、把所有協定用一套程式碼實作完的專案。</p><h2 id="一支-binary-裡面有什麼"><a href="#一支-binary-裡面有什麼" class="headerlink" title="一支 binary 裡面有什麼"></a>一支 binary 裡面有什麼</h2><p>Stalwart 的 GitHub 倉庫到 2026 年 5 月已經累積一萬兩千多顆星，AGPLv3 授權，作者宣布專案已經「feature complete」，下一步是收尾到 1.0。它對外提供的功能可以拆成三層：</p><p><strong>協定層</strong>完整支援現代與傳統的所有信箱協定：SMTP（含 ESMTP、Submission、TLS 全套）、IMAP4rev2 並向下相容 IMAP4rev1、POP3、JMAP for Mail。JMAP 是 IETF 在 2019 年標準化的新一代信箱協定，把 IMAP 那種「保持連線、逐封同步」的模式換成 HTTP&#x2F;JSON，行動裝置上的同步效率明顯改善。Fastmail 已經整套切過去，但開源端真正完整支援的伺服器不多，Stalwart 是其中之一。</p><p><strong>協作層</strong>順手把 CalDAV、CardDAV、WebDAV 也補齊，意思是同一支伺服器就能當行事曆、聯絡簿、雲端硬碟用。這部分在功能上類似 Radicale 加 Nextcloud 的子集，深度不及 Nextcloud，但對個人或小型團隊夠用。</p><p><strong>處理層</strong>包含 DKIM 簽章、SPF&#x2F;DMARC&#x2F;ARC 驗證、貝氏與統計分類的反垃圾、DNS 黑名單查詢、灰名單、ManageSieve 過濾腳本。較新版本還支援 LLM 輔助的內容分類，把可疑郵件丟給本地端模型再判斷一次。</p><p>後端儲存可以選 RocksDB（預設、零設定）、PostgreSQL、MySQL、SQLite、FoundationDB、S3 相容物件儲存。對個人架站來說，RocksDB 用起來最省事，啟動時自動建檔，不需要另外管資料庫。</p><h2 id="JMAP-的價值在哪裡"><a href="#JMAP-的價值在哪裡" class="headerlink" title="JMAP 的價值在哪裡"></a>JMAP 的價值在哪裡</h2><p>IMAP 的設計假設是「使用者開一個視窗、伺服器把資料夾狀態推給他、視窗關掉前連線一直開著」。在 PC 時代這個模型沒問題，到了行動裝置就開始崩壞——手機螢幕關掉、4G 切換、Wi-Fi 漫遊，每一次都意味著 IMAP 要重新 IDLE、重新對齊 UID。同一個信件清單可能被同步好幾次，電池一直被吃。</p><p>JMAP 改用 HTTP 短連線加上一個 state token。客戶端問伺服器「我上次同步到 state&#x3D;42，給我之後的變更」，伺服器丟回變更清單，連線就斷掉。手機從睡眠醒來時，只要再問一次 state，馬上就能對齊到最新狀態。對信箱應用程式來說，這個改動讓背景同步快上一個量級，行動數據用量也下降。</p><p>問題是 JMAP 客戶端還少。iOS 內建 Mail、Apple Mail、大多數 Android 客戶端目前仍是 IMAP-only。能用 JMAP 的有 Thunderbird 的實驗性支援、Fastmail 自家客戶端、少數開源專案。所以 Stalwart 同時支援 JMAP 與 IMAP 是務實的——現在用 IMAP，等客戶端跟上就直接切過去，不必再換伺服器。</p><h2 id="部署起來真的就一支-binary-嗎"><a href="#部署起來真的就一支-binary-嗎" class="headerlink" title="部署起來真的就一支 binary 嗎"></a>部署起來真的就一支 binary 嗎</h2><p>最簡單的安裝方式是執行官方提供的 shell script：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl --proto <span class="string">&#x27;=https&#x27;</span> --tlsv1.2 -sSf https://get.stalw.art/install.sh | sh -s -- /opt/stalwart all</span><br></pre></td></tr></table></figure><p>腳本會把 binary 放到 <code>/opt/stalwart</code>，建立 systemd unit，然後啟動一個 Web 設定精靈。第一次連上 <code>https://your-ip:8080</code> 會跳出初始化頁面，問你要不要產生 self-signed 憑證、建立 admin 帳號、選擇儲存後端。整個流程不到五分鐘。</p><p>Docker 方式則是：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">stalwart:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">stalwartlabs/stalwart:latest</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">stalwart</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;25:25&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;465:465&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;587:587&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;110:110&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;995:995&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;143:143&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;993:993&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;110:110&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;995:995&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;4190:4190&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;443:443&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./data:/opt/stalwart</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><p>對外只要開這幾個 port，反向代理可以直接掛在 8080 前面用 Caddy 或 Nginx 包 TLS。如果直接讓 Stalwart 自己處理 TLS，它內建 ACME 客戶端，把網域指向 IP 之後會自動申請 Let’s Encrypt 憑證。</p><h2 id="DNS-設定才是真的關卡"><a href="#DNS-設定才是真的關卡" class="headerlink" title="DNS 設定才是真的關卡"></a>DNS 設定才是真的關卡</h2><p>把 Stalwart 跑起來只佔自架信箱整件事的兩成，剩下八成是 DNS。少做任何一項都會被 Gmail、Outlook、iCloud 退信或直接送進垃圾匣。</p><p>最低必要的記錄有：</p><ol><li><code>mail.example.com</code> 的 A&#x2F;AAAA 記錄指向 VPS 公網 IP</li><li><code>example.com</code> 的 MX 記錄指向 <code>mail.example.com</code>，優先級 10</li><li>SPF：<code>example.com</code> TXT 記錄寫 <code>v=spf1 mx -all</code>，意思是只有 MX 指向的伺服器有權代寄</li><li>DKIM：在 Stalwart Web 後台產生金鑰，把 selector 那串 TXT 加到 DNS</li><li>DMARC：<code>_dmarc.example.com</code> TXT 記錄寫 <code>v=DMARC1; p=quarantine; rua=mailto:postmaster@example.com</code></li><li>PTR：請 VPS 服務商把 IP 的反查指向 <code>mail.example.com</code></li></ol><p>PTR 是最常被忽略的一項，也是最致命的。Gmail 收到郵件後會反查發送方 IP，如果反查結果跟 EHLO 宣告的主機名稱對不上，那封信幾乎一定進垃圾匣。臺灣本地的 VPS 服務商在這方面差異很大，有些後台直接讓你改 PTR，有些要開 ticket。下訂 VPS 之前先確認這項，比事後想搬家容易得多。</p><h2 id="Port-25-才是真正的天花板"><a href="#Port-25-才是真正的天花板" class="headerlink" title="Port 25 才是真正的天花板"></a>Port 25 才是真正的天花板</h2><p>技術設定全部做對，最後撞上的天花板叫 port 25。AWS、Azure、Google Cloud 預設封 25 對外出站，要解封要填表、說明用途、等審核。DigitalOcean、Vultr、Linode 通常要求新帳號跑滿幾個月才會開。對於想架信箱的人來說，VPS 提供商願不願意給 port 25 比規格高低重要得多。</p><p>選 VPS 時可以直接寫信問業者：是否預設開放 port 25 對外出站、是否能設定 PTR。臺灣本地的 VPS 業者在這方面通常彈性比較大，至少不會把 25 當預設封鎖。一旦業者點頭，剩下的就是把 Stalwart 跑好。</p><h2 id="反垃圾與信譽：技術解決不了的部分"><a href="#反垃圾與信譽：技術解決不了的部分" class="headerlink" title="反垃圾與信譽：技術解決不了的部分"></a>反垃圾與信譽：技術解決不了的部分</h2><p>Stalwart 內建的反垃圾在實測中表現不錯，貝氏分類器加上 DNSBL 查詢，對量小的個人信箱足以擋掉九成以上的垃圾郵件。但反向的問題更難：如何讓你寄出去的信不被當垃圾。</p><p>新申請的 IP 沒有信譽分數，前幾週寄出去的信很可能直接被 Gmail 丟掉。解法只有一個，叫 IP warm-up——前兩週只寄少量信給願意收的對象，慢慢累積發送量。Stalwart 沒有自動 warm-up 功能，這部分要靠人工控制。</p><p>如果是電子報這類大量發送，建議搭配 Postmark、Amazon SES 或自架 Postal 作為出站轉發。Stalwart 處理收信和個人寄信很適合，但商業級的大量寄送需要更專門的工具。</p><h2 id="Stalwart-還缺什麼"><a href="#Stalwart-還缺什麼" class="headerlink" title="Stalwart 還缺什麼"></a>Stalwart 還缺什麼</h2><p>要說缺點，目前最明顯的是沒有內建 Webmail。Stalwart 提供 JMAP API，但 Web 介面只有管理後台，沒有讓使用者收發信件的頁面。實務上要另外搭一個 Roundcube、SnappyMail 或新一代的 JMAP 客戶端如 Cypht。</p><p>中文文件相當稀缺，遇到問題大多要翻官方英文文件或 GitHub issue。社群規模比 Mailcow 小，踩到問題不一定能在中文討論區找到答案。</p><p>對於跑了五年以上的 Mailcow 用戶來說，從 Stalwart 0.x 跳過去是有風險的。1.0 還沒釋出，資料庫 schema 還在收斂，未來升級可能需要遷移工具。</p><h2 id="該換還是不該換"><a href="#該換還是不該換" class="headerlink" title="該換還是不該換"></a>該換還是不該換</h2><p>如果是從零開始架信箱、預算或機器資源有限、想用 JMAP、不需要太花俏的 Webmail，Stalwart 是目前最值得試的選項。512MB 記憶體就能跑、一支 binary 包辦所有事，運維負擔遠低於 Mailcow。</p><p>如果現有 Mailcow 跑得很順、團隊已經習慣 SOGo Webmail、需要 ClamAV 防毒整合、寄信量大且需要詳細 deliverability 追蹤，那暫時沒有非換不可的理由。等 Stalwart 1.0 釋出、生態圈成熟一些再評估也不遲。</p><p>如果只是想做企業出站郵件追蹤（電子報、API 寄信），Postal 仍然是專門解這個問題的工具，Stalwart 並不是這個方向。</p><hr><p>自架信箱的成敗從來不只在於選了哪個軟體，VPS 提供商願不願意開 port 25、能不能設定 PTR、機房 IP 的信譽分數，才是決定郵件能不能順利送達的根本。NCSE Network 在臺灣是方電訊機房提供的 VPS 預設開放 port 25 對外，PTR 可在後台自助設定，搭配 Intel Gold CPU 與 NVMe SSD，跑 Stalwart 這類 Rust 應用所需資源遠低於規格上限。想自架信箱伺服器或其他需要乾淨 IP 的服務，可以參考 <a href="https://ncse.tw/">NCSE Network 的 VPS 方案</a>。</p>]]>
    </content>
    <id>https://blog.ncse.tw/stalwart-mail-server-self-hosted-rust/</id>
    <link href="https://blog.ncse.tw/stalwart-mail-server-self-hosted-rust/"/>
    <published>2026-05-31T02:30:00.000Z</published>
    <summary>Mailcow 動輒十幾個容器、Postfix 加 Dovecot 加 Rspamd 拼裝起來又是另一場硬仗。Stalwart 用一支 Rust binary 把 SMTP、IMAP、JMAP、CalDAV、反垃圾全部收齊，512MB 記憶體就能跑。本文解析它的架構、部署細節，以及自架信箱該不該換上來。</summary>
    <title>自架信箱不必再拼裝十幾個容器：Stalwart 用一支 Rust binary 接管 SMTP、IMAP、JMAP</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="VPS" scheme="https://blog.ncse.tw/tags/VPS/"/>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="LLM" scheme="https://blog.ncse.tw/tags/LLM/"/>
    <category term="Karakeep" scheme="https://blog.ncse.tw/tags/Karakeep/"/>
    <category term="Ollama" scheme="https://blog.ncse.tw/tags/Ollama/"/>
    <category term="書籤管理" scheme="https://blog.ncse.tw/tags/%E6%9B%B8%E7%B1%A4%E7%AE%A1%E7%90%86/"/>
    <content>
      <![CDATA[<p>Mozilla 在 2025 年 5 月宣告關閉 Pocket，7 月正式停服，11 月把所有使用者資料連同伺服器一起銷毀。這個被收購十年、內建在 Firefox 裡的閱讀清單服務，從此走進歷史。對長期把文章丟進去「之後再讀」的人來說，Pocket 留下的空缺意外地難填——商業替代品要綁帳號，免費版動輒上限封頂；既有的自架選項 Wallabag 介面老派，AI 功能薄弱。</p><p>直到 Karakeep 把這件事重新定義。它的前身是 2024 年走紅的 Hoarder，今年初改名重整之後，把書籤管理、AI 自動分類、全文檢索、瀏覽器擴充與行動 App 整套打包，GitHub 星數半年內突破三萬。對於想擺脫 SaaS、又不願意放棄智慧型功能的使用者，這套自架書籤系統補上了長期缺席的那一塊。</p><h2 id="不只是書籤，是一個會自己歸檔的數位倉庫"><a href="#不只是書籤，是一個會自己歸檔的數位倉庫" class="headerlink" title="不只是書籤，是一個會自己歸檔的數位倉庫"></a>不只是書籤，是一個會自己歸檔的數位倉庫</h2><p>Karakeep 的定位比傳統閱讀清單更廣。除了存連結，它同時支援純文字筆記、圖片、PDF 三種主要型態，每一筆都會經過後台 worker 處理：抓取網頁原文、用 monolith 把完整頁面封存成單一 HTML、用 yt-dlp 把 YouTube 影片下載備份、用 Tesseract 對圖片做 OCR。原始來源網站關站了也不影響資料。</p><p>抓回來的內容會丟進 Meilisearch 做全文索引。輸入幾個關鍵字就能跨連結、筆記、PDF 一起搜，速度比想像中快——Meilisearch 在這個資料量級下單核心就能撐住每秒上百筆查詢。</p><p>對 RSS 玩家而言還有一個亮點：訂閱來源後 Karakeep 會自動把每篇新文章加進清單，整套變成「個人化的長期保存型 RSS 閱讀器」。這跟 Pocket 過去單純存連結的模式已經是兩種產品。</p><h2 id="AI-自動標籤把整理這件事從手動變成自動"><a href="#AI-自動標籤把整理這件事從手動變成自動" class="headerlink" title="AI 自動標籤把整理這件事從手動變成自動"></a>AI 自動標籤把整理這件事從手動變成自動</h2><p>Karakeep 最有辨識度的功能是 LLM 自動分類。每一筆新書籤進來，後台會把抓到的網頁內文送進設定好的語言模型，要求它產出三到五個語意相關的標籤，再附上一段不超過兩百字的摘要。</p><p>支援的後端不限於 OpenAI。設定 <code>OPENAI_BASE_URL</code> 與 <code>OPENAI_API_KEY</code> 兩個環境變數就能改指向任何 OpenAI 相容介面，這代表 Ollama、vLLM、LM Studio、甚至自架的 LiteLLM proxy 都能直接接上。在 VPS 上跑一個 7B 等級的開源模型（Llama 3、Qwen 2.5、Mistral），分類品質已經夠用，每筆書籤的成本是零。</p><p>實務上有兩個值得注意的細節。一是模型不需要很大：tagging 任務對推理品質的要求遠低於對話，3B 到 8B 的模型表現足夠，硬塞 70B 反而讓單筆處理時間從幾秒變成幾十秒。二是 tagging 與 summary 可以分開設定不同模型，常見組合是用小模型做標籤、用大一點的做摘要，整體吞吐量更高。</p><p>若手邊沒有 GPU 而 CPU 又跑不動 LLM，把 tagging 流量導向 OpenAI 也是務實選擇。粗估每千筆書籤的 API 成本不到一美元，遠低於把整套 LLM 自己撐起來的硬體投入。</p><h2 id="服務拆分：四到六個容器組成的後台"><a href="#服務拆分：四到六個容器組成的後台" class="headerlink" title="服務拆分：四到六個容器組成的後台"></a>服務拆分：四到六個容器組成的後台</h2><p>Karakeep 不是單體應用，docker-compose 一拉起來會跑出幾個獨立容器，各自負責不同職責：</p><ul><li><code>web</code>：Next.js 前端與 tRPC API，使用者點到的所有介面都從這裡來。</li><li><code>workers</code>：背景處理佇列，所有抓取、OCR、AI 標籤、影片下載都在這層完成，是整套系統最吃資源的部分。</li><li><code>meilisearch</code>：全文搜尋引擎，獨立常駐。</li><li><code>chrome</code> 或 <code>browserless</code>：無頭瀏覽器，負責渲染 JavaScript 站台與封存頁面。</li><li>可選的 <code>ollama</code>：本地 LLM 推論，若使用雲端 API 則不需要。</li></ul><p>最低資源建議是 2 vCPU、4 GB 記憶體。若同時啟用本地 LLM，至少要拉到 4 vCPU、8 GB；想跑 GPU 推論的話另外掛一張入門卡也夠用。儲存方面，Karakeep 會把每筆書籤的 HTML 快照與圖片存進磁碟，重度使用者每萬筆書籤大約佔用 10 到 20 GB，視是否啟用影片封存而定。</p><h2 id="一份能直接動的-docker-compose"><a href="#一份能直接動的-docker-compose" class="headerlink" title="一份能直接動的 docker-compose"></a>一份能直接動的 docker-compose</h2><p>官方 repo 提供完整範例，實際部署可以濃縮成這樣的骨架：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">web:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">ghcr.io/karakeep-app/karakeep:release</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">data:/data</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MEILI_ADDR:</span> <span class="string">http://meilisearch:7700</span></span><br><span class="line">      <span class="attr">BROWSER_WEB_URL:</span> <span class="string">http://chrome:9222</span></span><br><span class="line">      <span class="attr">OLLAMA_BASE_URL:</span> <span class="string">http://ollama:11434</span></span><br><span class="line">      <span class="attr">INFERENCE_TEXT_MODEL:</span> <span class="string">qwen2.5:7b</span></span><br><span class="line">      <span class="attr">DATA_DIR:</span> <span class="string">/data</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;3000:3000&quot;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">chrome:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">gcr.io/zenika-hub/alpine-chrome:123</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--no-sandbox</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--remote-debugging-address=0.0.0.0</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--remote-debugging-port=9222</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--hide-scrollbars</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">meilisearch:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">getmeili/meilisearch:v1.13</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MEILI_NO_ANALYTICS:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">meili_data:/meili_data</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">  <span class="attr">meili_data:</span></span><br></pre></td></tr></table></figure><p>外層通常套一層 Caddy 或 Nginx Proxy Manager 處理 HTTPS。授權設定建議走 OIDC：Karakeep 支援 Authentik、Pocket ID、Authelia 等任何 OIDC 提供者，這比預設的內建帳號好管理。</p><p><code>NEXTAUTH_SECRET</code> 與 <code>MEILI_MASTER_KEY</code> 千萬不要用範例值，用 <code>openssl rand -base64 36</code> 各產一組塞進去。生產環境還要把 <code>web</code> 的 port 從 host 拿掉，只透過反向代理對外。</p><h2 id="整理舊資料：從-Pocket-匯出到-Karakeep-的路徑"><a href="#整理舊資料：從-Pocket-匯出到-Karakeep-的路徑" class="headerlink" title="整理舊資料：從 Pocket 匯出到 Karakeep 的路徑"></a>整理舊資料：從 Pocket 匯出到 Karakeep 的路徑</h2><p>Mozilla 在關站前提供了完整的匯出機制，使用者可以下載一份 HTML 格式的 Pocket 書籤檔。Karakeep 內建解析這個格式的功能，從設定頁進入「Import &#x2F; Export」上傳就好，標籤與 archive 狀態都會保留。</p><p>匯入完還有一個有趣的玩法：對所有舊書籤強制重新跑一次 AI 標籤。Pocket 多年累積的資料往往沒分類或標籤亂七八糟，跑完 LLM 之後整套會被重新組織得很清爽。這個操作在 worker queue 會排隊跑很久，視書籤數量可能要幾小時到一兩天，安排在離峰時段比較不會卡到日常使用。</p><h2 id="自架的代價值不值得"><a href="#自架的代價值不值得" class="headerlink" title="自架的代價值不值得"></a>自架的代價值不值得</h2><p>Karakeep 把 Pocket 十年來做的事情重做一次，再加上 AI 與資料主權，問題是這份體驗的維護成本誰扛。</p><p>明顯的優勢有三點：資料完全在自己手上、AI 標籤可以選擇不出境、長期成本比商業方案低很多。代價也清楚——每月要花時間更新容器、處理 Meilisearch 偶爾出現的 index 失敗、注意 worker queue 是否塞車。對於每天會用到的工具來說，這份維護心力通常值得；對於一年點開兩次的服務則未必。</p><p>另一個常被低估的成本是頻寬。Karakeep 預設會把每個網頁完整封存下來，外加圖片和影片，重度使用者一個月吃掉幾十 GB 流量並不誇張。VPS 的傳輸量計算要事先看清楚，避免月底超量。</p><p>選 VPS 時建議優先看記憶體與磁碟，CPU 反而不是瓶頸——除非要在同一台跑本地 LLM。NVMe SSD 對 Meilisearch 的索引建立速度差異明顯，普通 SATA SSD 在書籤過萬之後會感受到搜尋變慢。</p><h2 id="結語與下一步"><a href="#結語與下一步" class="headerlink" title="結語與下一步"></a>結語與下一步</h2><p>Pocket 的退場讓自架書籤管理從「技術愛好者的玩具」變成正常人也該考慮的選項。Karakeep 在這個時間點補上 AI 自動化與行動端體驗，把過去自架方案最弱的兩個環節同時解決，幾乎是目前唯一能直接對標 SaaS 的選擇。</p><p>要把 Karakeep 跑得穩，底層 VPS 的選型比想像中重要。NCSE Network 提供位於臺灣是方電訊機房的 VPS，採用 Intel Gold CPU、NVMe SSD，IPv4 與 IPv6 雙堆疊，網路出口直連多家上游，適合需要長期保存資料又重視低延遲存取的自架應用。前往 ncse.tw 了解方案細節，把閱讀清單真正搬回自己的伺服器。</p>]]>
    </content>
    <id>https://blog.ncse.tw/karakeep-self-hosted-pocket-alternative-ai-bookmark/</id>
    <link href="https://blog.ncse.tw/karakeep-self-hosted-pocket-alternative-ai-bookmark/"/>
    <published>2026-05-30T07:00:00.000Z</published>
    <summary>Mozilla 在 2025 年 7 月關掉 Pocket，自架選項一直缺一個能跟 SaaS 體驗持平的解。Karakeep 從 Hoarder 改名而來，把 AI 自動標籤、全文檢索、瀏覽器擴充與行動 App 一次補齊，本文解析它的架構、Ollama 整合與 VPS 部署細節。</summary>
    <title>Pocket 在 2025 年熄燈之後，Karakeep 用本地 LLM 接手了閱讀清單這個位置</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="ClickHouse" scheme="https://blog.ncse.tw/tags/ClickHouse/"/>
    <category term="Docker" scheme="https://blog.ncse.tw/tags/Docker/"/>
    <category term="APM" scheme="https://blog.ncse.tw/tags/APM/"/>
    <category term="可觀測性" scheme="https://blog.ncse.tw/tags/%E5%8F%AF%E8%A7%80%E6%B8%AC%E6%80%A7/"/>
    <category term="OpenTelemetry" scheme="https://blog.ncse.tw/tags/OpenTelemetry/"/>
    <category term="SigNoz" scheme="https://blog.ncse.tw/tags/SigNoz/"/>
    <content>
      <![CDATA[<p>SigNoz 把 Trace、Log、Metric 三種訊號塞進同一個 ClickHouse，靠 OpenTelemetry 原生協定接收資料，整套自架方案在 4 GB RAM 的 VPS 上就能跑起來。對於還在用 Prometheus + Loki + Tempo 三個 backend 拼出可觀測性，又付不起 Datadog 帳單的團隊來說，SigNoz 提供了一條操作成本明顯更低的路線。</p><p>可觀測性（observability）這幾年從「裝個 Prometheus 看 CPU」變成「Trace、Log、Metric 三種訊號要能互相跳轉」的複雜需求，工具鏈跟著爆炸。SigNoz 不是要做最強的單項，而是用單一資料庫把這三件事整合在同一個查詢介面下。</p><h2 id="可觀測性會走到今天這麼複雜的原因"><a href="#可觀測性會走到今天這麼複雜的原因" class="headerlink" title="可觀測性會走到今天這麼複雜的原因"></a>可觀測性會走到今天這麼複雜的原因</h2><p>早期監控的世界很簡單：Nagios、Zabbix 之類的工具負責定期 ping 服務、看 CPU 高不高，出事就寄信。後來微服務跟容器化讓服務數量爆炸，光看單機指標不夠用，需要看「請求從 service A 走到 B 再到 C 中間哪一段慢」，distributed tracing 才成為必需品。再加上應用日誌也要集中收集才能查問題，三種訊號各自有不同的開源解法：</p><ul><li><strong>Metric</strong>：Prometheus 主導，pull-based 模型，靠 PromQL 查詢</li><li><strong>Log</strong>：早期是 ELK（Elasticsearch + Logstash + Kibana），後來出現 Loki 走 label-based 索引</li><li><strong>Trace</strong>：Jaeger、Zipkin，後期被 Tempo 跟 OpenTelemetry 收編</li></ul><p>Grafana 一家把 Loki、Tempo、Mimir、Pyroscope 全部做出來，包裝成 LGTM Stack（Loki + Grafana + Tempo + Mimir）。問題是這套東西要自己跑起來，至少要管四個 backend、四套儲存、四種查詢語言（PromQL、LogQL、TraceQL、自家 Profile 查詢）。對只有兩三個工程師的小團隊，光是運維這些 backend 就吃掉一個全職人力。</p><p>商業方案像 Datadog、New Relic 把這些整合得很好，但價格曲線非常陡——一個中型服務每月帳單破萬美元並不少見，而且資料一旦進去就很難搬走。</p><p>SigNoz 想解的就是這個夾在中間的空白。</p><h2 id="一個-ClickHouse-就吃下三種訊號"><a href="#一個-ClickHouse-就吃下三種訊號" class="headerlink" title="一個 ClickHouse 就吃下三種訊號"></a>一個 ClickHouse 就吃下三種訊號</h2><p>SigNoz 的核心架構決策是把 Trace、Log、Metric 全部寫到同一個 ClickHouse 叢集。為什麼這件事重要？</p><p>ClickHouse 是 columnar OLAP 資料庫，設計來處理 PB 級別的分析查詢。它的壓縮率極高（trace span 這種重複欄位多的資料常見壓到 10:1），掃描速度也快。更關鍵的是，三種訊號共享同一個 schema 後，從 Metric 異常跳到對應時間區間的 Trace、再從 Trace 跳到該請求的 Log，整個過程在同一個查詢引擎下完成，不需要切換工具或對齊 label。</p><p>實際架構長這樣：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">應用程式 (OTel SDK)</span><br><span class="line">    ↓ OTLP gRPC / HTTP</span><br><span class="line">OpenTelemetry Collector</span><br><span class="line">    ↓</span><br><span class="line">ClickHouse  ←  Query Service  ←  Frontend (React)</span><br></pre></td></tr></table></figure><p>四個服務外加一個 ZooKeeper（給 ClickHouse 做分散式協調），整套部署完是五個容器。對比之下，一個跑得起來的 LGTM Stack 至少要 Loki、Tempo、Mimir、Grafana、Prometheus、Promtail 六個元件，每個還可能拆成 write&#x2F;read&#x2F;backend 三種角色。</p><p>OpenTelemetry-native 這件事也值得一提。SigNoz 不收 Prometheus 格式、不收 Jaeger 原生協定（雖然支援，但屬於相容性模式）——主要管道就是 OTLP。這意味著應用程式只要裝一次 OpenTelemetry SDK，可以隨時換 backend，不會被 SigNoz 綁死。這點跟 Datadog 那種「裝自家 agent 才能用」的封閉模型剛好相反。</p><h2 id="SigNoz-跟-LGTM-Stack-的實際差異"><a href="#SigNoz-跟-LGTM-Stack-的實際差異" class="headerlink" title="SigNoz 跟 LGTM Stack 的實際差異"></a>SigNoz 跟 LGTM Stack 的實際差異</h2><p>把兩套東西攤開比較，差別主要在四個面向：</p><p><strong>索引策略</strong>：Loki 為了壓縮成本，預設只索引少數低基數的 label（通常上限 15 個）。這代表 trace_id、user_id 這類高基數欄位查不到，得用 grep 全表掃。SigNoz 把所有 OpenTelemetry attribute 都當作可查詢欄位，不需要事先決定哪些要當索引。除錯時想到什麼就查什麼，這個體驗差距很明顯。</p><p><strong>訊號關聯</strong>：在 LGTM Stack 裡，從 Tempo 的 trace 跳到 Loki 的 log，必須靠 exemplar 或 trace_id 對齊，而且要在 Grafana 的 panel 之間切換。SigNoz 在同一個 UI 內就能從 service map 的 latency spike 直接點進對應的 traces，再點 trace 內任何一個 span 看當下的 logs。</p><p><strong>運維成本</strong>：4 GB RAM 的 VPS 跑得起 SigNoz 全套（雖然官方建議至少給 ClickHouse 8 GB 才好用）。LGTM Stack 的最小部署即使把所有元件壓到單機，記憶體基線通常也要 6–8 GB。多了一倍的元件數，等於多了一倍要更新、要監控、要排錯的維運表面。</p><p><strong>生態廣度</strong>：這是 LGTM 勝出的地方。Grafana 的 dashboard 生態系成熟太多，社群現成的 panel 模板從 PostgreSQL、Kafka 到 Kubernetes 應有盡有。SigNoz 內建的 dashboard 數量遠不及，雖然支援匯入 Grafana 格式，但相容性不是 100%。</p><p>如果只看純技術觀點，OpenTelemetry-native 的單一資料庫架構是更乾淨的設計。但若團隊已經深度依賴 Grafana 的視覺化生態，要評估的就不只是 backend 好不好用，還包括轉移成本。</p><h2 id="自架的硬體配置怎麼抓"><a href="#自架的硬體配置怎麼抓" class="headerlink" title="自架的硬體配置怎麼抓"></a>自架的硬體配置怎麼抓</h2><p>SigNoz 官方文件給的數字是「最低 4 GB RAM」，但這個數字其實只夠跑示範用途。實際生產環境的需求要看每秒處理多少 span、要保留多久。</p><p>幾個實務參考點：</p><ul><li><strong>小型場景</strong>：每秒 100–500 span、保留 15 天 → 4 vCPU、8 GB RAM、100 GB NVMe SSD 通常夠用</li><li><strong>中型場景</strong>：每秒 1k–5k span、保留 30 天 → 8 vCPU、16 GB RAM、500 GB SSD，ClickHouse 建議獨立部署</li><li><strong>大型場景</strong>：每秒超過 5k span → ClickHouse 需要分片，OTel Collector 要起多個 replica 做負載分攤</li></ul><p>ClickHouse 是整套架構裡最吃資源的元件，記憶體不足會導致 merge 跟不上，徵兆是 part count 飆破幾千。這時候不是加 CPU 能解決的，得加 RAM 或減少 ingestion 速率。</p><p>儲存方面，trace 跟 log 的資料量會遠大於 metric。實務上 trace 採樣率調到 1–10% 是常態，全收只在開發或除錯期間做。SigNoz 內建的採樣設定可以在 collector 層做機率採樣或 tail-based 採樣，後者會優先保留有錯誤的 trace。</p><h2 id="Docker-Compose-自架實作"><a href="#Docker-Compose-自架實作" class="headerlink" title="Docker Compose 自架實作"></a>Docker Compose 自架實作</h2><p>SigNoz 官方提供 install script，但實務上直接用 Docker Compose 部署更可控。從 GitHub 拉下程式碼後，主要的 compose 檔案在 <code>deploy/docker/clickhouse-setup/</code> 底下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> -b main https://github.com/SigNoz/signoz.git</span><br><span class="line"><span class="built_in">cd</span> signoz/deploy/docker/clickhouse-setup</span><br><span class="line">docker compose up -d --remove-orphans</span><br></pre></td></tr></table></figure><p>預設會開三個對外連線埠：</p><ul><li><strong>8080</strong>：Web UI 跟 Query API</li><li><strong>4317</strong>：OTLP gRPC（給 SDK 推 telemetry）</li><li><strong>4318</strong>：OTLP HTTP（同樣推 telemetry，給 browser 或 webhook 場景用）</li></ul><p>實際上線前該調整的幾個地方：</p><p><strong>ClickHouse 記憶體上限</strong>：預設沒有設 <code>max_memory_usage</code>，被一個大查詢吃光記憶體就會 OOM 整個服務。在 <code>clickhouse-config.xml</code> 加上 <code>&lt;max_memory_usage&gt;4000000000&lt;/max_memory_usage&gt;</code>（4 GB 上限）是基本款。</p><p><strong>資料保留策略</strong>：在 SigNoz UI 的「Settings → General」可以設定 trace、log、metric 各自的 TTL。預設 15 天對小流量沒問題，高流量場景要往下調。</p><p><strong>反向代理</strong>：8080 不要直接對公網。實務上用 Caddy 或 Nginx 套一層 TLS 跟 basic auth，或者更安全的做法是讓 Web UI 只對內網開放，遠端存取走 WireGuard。</p><p><strong>OTLP 端點認證</strong>：4317 跟 4318 預設沒有認證，誰連得到就誰能塞資料進來。生產環境一定要在 collector 層加 bearer token 或者把這兩個 port 鎖在內網。</p><p>驗證部署成功最快的方法是用 OpenTelemetry 官方的 sample app：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">docker run --<span class="built_in">rm</span> \</span><br><span class="line">  -e OTEL_EXPORTER_OTLP_ENDPOINT=http://YOUR_SIGNOZ_HOST:4318 \</span><br><span class="line">  -e OTEL_SERVICE_NAME=test-service \</span><br><span class="line">  ghcr.io/open-telemetry/opentelemetry-demo:latest-frontend</span><br></pre></td></tr></table></figure><p>等個一分鐘，SigNoz UI 的 Services 頁面就會出現 <code>test-service</code>，點進去能看到 trace 跟 metric。</p><h2 id="把現有服務接上-OpenTelemetry"><a href="#把現有服務接上-OpenTelemetry" class="headerlink" title="把現有服務接上 OpenTelemetry"></a>把現有服務接上 OpenTelemetry</h2><p>SigNoz 自架完成只是起點，真正的工程量在於應用程式端的儀器化（instrumentation）。OpenTelemetry 在主流語言都有 SDK：</p><ul><li><strong>Node.js、Python、Java、.NET</strong>：支援 auto-instrumentation，幾乎不用改程式碼</li><li><strong>Go、Rust</strong>：需要手動加 tracer，但 SDK 設計乾淨，不會侵入業務邏輯</li><li><strong>PHP</strong>：8.0 以上版本有 OpenTelemetry extension，效能影響可接受</li></ul><p>對於跑現成軟體（Nginx、PostgreSQL、Kafka）的場景，OpenTelemetry Collector 有 receiver 可以直接抓 Prometheus metric 跟 syslog，不需要改服務本身。</p><p>Kubernetes 環境的話，OpenTelemetry Operator 可以做 sidecar 自動注入，pod 啟動時就掛上 instrumentation agent。這部分配置比 Datadog 的 daemonset 模型複雜一些，但好處是換 backend 不用重新部署。</p><h2 id="什麼情況下不該選-SigNoz"><a href="#什麼情況下不該選-SigNoz" class="headerlink" title="什麼情況下不該選 SigNoz"></a>什麼情況下不該選 SigNoz</h2><p>SigNoz 不是萬用解，幾個明顯不適合的場景：</p><p><strong>團隊已經重度投資 Grafana</strong>：累積了幾十個 dashboard、團隊熟練 PromQL 跟 LogQL，從 LGTM 遷移到 SigNoz 的學習曲線跟 dashboard 重寫成本可能不划算。這種情況反而適合保留 Grafana 當前端，後面接 SigNoz 當 unified backend——SigNoz 有 Grafana datasource plugin 可以這樣用。</p><p><strong>需要 long-term metric retention</strong>：ClickHouse 跑年單位的 metric 保留會吃很多儲存，沒有 Mimir 那種專門的長期儲存層划算。如果是合規或審計需求要保留兩年以上的指標，混合架構（SigNoz 處理近期、Cortex&#x2F;Mimir 處理長期）會比較合理。</p><p><strong>單機規模太小</strong>：如果只是想看一台 VPS 的 CPU、RAM、disk，SigNoz 整套架構是大砲打蚊子。Beszel、Netdata 這類輕量工具更合適。SigNoz 的甜蜜點是「有 5 個以上的微服務、想看完整呼叫鏈」。</p><p><strong>完全純 metric 場景</strong>：如果根本沒有 trace 需求，純 Prometheus + Grafana 還是更直接的方案。SigNoz 的設計優勢來自三種訊號的整合，只用一種等於浪費掉它的核心價值。</p><h2 id="何時值得把可觀測性從-SaaS-搬回自家"><a href="#何時值得把可觀測性從-SaaS-搬回自家" class="headerlink" title="何時值得把可觀測性從 SaaS 搬回自家"></a>何時值得把可觀測性從 SaaS 搬回自家</h2><p>Datadog、New Relic 這類 SaaS 方案的價值在於零維運、立即上線。但隨著服務規模成長，月費爆炸式上漲是常態——尤其是 log ingestion 跟 high-cardinality metric 這兩塊。SigNoz 自架的成本曲線完全不同：硬體跟頻寬是固定支出，跟業務量沒有線性關係。</p><p>對於資料敏感度高的場景（金融、醫療、政府單位），把 trace 跟 log 留在自家機房而不是送到境外 SaaS，本來就是合規硬性需求。SigNoz 的開源授權（Apache 2.0）加上 OpenTelemetry 標準協定，意味著任何時候想換 backend 都還是可以——不會像當年用 Datadog Agent 那樣被卡死。</p><p>可觀測性走到 2026 年，OpenTelemetry 已經是事實上的標準，後端的競爭重新回到「誰的查詢體驗最好、運維最簡單」這個原始問題上。SigNoz 在小到中型團隊這個區間提供了一條清楚的路線。</p><p>要在臺灣本地部署 SigNoz 跑生產級可觀測性，硬體規格跟網路延遲都是關鍵。NCSE Network 提供搭載 Intel Xeon Gold CPU、NVMe SSD 的 VPS 主機，是方電訊機房直連臺灣骨幹，適合 ClickHouse 這類對 IO 跟記憶體敏感的服務。前往 <a href="https://ncse.tw/">ncse.tw</a> 了解規格與方案細節。</p>]]>
    </content>
    <id>https://blog.ncse.tw/signoz-self-hosted-opentelemetry-observability/</id>
    <link href="https://blog.ncse.tw/signoz-self-hosted-opentelemetry-observability/"/>
    <published>2026-05-29T07:00:00.000Z</published>
    <summary>SigNoz 是少數從第一天就以 OpenTelemetry 為核心設計的自架可觀測平台。本文拆解它跟 LGTM Stack 的架構差異、ClickHouse 單一資料庫的取捨、Docker Compose 自架步驟，以及 Trace、Log、Metric 共享 schema 對除錯效率的實際影響。</summary>
    <title>Datadog 太貴、Grafana Stack 太散：SigNoz 用一個 ClickHouse 把可觀測性收齊</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="VPS" scheme="https://blog.ncse.tw/tags/VPS/"/>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Linux" scheme="https://blog.ncse.tw/tags/Linux/"/>
    <category term="備份" scheme="https://blog.ncse.tw/tags/%E5%82%99%E4%BB%BD/"/>
    <category term="Restic" scheme="https://blog.ncse.tw/tags/Restic/"/>
    <category term="systemd" scheme="https://blog.ncse.tw/tags/systemd/"/>
    <content>
      <![CDATA[<p>備份這件事，大多數人在第一次伺服器爆炸前都不會認真看待。打開 crontab、每天 <code>rsync -avz /data backup-server:/backup/</code>，覺得這樣就算備份了。</p><p>但這不是備份，這只是把資料複製一份。誤刪同步過去之後就回不來；勒索病毒加密原始檔的同時，也加密了 rsync 出來的副本；異地儲存的 SSH 金鑰一旦外洩，整批備份檔案立刻變成攻擊者的提款機。</p><p>正式的備份系統需要去重、加密、版本化、可還原驗證、自動排程。Restic 把這些事情收進同一支工具裡，2026 年的 0.18 版已經夠成熟到可以直接放上生產環境。</p><h2 id="rsync-為什麼不適合當備份方案"><a href="#rsync-為什麼不適合當備份方案" class="headerlink" title="rsync 為什麼不適合當備份方案"></a>rsync 為什麼不適合當備份方案</h2><p>rsync 是同步工具，不是備份工具。這兩件事的差別在於「歷史」。</p><p>一個健康的備份系統，必須能讓使用者回到任何一個過去的時間點還原資料。rsync 預設會把目標端覆寫成跟來源一致，意思是上週某個檔案被誤改、誤刪，下次 rsync 跑完，這個錯誤就被永久同步到備份端。<code>--backup</code> 旗標雖然能保留舊版本，但管理複雜度極高，沒有去重，磁碟空間爆炸只是時間問題。</p><p>加密也是個問題。rsync 透過 SSH 傳輸時是加密的，但備份端的檔案本身是明文。備份伺服器一旦被入侵，等於資料外洩。Restic 採用的是 client-side encryption，連備份儲存空間的擁有者都讀不到內容。</p><p>最後是去重。同一個檔案的不同版本之間，rsync 看到的是兩個獨立檔案；Restic 看到的是 4MB 一塊的 chunk，相同的 chunk 只存一次。一個 100GB 的目錄，每天備份跑滿一年，Restic 實際用掉的儲存空間可能只有 150GB。rsync 配合 hardlink 也能做到類似效果，但複雜度和邏輯漏洞都遠超合理範圍。</p><h2 id="Restic-的儲存格式只有三種物件"><a href="#Restic-的儲存格式只有三種物件" class="headerlink" title="Restic 的儲存格式只有三種物件"></a>Restic 的儲存格式只有三種物件</h2><p>Restic 的儲存格式只用 snapshot、tree、blob 三種物件。snapshot 是某個時間點的備份指標，tree 描述目錄結構，blob 是實際的檔案內容切片。每個 blob 用 SHA-256 雜湊命名，全 repository 範圍去重——同一段內容只存一次，無論它在多少個檔案、多少次 snapshot 裡出現。</p><p>寫入儲存空間之前，所有資料都會用 AES-256-CTR 加密、Poly1305 簽章。repository 密碼透過 scrypt 衍生出主金鑰，主金鑰再加密每個 blob 的內容金鑰。只要密碼夠強，備份檔案放在誰的雲端都不會構成資料外洩風險。</p><p>snapshot 本質上是不可變的。新的備份不會修改舊的 blob，只會新增新的 chunk 跟新的 snapshot 指標。除非主動執行 <code>forget</code> 加 <code>prune</code>，否則歷史紀錄不會消失。配上儲存端的物件鎖定（例如 S3 Object Lock 或 Backblaze B2 file retention），可以做到真正能抵擋勒索病毒的不可變備份。</p><h2 id="Restic-跟-Borg-該選哪一個"><a href="#Restic-跟-Borg-該選哪一個" class="headerlink" title="Restic 跟 Borg 該選哪一個"></a>Restic 跟 Borg 該選哪一個</h2><p>兩個都是去重加密的開源備份工具。Borg 比較早出，效能略勝一籌，壓縮選項更豐富。Restic 比較晚出，但內建原生 S3、B2、Azure Blob、Google Cloud Storage 支援，不需要額外 wrapper。</p><p>對 2026 年新建立的備份流程，Restic 是更直接的選擇，理由有三個：</p><p>現代備份策略越來越多以物件儲存為目的地，Restic 跟物件儲存的整合是原生的。Borg 需要透過 borgbackup-S3 之類的 wrapper，多一層整合就多一層出錯機會。</p><p>跨平臺支援上，Restic 是純 Go 寫的，Linux、macOS、Windows、FreeBSD 都有原生二進位檔；Borg 在 Windows 上只能透過 WSL，對混合環境不友善。</p><p>最後是多客戶端共用 repository。Restic 設計上就支援多臺機器同時寫入同一個 repo，整個 fleet 之間的去重會自動進行。Borg 雖然有相關修補，但官方仍建議一臺機器一個 repo。</p><p>效能上 Borg 確實仍有優勢，但對絕大多數 VPS 工作負載來說，差距已經不足以構成決策因素。</p><h2 id="備份目的地的選擇"><a href="#備份目的地的選擇" class="headerlink" title="備份目的地的選擇"></a>備份目的地的選擇</h2><p>備份的第一原則：不能跟原始資料放在同一臺機器上。VPS 硬碟壞掉的時候，本地備份跟原始檔案一起陪葬。</p><p>實務上推薦的目的地有三種：</p><p><strong>物件儲存（S3、B2、R2、Wasabi）</strong>：Restic 原生支援，價格便宜，可靠性高。Backblaze B2 跟 Cloudflare R2 沒有取回費用，比 AWS S3 標準等級實惠很多。跨境傳輸延遲不是問題的話，這是最省事的選擇。</p><p><strong>另一臺 VPS 的 SFTP</strong>：兩臺機器分散在不同機房，互相備份。延遲低、頻寬通常充足，但需要自己管理那臺機器的可用性。</p><p><strong>rclone 中繼</strong>：當儲存端是 WebDAV、OneDrive、Google Drive 之類 Restic 不直接支援的協定時，可以用 rclone 當橋接。設定稍微複雜，但選項變得非常多。</p><p>備份頻寬會吃對外流量。如果 VPS 採流量計費，需要評估每日增量大小跟資費的關係——Restic 的 chunk 去重會讓增量小很多，通常一個 50GB 的服務每天新增資料量在 100MB 以下。</p><h2 id="從零建立第一個備份"><a href="#從零建立第一個備份" class="headerlink" title="從零建立第一個備份"></a>從零建立第一個備份</h2><p>Debian&#x2F;Ubuntu 直接用套件管理器安裝：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">apt install restic</span><br><span class="line">restic self-update</span><br></pre></td></tr></table></figure><p><code>self-update</code> 會把套件管理器裝的舊版升級到最新二進位檔。Restic 的版本更新節奏快，套件庫的版本通常落後好幾個版本，影響效能跟相容性。</p><p>以 Backblaze B2 為例，初始化 repository：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> B2_ACCOUNT_ID=your_key_id</span><br><span class="line"><span class="built_in">export</span> B2_ACCOUNT_KEY=your_application_key</span><br><span class="line"><span class="built_in">export</span> RESTIC_REPOSITORY=b2:my-backup-bucket:/server01</span><br><span class="line"><span class="built_in">export</span> RESTIC_PASSWORD_FILE=/root/.restic-password</span><br><span class="line"></span><br><span class="line">restic init</span><br></pre></td></tr></table></figure><p>密碼放在 <code>/root/.restic-password</code> 並設定 <code>chmod 600</code>，避免出現在 shell 歷史紀錄裡。這個密碼一旦遺失，整個 repository 將永遠無法解密。建議用 1Password、Bitwarden 之類的工具離線備份這串密碼。</p><p>第一次備份：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">restic backup /etc /home /var/lib/docker/volumes \</span><br><span class="line">  --exclude=<span class="string">&#x27;/var/lib/docker/volumes/*/_data/*.log&#x27;</span> \</span><br><span class="line">  --tag daily</span><br></pre></td></tr></table></figure><p><code>--exclude</code> 可以避開不必要的大檔（log、快取、臨時檔），減少 chunk 處理時間跟儲存用量。<code>--tag</code> 之後可以用 <code>restic snapshots --tag daily</code> 篩選。</p><h2 id="用-systemd-Timer-排程"><a href="#用-systemd-Timer-排程" class="headerlink" title="用 systemd Timer 排程"></a>用 systemd Timer 排程</h2><p>cron 也能跑，但 systemd Timer 在錯過排程後可以補跑、有 journal 紀錄、能跟其他 unit 串接相依，幾乎所有現代 Linux 發行版都應該優先用 timer。</p><p><code>/etc/systemd/system/restic-backup.service</code>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Restic backup</span><br><span class="line"><span class="attr">After</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"><span class="attr">Wants</span>=network-<span class="literal">on</span>line.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=<span class="literal">on</span>eshot</span><br><span class="line"><span class="attr">EnvironmentFile</span>=/etc/restic/env</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/restic backup /etc /home /var/lib/docker/volumes --tag daily</span><br><span class="line"><span class="attr">ExecStartPost</span>=/usr/bin/restic forget --keep-daily <span class="number">7</span> --keep-weekly <span class="number">4</span> --keep-monthly <span class="number">12</span> --prune</span><br><span class="line"><span class="attr">Nice</span>=<span class="number">19</span></span><br><span class="line"><span class="attr">IOSchedulingClass</span>=idle</span><br></pre></td></tr></table></figure><p><code>/etc/systemd/system/restic-backup.timer</code>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Daily restic backup</span><br><span class="line"></span><br><span class="line"><span class="section">[Timer]</span></span><br><span class="line"><span class="attr">OnCalendar</span>=<span class="number">03</span>:<span class="number">30</span></span><br><span class="line"><span class="attr">RandomizedDelaySec</span>=<span class="number">30</span>m</span><br><span class="line"><span class="attr">Persistent</span>=<span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=timers.target</span><br></pre></td></tr></table></figure><p><code>Persistent=true</code> 確保機器當機過、錯過排程後，下次啟動會立刻補跑。<code>RandomizedDelaySec</code> 避免 fleet 裡所有機器同時打物件儲存的 API rate limit。<code>Nice=19</code> 跟 <code>IOSchedulingClass=idle</code> 讓備份在系統閒置時才搶 CPU 跟磁碟。</p><p>啟用：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">systemctl daemon-reload</span><br><span class="line">systemctl <span class="built_in">enable</span> --now restic-backup.timer</span><br><span class="line">systemctl list-timers restic-backup.timer</span><br></pre></td></tr></table></figure><h2 id="保留策略不是越久越好"><a href="#保留策略不是越久越好" class="headerlink" title="保留策略不是越久越好"></a>保留策略不是越久越好</h2><p><code>restic forget</code> 跟 <code>restic prune</code> 是兩件事。<code>forget</code> 移除 snapshot 指標，<code>prune</code> 真的把不再被任何 snapshot 引用的 blob 刪掉。沒跑 prune，磁碟用量只會一路漲。</p><p>合理的保留策略應該根據 RPO（Recovery Point Objective）反推。常見的範本是 GFS（Grandfather-Father-Son）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">--keep-daily 7       # 過去 7 天每天留一份</span><br><span class="line">--keep-weekly 4      # 過去 4 週每週留一份</span><br><span class="line">--keep-monthly 12    # 過去 12 個月每月留一份</span><br><span class="line">--keep-yearly 3      # 過去 3 年每年留一份</span><br></pre></td></tr></table></figure><p>這份範本能在約 25 個 snapshot 內覆蓋三年的還原能力。實際儲存用量取決於去重率，通常落在原始資料量的 1.2 到 2 倍之間。</p><p><code>prune</code> 是高 IO 的操作，可能跑數小時。生產環境建議每週跑一次，搭配 <code>--max-unused 10%</code> 控制觸發門檻，平衡儲存成本跟 IO 影響。</p><h2 id="沒測試過的備份等於沒備份"><a href="#沒測試過的備份等於沒備份" class="headerlink" title="沒測試過的備份等於沒備份"></a>沒測試過的備份等於沒備份</h2><p>備份系統最常見的失敗模式不是備份跑不起來，而是備份跑了很多年，結果發現還原不出來。原因可能是密碼忘了、權限錯了、依賴的元資料沒一起備、還原指令從來沒人練習過。</p><p>每個季度應該做一次完整還原演練。挑一臺乾淨的 VPS 或本地 VM，用備份還原出一份完整資料，啟動服務，確認業務邏輯能跑。</p><p>Restic 提供兩個輕量的常態檢查：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">restic check                          <span class="comment"># 檢查 metadata 完整性</span></span><br><span class="line">restic check --read-data-subset=5%    <span class="comment"># 隨機抽 5% data blob 驗證雜湊</span></span><br></pre></td></tr></table></figure><p><code>check --read-data-subset=5%</code> 每週跑一次，可以及早發現儲存端的位元損壞或加密金鑰錯誤。完整的 <code>--read-data</code> 會把整個 repo 重新下載校驗，每季跑一次足夠。</p><h2 id="把備份狀態接上監控"><a href="#把備份狀態接上監控" class="headerlink" title="把備份狀態接上監控"></a>把備份狀態接上監控</h2><p>備份失敗如果沒人發現，就跟沒備份一樣。最簡單的做法是接 Healthchecks.io 或自架版本：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">ExecStartPre</span>=-/usr/bin/curl -fsS -m <span class="number">10</span> --retry <span class="number">5</span> \</span><br><span class="line">  https://healthchecks.example.com/ping/&lt;uuid&gt;/start</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/restic backup ...</span><br><span class="line"><span class="attr">ExecStartPost</span>=/usr/bin/curl -fsS -m <span class="number">10</span> --retry <span class="number">5</span> \</span><br><span class="line">  https://healthchecks.example.com/ping/&lt;uuid&gt;</span><br></pre></td></tr></table></figure><p>只要備份失敗或漏跑，Healthchecks 就會發 email、Telegram 或 webhook 通知。重點是這個監控本身要部署在備份目標所在地以外的機器，避免單點故障同時殺掉備份跟告警。</p><p>Restic 0.18 開始強化 <code>--json</code> 輸出，方便接 Prometheus 或自家儀表板：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">restic stats --mode raw-data --json | jq <span class="string">&#x27;.total_size&#x27;</span></span><br></pre></td></tr></table></figure><p>把這個數字推上 Grafana 之後，repository 大小的異常變化（突然暴漲、停止增長）會被看見。</p><h2 id="把備份當成基礎設施的一部分"><a href="#把備份當成基礎設施的一部分" class="headerlink" title="把備份當成基礎設施的一部分"></a>把備份當成基礎設施的一部分</h2><p>備份不是「設定一次就忘了」的東西，而是一條需要持續驗證的流程。Restic 把加密、去重、增量、雲端整合這幾件本來要拼湊好幾個工具才能做完的事，收進一支二進位檔，搭配 systemd Timer 跟物件儲存，能在一個下午內把整套流程建立起來。</p><p>剩下的就是堅持每季做還原演練、確保監控還活著、密碼還記得。資料的命運不該交給天意。</p><p>NCSE Network 的 VPS 主機座落於臺灣是方電訊機房，採用 Intel Gold CPU 與 NVMe SSD，提供穩定的 I&#x2F;O 表現，是 Restic 備份來源端的理想選擇。如需規劃跨機房備援架構，歡迎前往 <a href="https://ncse.tw/">ncse.tw</a> 了解 VPS 與 IP Transit 服務。</p>]]>
    </content>
    <id>https://blog.ncse.tw/restic-vps-backup-encryption-deduplication/</id>
    <link href="https://blog.ncse.tw/restic-vps-backup-encryption-deduplication/"/>
    <published>2026-05-28T02:00:00.000Z</published>
    <summary>rsync 是同步工具，不是備份工具。Restic 把加密、去重、版本化、雲端整合收進同一支二進位檔，本文涵蓋 0.18 版的儲存格式設計、與 Borg 的取捨、systemd Timer 排程、保留策略、還原演練到監控整合，把 VPS 備份建到真正可還原的程度。</summary>
    <title>備份這件事，rsync 已經不夠用了：Restic 用去重、加密、增量重做 VPS 備份</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Docker" scheme="https://blog.ncse.tw/tags/Docker/"/>
    <category term="GitHub Actions" scheme="https://blog.ncse.tw/tags/GitHub-Actions/"/>
    <category term="CI/CD" scheme="https://blog.ncse.tw/tags/CI-CD/"/>
    <category term="Forgejo" scheme="https://blog.ncse.tw/tags/Forgejo/"/>
    <category term="Git" scheme="https://blog.ncse.tw/tags/Git/"/>
    <content>
      <![CDATA[<p>GitHub 免費、好用、生態系完整，但它本質上是一個中心化的 SaaS——程式碼存在微軟的伺服器上，CI&#x2F;CD 的 runner 跑在微軟的機器上，Actions 的免費額度用完就得付錢或等月初重置。對個人 side project 來說這不是問題；但當程式碼涉及客戶資料、商業邏輯，或是團隊的部署流程全部綁在 GitHub Actions 上時，把所有雞蛋放在同一個籃子裡就值得重新考慮。</p><p>Forgejo 是目前自架 Git 平台最值得關注的選項。它從 Gitea 分支出來，由非營利社群治理，用 Go 寫成單一 binary，512MB RAM 就能跑起來。關鍵賣點是 Forgejo Actions——一套與 GitHub Actions 語法高度相容的 CI&#x2F;CD 系統，既有的 workflow YAML 幾乎不用改就能搬過來。2026 年 4 月釋出的 v15.0 LTS 進一步補上了 ephemeral runner、OIDC 認證、repository-scoped token 等生產環境需要的功能。</p><h2 id="Gitea-還在，為什麼要選-Forgejo"><a href="#Gitea-還在，為什麼要選-Forgejo" class="headerlink" title="Gitea 還在，為什麼要選 Forgejo"></a>Gitea 還在，為什麼要選 Forgejo</h2><p>Gitea 跟 Forgejo 長得幾乎一樣，功能也高度重疊，但治理模式完全不同。</p><p>Gitea 在 2022 年成立了商業公司 Gitea Ltd.，核心開發資源開始向付費的 Gitea Cloud 傾斜。社群貢獻者發現 PR review 變慢、討論透明度下降，於是一群人在同年分支出 Forgejo，交給 Codeberg e.V.（德國非營利組織）管理。Forgejo 承諾所有程式碼永遠是自由軟體，不走開放核心（open core）的商業路線。</p><p>實務上的差異：Forgejo 的安全修補通常比 Gitea 快，每月固定出 patch release。v15.0 拿到 LTS 標記，官方支援到 2027 年 7 月。授權方面，Forgejo 採 GPL，Gitea 是 MIT——前者確保任何衍生版本都必須開源，後者允許被包進專有產品。長期維運來看，Forgejo 是比較穩的選擇。</p><h2 id="Docker-Compose-部署"><a href="#Docker-Compose-部署" class="headerlink" title="Docker Compose 部署"></a>Docker Compose 部署</h2><p>一台跑 Ubuntu 24.04 的 VPS，2 核 CPU、2GB RAM 就夠。先確認 Docker Engine 24 以上和 Docker Compose plugin 已經裝好。</p><p>建立工作目錄：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /opt/forgejo &amp;&amp; <span class="built_in">cd</span> /opt/forgejo</span><br></pre></td></tr></table></figure><p>建立 <code>docker-compose.yml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">networks:</span></span><br><span class="line">  <span class="attr">forgejo:</span></span><br><span class="line">    <span class="attr">external:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">server:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">codeberg.org/forgejo/forgejo:15</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">forgejo</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">USER_UID=1000</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">USER_GID=1000</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__database__DB_TYPE=postgres</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__database__HOST=db:5432</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__database__NAME=forgejo</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__database__USER=forgejo</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__database__PASSWD=changeme</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">FORGEJO__actions__ENABLED=true</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">forgejo</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./forgejo-data:/data</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">/etc/localtime:/etc/localtime:ro</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;3000:3000&#x27;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;222:22&#x27;</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">db:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">db:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">postgres:16-alpine</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">forgejo-db</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_USER=forgejo</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_PASSWORD=changeme</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">POSTGRES_DB=forgejo</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">    <span class="attr">networks:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">forgejo</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./postgres-data:/var/lib/postgresql/data</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;pg_isready&quot;</span>, <span class="string">&quot;-U&quot;</span>, <span class="string">&quot;forgejo&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">5</span></span><br></pre></td></tr></table></figure><p>啟動服務：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose up -d</span><br></pre></td></tr></table></figure><p>打開 <code>http://你的IP:3000</code>，第一次進入會看到安裝精靈。資料庫欄位已被環境變數填好，確認網站 URL 和管理員帳號後按下安裝就完成了。整個過程不到五分鐘。</p><p>裝完後立刻建立管理員帳號——Forgejo 預設讓第一位註冊者成為管理員，拖著不設定等於把管理權限暴露在公網上。</p><h2 id="正式環境加上反向代理"><a href="#正式環境加上反向代理" class="headerlink" title="正式環境加上反向代理"></a>正式環境加上反向代理</h2><p>3000 port 直連只適合測試。正式環境該在前面架 Caddy 或 Nginx 處理 HTTPS。以 Caddy 為例，<code>Caddyfile</code> 三行搞定：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git.yourdomain.com &#123;</span><br><span class="line">    reverse_proxy localhost:3000</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同時把 <code>docker-compose.yml</code> 裡的 ports 從 <code>&#39;3000:3000&#39;</code> 改成 <code>&#39;127.0.0.1:3000:3000&#39;</code>，讓 Forgejo 只接受本機連線。SSH 的 222 port 維持對外，方便 <code>git clone</code> 走 SSH 協定。</p><h2 id="Forgejo-Actions：自己的伺服器跑自己的-CI-CD"><a href="#Forgejo-Actions：自己的伺服器跑自己的-CI-CD" class="headerlink" title="Forgejo Actions：自己的伺服器跑自己的 CI&#x2F;CD"></a>Forgejo Actions：自己的伺服器跑自己的 CI&#x2F;CD</h2><p>Forgejo Actions 直接相容 GitHub Actions 的 workflow 語法。<code>.forgejo/workflows/</code> 目錄放 YAML 檔，<code>on: [push]</code>、<code>jobs</code>、<code>steps</code>、<code>uses</code> 這些關鍵字和 GitHub 一模一樣。GitHub Marketplace 上多數主流 action（像 <code>actions/checkout@v4</code>、<code>actions/setup-node@v4</code>）直接搬過來就能用，不需要改寫。</p><p>上面的 <code>docker-compose.yml</code> 已經透過 <code>FORGEJO__actions__ENABLED=true</code> 啟用了 Actions。接下來要起一個 runner 來實際執行 CI job。</p><p>到 Forgejo 管理介面的 Site Administration → Runners，點選 Create Runner 取得 registration token。然後回到 <code>/opt/forgejo</code>，在 <code>docker-compose.yml</code> 裡加入 runner 服務：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">runner:</span></span><br><span class="line">  <span class="attr">image:</span> <span class="string">data.forgejo.org/forgejo/runner:6</span></span><br><span class="line">  <span class="attr">container_name:</span> <span class="string">forgejo-runner</span></span><br><span class="line">  <span class="attr">depends_on:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">server</span></span><br><span class="line">  <span class="attr">environment:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">DOCKER_HOST=unix:///var/run/docker.sock</span></span><br><span class="line">  <span class="attr">volumes:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">./runner-data:/data</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">/var/run/docker.sock:/var/run/docker.sock</span></span><br><span class="line">  <span class="attr">restart:</span> <span class="string">always</span></span><br><span class="line">  <span class="attr">networks:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">forgejo</span></span><br></pre></td></tr></table></figure><p>注意：runner 掛載 <code>/var/run/docker.sock</code> 代表它擁有主機 Docker 的完整控制權（等同 root 權限）。正式環境建議把 runner 放在獨立 VM&#x2F;主機或採用更強隔離的執行方式，避免 CI 工作負載影響到整台伺服器。</p><p>啟動 runner 容器後，執行註冊指令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker compose <span class="built_in">exec</span> runner forgejo-runner register \</span><br><span class="line">  --instance http://server:3000 \</span><br><span class="line">  --token &lt;你的-registration-token&gt; \</span><br><span class="line">  --name vps-runner \</span><br><span class="line">  --no-interactive</span><br></pre></td></tr></table></figure><p>註冊成功後重啟 runner 讓設定生效：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker compose restart runner</span><br></pre></td></tr></table></figure><p>到任一 repository 的 Settings → Units 確認 Actions 已勾選，然後在專案根目錄建 <code>.forgejo/workflows/ci.yaml</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">docker</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          echo &quot;Running tests...&quot;</span></span><br><span class="line"><span class="string">          npm ci &amp;&amp; npm test</span></span><br></pre></td></tr></table></figure><p>Push 到 main，Actions 頁籤就會出現執行記錄。從原始碼到 build artifact 到 runner 的運算時間，全部在自己的 VPS 上完成，沒有免費額度的問題，也不會因為 GitHub 的 runner queue 排隊等半天。</p><h2 id="v15-0-值得注意的三件事"><a href="#v15-0-值得注意的三件事" class="headerlink" title="v15.0 值得注意的三件事"></a>v15.0 值得注意的三件事</h2><p><strong>Ephemeral runner</strong> 讓 runner 執行完一個 job 後自動銷毀 credential 並退出。搭配 Docker 或 systemd 的自動重啟機制，就能做到「用完即丟」的 runner 池——每個 CI job 拿到的都是一次性 token，沒有長期憑證留在機器上被利用。</p><p><strong>Repository-scoped access token</strong> 把 API token 的權限收窄到特定 repository。以前只能建 user-level token，權限過大；現在可以為每個自動化流程只開放它需要存取的 repo，符合最小權限原則。</p><p><strong>Workflow OIDC 認證</strong> 讓 CI job 用 Forgejo 簽發的 JWT 存取第三方服務，不需要把 static token 塞進 CI secret。這個功能等同 GitHub 的 OIDC identity provider，想串接雲端部署或 container registry 時能避免靜態金鑰外洩的風險。</p><h2 id="什麼情境不適合自架"><a href="#什麼情境不適合自架" class="headerlink" title="什麼情境不適合自架"></a>什麼情境不適合自架</h2><p>自架 Git 平台不是零成本。Forgejo 本身很輕，但維護負擔在其他地方。</p><p>備份是最容易被忽略的環節。Git repository 資料、PostgreSQL metadata、Actions artifact——三個地方都要定時備份到異地儲存。Forgejo 內建 <code>forgejo dump</code> 指令能打包整份資料，但排程和遠端同步得自己用 cron 加 rclone 或類似工具串起來，沒人盯就等於沒備份。</p><p>升級也需要持續關注。Forgejo 每月出 patch，LTS 每季一次，偶爾會有 breaking change——v15.0 就改了預設 cookie 名稱和 rootless 部署的設定路徑。小團隊如果沒有人願意定期讀 release notes，GitHub 或 GitLab SaaS 反而更省心。</p><p>Action 的生態系也比不上 GitHub Marketplace。多數主流 action 能用，但遇到深度依賴 GitHub API 的 action（例如直接呼叫 GitHub REST API 取 PR 資訊的套件）就需要改寫。如果團隊的 workflow 大量仰賴 GitHub-specific 的整合，搬遷成本可能高於預期。</p><p>適合的場景很明確：三到五人的開發團隊、希望程式碼和 CI&#x2F;CD 留在自己的基礎設施上、有人願意每月花一小時處理更新與備份。符合這些條件，Forgejo 的投資報酬率很高。</p><h2 id="把開發基礎設施留在自己手上"><a href="#把開發基礎設施留在自己手上" class="headerlink" title="把開發基礎設施留在自己手上"></a>把開發基礎設施留在自己手上</h2><p>Forgejo 把 Git hosting、code review、issue tracking、container registry 和 CI&#x2F;CD 收進同一套平台，跑在一台 VPS 上只佔 512MB RAM。對重視資料主權和部署自主性的團隊來說，這是門檻最低的完整方案。需要一台穩定的 VPS 來跑 Forgejo，NCSE Network 提供臺灣是方電訊機房的雲端主機，搭載 Intel Gold CPU 與 NVMe SSD，延遲低、效能穩定，支援 7 天免費試用，直接開機測試整套部署流程。</p>]]>
    </content>
    <id>https://blog.ncse.tw/forgejo-self-hosted-git-cicd-github-actions-alternative/</id>
    <link href="https://blog.ncse.tw/forgejo-self-hosted-git-cicd-github-actions-alternative/"/>
    <published>2026-05-27T06:00:00.000Z</published>
    <summary>Forgejo 是 Gitea 的社群分支，單一 binary 就能跑起完整的 Git 平台，內建與 GitHub Actions 相容的 CI/CD 系統。本文涵蓋 Docker Compose 部署、Forgejo Actions runner 設定、workflow 撰寫，以及什麼情境該自架、什麼情境別碰。</summary>
    <title>GitHub 太方便但程式碼放在別人手上：Forgejo 讓你在 VPS 上自架 Git 加 CI/CD</title>
    <updated>2026-06-13T08:06:37.111Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="自架服務" scheme="https://blog.ncse.tw/tags/%E8%87%AA%E6%9E%B6%E6%9C%8D%E5%8B%99/"/>
    <category term="Docker" scheme="https://blog.ncse.tw/tags/Docker/"/>
    <category term="Valkey" scheme="https://blog.ncse.tw/tags/Valkey/"/>
    <category term="Redis" scheme="https://blog.ncse.tw/tags/Redis/"/>
    <category term="快取" scheme="https://blog.ncse.tw/tags/%E5%BF%AB%E5%8F%96/"/>
    <category term="資料庫" scheme="https://blog.ncse.tw/tags/%E8%B3%87%E6%96%99%E5%BA%AB/"/>
    <category term="開源" scheme="https://blog.ncse.tw/tags/%E9%96%8B%E6%BA%90/"/>
    <content>
      <![CDATA[<p>Redis 在 2024 年 3 月把授權從 BSD 改成 SSPL + RSALv2，等於把雲端服務商和部分商業使用者擋在門外。一個月後，Linux Foundation 就集結了 AWS、Google Cloud、Oracle 等主要雲端供應商，推出 Valkey 作為 BSD 授權的社群分支。到了 2026 年中，Valkey 已經推進到 9.1 版，不只是 Redis 的替代品——它在效能和功能上都已經走出自己的路線。</p><h2 id="授權風波的來龍去脈"><a href="#授權風波的來龍去脈" class="headerlink" title="授權風波的來龍去脈"></a>授權風波的來龍去脈</h2><p>Redis Labs（後改名 Redis Inc.）的授權異動早有跡可循。2018 年先把部分模組改為 Commons Clause，2019 年再換成 RSAL，到了 2024 年索性把核心引擎也一併改成 SSPL + RSALv2 雙授權。OSI（Open Source Initiative）不承認 SSPL 為開源授權，這代表任何需要在合規框架內運作的組織，都無法再將 Redis 視為開源軟體使用。</p><p>社群反應極其迅速。Valkey 從 Redis 7.2.4 分支出來，在短短幾週內就累積超過 150 位貢獻者和上千個 commit。到 2024 年底的調查顯示，83% 的大型企業已經在生產環境中採用 Valkey 或正在測試。Aiven 在幾個月內就把數千台 Redis 伺服器遷移到 Valkey。</p><p>Redis Inc. 在 2025 年 5 月試圖補救，跟著 Redis 8.0 加入 AGPLv3 作為第三授權選項。但傷害已經造成——核心貢獻者永久遷移到 Valkey，主要雲端供應商的託管服務全部轉向 Valkey，回頭的機率極低。</p><h2 id="Valkey-9-x-帶來的技術跳躍"><a href="#Valkey-9-x-帶來的技術跳躍" class="headerlink" title="Valkey 9.x 帶來的技術跳躍"></a>Valkey 9.x 帶來的技術跳躍</h2><p>Valkey 不只是換個名字繼續跑。9.0 和 9.1 兩個版本累積了大量工程投入，以下是幾個值得注意的改進。</p><p><strong>Pipeline 記憶體預取與零拷貝回應</strong>：9.0 針對管線化工作負載做了記憶體預取最佳化，吞吐量提升最高達 40%。零拷貝回應機制則帶來額外 20% 的效能增長。對大量使用 pipeline 的應用場景——像是批量寫入日誌或即時分析——這兩項改進直接反映在延遲數字上。</p><p><strong>原子化 Slot 遷移</strong>：過去 Redis Cluster 的 slot 遷移是逐 key 進行的，碰到大型集合就容易卡住，客戶端還得處理部分遷移狀態的重試邏輯。Valkey 9.0 改成以 AOF 格式整個 slot 原子搬移，遷移期間不再出現中間狀態。</p><p><strong>Hash Field 獨立過期</strong>：新增 <code>HEXPIRE</code>、<code>HPERSIST</code>、<code>HTTL</code> 等指令，讓 hash 內的個別欄位可以設定獨立的 TTL。以前要做到這件事得拆成多個 key 再配合命名規則管理，現在一個 hash 就搞定。這對 session 管理、使用者偏好快取這類場景特別實用。</p><p><strong>Cluster 模式支援編號資料庫</strong>：Redis Cluster 從設計之初就只允許 db 0，要從 standalone 模式搬到 cluster 得先把所有 <code>SELECT</code> 呼叫拿掉。Valkey 9.0 拿掉了這個限制，cluster 模式下可以直接使用編號資料庫，大幅降低遷移門檻。</p><p><strong>記憶體用量下降</strong>：9.1 版重新設計了內部資料儲存結構，常見工作負載的每 key 記憶體用量減少約 10%。聽起來不多，但在百萬、千萬 key 的規模下，省下來的記憶體非常可觀。</p><h2 id="模組生態系不再被授權綁架"><a href="#模組生態系不再被授權綁架" class="headerlink" title="模組生態系不再被授權綁架"></a>模組生態系不再被授權綁架</h2><p>Redis Inc. 過去把 RediSearch、RedisJSON、RedisBloom、RedisTimeSeries 等模組鎖在自家授權下，社群版使用者只能看得到吃不到。Valkey 9.0 推出了 Valkey Bundle，以 BSD 授權提供 JSON、Bloom Filter 和搜尋模組（含向量搜尋）。</p><p>Valkey Search 在 1.2 版更進一步加入了全文檢索和聚合查詢功能，支援數值篩選、標籤查找和向量相似度搜尋。對需要在快取層同時處理搜尋需求的架構來說，這代表可以少架一套 Elasticsearch 或 MeiliSearch。</p><p>不過要注意的是，時序資料（Time Series）模組目前還沒有完整的 Valkey 對應方案。如果生產環境重度依賴 RedisTimeSeries，遷移前需要評估是否改用 InfluxDB、QuestDB 等專門的時序資料庫。</p><h2 id="用-Docker-Compose-部署-Valkey"><a href="#用-Docker-Compose-部署-Valkey" class="headerlink" title="用 Docker Compose 部署 Valkey"></a>用 Docker Compose 部署 Valkey</h2><p>單機部署 Valkey 非常直觀。以下是一個適合生產環境的 Docker Compose 設定：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">valkey:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">valkey/valkey:9.1</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">valkey</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">    <span class="attr">command:</span> <span class="string">&gt;</span></span><br><span class="line"><span class="string">      valkey-server</span></span><br><span class="line"><span class="string">      --requirepass YOUR_STRONG_PASSWORD</span></span><br><span class="line"><span class="string">      --appendonly yes</span></span><br><span class="line"><span class="string">      --maxmemory 512mb</span></span><br><span class="line"><span class="string">      --maxmemory-policy allkeys-lru</span></span><br><span class="line"><span class="string"></span>    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;127.0.0.1:6379:6379&quot;</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">valkey-data:/data</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">valkey-data:</span></span><br></pre></td></tr></table></figure><p>幾個設定值得說明：</p><ul><li><code>--appendonly yes</code> 啟用 AOF 持久化，確保重啟後資料不遺失。如果對持久化需求不高（純快取場景），可以關掉以換取寫入效能。</li><li><code>--maxmemory</code> 和 <code>--maxmemory-policy</code> 控制記憶體上限和淘汰策略。<code>allkeys-lru</code> 適合快取場景；如果是當作主資料庫用，改成 <code>noeviction</code> 避免資料被靜默刪除。</li><li>port binding 限制在 <code>127.0.0.1</code>，避免直接暴露到公網。如果需要遠端存取，透過 WireGuard 或 SSH tunnel 比開放防火牆安全得多。</li></ul><p>用 <code>docker compose up -d</code> 啟動後，可以用 <code>valkey-cli</code> 驗證連線：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -e VALKEYCLI_AUTH=YOUR_STRONG_PASSWORD -it valkey valkey-cli ping</span><br><span class="line"><span class="comment"># 預期回應：PONG</span></span><br></pre></td></tr></table></figure><h2 id="從-Redis-搬到-Valkey-的實務路徑"><a href="#從-Redis-搬到-Valkey-的實務路徑" class="headerlink" title="從 Redis 搬到 Valkey 的實務路徑"></a>從 Redis 搬到 Valkey 的實務路徑</h2><p>Valkey 跟 Redis OSS 7.2 維持完整的協定相容性——RESP2、RESP3 協定一致，RDB 和 AOF 檔案格式可以直接互通，指令集完全相同。這代表應用程式端通常不需要改任何一行程式碼，連線字串換掉就完事。</p><p>搬遷的實際步驟：</p><p><strong>盤點模組依賴</strong>：在每台 Redis primary 上執行 <code>MODULE LIST</code>，確認是否使用了 RediSearch、RedisJSON、RedisBloom、RedisTimeSeries 等模組。前三者有 Valkey Bundle 的 BSD 替代品，RedisTimeSeries 則需要另外規劃。</p><p><strong>匯出資料</strong>：用 <code>redis-cli --rdb dump.rdb</code> 匯出完整的 RDB 快照，或者直接複製 AOF 檔案。Valkey 可以直接載入這些檔案。</p><p><strong>部署 Valkey 並載入資料</strong>：把匯出的 RDB 或 AOF 檔案放到 Valkey 的資料目錄，啟動 Valkey 即可自動載入。</p><p><strong>切換應用程式連線</strong>：大多數 Redis 客戶端函式庫（ioredis、redis-py、Jedis、go-redis）不需要更換，只需要把連線位址指向新的 Valkey 實例。部分函式庫已經直接更名或新增了 Valkey 專屬的 client（例如 valkey-py），但 Redis 版本的 client 同樣能正常連線。</p><p><strong>驗證功能</strong>：重點檢查 Lua script、MULTI&#x2F;EXEC 交易、Pub&#x2F;Sub 訂閱和 Cluster 的 failover 行為。這些在協定層面完全相容，但複雜的使用場景值得跑一輪整合測試。</p><p>如果是 Cluster 部署，遷移時需要額外注意 slot 分配和節點拓撲。建議先建立一組新的 Valkey Cluster，透過應用層的雙寫（dual-write）策略逐步切換，而不是嘗試就地升級。</p><h2 id="什麼情況下該留在-Redis"><a href="#什麼情況下該留在-Redis" class="headerlink" title="什麼情況下該留在 Redis"></a>什麼情況下該留在 Redis</h2><p>Valkey 不是在所有場景都優於 Redis。如果已經在使用 Redis Stack（含 RedisTimeSeries、RedisGraph）的完整功能，且這些模組目前在 Valkey 生態系中還沒有成熟的替代品，那麼留在 Redis 並接受 AGPLv3 授權可能是比較務實的選擇。</p><p>另外，如果團隊依賴 Redis Inc. 的商業支援和 SLA，Valkey 目前的支援管道主要來自社群和雲端供應商的託管服務，不提供直接的廠商支援合約。</p><p>但對大多數使用場景——快取、session store、訊息佇列、排行榜、速率限制——Valkey 已經是更好的選擇。BSD 授權沒有法律灰色地帶，Linux Foundation 的中立治理確保不會再出現單一廠商說改授權就改授權的風險，效能表現也已經超越 Redis OSS。</p><h2 id="一個值得長期投入的方向"><a href="#一個值得長期投入的方向" class="headerlink" title="一個值得長期投入的方向"></a>一個值得長期投入的方向</h2><p>Valkey 從倉促分支到站穩腳步只花了不到兩年。9.1 版有超過 80 位開發者參與，AWS ElastiCache、Google Cloud Memorystore、Oracle OCI Cache 都已經提供託管服務，生態系的成熟度已經不是實驗性專案的等級。</p><p>對於正在評估快取或 in-memory 資料儲存方案的技術團隊，Valkey 是目前最穩健的開源選項——不用擔心授權突然變卦，不用擔心功能被鎖進付費版，社群的開發速度也沒有放緩的跡象。</p><p>需要一台穩定的 VPS 來部署 Valkey？NCSE Network 提供搭載 Intel Xeon Gold CPU 和 NVMe SSD 的臺灣本地 VPS 主機，是方電訊機房直連，適合對延遲敏感的快取服務部署。前往 <a href="https://ncse.tw/">ncse.tw</a> 了解方案細節。</p>]]>
    </content>
    <id>https://blog.ncse.tw/valkey-redis-open-source-fork-migration-guide/</id>
    <link href="https://blog.ncse.tw/valkey-redis-open-source-fork-migration-guide/"/>
    <published>2026-05-26T06:00:00.000Z</published>
    <summary>Redis 在 2024 年改用 SSPL 授權後，Linux Foundation 支持的 Valkey 迅速成為主流替代方案。本文拆解 Valkey 的技術架構、9.x 版本的關鍵改進、Docker Compose 部署實務，以及從 Redis 無痛搬遷的具體步驟。</summary>
    <title>Redis 換了授權，Valkey 接手了整個生態系：從分支背景到自架部署全解析</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
  <entry>
    <author>
      <name>NCSE Network</name>
    </author>
    <category term="VPS" scheme="https://blog.ncse.tw/tags/VPS/"/>
    <category term="Linux" scheme="https://blog.ncse.tw/tags/Linux/"/>
    <category term="Docker" scheme="https://blog.ncse.tw/tags/Docker/"/>
    <category term="DevOps" scheme="https://blog.ncse.tw/tags/DevOps/"/>
    <category term="容器" scheme="https://blog.ncse.tw/tags/%E5%AE%B9%E5%99%A8/"/>
    <category term="Podman" scheme="https://blog.ncse.tw/tags/Podman/"/>
    <content>
      <![CDATA[<p>Docker 改變了軟體部署的方式，但它的架構有一個從第一天就存在的問題：所有容器操作都通過一支以 root 身分執行的常駐 daemon。這支 <code>dockerd</code> 一旦被攻破，攻擊者等於拿到整台主機的最高權限。Docker 後來加了 rootless mode，但那是外掛上去的，不是預設行為。</p><p>Podman 從設計階段就走了不同的路。沒有 daemon、預設 rootless、CLI 語法和 Docker 幾乎完全相容。對已經熟悉 Docker 的人來說，遷移成本低到可以忽略。</p><h2 id="架構差異：daemon-與-fork-exec-的根本分歧"><a href="#架構差異：daemon-與-fork-exec-的根本分歧" class="headerlink" title="架構差異：daemon 與 fork-exec 的根本分歧"></a>架構差異：daemon 與 fork-exec 的根本分歧</h2><p>Docker 的運作模型是 client-server：你下的每一條 <code>docker run</code> 指令都是透過 REST API 送給 <code>dockerd</code>，由 daemon 去呼叫 containerd 和 runc 來建立容器。這代表 daemon 是單點故障——它 crash 了，所有容器的管理介面就斷了。</p><p>Podman 用的是 fork-exec 模型。每次執行 <code>podman run</code>，Podman 直接 fork 出 conmon（container monitor）程序來管理容器的生命週期，不需要任何背景 daemon。容器程序是你的 shell 的子程序，用 <code>ps</code> 就能看到完整的程序樹。</p><p>這個設計帶來一個附帶好處：systemd 可以直接管理容器。Docker 需要額外的 wrapper script 才能被 systemd 正確追蹤，Podman 天生就跟 systemd 合得來。</p><h2 id="安裝與基本使用"><a href="#安裝與基本使用" class="headerlink" title="安裝與基本使用"></a>安裝與基本使用</h2><p>在 Ubuntu 24.04 LTS 和 Debian 12 上：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> apt update &amp;&amp; <span class="built_in">sudo</span> apt install -y podman</span><br></pre></td></tr></table></figure><p>RHEL 系列（AlmaLinux、Rocky Linux）預設就有 Podman，不需要額外安裝。</p><p>裝完之後，試試看：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">podman run --<span class="built_in">rm</span> docker.io/library/alpine <span class="built_in">echo</span> <span class="string">&quot;hello from podman&quot;</span></span><br></pre></td></tr></table></figure><p>注意 Podman 預設需要完整的 image reference（<code>docker.io/library/alpine</code> 而不只是 <code>alpine</code>）。如果你不想每次都打完整路徑，編輯 <code>/etc/containers/registries.conf</code>：</p><figure class="highlight toml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">unqualified-search-registries</span> = [<span class="string">&quot;docker.io&quot;</span>]</span><br></pre></td></tr></table></figure><p>加了這行之後 <code>podman run alpine</code> 就跟 Docker 一樣的行為。</p><p>對於已經習慣打 <code>docker</code> 的人，設定一個 alias 就能無痛過渡：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&#x27;alias docker=podman&#x27;</span> &gt;&gt; ~/.bashrc</span><br></pre></td></tr></table></figure><p>這不是開玩笑——Podman 的 CLI 介面就是照著 Docker 做的，超過 95% 的指令和參數都完全相同。</p><h2 id="Rootless-容器到底改變了什麼"><a href="#Rootless-容器到底改變了什麼" class="headerlink" title="Rootless 容器到底改變了什麼"></a>Rootless 容器到底改變了什麼</h2><p>Docker 的 rootless mode 要另外設定，而且有不少限制。Podman 的 rootless 是預設值，不需要任何額外設定。</p><p>在 rootless 模式下，容器程序映射到你的使用者 UID namespace 裡。即使容器內部的程序以 root（UID 0）執行，在宿主機上它對應的是一個無特權的 UID。這表示就算容器被攻破、攻擊者拿到容器內的 root，他在宿主機上什麼都做不了。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 以一般使用者執行</span></span><br><span class="line">podman run -d --name web -p 8080:80 docker.io/library/nginx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 檢查宿主機上的程序</span></span><br><span class="line">ps aux | grep nginx</span><br><span class="line"><span class="comment"># 你會看到 nginx 程序跑在你的使用者帳號下，不是 root</span></span><br></pre></td></tr></table></figure><p>有一個實務上要注意的地方：rootless 模式下，bind mount 的檔案權限可能跟你預期的不同。容器內的 root（UID 0）對應到宿主機上的 subuid 範圍，所以直接掛載的目錄可能出現權限被拒。解決方式是在 volume 掛載時加上 <code>:Z</code> 標籤（SELinux 環境）或使用 <code>--userns=keep-id</code> 把容器的 UID 對應回宿主機使用者：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">podman run -v ./data:/app/data:Z --userns=keep-id myapp</span><br></pre></td></tr></table></figure><h2 id="Quadlet：用-systemd-管理容器的正確方式"><a href="#Quadlet：用-systemd-管理容器的正確方式" class="headerlink" title="Quadlet：用 systemd 管理容器的正確方式"></a>Quadlet：用 systemd 管理容器的正確方式</h2><p>Podman 4.4 之後引入的 Quadlet 是目前在單機上管理容器最優雅的方式。你寫一個類似 INI 格式的 <code>.container</code> 檔案，systemd 會在 <code>daemon-reload</code> 時自動把它轉成標準的 service unit。</p><p>以跑一個 Nginx 容器為例，建立 <code>~/.config/containers/systemd/web.container</code>：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Container]</span></span><br><span class="line"><span class="attr">Image</span>=docker.io/library/nginx:<span class="number">1.27</span>-alpine</span><br><span class="line"><span class="attr">PublishPort</span>=<span class="number">8080</span>:<span class="number">80</span></span><br><span class="line"><span class="attr">Volume</span>=./html:/usr/share/nginx/html:ro,Z</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Restart</span>=always</span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=default.target</span><br></pre></td></tr></table></figure><p>然後：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">systemctl --user daemon-reload</span><br><span class="line">systemctl --user start web</span><br><span class="line">systemctl --user <span class="built_in">enable</span> web</span><br></pre></td></tr></table></figure><p>就這樣。你的 Nginx 容器現在是一個標準的 systemd service，可以用 <code>systemctl status</code>、<code>journalctl</code> 來查看狀態和日誌，機器重開後自動啟動，crash 後自動重啟。</p><p>Quadlet 也支援 <code>.volume</code>、<code>.network</code>、<code>.pod</code> 檔案類型，可以把完整的多容器架構用純宣告式的方式管理。如果是系統級的服務（需要 root），把檔案放到 <code>/etc/containers/systemd/</code> 就好。</p><p>跟 Docker Compose 比起來，Quadlet 的優勢是跟作業系統的服務管理完全整合。你不需要記住 <code>docker compose up -d</code> 之後還要確認服務有沒有正確啟動，systemd 會處理所有的生命週期管理，包含開機啟動、失敗重試、資源限制和日誌收集。</p><h2 id="從-Docker-Compose-遷移"><a href="#從-Docker-Compose-遷移" class="headerlink" title="從 Docker Compose 遷移"></a>從 Docker Compose 遷移</h2><p>已經有一堆 <code>docker-compose.yml</code> 的專案怎麼辦？兩條路可以選。</p><p><strong>用 podman-compose（低門檻）：</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">pip install podman-compose</span><br><span class="line">podman-compose up -d</span><br></pre></td></tr></table></figure><p><code>podman-compose</code> 能直接讀取既有的 <code>docker-compose.yml</code>。大部分簡單到中等複雜度的 Compose 檔案可以直接跑，不需要修改。</p><p><strong>用原生 Docker Compose + Podman socket（高相容性）：</strong></p><p>Podman 可以啟動一個 Docker 相容的 API socket，讓 Docker Compose v2 直接對接：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">systemctl --user <span class="built_in">enable</span> --now podman.socket</span><br><span class="line"><span class="built_in">export</span> DOCKER_HOST=unix://<span class="variable">$XDG_RUNTIME_DIR</span>/podman/podman.sock</span><br><span class="line">docker compose up -d  <span class="comment"># 這裡用的是原生 Docker Compose，但後端是 Podman</span></span><br></pre></td></tr></table></figure><p>這種方式的相容性更高，因為你用的是正式的 Docker Compose binary，只是底層執行引擎換成了 Podman。複雜的 Compose 功能（build、healthcheck、depends_on condition）都能正常運作。</p><p>遷移時最常遇到的三個問題：</p><ol><li><strong>Image 名稱</strong>：Podman 預設要求完整的 registry 路徑，<code>image: nginx</code> 要改成 <code>image: docker.io/library/nginx</code>（或設定 <code>unqualified-search-registries</code>）</li><li><strong>Volume 權限</strong>：rootless 模式下的 UID 映射可能導致檔案權限問題，加 <code>:Z</code> 或 <code>userns_mode: keep-id</code> 處理</li><li><strong>特權 port</strong>：rootless 模式下無法綁定 1024 以下的 port，需要調整 <code>sysctl net.ipv4.ip_unprivileged_port_start=80</code> 或改用 rootful 模式</li></ol><h2 id="什麼場景該用-Podman，什麼場景留在-Docker"><a href="#什麼場景該用-Podman，什麼場景留在-Docker" class="headerlink" title="什麼場景該用 Podman，什麼場景留在 Docker"></a>什麼場景該用 Podman，什麼場景留在 Docker</h2><p>建議直接用 Podman 的情境：全新的 Linux 伺服器部署、對安全有嚴格要求的環境（金融、醫療、政府）、RHEL&#x2F;CentOS Stream 生態系（Podman 是原生工具）、以及跑在資源有限的 VPS 上的單機服務（少一支 daemon 就是少一份記憶體開銷）。</p><p>留在 Docker 比較合理的情境：團隊已經重度依賴 Docker Desktop 的 GUI 工作流、CI&#x2F;CD pipeline 綁定了 Docker-in-Docker（DinD）、或者使用了 Docker Swarm（Podman 沒有對應的 orchestration 功能，不過這個需求通常該往 Kubernetes 走）。</p><p>兩者建出來的 image 完全相容——都是 OCI 標準格式。在開發機用 Docker 建 image，在生產伺服器用 Podman 跑，完全沒問題。</p><hr><p>在 VPS 上部署容器化服務，底層硬體的穩定性直接影響運行品質。NCSE Network 提供搭載 Intel Gold CPU 與 NVMe SSD 的臺灣本地 VPS，機房位於是方電訊，適合需要低延遲、穩定網路的容器部署場景。更多資訊可到 <a href="https://ncse.tw/">ncse.tw</a> 了解。</p>]]>
    </content>
    <id>https://blog.ncse.tw/podman-rootless-container-docker-alternative/</id>
    <link href="https://blog.ncse.tw/podman-rootless-container-docker-alternative/"/>
    <published>2026-05-25T02:00:00.000Z</published>
    <summary>Podman 是無 daemon、預設 rootless 的容器引擎，與 Docker CLI 幾乎完全相容。本文從架構差異、安裝、Quadlet 整合 systemd、Compose 遷移到生產部署，完整解析為什麼 2026 年新專案該優先考慮 Podman。</summary>
    <title>Docker 跑在 root daemon 上讓你不安？Podman 用 rootless 架構從根本解決這件事</title>
    <updated>2026-06-13T08:06:37.114Z</updated>
  </entry>
</feed>
