<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Prod-Parity on Tarragon</title><link>https://tarrragon.github.io/blog/tags/prod-parity/</link><description>Recent content in Prod-Parity on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 06 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/prod-parity/index.xml" rel="self" type="application/rss+xml"/><item><title>工作站 dotfile 跨發行版落地</title><link>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/</guid><description>&lt;p>這篇的目的是讓一份照 Arch 寫的工作站 dotfile，能在 client 常見的 Debian/Ubuntu 機器上還原出同樣的環境。要改的部分比想像少：綁發行版的只有「用哪個 package manager 裝套件」這一層，config 檔內容、symlink 部署、shell 框架安裝全部可攜。原理見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a>，這裡只講怎麼判斷跟怎麼落地。&lt;/p>
&lt;h2 id="哪些綁-distro哪些共用">哪些綁 distro、哪些共用&lt;/h2>
&lt;p>拿到一份 dotfile 要跨 distro 時，先把內容分成兩堆，判準是「換 distro 會不會變」：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>會變、要下沉到平台層&lt;/strong>：套件安裝指令（&lt;code>pacman&lt;/code> vs &lt;code>apt-get&lt;/code>）、套件清單、以及套件名分歧的工具。&lt;/li>
&lt;li>&lt;strong>不會變、留共通層&lt;/strong>：&lt;code>.zshrc&lt;/code> 內容、&lt;code>stow&lt;/code> 建 symlink 的邏輯、oh-my-zsh 的 &lt;code>git clone&lt;/code>、所有 config 檔本身。&lt;/li>
&lt;/ul>
&lt;p>判斷錯的代價是把套件名寫死在共通層——換 distro 就壞。正確形態是共通層只呼叫「裝套件」這個抽象動作，具體套件名交給各平台清單。&lt;/p>
&lt;h2 id="落地入口-detection--平台清單">落地：入口 detection + 平台清單&lt;/h2>
&lt;p>工作站的套件集在不同 distro 差異大，適合用入口 detection：安裝腳本開頭偵測 package manager，dispatch 到對應的平台腳本。dotfiles repo 的形態是 &lt;code>install.sh&lt;/code> 判斷 OS 後委派給 &lt;code>install-arch.sh&lt;/code> / &lt;code>install-macos.sh&lt;/code> / &lt;code>install-debian.sh&lt;/code>，套件清單各一份（&lt;code>packages/arch-*.txt&lt;/code>、&lt;code>packages/debian-*.txt&lt;/code>），共通的環境組裝（stow、框架 clone）留在 &lt;code>install.sh&lt;/code>。&lt;/p>
&lt;p>跨到 Debian 只需要補一支 &lt;code>install-debian.sh&lt;/code> 跟對應清單，共通層一行都不用動——這正是抽象層有效的證據。&lt;/p>
&lt;h2 id="套件名分歧裝好不等於叫得動">套件名分歧：裝好不等於叫得動&lt;/h2>
&lt;p>同一個工具在不同 distro 的名字有好幾種分歧（有哪些形態、為什麼，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a>）。落地時按形態各自處理：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>套件名不同&lt;/strong>（&lt;code>fd&lt;/code> → &lt;code>fd-find&lt;/code>、&lt;code>github-cli&lt;/code> → &lt;code>gh&lt;/code>）：靠各平台清單逐項對照，不假設同名。&lt;/li>
&lt;li>&lt;strong>binary 被改名&lt;/strong>（&lt;code>fd-find&lt;/code> 的 binary 是 &lt;code>fdfind&lt;/code>、&lt;code>bat&lt;/code> 是 &lt;code>batcat&lt;/code>）：&lt;code>.zshrc&lt;/code> 按平台補 alias（&lt;code>alias fd=fdfind&lt;/code>）——「套件裝好了」不等於「指令叫得動」。&lt;/li>
&lt;li>&lt;strong>名字撞到別的工具&lt;/strong>（&lt;code>apt install delta&lt;/code> 裝到的不是 &lt;code>.gitconfig&lt;/code> 要的 git-delta）：別照抄，確認同名同物。&lt;/li>
&lt;/ul>
&lt;p>&lt;code>ripgrep&lt;/code> 兩邊同名（binary 都是 &lt;code>rg&lt;/code>）不用處理，&lt;code>fd&lt;/code> / &lt;code>bat&lt;/code> 就要按上面吸收。&lt;/p>
&lt;h2 id="實作會遇到的狀況與排除">實作會遇到的狀況與排除&lt;/h2>
&lt;p>跨到 Debian 實跑時，除了名字分歧，還有幾個操作面的狀況會擋住 bootstrap。先知道怎麼排除：&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>全新映像連 sudo/git 都沒有&lt;/strong>。乾淨的 Debian 映像預裝極簡，&lt;code>install.sh&lt;/code> 要用到的 &lt;code>sudo&lt;/code>、&lt;code>git&lt;/code> 本身都缺。排除：bootstrap 第一步先 &lt;code>apt-get install -y sudo git&lt;/code>，才有辦法 clone repo、跑後續。這是最小映像的預期形態，不是壞掉。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>清單裡一個沒打包的名字讓整批全滅&lt;/strong>。&lt;code>apt-get install&lt;/code> 是一筆全有或全無的交易，清單塞一個 Debian 沒有的名字（實測 broot、zellij 在 bookworm 沒打包），apt 直接中止，同批存在的工具也一個都沒裝。排除：清單逐項對齊該 distro 實際有的套件，動手前用 &lt;code>apt-get install -s &amp;lt;pkg&amp;gt;&lt;/code> 逐一 dry-run 篩掉解不開的。原理見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性&lt;/a>。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>某些工具 Debian 根本沒打包&lt;/strong>。broot、zellij、git-delta、lazygit、yazi 在 bookworm 預設 repo 都不存在（Arch 都有）。排除：把它們移出 apt 清單，改從 GitHub releases 裝——抓對應架構的預編譯檔（&lt;code>curl -L -O &amp;lt;release-url&amp;gt;&lt;/code>）、解壓、&lt;code>chmod +x&lt;/code>、&lt;code>mv&lt;/code> 到 &lt;code>~/.local/bin&lt;/code>（確認它在 &lt;code>PATH&lt;/code>）；若走 &lt;code>cargo install&lt;/code>，cargo 本身要先用 rustup 裝（裸 Debian 沒有）。原理見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這篇的目的是讓一份照 Arch 寫的工作站 dotfile，能在 client 常見的 Debian/Ubuntu 機器上還原出同樣的環境。要改的部分比想像少：綁發行版的只有「用哪個 package manager 裝套件」這一層，config 檔內容、symlink 部署、shell 框架安裝全部可攜。原理見 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a>，這裡只講怎麼判斷跟怎麼落地。</p>
<h2 id="哪些綁-distro哪些共用">哪些綁 distro、哪些共用</h2>
<p>拿到一份 dotfile 要跨 distro 時，先把內容分成兩堆，判準是「換 distro 會不會變」：</p>
<ul>
<li><strong>會變、要下沉到平台層</strong>：套件安裝指令（<code>pacman</code> vs <code>apt-get</code>）、套件清單、以及套件名分歧的工具。</li>
<li><strong>不會變、留共通層</strong>：<code>.zshrc</code> 內容、<code>stow</code> 建 symlink 的邏輯、oh-my-zsh 的 <code>git clone</code>、所有 config 檔本身。</li>
</ul>
<p>判斷錯的代價是把套件名寫死在共通層——換 distro 就壞。正確形態是共通層只呼叫「裝套件」這個抽象動作，具體套件名交給各平台清單。</p>
<h2 id="落地入口-detection--平台清單">落地：入口 detection + 平台清單</h2>
<p>工作站的套件集在不同 distro 差異大，適合用入口 detection：安裝腳本開頭偵測 package manager，dispatch 到對應的平台腳本。dotfiles repo 的形態是 <code>install.sh</code> 判斷 OS 後委派給 <code>install-arch.sh</code> / <code>install-macos.sh</code> / <code>install-debian.sh</code>，套件清單各一份（<code>packages/arch-*.txt</code>、<code>packages/debian-*.txt</code>），共通的環境組裝（stow、框架 clone）留在 <code>install.sh</code>。</p>
<p>跨到 Debian 只需要補一支 <code>install-debian.sh</code> 跟對應清單，共通層一行都不用動——這正是抽象層有效的證據。</p>
<h2 id="套件名分歧裝好不等於叫得動">套件名分歧：裝好不等於叫得動</h2>
<p>同一個工具在不同 distro 的名字有好幾種分歧（有哪些形態、為什麼，見 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a>）。落地時按形態各自處理：</p>
<ul>
<li><strong>套件名不同</strong>（<code>fd</code> → <code>fd-find</code>、<code>github-cli</code> → <code>gh</code>）：靠各平台清單逐項對照，不假設同名。</li>
<li><strong>binary 被改名</strong>（<code>fd-find</code> 的 binary 是 <code>fdfind</code>、<code>bat</code> 是 <code>batcat</code>）：<code>.zshrc</code> 按平台補 alias（<code>alias fd=fdfind</code>）——「套件裝好了」不等於「指令叫得動」。</li>
<li><strong>名字撞到別的工具</strong>（<code>apt install delta</code> 裝到的不是 <code>.gitconfig</code> 要的 git-delta）：別照抄，確認同名同物。</li>
</ul>
<p><code>ripgrep</code> 兩邊同名（binary 都是 <code>rg</code>）不用處理，<code>fd</code> / <code>bat</code> 就要按上面吸收。</p>
<h2 id="實作會遇到的狀況與排除">實作會遇到的狀況與排除</h2>
<p>跨到 Debian 實跑時，除了名字分歧，還有幾個操作面的狀況會擋住 bootstrap。先知道怎麼排除：</p>
<ul>
<li>
<p><strong>全新映像連 sudo/git 都沒有</strong>。乾淨的 Debian 映像預裝極簡，<code>install.sh</code> 要用到的 <code>sudo</code>、<code>git</code> 本身都缺。排除：bootstrap 第一步先 <code>apt-get install -y sudo git</code>，才有辦法 clone repo、跑後續。這是最小映像的預期形態，不是壞掉。</p>
</li>
<li>
<p><strong>清單裡一個沒打包的名字讓整批全滅</strong>。<code>apt-get install</code> 是一筆全有或全無的交易，清單塞一個 Debian 沒有的名字（實測 broot、zellij 在 bookworm 沒打包），apt 直接中止，同批存在的工具也一個都沒裝。排除：清單逐項對齊該 distro 實際有的套件，動手前用 <code>apt-get install -s &lt;pkg&gt;</code> 逐一 dry-run 篩掉解不開的。原理見 <a href="/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性</a>。</p>
</li>
<li>
<p><strong>某些工具 Debian 根本沒打包</strong>。broot、zellij、git-delta、lazygit、yazi 在 bookworm 預設 repo 都不存在（Arch 都有）。排除：把它們移出 apt 清單，改從 GitHub releases 裝——抓對應架構的預編譯檔（<code>curl -L -O &lt;release-url&gt;</code>）、解壓、<code>chmod +x</code>、<code>mv</code> 到 <code>~/.local/bin</code>（確認它在 <code>PATH</code>）；若走 <code>cargo install</code>，cargo 本身要先用 rustup 裝（裸 Debian 沒有）。原理見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>。</p>
</li>
<li>
<p><strong>裝個 node 拉進幾百個系統套件</strong>。<code>apt install nodejs npm</code> 在 Debian 會連帶 300 多個 <code>node-*</code> 套件（實測 bookworm 一次裝進 402 個新套件、其中 336 個是 node-*）。要避免的是拿系統套件管理器裝語言執行環境。排除：node / python / ruby 這類語言生態走 version manager（fnm / pyenv）在家目錄管，別放進 apt 清單。原理見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>。</p>
</li>
</ul>
<h2 id="邊界與下一步">邊界與下一步</h2>
<p>桌面層（Hyprland 的 <a href="/blog/linux/dotfile/knowledge-cards/rice/" data-link-title="Rice（桌面視覺客製化）" data-link-desc="Linux 桌面文章裡看到 rice / ricing / ricer 不確定意思時回來讀">rice</a>，即桌面視覺客製化）的跨 distro 成本遠高於 CLI 層：Hyprland 在 Debian 要較新版才有打包，套件名與可用性都要重驗，所以工作站跨 distro 通常只做到 terminal 層，桌面留在實測過的 Arch。</p>
<p>工作站跨好之後，開發還缺一個對齊 client 線上的 runtime——那是下一篇 <a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a>。工作站是你的機器、可以最新；runtime 要退回線上那個凍結的舊形狀，兩者對齊的目標相反。</p>
]]></content:encoded></item><item><title>對齊 prod 的 runtime container</title><link>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/</guid><description>&lt;p>這篇的目的是建一個跟 client 線上逐項對齊的 runtime，讓「本機能跑」直接等於「線上能跑」。做法的核心是刻意把環境退回線上那個凍結的舊形狀，而非維持最新最乾淨——為什麼方向跟直覺相反、要對齊哪些維度，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則&lt;/a>。Dockerfile 與 compose 本身怎麼運作（指令、layer、多 service 編排），見 Docker vendor 的 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/dockerfile-design/" data-link-title="Dockerfile 設計：指令、layer 與 multi-stage" data-link-desc="image 大得離譜、code 一改就整包重 build、或 multi-stage 複製 binary 後 container 起不來說找不到 library 時回來讀 — Dockerfile 指令怎麼變成 layer、build 與 runtime 怎麼分離">Dockerfile 設計&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/docker-compose/" data-link-title="Docker Compose：多 service dev 環境編排" data-link-desc="一個 app 要好幾個 container(DB / cache / web)、手動 docker run 串不起來、或 compose 起來後 app 連不到 DB 或 DB 還沒 ready 就被連時回來讀 — 多 service 怎麼宣告式編排">Docker Compose&lt;/a>。這裡只講怎麼判讀跟一個實測過的基線。&lt;/p>
&lt;h2 id="對齊的載體是-image-tag-跟-config不是主機">對齊的載體是 image tag 跟 config，不是主機&lt;/h2>
&lt;p>Parity 不靠「把主機裝成跟 prod 一樣」達成——你的主機是 Arch 或 macOS 都無所謂。對齊的載體是 container 的 image tag 跟掛進去的 config：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>image tag 釘到 OS 世代&lt;/strong>：用 &lt;code>php:7.2-fpm-buster&lt;/code> 而不是 &lt;code>php:7.2-fpm&lt;/code>，把 PHP 版本連同底層 Debian 世代一起凍結（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>別為了省體積換 libc&lt;/strong>：prod 是 Debian（glibc）就別用 &lt;code>php:7.2-alpine&lt;/code>（musl），DNS 與原生擴充行為會分岔（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>服務設定抄 prod&lt;/strong>：MySQL 的 &lt;code>sql_mode&lt;/code>、時區，PHP 的擴充清單與 &lt;code>php.ini&lt;/code>，都從線上的權威來源抄、不憑印象填。SSH 進 prod 後用 &lt;code>mysql -N -e &amp;quot;SELECT @@sql_mode&amp;quot;&lt;/code> 抓 sql_mode、&lt;code>php -m&lt;/code> 列擴充、&lt;code>php -i&lt;/code>（或 web 端存一支 &lt;code>&amp;lt;?php phpinfo();&lt;/code>）看 &lt;code>php.ini&lt;/code> 值；抄回來分別填進 db 的 &lt;code>my.cnf&lt;/code>（&lt;code>sql_mode&lt;/code> / 時區）、Dockerfile 的 &lt;code>docker-php-ext-install&lt;/code> 清單（對照 &lt;code>php -m&lt;/code>）、掛進去的 &lt;code>php.ini&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>主機不變、只有這幾樣對齊——這是 parity 能在任何工作站上重現的原因。&lt;/p>
&lt;h2 id="要逐項比對的維度">要逐項比對的維度&lt;/h2>
&lt;p>跑起來後，用一個 probe 印出 runtime 的每個 parity 維度，跟 prod 逐行比對。每一行對應一個會影響行為的維度：&lt;/p>
&lt;ul>
&lt;li>PHP 主版 + patch 版&lt;/li>
&lt;li>時區（PHP 與 MySQL 各一份，跨時區的 &lt;code>NOW()&lt;/code> 是常見隱形 bug）&lt;/li>
&lt;li>擴充清單（要相等、不是涵蓋；多裝的擴充讓本機能跑、prod 掛掉）&lt;/li>
&lt;li>MySQL 版本與 &lt;code>sql_mode&lt;/code>（5.7 預設開 &lt;code>ONLY_FULL_GROUP_BY&lt;/code>，舊 app 的 &lt;code>SELECT ... GROUP BY&lt;/code> 未列全欄位會回 error 1055）&lt;/li>
&lt;/ul>
&lt;h2 id="實測基線">實測基線&lt;/h2>
&lt;p>dotfiles repo 的 &lt;code>runtimes/php72-mysql57/&lt;/code> 是一個跑得起來的 variant（PHP-FPM + MySQL 5.7 + nginx，docker-compose 組合）。&lt;code>docker compose up -d --build&lt;/code> 起來後 &lt;code>curl http://localhost:8080/&lt;/code>——probe 是一支 &lt;code>src/index.php&lt;/code>、由 nginx 服務，逐行印出每個 parity 維度。arm64 macOS 實測（2026-07）輸出：&lt;/p></description><content:encoded><![CDATA[<p>這篇的目的是建一個跟 client 線上逐項對齊的 runtime，讓「本機能跑」直接等於「線上能跑」。做法的核心是刻意把環境退回線上那個凍結的舊形狀，而非維持最新最乾淨——為什麼方向跟直覺相反、要對齊哪些維度，見 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則</a>。Dockerfile 與 compose 本身怎麼運作（指令、layer、多 service 編排），見 Docker vendor 的 <a href="/blog/backend/05-deployment-platform/vendors/docker/dockerfile-design/" data-link-title="Dockerfile 設計：指令、layer 與 multi-stage" data-link-desc="image 大得離譜、code 一改就整包重 build、或 multi-stage 複製 binary 後 container 起不來說找不到 library 時回來讀 — Dockerfile 指令怎麼變成 layer、build 與 runtime 怎麼分離">Dockerfile 設計</a> 與 <a href="/blog/backend/05-deployment-platform/vendors/docker/docker-compose/" data-link-title="Docker Compose：多 service dev 環境編排" data-link-desc="一個 app 要好幾個 container(DB / cache / web)、手動 docker run 串不起來、或 compose 起來後 app 連不到 DB 或 DB 還沒 ready 就被連時回來讀 — 多 service 怎麼宣告式編排">Docker Compose</a>。這裡只講怎麼判讀跟一個實測過的基線。</p>
<h2 id="對齊的載體是-image-tag-跟-config不是主機">對齊的載體是 image tag 跟 config，不是主機</h2>
<p>Parity 不靠「把主機裝成跟 prod 一樣」達成——你的主機是 Arch 或 macOS 都無所謂。對齊的載體是 container 的 image tag 跟掛進去的 config：</p>
<ul>
<li><strong>image tag 釘到 OS 世代</strong>：用 <code>php:7.2-fpm-buster</code> 而不是 <code>php:7.2-fpm</code>，把 PHP 版本連同底層 Debian 世代一起凍結（見 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning</a>）。</li>
<li><strong>別為了省體積換 libc</strong>：prod 是 Debian（glibc）就別用 <code>php:7.2-alpine</code>（musl），DNS 與原生擴充行為會分岔（見 <a href="/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl</a>）。</li>
<li><strong>服務設定抄 prod</strong>：MySQL 的 <code>sql_mode</code>、時區，PHP 的擴充清單與 <code>php.ini</code>，都從線上的權威來源抄、不憑印象填。SSH 進 prod 後用 <code>mysql -N -e &quot;SELECT @@sql_mode&quot;</code> 抓 sql_mode、<code>php -m</code> 列擴充、<code>php -i</code>（或 web 端存一支 <code>&lt;?php phpinfo();</code>）看 <code>php.ini</code> 值；抄回來分別填進 db 的 <code>my.cnf</code>（<code>sql_mode</code> / 時區）、Dockerfile 的 <code>docker-php-ext-install</code> 清單（對照 <code>php -m</code>）、掛進去的 <code>php.ini</code>。</li>
</ul>
<p>主機不變、只有這幾樣對齊——這是 parity 能在任何工作站上重現的原因。</p>
<h2 id="要逐項比對的維度">要逐項比對的維度</h2>
<p>跑起來後，用一個 probe 印出 runtime 的每個 parity 維度，跟 prod 逐行比對。每一行對應一個會影響行為的維度：</p>
<ul>
<li>PHP 主版 + patch 版</li>
<li>時區（PHP 與 MySQL 各一份，跨時區的 <code>NOW()</code> 是常見隱形 bug）</li>
<li>擴充清單（要相等、不是涵蓋；多裝的擴充讓本機能跑、prod 掛掉）</li>
<li>MySQL 版本與 <code>sql_mode</code>（5.7 預設開 <code>ONLY_FULL_GROUP_BY</code>，舊 app 的 <code>SELECT ... GROUP BY</code> 未列全欄位會回 error 1055）</li>
</ul>
<h2 id="實測基線">實測基線</h2>
<p>dotfiles repo 的 <code>runtimes/php72-mysql57/</code> 是一個跑得起來的 variant（PHP-FPM + MySQL 5.7 + nginx，docker-compose 組合）。<code>docker compose up -d --build</code> 起來後 <code>curl http://localhost:8080/</code>——probe 是一支 <code>src/index.php</code>、由 nginx 服務，逐行印出每個 parity 維度。arm64 macOS 實測（2026-07）輸出：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">PHP version : 7.2.34
</span></span><span class="line"><span class="ln">2</span><span class="cl">timezone    : Asia/Taipei
</span></span><span class="line"><span class="ln">3</span><span class="cl">MySQL ver   : 5.7.44
</span></span><span class="line"><span class="ln">4</span><span class="cl">sql_mode    : ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,...
</span></span><span class="line"><span class="ln">5</span><span class="cl">db timezone : +08:00</span></span></code></pre></div><p>版本、時區、<code>sql_mode</code> 都跟 config 逐項對齊——這就是 parity 達成的樣子。</p>
<h2 id="凍結舊環境的兩個稅">凍結舊環境的兩個稅</h2>
<p>實跑會撞到兩個「照官方 docs 寫會 build 不起來、只有實機才知道」的問題，兩個都是凍結舊環境特有的：</p>
<ul>
<li><strong>Debian buster 已 EOL</strong>：套件庫從主 mirror 移到 <code>archive.debian.org</code>，Dockerfile 裡不改 apt source 直接 <code>apt-get update</code> 就 404（exit 100）。要改指 archive 並關掉過期檢查。</li>
<li><strong>mysql:5.7 無 arm64 原生 image</strong>：在 Apple Silicon 主機上要 <code>platform: linux/amd64</code> 走模擬（機制見 <a href="/blog/backend/knowledge-cards/qemu-binfmt-emulation/" data-link-title="QEMU binfmt Emulation（跨架構模擬）" data-link-desc="docker 跑非原生架構的 image 報 exec format error、mysql:5.7 在 Apple Silicon 要 platform: linux/amd64、或跨平台 build 特別慢時回來讀 — 非原生 image 怎麼被模擬跑起來">QEMU binfmt 跨架構模擬</a>）。這本身也是一種 parity——dev 是模擬 amd64、prod 是原生 amd64，架構對齊。</li>
</ul>
<p>這兩個稅是 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則</a> 講的「對齊凍結環境要付的代價」的具體形態：凍結舊版意味著也繼承了它 EOL 與架構支援退場的後果。</p>
<h2 id="什麼時候值得做到逐項">什麼時候值得做到逐項</h2>
<p>Parity 是有成本的紀律，不是每個專案都要做到逐項——值得與可放寬的完整判準見 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則</a> 的判讀訊號段。這篇的 PHP 7.2 場景屬「值得」那一類：有原生擴充、MySQL 嚴格模式、時區邏輯都在，本機不對齊就會在線上才發現行為不同。</p>
<h2 id="下一步">下一步</h2>
<p>runtime 對齊好之後，你會想在 container 裡用順手的 shell 跟 editor——但那不能直接塞進這個 image，否則 image 就不再等於 prod。怎麼把 ergonomics 帶進來又不污染 parity，見 <a href="/blog/linux/dotfile/10-prod-parity/container-ergonomics/" data-link-title="dotfile 跨進 runtime container" data-link-desc="想在對齊 prod 的 container 裡用自己順手的 shell / vim、又不想把工具裝進 image 破壞 parity 時回來讀 — 在 running container 裝開發工具、跟 runtime 分層">dotfile 跨進 runtime container</a>。</p>
]]></content:encoded></item><item><title>dotfile 跨進 runtime container</title><link>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/container-ergonomics/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/container-ergonomics/</guid><description>&lt;p>有了對齊 prod 的 runtime，你會想在裡面用順手的 shell 跟 vim——但直接把這些裝進 image，它就不再跟線上逐項相同（parity 破功）。這篇把開發 ergonomics（shell、vim、git config）帶進 container、同時保住 parity，做法是把「這個 image 跟線上一不一樣」跟「我用得順不順手」當成兩層分開處理。&lt;/p>
&lt;h2 id="ergonomics-進可寫層不進-image">ergonomics 進可寫層，不進 image&lt;/h2>
&lt;p>最容易犯的錯是把 zsh、vim、個人 config 寫進 runtime 的 Dockerfile。一旦 ergonomics 進了 image，這個 image 就不再等於 prod——線上不會有你的 oh-my-zsh，parity 當場破功。判準很硬：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>runtime image 只裝 app 需要、prod 也有的套件&lt;/strong>（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container&lt;/a>）。&lt;/li>
&lt;li>&lt;strong>ergonomics 裝進「已啟動的 container」&lt;/strong>：&lt;code>docker compose exec &amp;lt;service&amp;gt; bash&lt;/code> 進去後才裝，或 &lt;code>docker compose cp&lt;/code> 把檔案送進去（已啟動的 container 無法事後 mount，掛載只能在 compose / &lt;code>docker run&lt;/code> 當下宣告）。它活在容器的可寫層、不進 image。&lt;/li>
&lt;/ul>
&lt;p>這條分層讓同一個 runtime image 既是「線上的忠實複製」又能「開發時很好用」，兩個需求不打架。&lt;/p>
&lt;h2 id="ergonomics-層自己也要可攜">ergonomics 層自己也要可攜&lt;/h2>
&lt;p>runtime 的 base 是 Debian，但你的 dotfile 是照 Arch 或 macOS 寫的。ergonomics 安裝腳本要能在 Debian container 裡跑，靠的是 package manager detection——同一支腳本用 &lt;code>command -v apt-get / pacman / apk&lt;/code> 分支，適配當下所在的環境。原理見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a>。&lt;/p>
&lt;p>跟工作站的入口 detection 不同，container 內只裝少數幾個工具、不值得拆多檔，適合行內 detection：一支腳本內分支、共用同一份工具清單。dotfiles repo 的 &lt;code>runtimes/php72-mysql57/ergonomics/setup.sh&lt;/code> 就是這個形態。&lt;/p>
&lt;h2 id="落地形態">落地形態&lt;/h2>
&lt;p>裝套件那一步分 distro，config 部署完全共通：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>裝工具&lt;/strong>：把 &lt;code>setup.sh&lt;/code> 送進已啟動的 container 再跑——&lt;code>docker compose cp ergonomics/setup.sh php:/tmp/setup.sh &amp;amp;&amp;amp; docker compose exec php bash /tmp/setup.sh&lt;/code>，它偵測 package manager 裝 zsh/git/vim。&lt;/li>
&lt;li>&lt;strong>部署 config&lt;/strong>：dotfiles repo 送進去再 stow——&lt;code>docker compose cp ~/dotfiles php:/root/dotfiles &amp;amp;&amp;amp; docker compose exec php bash -c 'cd /root/dotfiles &amp;amp;&amp;amp; stow zsh git vim'&lt;/code>。config 本身可攜、不分 distro（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow&lt;/a>）。&lt;/li>
&lt;/ul>
&lt;p>這樣 config 只維護一份，跟工作站共用；container 內只多了「裝套件」那一層 distro 分支。&lt;/p></description><content:encoded><![CDATA[<p>有了對齊 prod 的 runtime，你會想在裡面用順手的 shell 跟 vim——但直接把這些裝進 image，它就不再跟線上逐項相同（parity 破功）。這篇把開發 ergonomics（shell、vim、git config）帶進 container、同時保住 parity，做法是把「這個 image 跟線上一不一樣」跟「我用得順不順手」當成兩層分開處理。</p>
<h2 id="ergonomics-進可寫層不進-image">ergonomics 進可寫層，不進 image</h2>
<p>最容易犯的錯是把 zsh、vim、個人 config 寫進 runtime 的 Dockerfile。一旦 ergonomics 進了 image，這個 image 就不再等於 prod——線上不會有你的 oh-my-zsh，parity 當場破功。判準很硬：</p>
<ul>
<li><strong>runtime image 只裝 app 需要、prod 也有的套件</strong>（見 <a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a>）。</li>
<li><strong>ergonomics 裝進「已啟動的 container」</strong>：<code>docker compose exec &lt;service&gt; bash</code> 進去後才裝，或 <code>docker compose cp</code> 把檔案送進去（已啟動的 container 無法事後 mount，掛載只能在 compose / <code>docker run</code> 當下宣告）。它活在容器的可寫層、不進 image。</li>
</ul>
<p>這條分層讓同一個 runtime image 既是「線上的忠實複製」又能「開發時很好用」，兩個需求不打架。</p>
<h2 id="ergonomics-層自己也要可攜">ergonomics 層自己也要可攜</h2>
<p>runtime 的 base 是 Debian，但你的 dotfile 是照 Arch 或 macOS 寫的。ergonomics 安裝腳本要能在 Debian container 裡跑，靠的是 package manager detection——同一支腳本用 <code>command -v apt-get / pacman / apk</code> 分支，適配當下所在的環境。原理見 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a>。</p>
<p>跟工作站的入口 detection 不同，container 內只裝少數幾個工具、不值得拆多檔，適合行內 detection：一支腳本內分支、共用同一份工具清單。dotfiles repo 的 <code>runtimes/php72-mysql57/ergonomics/setup.sh</code> 就是這個形態。</p>
<h2 id="落地形態">落地形態</h2>
<p>裝套件那一步分 distro，config 部署完全共通：</p>
<ul>
<li><strong>裝工具</strong>：把 <code>setup.sh</code> 送進已啟動的 container 再跑——<code>docker compose cp ergonomics/setup.sh php:/tmp/setup.sh &amp;&amp; docker compose exec php bash /tmp/setup.sh</code>，它偵測 package manager 裝 zsh/git/vim。</li>
<li><strong>部署 config</strong>：dotfiles repo 送進去再 stow——<code>docker compose cp ~/dotfiles php:/root/dotfiles &amp;&amp; docker compose exec php bash -c 'cd /root/dotfiles &amp;&amp; stow zsh git vim'</code>。config 本身可攜、不分 distro（見 <a href="/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow</a>）。</li>
</ul>
<p>這樣 config 只維護一份，跟工作站共用；container 內只多了「裝套件」那一層 distro 分支。</p>
<h2 id="邊界">邊界</h2>
<p>ergonomics 進可寫層的代價是「不持久」：container 重建就沒了，每次 <code>docker exec</code> 重來或寫成啟動時自動跑的腳本。這是刻意的取捨——為了保住 image 的 parity，換來 ergonomics 每次重建要重裝。想持久化 ergonomics 又不碰 runtime image，正解是走 devcontainer 那套（見 <a href="/blog/linux/dotfile/09-team-environment/devcontainer-nix/" data-link-title="Devcontainer 與 Nix：容器化和宣告式的開發環境" data-link-desc="團隊開發環境要標準化、或評估 devcontainer 和 nix 跟個人 dotfile 怎麼共存時回來讀">模組九：從個人到團隊</a>），它把開發環境跟 runtime image 明確拆成兩個 image。</p>
]]></content:encoded></item><item><title>模組十：Prod Parity — 跨發行版與線上環境對齊</title><link>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/</guid><description>&lt;p>前面的模組把個人工作環境用 dotfile 管起來，預設工作站跟你天天用的機器是同一套。實際接案 / 商用開發會撞上一個落差：工作站是 Arch 這種滾動更新、永遠最新版的環境，但要開發的 client 線上跑的是幾年前定版後就凍結的舊環境——PHP 7.2、MySQL 5.7、某個特定 Debian 世代。直接拿最新的 Arch 環境開發，寫出來的成品可能在線上根本跑不起來。&lt;/p>
&lt;p>這個模組把 dotfile 的「環境可重現」思想延伸到兩個新對象：一是讓工作站 dotfile 跨到 client 常見的非 Arch 發行版，二是建一個跟線上逐項對齊的 runtime container。核心切分是三層，各自可重現、各自對齊不同目標。&lt;/p>
&lt;h2 id="三層切分">三層切分&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層&lt;/th>
 &lt;th>是什麼&lt;/th>
 &lt;th>對齊誰&lt;/th>
 &lt;th>綁 distro 嗎&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>工作站&lt;/td>
 &lt;td>你的個人機（shell / wm / editor）&lt;/td>
 &lt;td>對齊你自己的順手&lt;/td>
 &lt;td>綁 pacman/brew/apt，靠抽象層跨 distro&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>runtime&lt;/td>
 &lt;td>跑 app 的 PHP / MySQL / web server&lt;/td>
 &lt;td>逐項對齊 client 的 prod&lt;/td>
 &lt;td>不碰主機 package manager，parity 靠 image tag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>container ergonomics&lt;/td>
 &lt;td>你在 container 裡用的 shell / vim&lt;/td>
 &lt;td>純開發舒適&lt;/td>
 &lt;td>靠 detection 分支同時支援多 distro&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三層的哲學是同一套（環境 as code、可重現），差別只在對齊誰、綁哪個 package manager。把三者混進同一個 artifact 是常見錯誤：ergonomics 混進 runtime image，image 就不再等於 prod、parity 破功。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>回答什麼問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地&lt;/a>&lt;/td>
 &lt;td>同一份 dotfile 從 Arch 跨到 Debian/Ubuntu&lt;/td>
 &lt;td>換 client 環境時哪些要改、哪些能共用&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container&lt;/a>&lt;/td>
 &lt;td>建跟線上逐項對齊的 PHP 7.2 + MySQL 5.7 runtime&lt;/td>
 &lt;td>parity 要對齊到多細、什麼時候值得&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/container-ergonomics/" data-link-title="dotfile 跨進 runtime container" data-link-desc="想在對齊 prod 的 container 裡用自己順手的 shell / vim、又不想把工具裝進 image 破壞 parity 時回來讀 — 在 running container 裝開發工具、跟 runtime 分層">dotfile 跨進 runtime container&lt;/a>&lt;/td>
 &lt;td>把開發 ergonomics 帶進 Debian container 又不污染 parity&lt;/td>
 &lt;td>ergonomics 跟 runtime 怎麼分層&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="這個模組的知識點">這個模組的知識點&lt;/h2>
&lt;p>三篇實作文章都薄，只講目的與判讀；背後的設定原理拆成獨立術語卡：&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則&lt;/a>：對齊凍結舊環境而非最新&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning&lt;/a>：tag 要釘到 OS 世代才凍結得住&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl&lt;/a>：prod 是 Debian 就別用 alpine&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a>：綁 distro 的只有裝套件那一層&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a>：同一指令跨發行版後果量級差多少&lt;/li>
&lt;li>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性&lt;/a>：批次安裝為何全有或全無&lt;/li>
&lt;/ul>
&lt;h2 id="實作-artifact">實作 artifact&lt;/h2>
&lt;p>三層的實際檔案放在 dotfiles repo（不在教材裡）：工作站層是 repo 主體加 &lt;code>scripts/install-debian.sh&lt;/code>，runtime 層是 &lt;code>runtimes/php72-mysql57/&lt;/code>（Dockerfile + docker-compose），ergonomics 層是同目錄的 &lt;code>ergonomics/setup.sh&lt;/code>。教材講判讀，repo 放可跑的設定。&lt;/p></description><content:encoded><![CDATA[<p>前面的模組把個人工作環境用 dotfile 管起來，預設工作站跟你天天用的機器是同一套。實際接案 / 商用開發會撞上一個落差：工作站是 Arch 這種滾動更新、永遠最新版的環境，但要開發的 client 線上跑的是幾年前定版後就凍結的舊環境——PHP 7.2、MySQL 5.7、某個特定 Debian 世代。直接拿最新的 Arch 環境開發，寫出來的成品可能在線上根本跑不起來。</p>
<p>這個模組把 dotfile 的「環境可重現」思想延伸到兩個新對象：一是讓工作站 dotfile 跨到 client 常見的非 Arch 發行版，二是建一個跟線上逐項對齊的 runtime container。核心切分是三層，各自可重現、各自對齊不同目標。</p>
<h2 id="三層切分">三層切分</h2>
<table>
  <thead>
      <tr>
          <th>層</th>
          <th>是什麼</th>
          <th>對齊誰</th>
          <th>綁 distro 嗎</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工作站</td>
          <td>你的個人機（shell / wm / editor）</td>
          <td>對齊你自己的順手</td>
          <td>綁 pacman/brew/apt，靠抽象層跨 distro</td>
      </tr>
      <tr>
          <td>runtime</td>
          <td>跑 app 的 PHP / MySQL / web server</td>
          <td>逐項對齊 client 的 prod</td>
          <td>不碰主機 package manager，parity 靠 image tag</td>
      </tr>
      <tr>
          <td>container ergonomics</td>
          <td>你在 container 裡用的 shell / vim</td>
          <td>純開發舒適</td>
          <td>靠 detection 分支同時支援多 distro</td>
      </tr>
  </tbody>
</table>
<p>三層的哲學是同一套（環境 as code、可重現），差別只在對齊誰、綁哪個 package manager。把三者混進同一個 artifact 是常見錯誤：ergonomics 混進 runtime image，image 就不再等於 prod、parity 破功。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
          <th>回答什麼問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地</a></td>
          <td>同一份 dotfile 從 Arch 跨到 Debian/Ubuntu</td>
          <td>換 client 環境時哪些要改、哪些能共用</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a></td>
          <td>建跟線上逐項對齊的 PHP 7.2 + MySQL 5.7 runtime</td>
          <td>parity 要對齊到多細、什麼時候值得</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/10-prod-parity/container-ergonomics/" data-link-title="dotfile 跨進 runtime container" data-link-desc="想在對齊 prod 的 container 裡用自己順手的 shell / vim、又不想把工具裝進 image 破壞 parity 時回來讀 — 在 running container 裝開發工具、跟 runtime 分層">dotfile 跨進 runtime container</a></td>
          <td>把開發 ergonomics 帶進 Debian container 又不污染 parity</td>
          <td>ergonomics 跟 runtime 怎麼分層</td>
      </tr>
  </tbody>
</table>
<h2 id="這個模組的知識點">這個模組的知識點</h2>
<p>三篇實作文章都薄，只講目的與判讀；背後的設定原理拆成獨立術語卡：</p>
<ul>
<li><a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則</a>：對齊凍結舊環境而非最新</li>
<li><a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning</a>：tag 要釘到 OS 世代才凍結得住</li>
<li><a href="/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl</a>：prod 是 Debian 就別用 alpine</li>
<li><a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a>：綁 distro 的只有裝套件那一層</li>
<li><a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>：同一指令跨發行版後果量級差多少</li>
<li><a href="/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性</a>：批次安裝為何全有或全無</li>
</ul>
<h2 id="實作-artifact">實作 artifact</h2>
<p>三層的實際檔案放在 dotfiles repo（不在教材裡）：工作站層是 repo 主體加 <code>scripts/install-debian.sh</code>，runtime 層是 <code>runtimes/php72-mysql57/</code>（Dockerfile + docker-compose），ergonomics 層是同目錄的 <code>ergonomics/setup.sh</code>。教材講判讀，repo 放可跑的設定。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/linux/dotfile/09-team-environment/" data-link-title="模組九：從個人到團隊" data-link-desc="個人 dotfile 管理的思想要延伸到團隊開發環境標準化時回來讀 — devcontainer、nix、商業環境配置管理">模組九：從個人到團隊</a>：devcontainer 是同一個「環境 as code」思想的團隊化形態</li>
<li>→ <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：跨 distro 安裝腳本是 bootstrap 的延伸</li>
<li>→ <a href="/blog/backend/knowledge-cards/container/" data-link-title="Container" data-link-desc="說明容器如何包裝服務、隔離依賴與影響部署方式">Container 術語卡</a>：container 作為服務交付單位的服務設計視角</li>
</ul>
]]></content:encoded></item><item><title>Image Tag Pinning</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/</guid><description>&lt;p>Image tag pinning 是「用夠精確的 tag 把 base image 凍結成一個固定形狀」的紀律。它決定的是可重現性：同一個 Dockerfile 今天 build 跟三個月後 build，跑出來的 runtime 是不是同一個——這也是讓本機跟線上逐項相同（即 parity）的前提。「本機能 build、CI 卻掛掉」而 code 沒動時，第一個要查的就是 tag 夠不夠精確。這條紀律是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則&lt;/a> 底下的一個具體維度。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>底層 OS 世代為什麼影響行為，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl&lt;/a>。實作見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container&lt;/a>。&lt;/p>
&lt;h2 id="浮動-tag-與凍結-tag">浮動 tag 與凍結 tag&lt;/h2>
&lt;p>同一個 image 的 tag 有不同精確度，愈短愈會漂：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">php:7.2-fpm 浮動 — 官方隨時 rebuild，底層 OS 從 stretch 換到 buster 你不會知道
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">php:7.2-fpm-buster 釘住 PHP 主版 + OS 世代 — 這是 parity 的最低精確度
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">php:7.2.34-fpm-buster 連 patch 版也釘死 — 完全凍結&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>php:7.2-fpm&lt;/code> 這種浮動 tag 的問題是它「同名不同物」：官方每次安全更新都會用同一個 tag 重推，底層 Debian 世代、內建套件版本、glibc 版本都可能換掉。你本機三個月前 pull 的 &lt;code>7.2-fpm&lt;/code> 跟 CI 現在 pull 的 &lt;code>7.2-fpm&lt;/code>，可能是兩個不同 OS 世代的 image。&lt;/p>
&lt;h2 id="為什麼-os-世代是最低精確度">為什麼 OS 世代是最低精確度&lt;/h2>
&lt;p>Parity 要對齊的不只是 PHP 版本，是整個 runtime 的行為，而行為很大一部分由底層 OS 世代決定：glibc 版本、內建 CA 憑證、時區資料庫、系統套件版本。釘到 &lt;code>-buster&lt;/code> 才把這層凍結；只釘 &lt;code>7.2&lt;/code> 等於把 OS 世代交給官方的 rebuild 節奏決定。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>「本機能跑、CI 掛掉」或「上週還好、今天重 build 就壞」而 code 沒動時，第一個要查的就是有沒有用浮動 tag。凍結 tag 把「環境變了」這個變因從除錯空間裡消掉，讓你能專注在 code 差異。&lt;/p>
&lt;h2 id="邊界">邊界&lt;/h2>
&lt;p>釘死 tag 的代價是安全更新不會自動進來——凍結的 image 也凍結了已知漏洞。所以 pinning 搭配的是「明確的升級動作」：要更新時改 tag、重測、記錄（怎麼建升級版、又留住舊版並跑對照，見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/image-versioning-upgrade/" data-link-title="image 版本管理與升級：不動舊的、建新的" data-link-desc="有一個跑著的 stack 要升版（PHP / MySQL 升級）、不想弄壞現在能跑的、又不確定多個版本的 image 跟 Dockerfile 怎麼管時回來讀 — image 不可變下的升級與版本管理">image 版本管理與升級&lt;/a>），而不是靠浮動 tag 偷偷幫你更新（那正是不可重現的來源）。取捨依 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則&lt;/a>——對齊凍結環境本來就要付這個稅。&lt;/p></description><content:encoded><![CDATA[<p>Image tag pinning 是「用夠精確的 tag 把 base image 凍結成一個固定形狀」的紀律。它決定的是可重現性：同一個 Dockerfile 今天 build 跟三個月後 build，跑出來的 runtime 是不是同一個——這也是讓本機跟線上逐項相同（即 parity）的前提。「本機能 build、CI 卻掛掉」而 code 沒動時，第一個要查的就是 tag 夠不夠精確。這條紀律是 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">Prod Parity 原則</a> 底下的一個具體維度。</p>
<h2 id="概念位置">概念位置</h2>
<p>底層 OS 世代為什麼影響行為，見 <a href="/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl</a>。實作見 <a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a>。</p>
<h2 id="浮動-tag-與凍結-tag">浮動 tag 與凍結 tag</h2>
<p>同一個 image 的 tag 有不同精確度，愈短愈會漂：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">php:7.2-fpm          浮動 — 官方隨時 rebuild，底層 OS 從 stretch 換到 buster 你不會知道
</span></span><span class="line"><span class="ln">2</span><span class="cl">php:7.2-fpm-buster   釘住 PHP 主版 + OS 世代 — 這是 parity 的最低精確度
</span></span><span class="line"><span class="ln">3</span><span class="cl">php:7.2.34-fpm-buster 連 patch 版也釘死 — 完全凍結</span></span></code></pre></div><p><code>php:7.2-fpm</code> 這種浮動 tag 的問題是它「同名不同物」：官方每次安全更新都會用同一個 tag 重推，底層 Debian 世代、內建套件版本、glibc 版本都可能換掉。你本機三個月前 pull 的 <code>7.2-fpm</code> 跟 CI 現在 pull 的 <code>7.2-fpm</code>，可能是兩個不同 OS 世代的 image。</p>
<h2 id="為什麼-os-世代是最低精確度">為什麼 OS 世代是最低精確度</h2>
<p>Parity 要對齊的不只是 PHP 版本，是整個 runtime 的行為，而行為很大一部分由底層 OS 世代決定：glibc 版本、內建 CA 憑證、時區資料庫、系統套件版本。釘到 <code>-buster</code> 才把這層凍結；只釘 <code>7.2</code> 等於把 OS 世代交給官方的 rebuild 節奏決定。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>「本機能跑、CI 掛掉」或「上週還好、今天重 build 就壞」而 code 沒動時，第一個要查的就是有沒有用浮動 tag。凍結 tag 把「環境變了」這個變因從除錯空間裡消掉，讓你能專注在 code 差異。</p>
<h2 id="邊界">邊界</h2>
<p>釘死 tag 的代價是安全更新不會自動進來——凍結的 image 也凍結了已知漏洞。所以 pinning 搭配的是「明確的升級動作」：要更新時改 tag、重測、記錄（怎麼建升級版、又留住舊版並跑對照，見 <a href="/blog/backend/05-deployment-platform/vendors/docker/image-versioning-upgrade/" data-link-title="image 版本管理與升級：不動舊的、建新的" data-link-desc="有一個跑著的 stack 要升版（PHP / MySQL 升級）、不想弄壞現在能跑的、又不確定多個版本的 image 跟 Dockerfile 怎麼管時回來讀 — image 不可變下的升級與版本管理">image 版本管理與升級</a>），而不是靠浮動 tag 偷偷幫你更新（那正是不可重現的來源）。取捨依 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則</a>——對齊凍結環境本來就要付這個稅。</p>
]]></content:encoded></item><item><title>glibc 與 musl</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/</guid><description>&lt;p>glibc 與 musl 是兩套不同的 C 標準函式庫實作。C 標準庫是幾乎每個 Linux 程式都會鏈結的最底層，負責記憶體配置、字串處理、DNS 解析、locale、執行緒這些基本能力。選哪一套會滲透到上層程式的行為，而不只是體積差異，是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則&lt;/a> 要盯的底層項目之一。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這條規則跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning&lt;/a> 同源：都是把 runtime 底層凍結到跟 prod 相同。判準見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則&lt;/a>。選 base image 的實作見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/dockerfile-design/" data-link-title="Dockerfile 設計：指令、layer 與 multi-stage" data-link-desc="image 大得離譜、code 一改就整包重 build、或 multi-stage 複製 binary 後 container 起不來說找不到 library 時回來讀 — Dockerfile 指令怎麼變成 layer、build 與 runtime 怎麼分離">Dockerfile 設計&lt;/a>。&lt;/p>
&lt;h2 id="誰用哪一套">誰用哪一套&lt;/h2>
&lt;p>主流商用發行版跟輕量容器發行版分屬兩邊：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">glibc Debian, Ubuntu, RHEL/Rocky/Alma, CentOS ← 商用 prod 幾乎都在這邊
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">musl Alpine Linux ← 容器界為了縮小體積常選這邊&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Alpine 的賣點是 image 極小（base 約 5MB vs Debian 約 70MB），所以很多人反射性地選 &lt;code>php:7.2-alpine&lt;/code> 來省空間。&lt;/p>
&lt;h2 id="為什麼-prod-是-glibc-就別用-musl">為什麼 prod 是 glibc 就別用 musl&lt;/h2>
&lt;p>當 prod 跑在 Debian/Ubuntu（glibc）而你本機 image 用 Alpine（musl），你就在一個跟線上不同 libc 的環境開發，差異會在幾個地方咬人：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DNS 解析行為不同&lt;/strong>：musl 不讀 &lt;code>/etc/nsswitch.conf&lt;/code>、對 &lt;code>search&lt;/code> domain 與並發查詢的處理跟 glibc 有別，容器內連內部服務時可能出現「本機解得到、線上解不到」或反過來。&lt;/li>
&lt;li>&lt;strong>原生擴充要重編&lt;/strong>：PHP/Python 的 C 擴充是對著某套 libc 編的；某些預編譯 wheel/extension 只提供 glibc 版，在 musl 上要自己編、甚至編不起來。&lt;/li>
&lt;li>&lt;strong>locale 與時間格式&lt;/strong>：musl 的 locale 支援比 glibc 精簡，依賴特定 locale 的字串排序、日期格式可能不一致。&lt;/li>
&lt;/ul>
&lt;p>這些差異單獨看都小，但它們的共通點是「在你最不預期的地方，讓本機跟線上行為分岔」——正好抵消了本機模擬 prod 的目的。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>「本機容器跑得好好的、上線卻在 DNS 或某個原生擴充炸掉」是 libc 不一致的典型症狀。選 base image 時的判準很直接：&lt;strong>prod 是什麼 libc，本機就用什麼&lt;/strong>。省那 60MB 換來的除錯成本遠高於節省。&lt;/p>
&lt;h2 id="邊界">邊界&lt;/h2>
&lt;p>musl 本身沒有問題——如果 prod 也跑 Alpine，那本機就該用 Alpine，parity 依然成立。重點不是「musl 比較差」，是「本機跟 prod 的 libc 要一致」。一個例外：如果 app 是純靜態連結的 binary（&lt;code>CGO_ENABLED=0&lt;/code> 的 Go、Rust 靜態編譯），它不帶 libc 相依，上面 DNS / 原生擴充 / locale 三個咬人點全部消失——這時 base image 用 alpine 完全可行，即使 prod 的 base 不同（binary 自帶一切、不吃 image 的 libc）。&lt;/p></description><content:encoded><![CDATA[<p>glibc 與 musl 是兩套不同的 C 標準函式庫實作。C 標準庫是幾乎每個 Linux 程式都會鏈結的最底層，負責記憶體配置、字串處理、DNS 解析、locale、執行緒這些基本能力。選哪一套會滲透到上層程式的行為，而不只是體積差異，是 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則</a> 要盯的底層項目之一。</p>
<h2 id="概念位置">概念位置</h2>
<p>這條規則跟 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning</a> 同源：都是把 runtime 底層凍結到跟 prod 相同。判準見 <a href="/blog/linux/dotfile/knowledge-cards/prod-parity-principle/" data-link-title="Prod Parity Principle（生產環境對等原則）" data-link-desc="要建一個對齊 client 線上環境的本機 runtime、不知道該對齊到多細時回來讀 — parity 對齊的是凍結舊環境而非最新版">prod-parity 原則</a>。選 base image 的實作見 <a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a> 與 <a href="/blog/backend/05-deployment-platform/vendors/docker/dockerfile-design/" data-link-title="Dockerfile 設計：指令、layer 與 multi-stage" data-link-desc="image 大得離譜、code 一改就整包重 build、或 multi-stage 複製 binary 後 container 起不來說找不到 library 時回來讀 — Dockerfile 指令怎麼變成 layer、build 與 runtime 怎麼分離">Dockerfile 設計</a>。</p>
<h2 id="誰用哪一套">誰用哪一套</h2>
<p>主流商用發行版跟輕量容器發行版分屬兩邊：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">glibc  Debian, Ubuntu, RHEL/Rocky/Alma, CentOS  ← 商用 prod 幾乎都在這邊
</span></span><span class="line"><span class="ln">2</span><span class="cl">musl   Alpine Linux                              ← 容器界為了縮小體積常選這邊</span></span></code></pre></div><p>Alpine 的賣點是 image 極小（base 約 5MB vs Debian 約 70MB），所以很多人反射性地選 <code>php:7.2-alpine</code> 來省空間。</p>
<h2 id="為什麼-prod-是-glibc-就別用-musl">為什麼 prod 是 glibc 就別用 musl</h2>
<p>當 prod 跑在 Debian/Ubuntu（glibc）而你本機 image 用 Alpine（musl），你就在一個跟線上不同 libc 的環境開發，差異會在幾個地方咬人：</p>
<ul>
<li><strong>DNS 解析行為不同</strong>：musl 不讀 <code>/etc/nsswitch.conf</code>、對 <code>search</code> domain 與並發查詢的處理跟 glibc 有別，容器內連內部服務時可能出現「本機解得到、線上解不到」或反過來。</li>
<li><strong>原生擴充要重編</strong>：PHP/Python 的 C 擴充是對著某套 libc 編的；某些預編譯 wheel/extension 只提供 glibc 版，在 musl 上要自己編、甚至編不起來。</li>
<li><strong>locale 與時間格式</strong>：musl 的 locale 支援比 glibc 精簡，依賴特定 locale 的字串排序、日期格式可能不一致。</li>
</ul>
<p>這些差異單獨看都小，但它們的共通點是「在你最不預期的地方，讓本機跟線上行為分岔」——正好抵消了本機模擬 prod 的目的。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>「本機容器跑得好好的、上線卻在 DNS 或某個原生擴充炸掉」是 libc 不一致的典型症狀。選 base image 時的判準很直接：<strong>prod 是什麼 libc，本機就用什麼</strong>。省那 60MB 換來的除錯成本遠高於節省。</p>
<h2 id="邊界">邊界</h2>
<p>musl 本身沒有問題——如果 prod 也跑 Alpine，那本機就該用 Alpine，parity 依然成立。重點不是「musl 比較差」，是「本機跟 prod 的 libc 要一致」。一個例外：如果 app 是純靜態連結的 binary（<code>CGO_ENABLED=0</code> 的 Go、Rust 靜態編譯），它不帶 libc 相依，上面 DNS / 原生擴充 / locale 三個咬人點全部消失——這時 base image 用 alpine 完全可行，即使 prod 的 base 不同（binary 自帶一切、不吃 image 的 libc）。</p>
]]></content:encoded></item><item><title>Prod Parity Principle（生產環境對等原則）</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/prod-parity-principle/</guid><description>&lt;p>Prod parity 是「讓本機 runtime 跟線上環境逐項相同」的目標。它對齊的方向常跟開發者的直覺相反：要把環境&lt;strong>凍結成線上那個特定的、通常偏舊的形狀&lt;/strong>，而非升到最新、最乾淨。凍結的目標是行為對齊、不是版本追新，這跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning&lt;/a> 用 tag 精確度落地凍結是同一件事的兩層。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這條原則是個人 dotfile「環境可重現性」思想往 runtime 層的延伸：&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型&lt;/a> 教的是工作站可重現，parity 教的是 runtime 可重現。要凍結到多細見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning&lt;/a>。實作見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container&lt;/a>。&lt;/p>
&lt;h2 id="對齊的是凍結環境不是最新">對齊的是凍結環境，不是最新&lt;/h2>
&lt;p>接案 / 商用場景的 prod 通常是幾年前定版後就凍結的環境：PHP 7.2、MySQL 5.7、某個特定的 Debian 世代。這跟 rolling release 發行版（Arch）「永遠最新」的方向剛好相反。用你的 Arch 工作站直接當開發環境，等於在一個比線上新好幾代的環境寫 code，寫出來的成品可能用了線上沒有的語法、擴充或預設行為。&lt;/p>
&lt;p>Parity 的意義就是把這個差距關掉：本機刻意退回線上那個舊形狀，讓「本機能跑」直接等於「線上能跑」。&lt;/p>
&lt;h2 id="要對齊哪些維度">要對齊哪些維度&lt;/h2>
&lt;p>Parity 不是只對齊語言版本號，是對齊會影響 runtime 行為的每一層：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>語言版本&lt;/strong>：PHP/Python/Node 的主版與 patch 版&lt;/li>
&lt;li>&lt;strong>底層 OS 與 libc&lt;/strong>：Debian 世代、glibc vs musl（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl&lt;/a>）&lt;/li>
&lt;li>&lt;strong>擴充/套件清單&lt;/strong>：&lt;code>php -m&lt;/code> 逐項相等，不是涵蓋&lt;/li>
&lt;li>&lt;strong>服務版本&lt;/strong>：MySQL/Redis/nginx 的版本與關鍵設定（如 &lt;code>sql_mode&lt;/code>、時區）&lt;/li>
&lt;li>&lt;strong>image 精確度&lt;/strong>：用凍結 tag 把以上全部釘死（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning&lt;/a>）&lt;/li>
&lt;/ul>
&lt;p>取得這些維度的權威來源是線上環境本身：&lt;code>php -m&lt;/code>、&lt;code>SELECT @@sql_mode&lt;/code>、&lt;code>phpinfo()&lt;/code> 抄回來，而不是憑印象填。&lt;/p>
&lt;h2 id="判讀訊號什麼時候值得逐項對齊">判讀訊號：什麼時候值得逐項對齊&lt;/h2>
&lt;p>Parity 是有成本的紀律，不是每個專案都要做到逐項。值得的訊號：app 有原生擴充、依賴 DB 嚴格模式行為、有時區 / locale / charset collation 邏輯（MySQL 的 collation，以及 macOS 開發 ↔ Linux 部署的檔名大小寫敏感度差異，都常在這裡咬人）、或 client 明確凍結某版環境。可放寬的訊號：app 無原生擴充、不碰 DB 嚴格模式與時區 / collation（純直譯、行為跟 OS 世代解耦），或你完全掌控 prod（自己的 VPS，想升就升）——這時本機可以直接用較新的 stable，行為分岔風險低。&lt;/p>
&lt;p>把這個判斷轉成對非技術決策者的話（接案時常要向業主 / PM 說明值不值得投入）：不對齊的代價是行為分岔只在線上才炸——本機測不出、上線才發現，變成緊急修加上客戶信任損耗；對齊的投入則是一次性建一個 parity runtime（通常幾小時到一天）。比較的是「一次性投入」對上「反覆的線上驚嚇與救火」，而不是純技術潔癖。&lt;/p>
&lt;h2 id="邊界">邊界&lt;/h2>
&lt;p>Parity 對齊的是「行為」，不是「連漏洞一起複製」。凍結舊環境意味著也繼承了它的已知漏洞與 EOL 稅（如 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning&lt;/a> 提到的 base image mirror 退役）。Parity 讓你&lt;strong>在本機重現線上問題&lt;/strong>，不代表線上該永遠停在舊版——它跟「該不該升級 prod」是兩個獨立決策。&lt;/p></description><content:encoded><![CDATA[<p>Prod parity 是「讓本機 runtime 跟線上環境逐項相同」的目標。它對齊的方向常跟開發者的直覺相反：要把環境<strong>凍結成線上那個特定的、通常偏舊的形狀</strong>，而非升到最新、最乾淨。凍結的目標是行為對齊、不是版本追新，這跟 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning</a> 用 tag 精確度落地凍結是同一件事的兩層。</p>
<h2 id="概念位置">概念位置</h2>
<p>這條原則是個人 dotfile「環境可重現性」思想往 runtime 層的延伸：<a href="/blog/linux/dotfile/00-dotfile-mindset/" data-link-title="模組零：Dotfile 心智模型" data-link-desc="換機器、開 VM、重灌系統時需要快速還原開發環境，或想釐清哪些配置該版控、哪些該排除時回來讀">模組零：Dotfile 心智模型</a> 教的是工作站可重現，parity 教的是 runtime 可重現。要凍結到多細見 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">Image Tag Pinning</a>。實作見 <a href="/blog/linux/dotfile/10-prod-parity/prod-parity-runtime/" data-link-title="對齊 prod 的 runtime container" data-link-desc="要開發一個線上跑 PHP 7.2 / MySQL 5.7 舊環境的專案、或要在本機重現線上事故時回來讀 — 對齊哪些維度、怎麼從線上抄設定、什麼時候值得">對齊 prod 的 runtime container</a>。</p>
<h2 id="對齊的是凍結環境不是最新">對齊的是凍結環境，不是最新</h2>
<p>接案 / 商用場景的 prod 通常是幾年前定版後就凍結的環境：PHP 7.2、MySQL 5.7、某個特定的 Debian 世代。這跟 rolling release 發行版（Arch）「永遠最新」的方向剛好相反。用你的 Arch 工作站直接當開發環境，等於在一個比線上新好幾代的環境寫 code，寫出來的成品可能用了線上沒有的語法、擴充或預設行為。</p>
<p>Parity 的意義就是把這個差距關掉：本機刻意退回線上那個舊形狀，讓「本機能跑」直接等於「線上能跑」。</p>
<h2 id="要對齊哪些維度">要對齊哪些維度</h2>
<p>Parity 不是只對齊語言版本號，是對齊會影響 runtime 行為的每一層：</p>
<ul>
<li><strong>語言版本</strong>：PHP/Python/Node 的主版與 patch 版</li>
<li><strong>底層 OS 與 libc</strong>：Debian 世代、glibc vs musl（見 <a href="/blog/linux/dotfile/knowledge-cards/glibc-vs-musl/" data-link-title="glibc 與 musl" data-link-desc="考慮用 alpine image 縮小體積、或 PHP/Python 擴充在容器裡行為跟線上不同時回來讀 — 兩種 libc 的差異與怎麼選">glibc 與 musl</a>）</li>
<li><strong>擴充/套件清單</strong>：<code>php -m</code> 逐項相等，不是涵蓋</li>
<li><strong>服務版本</strong>：MySQL/Redis/nginx 的版本與關鍵設定（如 <code>sql_mode</code>、時區）</li>
<li><strong>image 精確度</strong>：用凍結 tag 把以上全部釘死（見 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning</a>）</li>
</ul>
<p>取得這些維度的權威來源是線上環境本身：<code>php -m</code>、<code>SELECT @@sql_mode</code>、<code>phpinfo()</code> 抄回來，而不是憑印象填。</p>
<h2 id="判讀訊號什麼時候值得逐項對齊">判讀訊號：什麼時候值得逐項對齊</h2>
<p>Parity 是有成本的紀律，不是每個專案都要做到逐項。值得的訊號：app 有原生擴充、依賴 DB 嚴格模式行為、有時區 / locale / charset collation 邏輯（MySQL 的 collation，以及 macOS 開發 ↔ Linux 部署的檔名大小寫敏感度差異，都常在這裡咬人）、或 client 明確凍結某版環境。可放寬的訊號：app 無原生擴充、不碰 DB 嚴格模式與時區 / collation（純直譯、行為跟 OS 世代解耦），或你完全掌控 prod（自己的 VPS，想升就升）——這時本機可以直接用較新的 stable，行為分岔風險低。</p>
<p>把這個判斷轉成對非技術決策者的話（接案時常要向業主 / PM 說明值不值得投入）：不對齊的代價是行為分岔只在線上才炸——本機測不出、上線才發現，變成緊急修加上客戶信任損耗；對齊的投入則是一次性建一個 parity runtime（通常幾小時到一天）。比較的是「一次性投入」對上「反覆的線上驚嚇與救火」，而不是純技術潔癖。</p>
<h2 id="邊界">邊界</h2>
<p>Parity 對齊的是「行為」，不是「連漏洞一起複製」。凍結舊環境意味著也繼承了它的已知漏洞與 EOL 稅（如 <a href="/blog/linux/dotfile/knowledge-cards/image-tag-pinning/" data-link-title="Image Tag Pinning" data-link-desc="本機跟線上跑同一份 code 卻行為不一致、或 image 隔幾週重 build 就變樣時回來讀 — 為什麼 tag 要釘到 OS 世代">image tag pinning</a> 提到的 base image mirror 退役）。Parity 讓你<strong>在本機重現線上問題</strong>，不代表線上該永遠停在舊版——它跟「該不該升級 prod」是兩個獨立決策。</p>
]]></content:encoded></item><item><title>Package Manager 抽象層</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/</guid><description>&lt;p>Package manager 抽象層是 dotfile 跨發行版可攜的關鍵切分：一份 dotfile repo 裡，真正綁特定發行版的只有「用哪個 package manager 裝套件」這一層，其餘（config 檔內容、symlink 部署、shell 框架安裝）都是跨 distro 共通的。把這一層隔離出來，同一份 repo 就能同時服務 Arch 工作站、Debian 容器與 macOS。這層抽象是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a> 差異的因應之道。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這個抽象讓 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow&lt;/a> 管的 config 層完全可攜。實作見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/container-ergonomics/" data-link-title="dotfile 跨進 runtime container" data-link-desc="想在對齊 prod 的 container 裡用自己順手的 shell / vim、又不想把工具裝進 image 破壞 parity 時回來讀 — 在 running container 裝開發工具、跟 runtime 分層">dotfile 跨進 runtime container&lt;/a>。&lt;/p>
&lt;h2 id="綁-distro-的只有裝套件那一步">綁 distro 的只有裝套件那一步&lt;/h2>
&lt;p>各發行版的差異集中在 package manager 的指令與套件名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Arch pacman -S --needed --noconfirm &amp;lt;pkg&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Debian/Ubuntu apt-get install -y --no-install-recommends &amp;lt;pkg&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">macOS brew install &amp;lt;pkg&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">Alpine apk add --no-cache &amp;lt;pkg&amp;gt;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>除了這一步，其餘幾乎都可攜：&lt;code>.zshrc&lt;/code> 的內容、&lt;code>stow&lt;/code> 建 symlink 的方式、oh-my-zsh 的 &lt;code>git clone&lt;/code>、config 檔本身——這些不管在哪個 distro 都一樣。所以跨 distro 的正解不是維護多份 dotfile，是維護一份 config + 一個依 package manager 分支的安裝層。&lt;/p>
&lt;h2 id="detection-分支的兩種形態">Detection 分支的兩種形態&lt;/h2>
&lt;p>抽象這一層有兩種常見寫法，差別在分支放哪：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>入口 detection&lt;/strong>：安裝腳本開頭偵測 package manager，dispatch 到對應的 &lt;code>install-&amp;lt;platform&amp;gt;.sh&lt;/code>，套件清單各平台一份（&lt;code>packages/arch-*.txt&lt;/code>、&lt;code>packages/debian-*.txt&lt;/code>）。適合工作站，因為各平台的套件集差異大。&lt;/li>
&lt;li>&lt;strong>行內 detection&lt;/strong>：一支腳本內用 &lt;code>command -v apt-get / pacman / apk&lt;/code> 分支，共用同一份套件名。適合容器內的輕量 ergonomics 安裝——只裝少數幾個工具、不值得拆多檔。&lt;/li>
&lt;/ul>
&lt;h2 id="判讀訊號什麼進分支什麼是共通層">判讀訊號：什麼進分支、什麼是共通層&lt;/h2>
&lt;p>判準是「這一項換 distro 會不會變」：會變的（套件名、安裝指令）進 detection 分支；不會變的（config 內容、symlink 邏輯、框架 clone）留共通層。一個常見的誤置是把套件名寫死在共通層，結果換 distro 就壞——正確做法是共通層只呼叫抽象後的安裝函式，具體套件名下沉到各平台清單。&lt;/p>
&lt;h2 id="套件名分歧的常見形態">套件名分歧的常見形態&lt;/h2>
&lt;p>「換 package manager」不只是換指令前綴，套件名本身在不同 distro 有好幾種分歧，各要不同處理：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>同工具不同套件名&lt;/strong>：Arch 的 &lt;code>fd&lt;/code> 在 Debian 叫 &lt;code>fd-find&lt;/code>、&lt;code>github-cli&lt;/code> 在 Debian 叫 &lt;code>gh&lt;/code>。要靠各平台清單逐項對照，不能假設同名。&lt;/li>
&lt;li>&lt;strong>binary 名被改&lt;/strong>：Debian 為了讓整個 archive 的 &lt;code>/usr/bin&lt;/code> 名字全域唯一，會在上游 binary 名跟現有套件撞名時改名——&lt;code>fd-find&lt;/code> 裝出來的 binary 是 &lt;code>fdfind&lt;/code>、&lt;code>bat&lt;/code> 是 &lt;code>batcat&lt;/code>。所以「套件裝好了」不等於「指令叫得動」，&lt;code>.zshrc&lt;/code> 的 alias 要按平台補（Debian 上 &lt;code>alias fd=fdfind&lt;/code>）。&lt;/li>
&lt;li>&lt;strong>名字撞到別的工具&lt;/strong>：&lt;code>apt install delta&lt;/code> 裝到的是一個 heuristic minimizer、不是 &lt;code>.gitconfig&lt;/code> 指的那個 diff pager（那個叫 git-delta、且 bookworm 沒打包）。同名不同物，照抄會裝錯東西。&lt;/li>
&lt;li>&lt;strong>套件被拆分或合併&lt;/strong>：同一個上游工具在 Debian 常被拆成 &lt;code>foo&lt;/code> + &lt;code>foo-dev&lt;/code> + &lt;code>foo-common&lt;/code>（照抄單一名字會漏裝 &lt;code>-dev&lt;/code>、編不起來），或反過來多個工具併進一個 meta-package（如 &lt;code>build-essential&lt;/code>）。安裝顆粒度跟 Arch 不對齊，要看該 distro 怎麼切。&lt;/li>
&lt;/ul>
&lt;p>除了名字，同名套件的&lt;strong>版本&lt;/strong>也常分歧：Debian stable 凍結的版本往往比 Arch 舊一個世代，名字一樣、行為卻不同——這不是名字問題，但一樣影響對齊，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Package manager 抽象層是 dotfile 跨發行版可攜的關鍵切分：一份 dotfile repo 裡，真正綁特定發行版的只有「用哪個 package manager 裝套件」這一層，其餘（config 檔內容、symlink 部署、shell 框架安裝）都是跨 distro 共通的。把這一層隔離出來，同一份 repo 就能同時服務 Arch 工作站、Debian 容器與 macOS。這層抽象是 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a> 差異的因應之道。</p>
<h2 id="概念位置">概念位置</h2>
<p>這個抽象讓 <a href="/blog/linux/dotfile/knowledge-cards/gnu-stow/" data-link-title="GNU Stow" data-link-desc="dotfile 管理文章裡提到 stow、symlink、package 看不懂時回來讀 — stow 的核心概念和常用指令">GNU Stow</a> 管的 config 層完全可攜。實作見 <a href="/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地</a> 與 <a href="/blog/linux/dotfile/10-prod-parity/container-ergonomics/" data-link-title="dotfile 跨進 runtime container" data-link-desc="想在對齊 prod 的 container 裡用自己順手的 shell / vim、又不想把工具裝進 image 破壞 parity 時回來讀 — 在 running container 裝開發工具、跟 runtime 分層">dotfile 跨進 runtime container</a>。</p>
<h2 id="綁-distro-的只有裝套件那一步">綁 distro 的只有裝套件那一步</h2>
<p>各發行版的差異集中在 package manager 的指令與套件名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Arch          pacman -S --needed --noconfirm &lt;pkg&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">Debian/Ubuntu apt-get install -y --no-install-recommends &lt;pkg&gt;
</span></span><span class="line"><span class="ln">3</span><span class="cl">macOS         brew install &lt;pkg&gt;
</span></span><span class="line"><span class="ln">4</span><span class="cl">Alpine        apk add --no-cache &lt;pkg&gt;</span></span></code></pre></div><p>除了這一步，其餘幾乎都可攜：<code>.zshrc</code> 的內容、<code>stow</code> 建 symlink 的方式、oh-my-zsh 的 <code>git clone</code>、config 檔本身——這些不管在哪個 distro 都一樣。所以跨 distro 的正解不是維護多份 dotfile，是維護一份 config + 一個依 package manager 分支的安裝層。</p>
<h2 id="detection-分支的兩種形態">Detection 分支的兩種形態</h2>
<p>抽象這一層有兩種常見寫法，差別在分支放哪：</p>
<ul>
<li><strong>入口 detection</strong>：安裝腳本開頭偵測 package manager，dispatch 到對應的 <code>install-&lt;platform&gt;.sh</code>，套件清單各平台一份（<code>packages/arch-*.txt</code>、<code>packages/debian-*.txt</code>）。適合工作站，因為各平台的套件集差異大。</li>
<li><strong>行內 detection</strong>：一支腳本內用 <code>command -v apt-get / pacman / apk</code> 分支，共用同一份套件名。適合容器內的輕量 ergonomics 安裝——只裝少數幾個工具、不值得拆多檔。</li>
</ul>
<h2 id="判讀訊號什麼進分支什麼是共通層">判讀訊號：什麼進分支、什麼是共通層</h2>
<p>判準是「這一項換 distro 會不會變」：會變的（套件名、安裝指令）進 detection 分支；不會變的（config 內容、symlink 邏輯、框架 clone）留共通層。一個常見的誤置是把套件名寫死在共通層，結果換 distro 就壞——正確做法是共通層只呼叫抽象後的安裝函式，具體套件名下沉到各平台清單。</p>
<h2 id="套件名分歧的常見形態">套件名分歧的常見形態</h2>
<p>「換 package manager」不只是換指令前綴，套件名本身在不同 distro 有好幾種分歧，各要不同處理：</p>
<ul>
<li><strong>同工具不同套件名</strong>：Arch 的 <code>fd</code> 在 Debian 叫 <code>fd-find</code>、<code>github-cli</code> 在 Debian 叫 <code>gh</code>。要靠各平台清單逐項對照，不能假設同名。</li>
<li><strong>binary 名被改</strong>：Debian 為了讓整個 archive 的 <code>/usr/bin</code> 名字全域唯一，會在上游 binary 名跟現有套件撞名時改名——<code>fd-find</code> 裝出來的 binary 是 <code>fdfind</code>、<code>bat</code> 是 <code>batcat</code>。所以「套件裝好了」不等於「指令叫得動」，<code>.zshrc</code> 的 alias 要按平台補（Debian 上 <code>alias fd=fdfind</code>）。</li>
<li><strong>名字撞到別的工具</strong>：<code>apt install delta</code> 裝到的是一個 heuristic minimizer、不是 <code>.gitconfig</code> 指的那個 diff pager（那個叫 git-delta、且 bookworm 沒打包）。同名不同物，照抄會裝錯東西。</li>
<li><strong>套件被拆分或合併</strong>：同一個上游工具在 Debian 常被拆成 <code>foo</code> + <code>foo-dev</code> + <code>foo-common</code>（照抄單一名字會漏裝 <code>-dev</code>、編不起來），或反過來多個工具併進一個 meta-package（如 <code>build-essential</code>）。安裝顆粒度跟 Arch 不對齊，要看該 distro 怎麼切。</li>
</ul>
<p>除了名字，同名套件的<strong>版本</strong>也常分歧：Debian stable 凍結的版本往往比 Arch 舊一個世代，名字一樣、行為卻不同——這不是名字問題，但一樣影響對齊，見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>。</p>
<h2 id="邊界">邊界</h2>
<p>當某工具在目標 distro 根本沒打包時，抽象層也擋不住——退回手動安裝或換等價工具。哪些工具在保守發行版容易缺席，見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>；為什麼清單裡一個爛名字會拖垮整批安裝，見 <a href="/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性</a>。</p>
]]></content:encoded></item><item><title>發行版打包粒度</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/</guid><description>&lt;p>發行版打包粒度是「一個發行版把軟體切成多細的系統套件」的策略。它決定同一個安裝指令的後果量級：有的發行版把每個 library 都包成獨立系統套件，有的把 library 留給語言自己的套件管理器管。同一個工具在前者可能拉進幾百個系統套件、在後者只拉幾個。這條粒度差異坐落在 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a> 底下、是各發行版打包策略的具體展開。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這條跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a> 互補：抽象層講「同一件事換 backend 怎麼寫」，這張講「同一個指令換 backend 後果量級差多少」。一個爛名字為何拖垮整批安裝，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性&lt;/a>。實作上怎麼判斷該不該進 apt 清單，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地&lt;/a>。&lt;/p>
&lt;h2 id="兩種粒度">兩種粒度&lt;/h2>
&lt;p>以 nodejs + npm 為例，同樣一個安裝動作在兩種發行版的展開差一個量級（Debian bookworm 實測）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Debian apt install nodejs npm → 連帶 402 個新套件（其中 336 個 node-*，eslint、node-glob…每個 JS lib 一個 .deb）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Arch pacman -S npm → 只拉少數幾個；JS library 交給 npm 在 node_modules 管&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Debian 的哲學是「所有東西都是系統套件」，所以它把 nodejs / npm 工具鏈本身的相依也拆成上百個 &lt;code>node-*&lt;/code> deb，一次 &lt;code>apt install npm&lt;/code> 就全灌進系統。要分清楚：這些 &lt;code>node-*&lt;/code> 是 Debian 為了打包 node 工具鏈而拉進的&lt;strong>系統層相依&lt;/strong>，跟你專案 &lt;code>npm install&lt;/code> 進 &lt;code>node_modules&lt;/code> 的依賴無關——後者不論哪個 distro 都走 npm、從不碰 apt。所以「402 套件」講的是系統工具鏈膨脹，不是應用依賴。Arch 不把這層 library 納入系統套件，同一個 npm 只在系統層多幾個檔案。&lt;/p>
&lt;h2 id="為什麼有的工具乾脆沒打包">為什麼有的工具乾脆沒打包&lt;/h2>
&lt;p>粒度策略也決定「哪些工具進得了官方 repo」。移動快速的新工具（多為 Rust 寫的 CLI）在保守、定版凍結的發行版常常還沒被打包：broot、zellij、git-delta、lazygit、yazi 在 Debian bookworm 的預設 repo 都不存在，但在 Arch 都有。發行版愈保守（stable 凍結愈久），這類缺口愈多。&lt;/p>
&lt;h2 id="repo-成員資格會隨時間漂移">repo 成員資格會隨時間漂移&lt;/h2>
&lt;p>「沒打包」不只是「保守發行版從沒收」的靜態狀態，也可能是「曾經收、後來被移出」的時間變化。官方 repo 會把不再維護的套件汰除：autojump（目錄跳轉工具）一度在 Arch 官方 repo，後來被移到 AUR，現在 &lt;code>pacman -S autojump&lt;/code> 回 &lt;code>target not found&lt;/code>——同一個發行版、同一個套件名，只是時間軸上的成員資格變了（維護版替代是 zoxide，仍在官方 repo）。這讓一份釘死的套件清單即使不換發行版也會腐化：清單寫的當下該套件在官方 repo、幾年後被移出，清單就在新機器上裝不起來。判讀是：&lt;code>target not found&lt;/code> 且你確定名字沒打錯、以前明明裝得起來，就往「它被移出官方 repo 了」查（多半移到 AUR，或改由上游自行發佈 binary）。若整批一次裝，一個被移出的名字還會拖垮整批（見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性&lt;/a>）。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>安裝一個工具卻拉進幾百個套件時，先問「這些是不是某個語言生態的 library」。是的話，通常代表這東西不該走系統套件管理器——語言生態的執行環境（node、python、ruby）交給 version manager（fnm / pyenv / rbenv）在家目錄管更乾淨：可切版本、不污染系統、不被發行版凍在舊版。系統套件管理器留給「系統層工具」本身。&lt;/p></description><content:encoded><![CDATA[<p>發行版打包粒度是「一個發行版把軟體切成多細的系統套件」的策略。它決定同一個安裝指令的後果量級：有的發行版把每個 library 都包成獨立系統套件，有的把 library 留給語言自己的套件管理器管。同一個工具在前者可能拉進幾百個系統套件、在後者只拉幾個。這條粒度差異坐落在 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a> 底下、是各發行版打包策略的具體展開。</p>
<h2 id="概念位置">概念位置</h2>
<p>這條跟 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a> 互補：抽象層講「同一件事換 backend 怎麼寫」，這張講「同一個指令換 backend 後果量級差多少」。一個爛名字為何拖垮整批安裝，見 <a href="/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性</a>。實作上怎麼判斷該不該進 apt 清單，見 <a href="/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地</a>。</p>
<h2 id="兩種粒度">兩種粒度</h2>
<p>以 nodejs + npm 為例，同樣一個安裝動作在兩種發行版的展開差一個量級（Debian bookworm 實測）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Debian   apt install nodejs npm   → 連帶 402 個新套件（其中 336 個 node-*，eslint、node-glob…每個 JS lib 一個 .deb）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Arch     pacman -S npm            → 只拉少數幾個；JS library 交給 npm 在 node_modules 管</span></span></code></pre></div><p>Debian 的哲學是「所有東西都是系統套件」，所以它把 nodejs / npm 工具鏈本身的相依也拆成上百個 <code>node-*</code> deb，一次 <code>apt install npm</code> 就全灌進系統。要分清楚：這些 <code>node-*</code> 是 Debian 為了打包 node 工具鏈而拉進的<strong>系統層相依</strong>，跟你專案 <code>npm install</code> 進 <code>node_modules</code> 的依賴無關——後者不論哪個 distro 都走 npm、從不碰 apt。所以「402 套件」講的是系統工具鏈膨脹，不是應用依賴。Arch 不把這層 library 納入系統套件，同一個 npm 只在系統層多幾個檔案。</p>
<h2 id="為什麼有的工具乾脆沒打包">為什麼有的工具乾脆沒打包</h2>
<p>粒度策略也決定「哪些工具進得了官方 repo」。移動快速的新工具（多為 Rust 寫的 CLI）在保守、定版凍結的發行版常常還沒被打包：broot、zellij、git-delta、lazygit、yazi 在 Debian bookworm 的預設 repo 都不存在，但在 Arch 都有。發行版愈保守（stable 凍結愈久），這類缺口愈多。</p>
<h2 id="repo-成員資格會隨時間漂移">repo 成員資格會隨時間漂移</h2>
<p>「沒打包」不只是「保守發行版從沒收」的靜態狀態，也可能是「曾經收、後來被移出」的時間變化。官方 repo 會把不再維護的套件汰除：autojump（目錄跳轉工具）一度在 Arch 官方 repo，後來被移到 AUR，現在 <code>pacman -S autojump</code> 回 <code>target not found</code>——同一個發行版、同一個套件名，只是時間軸上的成員資格變了（維護版替代是 zoxide，仍在官方 repo）。這讓一份釘死的套件清單即使不換發行版也會腐化：清單寫的當下該套件在官方 repo、幾年後被移出，清單就在新機器上裝不起來。判讀是：<code>target not found</code> 且你確定名字沒打錯、以前明明裝得起來，就往「它被移出官方 repo 了」查（多半移到 AUR，或改由上游自行發佈 binary）。若整批一次裝，一個被移出的名字還會拖垮整批（見 <a href="/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/" data-link-title="apt 安裝的交易原子性" data-link-desc="apt-get install 一次帶多個套件、其中一個名字沒打包卻導致整批都沒裝時回來讀 — apt 為什麼全有或全無">apt 安裝的交易原子性</a>）。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>安裝一個工具卻拉進幾百個套件時，先問「這些是不是某個語言生態的 library」。是的話，通常代表這東西不該走系統套件管理器——語言生態的執行環境（node、python、ruby）交給 version manager（fnm / pyenv / rbenv）在家目錄管更乾淨：可切版本、不污染系統、不被發行版凍在舊版。系統套件管理器留給「系統層工具」本身。</p>
<p>反過來，某工具 <code>apt install</code> 直接 unable to locate 時，多半是它還沒進這個發行版的 repo，不是名字打錯——退回 GitHub releases 的預編譯 binary 或 <code>cargo install</code>。</p>
<h2 id="邊界">邊界</h2>
<p>粒度細不是缺點：Debian 把 library 拆成系統套件，換來的是每個 library 都能收到發行版的安全更新與依賴一致性保證，這對伺服器場景有價值。問題只出在「拿它裝語言生態的開發依賴」——那是 version manager 的場域。判斷依用途，不是依發行版好壞。</p>
]]></content:encoded></item><item><title>apt 安裝的交易原子性</title><link>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/apt-transaction-atomicity/</guid><description>&lt;p>apt 的安裝是一筆交易（transaction）：&lt;code>apt-get install a b c&lt;/code> 會先把所有指定套件連同它們的依賴一起解析成一份完整的安裝計畫，全部解析成功才開始下載安裝；只要其中一個套件解析不到（名字不存在、沒打包、依賴衝突），整筆交易放棄，一個都不裝。apt 是 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a> 底下各發行版套件管理員之一，這個交易行為是 apt 特有的實作細節。&lt;/p>
&lt;h2 id="解析與安裝是兩階段">解析與安裝是兩階段&lt;/h2>
&lt;p>apt 把「算出要裝什麼」跟「實際裝」分成先後兩步：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">階段一 解析：把 a b c + 全部依賴解成一份計畫；任一個解不到 → 整筆 abort，什麼都沒動
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">階段二 安裝：計畫完整才下載、解包、設定&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這是刻意的設計。系統套件之間有依賴關係，半裝的狀態（裝了 a、b 因為 c 失敗而中途停）會留下依賴不完整的系統，比「全部沒裝」更難收拾。全有或全無讓系統永遠停在一致的狀態。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>這解釋了為什麼跨發行版的套件清單要 curate、不能照抄，跟 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層&lt;/a> 的「套件名分歧要逐項吸收」是同一件事的兩面。哪些工具在保守發行版沒打包，見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a>。清單 curate 的實作見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地&lt;/a>。&lt;/p>
&lt;h2 id="一個爛名字全滅">一個爛名字全滅&lt;/h2>
&lt;p>實務上最容易撞到的後果：批次清單裡塞了一個該發行版沒有的套件名，整批就全部不裝。實測在 Debian bookworm 跑一份含 broot、zellij（兩者 bookworm 沒打包）的清單，apt 回 &lt;code>Unable to locate package broot / zellij&lt;/code> 後直接中止，同批的 autojump、gh、tig 明明存在也一個都沒裝。&lt;/p>
&lt;p>症狀是「我明明列了十個工具、跑完一個都沒有」，根因不是每個都失敗，是其中一兩個爛名字讓整筆交易 abort。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;p>批次安裝清單必須逐項對齊該發行版實際有的套件，不能從別的發行版直接抄名字過來。動手前用 dry-run 先驗：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">apt-get install -s a b c &lt;span class="c1"># -s 模擬，只解析不安裝，先看有沒有 Unable to locate&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>一個一個 dry-run 能精準指出哪個名字解不開，再把它移出批次清單、或換成該發行版的正確名字。&lt;/p>
&lt;h2 id="邊界">邊界&lt;/h2>
&lt;p>pacman 的批次安裝有類似的原子性——實測 &lt;code>pacman -S&lt;/code> 一份含 autojump 的清單，因 autojump 已從 Arch 官方 repo 移到 AUR、回 &lt;code>target not found&lt;/code>，整批就 abort，同清單裡本來裝得起來的 zoxide、ripgrep、fd 一個都沒裝（這種「曾在官方 repo、後來被移出」的漂移見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度&lt;/a>）。想繞過「一個失敗全滅」可以逐包安裝（迴圈裡一個一個裝、失敗的跳過），但那會失去交易的一致性保證、也讓「哪些沒裝成功」變得難追。多數情況正解是把清單修對，而不是放棄原子性。&lt;/p></description><content:encoded><![CDATA[<p>apt 的安裝是一筆交易（transaction）：<code>apt-get install a b c</code> 會先把所有指定套件連同它們的依賴一起解析成一份完整的安裝計畫，全部解析成功才開始下載安裝；只要其中一個套件解析不到（名字不存在、沒打包、依賴衝突），整筆交易放棄，一個都不裝。apt 是 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a> 底下各發行版套件管理員之一，這個交易行為是 apt 特有的實作細節。</p>
<h2 id="解析與安裝是兩階段">解析與安裝是兩階段</h2>
<p>apt 把「算出要裝什麼」跟「實際裝」分成先後兩步：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">階段一 解析：把 a b c + 全部依賴解成一份計畫；任一個解不到 → 整筆 abort，什麼都沒動
</span></span><span class="line"><span class="ln">2</span><span class="cl">階段二 安裝：計畫完整才下載、解包、設定</span></span></code></pre></div><p>這是刻意的設計。系統套件之間有依賴關係，半裝的狀態（裝了 a、b 因為 c 失敗而中途停）會留下依賴不完整的系統，比「全部沒裝」更難收拾。全有或全無讓系統永遠停在一致的狀態。</p>
<h2 id="概念位置">概念位置</h2>
<p>這解釋了為什麼跨發行版的套件清單要 curate、不能照抄，跟 <a href="/blog/linux/dotfile/knowledge-cards/package-manager-abstraction/" data-link-title="Package Manager 抽象層" data-link-desc="同一份 dotfile 要從 Arch 跨到 Debian/macOS、不確定哪些要改哪些能共用時回來讀 — 綁 distro 的只有 package manager 這一層">Package Manager 抽象層</a> 的「套件名分歧要逐項吸收」是同一件事的兩面。哪些工具在保守發行版沒打包，見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>。清單 curate 的實作見 <a href="/blog/linux/dotfile/10-prod-parity/workstation-cross-distro/" data-link-title="工作站 dotfile 跨發行版落地" data-link-desc="手上 dotfile 是照 Arch 寫的、要在 client 常見的 Debian/Ubuntu 機器上還原工作環境時回來讀 — 哪些綁 pacman、哪些能直接共用">工作站 dotfile 跨發行版落地</a>。</p>
<h2 id="一個爛名字全滅">一個爛名字全滅</h2>
<p>實務上最容易撞到的後果：批次清單裡塞了一個該發行版沒有的套件名，整批就全部不裝。實測在 Debian bookworm 跑一份含 broot、zellij（兩者 bookworm 沒打包）的清單，apt 回 <code>Unable to locate package broot / zellij</code> 後直接中止，同批的 autojump、gh、tig 明明存在也一個都沒裝。</p>
<p>症狀是「我明明列了十個工具、跑完一個都沒有」，根因不是每個都失敗，是其中一兩個爛名字讓整筆交易 abort。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<p>批次安裝清單必須逐項對齊該發行版實際有的套件，不能從別的發行版直接抄名字過來。動手前用 dry-run 先驗：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">apt-get install -s a b c    <span class="c1"># -s 模擬，只解析不安裝，先看有沒有 Unable to locate</span></span></span></code></pre></div><p>一個一個 dry-run 能精準指出哪個名字解不開，再把它移出批次清單、或換成該發行版的正確名字。</p>
<h2 id="邊界">邊界</h2>
<p>pacman 的批次安裝有類似的原子性——實測 <code>pacman -S</code> 一份含 autojump 的清單，因 autojump 已從 Arch 官方 repo 移到 AUR、回 <code>target not found</code>，整批就 abort，同清單裡本來裝得起來的 zoxide、ripgrep、fd 一個都沒裝（這種「曾在官方 repo、後來被移出」的漂移見 <a href="/blog/linux/dotfile/knowledge-cards/distro-package-granularity/" data-link-title="發行版打包粒度" data-link-desc="同一個 install 指令在不同發行版拉的套件數差一個量級、或某工具在某發行版根本沒打包時回來讀 — 發行版怎麼切套件顆粒度">發行版打包粒度</a>）。想繞過「一個失敗全滅」可以逐包安裝（迴圈裡一個一個裝、失敗的跳過），但那會失去交易的一致性保證、也讓「哪些沒裝成功」變得難追。多數情況正解是把清單修對，而不是放棄原子性。</p>
]]></content:encoded></item></channel></rss>