<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Docker-Compose on Tarragon</title><link>https://tarrragon.github.io/blog/tags/docker-compose/</link><description>Recent content in Docker-Compose 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/docker-compose/index.xml" rel="self" type="application/rss+xml"/><item><title>Docker Compose：多 service dev 環境編排</title><link>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/docker-compose/</link><pubDate>Mon, 06 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/docker-compose/</guid><description>&lt;p>一個 app 要好幾個 container（app + DB + cache）一起跑、彼此連通，是單一 image 的 Dockerfile（&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>）之上的下一層問題。如果你正納悶「為什麼是好幾個 container、不是一個大 container 全塞」，先讀 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/container-per-service/" data-link-title="Container-per-Service（一容器一職責）" data-link-desc="第一次發現一個 web app 要開好幾個 container、不懂為什麼不塞一個大 container、或不確定什麼該獨立成一個 container 時回來讀 — 一容器一職責的理由與邊界">一容器一職責&lt;/a>——這篇假設你已經接受一服務一容器，往下講怎麼把它們串起來。上游是 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker vendor overview&lt;/a>。&lt;/p>
&lt;h2 id="手動串三個-container-的痛">手動串三個 container 的痛&lt;/h2>
&lt;p>一個典型的 web app 開發環境要三個 container：PHP-FPM 跑程式、MySQL 存資料、nginx 收 HTTP。用 &lt;code>docker run&lt;/code> 手動串，你得記住：先開一個 network、按順序啟動、每個都帶一長串 &lt;code>-v&lt;/code> 掛 volume、&lt;code>--network&lt;/code> 接網路、&lt;code>-e&lt;/code> 設環境變數、&lt;code>-p&lt;/code> 對 port，還要記得 nginx 要能用名字找到 PHP。換一台機器、或同事要跑，整套指令重來一次，漏一個 flag 就連不起來。&lt;/p>
&lt;p>Docker Compose 解決的就是這個：把「這個 app 需要哪些 container、怎麼連、掛什麼」寫成一個宣告式的 &lt;code>docker-compose.yml&lt;/code>，&lt;code>docker compose up&lt;/code> 一次拉起整套。&lt;/p>
&lt;h2 id="核心概念一個檔描述整個拓樸">核心概念：一個檔描述整個拓樸&lt;/h2>
&lt;p>Compose 的核心是「用一份宣告式檔案描述多 service 的完整拓樸」。你寫的是&lt;strong>期望的最終狀態&lt;/strong>（要哪些 service、各自的 image / network / volume），Compose 負責把它變成現實：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>service 是編排單位&lt;/strong>：每個 service 對應一個（或多個 replica）container，Compose 管它們的生命週期。&lt;/li>
&lt;li>&lt;strong>network 預設自動建&lt;/strong>：同一個 compose 檔的所有 service 自動進同一個 network，彼此用 &lt;strong>service name 當 hostname&lt;/strong> 互連——nginx 連 &lt;code>php:9000&lt;/code>、PHP 連 &lt;code>db:3306&lt;/code>，不需要知道 IP。&lt;/li>
&lt;li>&lt;strong>volume 讓資料活過 container&lt;/strong>：container 是可拋棄的，要保留的資料（DB 檔）放 named volume，&lt;code>down&lt;/code> 再 &lt;code>up&lt;/code> 資料還在。&lt;/li>
&lt;/ul>
&lt;h2 id="配置三件套逐段">配置：三件套逐段&lt;/h2>
&lt;p>以下是一個實際跑起來的 PHP 7.2 + MySQL 5.7 + nginx 組合（版本凍結的 dev runtime），逐段拆解。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">php&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">./php &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 用本地 Dockerfile build&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./src:/var/www/html &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># bind mount：改 code 立即反映進 container&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nginx:1.18 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 直接用 registry image，釘版本&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8080:80&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># host 8080 → container 80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./src:/var/www/html&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">php&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">mysql:5.7&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">platform&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">linux/amd64 &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># arm64 主機上跑無原生 image 的舊 MySQL（見故障演練）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_ROOT_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">secret&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">MYSQL_DATABASE&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">app&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">26&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">db-data:/var/lib/mysql&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># named volume：DB 資料活過 container 重建&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">27&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">28&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">29&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="l">db-data:&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>幾個決策點：&lt;/p></description><content:encoded><![CDATA[<p>一個 app 要好幾個 container（app + DB + cache）一起跑、彼此連通，是單一 image 的 Dockerfile（<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>）之上的下一層問題。如果你正納悶「為什麼是好幾個 container、不是一個大 container 全塞」，先讀 <a href="/blog/backend/knowledge-cards/container-per-service/" data-link-title="Container-per-Service（一容器一職責）" data-link-desc="第一次發現一個 web app 要開好幾個 container、不懂為什麼不塞一個大 container、或不確定什麼該獨立成一個 container 時回來讀 — 一容器一職責的理由與邊界">一容器一職責</a>——這篇假設你已經接受一服務一容器，往下講怎麼把它們串起來。上游是 <a href="/blog/backend/05-deployment-platform/vendors/docker/" data-link-title="Docker" data-link-desc="Container runtime / image 標準">Docker vendor overview</a>。</p>
<h2 id="手動串三個-container-的痛">手動串三個 container 的痛</h2>
<p>一個典型的 web app 開發環境要三個 container：PHP-FPM 跑程式、MySQL 存資料、nginx 收 HTTP。用 <code>docker run</code> 手動串，你得記住：先開一個 network、按順序啟動、每個都帶一長串 <code>-v</code> 掛 volume、<code>--network</code> 接網路、<code>-e</code> 設環境變數、<code>-p</code> 對 port，還要記得 nginx 要能用名字找到 PHP。換一台機器、或同事要跑，整套指令重來一次，漏一個 flag 就連不起來。</p>
<p>Docker Compose 解決的就是這個：把「這個 app 需要哪些 container、怎麼連、掛什麼」寫成一個宣告式的 <code>docker-compose.yml</code>，<code>docker compose up</code> 一次拉起整套。</p>
<h2 id="核心概念一個檔描述整個拓樸">核心概念：一個檔描述整個拓樸</h2>
<p>Compose 的核心是「用一份宣告式檔案描述多 service 的完整拓樸」。你寫的是<strong>期望的最終狀態</strong>（要哪些 service、各自的 image / network / volume），Compose 負責把它變成現實：</p>
<ul>
<li><strong>service 是編排單位</strong>：每個 service 對應一個（或多個 replica）container，Compose 管它們的生命週期。</li>
<li><strong>network 預設自動建</strong>：同一個 compose 檔的所有 service 自動進同一個 network，彼此用 <strong>service name 當 hostname</strong> 互連——nginx 連 <code>php:9000</code>、PHP 連 <code>db:3306</code>，不需要知道 IP。</li>
<li><strong>volume 讓資料活過 container</strong>：container 是可拋棄的，要保留的資料（DB 檔）放 named volume，<code>down</code> 再 <code>up</code> 資料還在。</li>
</ul>
<h2 id="配置三件套逐段">配置：三件套逐段</h2>
<p>以下是一個實際跑起來的 PHP 7.2 + MySQL 5.7 + nginx 組合（版本凍結的 dev runtime），逐段拆解。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">php</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">./php             </span><span class="w"> </span><span class="c"># 用本地 Dockerfile build</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">      </span>- <span class="l">./src:/var/www/html  </span><span class="w"> </span><span class="c"># bind mount：改 code 立即反映進 container</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span>- <span class="l">db</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">nginx:1.18        </span><span class="w"> </span><span class="c"># 直接用 registry image，釘版本</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span>- <span class="s2">&#34;8080:80&#34;</span><span class="w">             </span><span class="c"># host 8080 → container 80</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">      </span>- <span class="l">./src:/var/www/html</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">      </span>- <span class="l">./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">      </span>- <span class="l">php</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:5.7</span><span class="w">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="w">    </span><span class="nt">platform</span><span class="p">:</span><span class="w"> </span><span class="l">linux/amd64    </span><span class="w"> </span><span class="c"># arm64 主機上跑無原生 image 的舊 MySQL（見故障演練）</span><span class="w">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_ROOT_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">secret</span><span class="w">
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="w">      </span><span class="nt">MYSQL_DATABASE</span><span class="p">:</span><span class="w"> </span><span class="l">app</span><span class="w">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="w">      </span>- <span class="l">db-data:/var/lib/mysql</span><span class="w"> </span><span class="c"># named volume：DB 資料活過 container 重建</span><span class="w">
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="w"></span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="w">  </span><span class="l">db-data:</span></span></span></code></pre></div><p>幾個決策點：</p>
<ul>
<li><strong><code>build</code> vs <code>image</code></strong>：php service 用 <code>build: ./php</code> 從本地 Dockerfile 建（你要客製），web / db 用 <code>image:</code> 直接拉現成的。同一個 compose 檔可以混用。</li>
<li><strong>bind mount vs named volume</strong>：<code>./src:/var/www/html</code> 是 bind mount，把 host 的原始碼目錄掛進 container，改 code 立即生效、適合 dev；<code>db-data:/var/lib/mysql</code> 是 named volume，由 Docker 管理、適合要保留但不需要人去看的資料。兩者用途不同，不要混。</li>
<li><strong><code>depends_on</code></strong>：宣告啟動順序（db 先於 php 先於 web）。這條有個重要邊界，見故障演練。</li>
<li><strong><code>ports</code> 只在 web 開</strong>：只有 web 需要對外，php 跟 db 不開 <code>ports</code>——它們透過內部 network 被 web / php 用 service name 連到，不必也不該暴露到 host。</li>
</ul>
<p>這份是聚焦編排結構的最小版：web 掛的 <code>nginx/default.conf</code> 最小內容就是把 <code>.php</code> 請求 <code>fastcgi_pass</code> 到 <code>php:9000</code>；要對齊 prod 還得補時區（db 的 <code>TZ</code> + MySQL <code>default-time-zone</code>）、<code>sql_mode</code> 等設定。完整可跑並附 parity 驗證的版本見 <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>
<p>跑起來：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker compose up -d --build   <span class="c1"># build + 背景啟動整套</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker compose ps              <span class="c1"># 看三個 service 狀態</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker compose logs -f db      <span class="c1"># 追某個 service 的 log</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">docker compose down -v         <span class="c1"># 停掉並清掉（-v 連 named volume 一起清）</span></span></span></code></pre></div><h2 id="故障演練起得來卻連不通">故障演練：起得來卻連不通</h2>
<h3 id="depends_on-不等於服務-ready">depends_on 不等於「服務 ready」</h3>
<p>最常見的誤解：以為 <code>depends_on: db</code> 會等 MySQL 可以接受連線才啟動 php。實際上 <code>depends_on</code> 只等到 db container <strong>啟動了</strong>（process 起來），不等它<strong>服務 ready</strong>。MySQL container 起來後還要幾秒到幾十秒初始化資料目錄，這段期間 php 連過去會吃到 <code>Connection refused</code> 或 <code>Lost connection</code>。</p>
<p>徵兆是 <code>docker compose up</code> 後 app 立刻報連不到 DB，但過一下手動連又通了。兩種修法：</p>
<p>其一，用 healthcheck + <code>condition: service_healthy</code> 讓 Compose 真的等到 DB 健康（healthcheck 作為通用的服務健康判讀見 <a href="/blog/backend/knowledge-cards/health-check/" data-link-title="Health Check" data-link-desc="說明服務如何對外提供可供平台判斷狀態的健康回應">Health Check</a>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;mysqladmin&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;ping&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;-h&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;localhost&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">10</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">  </span><span class="nt">php</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">      </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</span></span></span></code></pre></div><p>其二，在應用層做連線重試（開機時連不上就 backoff 重試）。實務上兩者常一起用——healthcheck 處理啟動順序、應用層重試處理執行期間 DB 短暫重啟。應用層重試更穩健，因為 DB 在服務跑起來之後也可能短暫斷線。</p>
<h3 id="arm64-主機跑無原生-image-的舊服務">arm64 主機跑無原生 image 的舊服務</h3>
<p>在 Apple Silicon（arm64）主機上 <code>docker compose up</code>，mysql:5.7 可能報 image 找不到對應架構、或直接不啟動。根因是 MySQL 5.7 官方 image 只出 amd64、沒有 arm64 原生版。修法是明確宣告 <code>platform: linux/amd64</code>，讓它透過模擬跑：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="w">  </span><span class="nt">db</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">mysql:5.7</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">platform</span><span class="p">:</span><span class="w"> </span><span class="l">linux/amd64</span></span></span></code></pre></div><p>代價是模擬有效能損耗、啟動較慢。這本身也是一種環境對齊——dev 是模擬 amd64、prod 是原生 amd64，架構一致。為什麼沒有原生 image 就得靠模擬、<code>exec format error</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>；跨平台 build 的完整配置見 <a href="/blog/backend/05-deployment-platform/vendors/docker/buildkit-cross-platform/" data-link-title="BuildKit 與跨平台 build" data-link-desc="docker build 每次重下載套件很慢、要同時出 amd64 與 arm64 image、或 build 時要用私有憑證卻不想烤進 image 時回來讀 — BuildKit 的 cache/secret mount 與 buildx 跨平台 build">BuildKit 與跨平台 build</a>。</p>
<h3 id="service-間連線用-service-name不是-localhost">service 間連線用 service name，不是 localhost</h3>
<p>在 php container 裡連 MySQL，連線字串要用 <code>db</code>（service name）不是 <code>localhost</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-php" data-lang="php"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">$pdo</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">PDO</span><span class="p">(</span><span class="s1">&#39;mysql:host=db;dbname=app&#39;</span><span class="p">,</span> <span class="s1">&#39;root&#39;</span><span class="p">,</span> <span class="s1">&#39;secret&#39;</span><span class="p">);</span></span></span></code></pre></div><p>每個 container 有自己的 network namespace，<code>localhost</code> 指的是 container 自己、不是別的 service。Compose 的自動 network 讓 service name 解析成對的 container IP。寫成 <code>localhost:3306</code> 會連到 php container 自己的 3306（沒東西在聽）→ <code>Connection refused</code>。這是從「單機所有東西都在 localhost」搬進容器時最常見的錯。</p>
<h3 id="down-不帶--v資料還在">down 不帶 -v，資料還在</h3>
<p><code>docker compose down</code> 停掉並移除 container、network，但<strong>保留 named volume</strong>。所以 <code>down</code> 再 <code>up</code> 之後 DB 資料還在——這通常是你要的。但如果你想要一個全新的 DB（例如改了初始化 SQL、要重跑），必須 <code>down -v</code> 把 volume 也清掉，否則舊資料還在、初始化腳本不會重跑（MySQL image 只在資料目錄空的時候跑初始化）。徵兆是「我改了 init.sql 但 compose 起來沒生效」——因為 volume 裡的舊資料還在。</p>
<h2 id="容量compose-到哪裡為止">容量：compose 到哪裡為止</h2>
<p>Compose 的定位是 <strong>single-host 的多 container 編排</strong>，適用範圍要清楚：</p>
<ul>
<li><strong>適合</strong>：本機 dev 環境、CI 裡起依賴服務跑整合測試、單機小型部署（一台 VM 跑幾個 container 的內部工具）。</li>
<li><strong>不適合</strong>：多 host、需要自動擴縮、rolling update、self-healing 的 production。這些是 orchestrator 的職責。</li>
</ul>
<p>界線的訊號是「你開始想要跨機器調度、或要 replica 自動補」——那是該換 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes</a> 的訊號。好消息是 image 不用變，換的是編排層（compose → K8s manifest），Dockerfile build 出的 image 兩邊通用。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>compose 裡 <code>build:</code> 指向的 Dockerfile 怎麼設計，見 <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>。</li>
<li>一個完整的「用 compose 對齊 client 線上舊環境」實作（含 parity probe 驗證），見 <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>從 compose 搬到 production 編排，見 <a href="/blog/backend/05-deployment-platform/kubernetes-deployment/" data-link-title="5.2 Kubernetes 部署策略" data-link-desc="整理 deployment、probe 與 rolling update">Kubernetes deployment</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>