> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker Compose

> 用一份 compose.yaml 宣告多容器應用：服務、網路、群組名、環境變數、GPU、效能、volume 映射與 docker compose CLI。

export const TerminalDemo = ({lang = "zh", title, prompt, commands}) => {
  const L = ({
    zh: {
      play: "播放",
      pause: "暫停",
      step: "下一步",
      replay: "重新播放",
      reset: "重設",
      winTitle: "Windows PowerShell",
      hint: "點播放，逐行看驗證流程實際跑出來的樣子"
    },
    en: {
      play: "Play",
      pause: "Pause",
      step: "Step",
      replay: "Replay",
      reset: "Reset",
      winTitle: "Windows PowerShell",
      hint: "Press play to watch the verification run line by line"
    }
  })[lang] || ({});
  const defaultCmds = [{
    cmd: "nvidia-smi",
    output: ["Mon Jun  8 14:30:12 2026", "+-----------------------------------------------------------------------------+", "| NVIDIA-SMI 552.22       Driver Version: 552.22       CUDA Version: 12.4     |", "|-------------------------------+----------------------+----------------------+", "| GPU  Name           TCC/WDDM  | Bus-Id        Disp.A | Volatile Uncorr. ECC |", "| Fan  Temp Perf Pwr:Usage/Cap  | Memory-Usage         | GPU-Util  Compute M. |", "|===============================+======================+======================|", "|   0  GeForce RTX 3080  WDDM   | 00000000:01:00.0  On |                  N/A |", "| 30%   42C  P8    21W / 320W   |    842MiB / 10240MiB |      2%      Default |", "+-------------------------------+----------------------+----------------------+"]
  }, {
    cmd: "nvcc -V",
    output: ["nvcc: NVIDIA (R) Cuda compiler driver", "Copyright (c) 2005-2024 NVIDIA Corporation", "Built on Tue_Feb_27_16:28:36_2024", "Cuda compilation tools, release 12.4, V12.4.99", "Build cuda_12.4.r12.4/compiler.34097967_0"]
  }, {
    cmd: "python gpu_test.py",
    output: ["========================================", "PyTorch Version: 2.6.0+cu124", "CUDA Available:  True", "Device Name:     NVIDIA GeForce RTX 3080", "CUDA Version:    12.4", "✓ cuDNN Test Passed! Convolution executed successfully.", "========================================"]
  }];
  const raw = commands && commands.length ? commands : defaultCmds;
  const cmds = raw.map(c => ({
    cmd: c.cmd,
    lines: Array.isArray(c.output) ? c.output : String(c.output || "").split("\n")
  }));
  const PROMPT = prompt || "PS C:\\Users\\dev>";
  const TITLE = title || L.winTitle;
  const [idx, setIdx] = useState(0);
  const [typed, setTyped] = useState(0);
  const [outN, setOutN] = useState(0);
  const [stage, setStage] = useState("cmd");
  const [running, setRunning] = useState(false);
  const [done, setDone] = useState(false);
  useEffect(() => {
    if (!running || done) return;
    const cur = cmds[idx];
    if (!cur) {
      setRunning(false);
      setDone(true);
      return;
    }
    if (stage === "cmd") {
      if (typed < cur.cmd.length) {
        const id = setTimeout(() => setTyped(typed + 1), 28);
        return () => clearTimeout(id);
      }
      const id = setTimeout(() => setStage("out"), 200);
      return () => clearTimeout(id);
    }
    if (stage === "out") {
      if (outN < cur.lines.length) {
        const id = setTimeout(() => setOutN(outN + 1), 70);
        return () => clearTimeout(id);
      }
      const id = setTimeout(() => {
        if (idx + 1 < cmds.length) {
          setIdx(idx + 1);
          setTyped(0);
          setOutN(0);
          setStage("cmd");
        } else {
          setRunning(false);
          setDone(true);
        }
      }, 360);
      return () => clearTimeout(id);
    }
  }, [running, done, idx, typed, outN, stage]);
  const reset = () => {
    setRunning(false);
    setDone(false);
    setIdx(0);
    setTyped(0);
    setOutN(0);
    setStage("cmd");
  };
  const playPause = () => {
    if (done) {
      reset();
      setRunning(true);
      return;
    }
    setRunning(!running);
  };
  const step = () => {
    setRunning(false);
    if (done) return;
    if (idx + 1 < cmds.length) {
      setIdx(idx + 1);
      setTyped(0);
      setOutN(0);
      setStage("cmd");
    } else {
      const cur = cmds[idx];
      setTyped(cur.cmd.length);
      setOutN(cur.lines.length);
      setStage("out");
      setDone(true);
    }
  };
  const css = `
.td-root{--bg:#f4eee4;--bd:#e2d7c6;--ink:#5b5048;--accent:#bf7551;--accent-l:#cf8a68;
  --scr:#1c1815;--scr-fg:#e9e1d4;--scr-prompt:#cf8a68;--scr-dim:#8d8478;--scr-ok:#8fb573;--scr-err:#d98a72;
  border:1px solid var(--bd);border-radius:14px;overflow:hidden;background:var(--bg);
  font-family:ui-sans-serif,system-ui,"Noto Sans TC",sans-serif;margin:1.25rem 0;box-shadow:0 1px 2px rgba(60,40,25,.06);}
.dark .td-root{--bg:#26221e;--bd:#3a332c;--ink:#cabfb2;}
.td-bar{display:flex;align-items:center;gap:.6rem;padding:.55rem .8rem;background:linear-gradient(var(--bg),color-mix(in srgb,var(--bg) 90%,#000));border-bottom:1px solid var(--bd);}
.td-dots{display:flex;gap:.4rem;}
.td-dots i{width:11px;height:11px;border-radius:50%;display:block;}
.td-dots i:nth-child(1){background:#d98a72;} .td-dots i:nth-child(2){background:#dcb46a;} .td-dots i:nth-child(3){background:#8fb573;}
.td-title{font-size:.78rem;color:var(--ink);font-weight:600;letter-spacing:.01em;flex:1;text-align:center;opacity:.85;}
.td-ctrl{display:flex;align-items:center;gap:.3rem;}
.td-btn{display:inline-flex;align-items:center;gap:.32rem;border:1px solid var(--bd);background:transparent;color:var(--ink);
  border-radius:8px;padding:.3rem .55rem;font-size:.74rem;font-weight:600;cursor:pointer;transition:all .15s ease;line-height:1;}
.td-btn:hover{border-color:var(--accent);color:var(--accent);background:color-mix(in srgb,var(--accent) 10%,transparent);}
.td-btn svg{width:13px;height:13px;}
.td-btn--primary{background:var(--accent);border-color:var(--accent);color:#fff;}
.dark .td-btn--primary{background:var(--accent-l);border-color:var(--accent-l);}
.td-btn--primary:hover{background:var(--accent-l);color:#fff;}
.td-count{font-size:.72rem;color:var(--ink);opacity:.6;font-variant-numeric:tabular-nums;margin-left:.2rem;}
.td-screen{background:var(--scr);color:var(--scr-fg);padding:.85rem 1rem 1.05rem;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;
  font-size:.8rem;line-height:1.55;overflow-x:auto;min-height:120px;}
.td-block{margin-bottom:.5rem;}
.td-cmdline{white-space:pre;}
.td-prompt{color:var(--scr-prompt);font-weight:600;}
.td-cmd{color:#f3ecdf;}
.td-out{white-space:pre;color:var(--scr-fg);opacity:.92;}
.td-out.ok{color:var(--scr-ok);font-weight:600;}
.td-out.err{color:var(--scr-err);font-weight:600;}
.td-cursor{display:inline-block;width:8px;height:1em;background:var(--scr-prompt);margin-left:1px;vertical-align:text-bottom;
  animation:tdblink 1s steps(2,start) infinite;}
@keyframes tdblink{to{opacity:0;}}
.td-hint{padding:.5rem .9rem;font-size:.72rem;color:var(--ink);opacity:.6;border-top:1px solid var(--bd);background:var(--bg);}
@media (max-width:600px){.td-screen{font-size:.68rem;} .td-title{display:none;} .td-btn{padding:.28rem .42rem;}}
`;
  const PlayIcon = () => <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M7 5.5v13l11-6.5z" /></svg>;
  const PauseIcon = () => <svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6.5" y="5" width="4" height="14" rx="1" /><rect x="13.5" y="5" width="4" height="14" rx="1" /></svg>;
  const StepIcon = () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 4l10 8-10 8z" /><line x1="19" y1="5" x2="19" y2="19" /></svg>;
  const ResetIcon = () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8" /><path d="M3 3v5h5" /></svg>;
  const shown = Math.min(idx + (done ? 1 : 0), cmds.length);
  return <div className="td-root">
      <style>{css}</style>
      <div className="td-bar">
        <span className="td-dots"><i /><i /><i /></span>
        <span className="td-title">{TITLE}</span>
        <span className="td-ctrl">
          <button className="td-btn td-btn--primary" onClick={playPause}>
            {running ? <PauseIcon /> : <PlayIcon />}
            {done ? L.replay : running ? L.pause : L.play}
          </button>
          <button className="td-btn" onClick={step} disabled={done}><StepIcon />{L.step}</button>
          <button className="td-btn" onClick={reset}><ResetIcon />{L.reset}</button>
          <span className="td-count">{shown}/{cmds.length}</span>
        </span>
      </div>
      <div className="td-screen">
        {Array.from({
    length: idx + 1
  }).map((_, i) => {
    const c = cmds[i];
    if (!c) return null;
    const isCur = i === idx;
    const cmdText = isCur ? c.cmd.slice(0, typed) : c.cmd;
    const lines = isCur ? c.lines.slice(0, outN) : c.lines;
    const cursor = isCur && !done && stage === "cmd";
    return <div className="td-block" key={i}>
              <div className="td-cmdline">
                <span className="td-prompt">{PROMPT}</span> <span className="td-cmd">{cmdText}</span>
                {cursor && <span className="td-cursor" />}
              </div>
              {lines.map((ln, j) => {
      const tr = ln.trim();
      const cls = tr.startsWith("✓") ? "ok" : tr.startsWith("✗") || tr.startsWith("✘") ? "err" : "";
      return <div className={"td-out " + cls} key={j}>{ln.length ? ln : " "}</div>;
    })}
            </div>;
  })}
      </div>
      <div className="td-hint">{L.hint}</div>
    </div>;
};

export const ComposeAnatomy = ({lang = "zh"}) => {
  const t = lang === "en" ? {
    lead: "compose.yaml anatomy: click a key to see what it does."
  } : {
    lead: "compose.yaml 解剖：點一個鍵看它的用意。"
  };
  const LINES = [{
    t: "name: myapp",
    k: "name"
  }, {
    t: ""
  }, {
    t: "services:",
    k: "services"
  }, {
    t: "  web:",
    k: "services"
  }, {
    t: "    build: .",
    k: "build"
  }, {
    t: "    ports:",
    k: "ports"
  }, {
    t: '      - "8080:80"',
    k: "ports"
  }, {
    t: "    environment:",
    k: "environment"
  }, {
    t: "      DB_HOST: db",
    k: "environment"
  }, {
    t: "    env_file: .env",
    k: "env_file"
  }, {
    t: "    volumes:",
    k: "volumes"
  }, {
    t: "      - app-data:/var/lib/data",
    k: "volumes"
  }, {
    t: "      - ./src:/app/src",
    k: "volumes"
  }, {
    t: "    depends_on:",
    k: "depends_on"
  }, {
    t: "      db:",
    k: "depends_on"
  }, {
    t: "        condition: service_healthy",
    k: "depends_on"
  }, {
    t: "    networks: [backend]",
    k: "networks"
  }, {
    t: "    deploy:",
    k: "deploy"
  }, {
    t: "      resources:",
    k: "deploy"
  }, {
    t: "        reservations:",
    k: "deploy"
  }, {
    t: "          devices:",
    k: "deploy"
  }, {
    t: "            - driver: nvidia",
    k: "deploy"
  }, {
    t: "              count: all",
    k: "deploy"
  }, {
    t: "              capabilities: [gpu]",
    k: "deploy"
  }, {
    t: ""
  }, {
    t: "  db:",
    k: "services"
  }, {
    t: "    image: postgres:18",
    k: "image"
  }, {
    t: "    healthcheck:",
    k: "healthcheck"
  }, {
    t: '      test: ["CMD-SHELL", "pg_isready"]',
    k: "healthcheck"
  }, {
    t: ""
  }, {
    t: "networks:",
    k: "networks"
  }, {
    t: "  backend:",
    k: "networks"
  }, {
    t: "    driver: bridge",
    k: "networks"
  }, {
    t: ""
  }, {
    t: "volumes:",
    k: "volumes"
  }, {
    t: "  app-data:",
    k: "volumes"
  }];
  const KEYS = lang === "en" ? [{
    k: "name",
    label: "name",
    ex: "Project name. Becomes the prefix for containers (myapp-web-1), networks (myapp_default), and volumes. Defaults to the directory name; override with -p or COMPOSE_PROJECT_NAME. Note: there is no version: field any more, it is obsolete in the Compose Specification."
  }, {
    k: "services",
    label: "services",
    ex: "The containers that make up the app. Each service name (web, db) is also its DNS hostname on the project network, so web reaches the database at db:5432, no IP needed."
  }, {
    k: "build",
    label: "build",
    ex: "Build the image from a Dockerfile instead of pulling one. build: . uses the current dir as context; the long form takes context, dockerfile, args, and target (which stage of a multi-stage build to stop at)."
  }, {
    k: "image",
    label: "image",
    ex: "Pull a prebuilt image from a registry. Pin a tag (postgres:18), never latest. A service uses either build or image (or build plus image to tag the result)."
  }, {
    k: "ports",
    label: "ports",
    ex: "Publish container ports to the host, HOST:CONTAINER. \"8080:80\" maps host 8080 to container 80. Without this, the port is only reachable from inside the Compose network, not from your machine."
  }, {
    k: "environment",
    label: "environment",
    ex: "Inline environment variables passed into the container. Good for a few visible values; supports ${VAR} and ${VAR:-default} interpolation from the shell or .env file."
  }, {
    k: "env_file",
    label: "env_file",
    ex: "Load environment variables from a file instead of listing them inline. Better for many values or secrets you keep out of the YAML and gitignore. Note: the project .env file is for interpolation in the compose file itself, env_file injects into the container."
  }, {
    k: "volumes",
    label: "volumes",
    ex: "Two kinds here. app-data:/var/lib/data is a named volume (Docker-managed, persists, declared at the bottom). ./src:/app/src is a bind mount (your host path). Named volumes for data that must survive, bind mounts for live dev."
  }, {
    k: "depends_on",
    label: "depends_on",
    ex: "Startup ordering. condition: service_healthy waits until db passes its healthcheck before starting web, so the app does not race a database that is not ready yet. service_started only waits for the container to launch."
  }, {
    k: "healthcheck",
    label: "healthcheck",
    ex: "How Compose decides a service is healthy. The test command runs on an interval; depends_on: service_healthy hooks into this. start_period gives the container a grace window before failures count."
  }, {
    k: "networks",
    label: "networks",
    ex: "Compose auto-creates one bridge network (myapp_default) and joins every service. Declaring your own networks lets you isolate tiers: put db only on backend so the outside world cannot reach it directly."
  }, {
    k: "deploy",
    label: "deploy / GPU",
    ex: "resources.reservations.devices with driver: nvidia, count: all, capabilities: [gpu] is the official way to give a service GPUs (needs the NVIDIA Container Toolkit on the host). resources.limits (cpus, memory) also works in standalone mode; placement and replicas are Swarm-only."
  }] : [{
    k: "name",
    label: "name",
    ex: "專案名。會成為容器（myapp-web-1）、網路（myapp_default）、volume 的命名前綴。預設取目錄名，用 -p 或 COMPOSE_PROJECT_NAME 覆寫。注意：已經沒有 version: 欄位了，它在 Compose Specification 已 obsolete。"
  }, {
    k: "services",
    label: "services",
    ex: "組成應用的各個容器。每個服務名（web、db）同時是它在專案網路上的 DNS hostname，所以 web 直接用 db:5432 連資料庫，不必管 IP。"
  }, {
    k: "build",
    label: "build",
    ex: "從 Dockerfile 自己 build 映像，而非 pull 現成的。build: . 以當前目錄為 context；長語法可帶 context、dockerfile、args、target（multi-stage build 要停在哪個 stage）。"
  }, {
    k: "image",
    label: "image",
    ex: "從 registry 拉現成映像。固定 tag（postgres:18），別用 latest。一個服務用 build 或 image 二擇一（或 build 加 image 來替產物命名）。"
  }, {
    k: "ports",
    label: "ports",
    ex: "把容器埠發布到主機，格式 HOST:CONTAINER。\"8080:80\" 把主機 8080 對應到容器 80。不寫的話這個埠只在 Compose 網路內部連得到，你的電腦連不到。"
  }, {
    k: "environment",
    label: "environment",
    ex: "直接寫進容器的環境變數。少量、可見的值用這個；支援 ${VAR} 與 ${VAR:-default}，從 shell 或 .env 檔插值。"
  }, {
    k: "env_file",
    label: "env_file",
    ex: "從檔案讀環境變數，不逐條寫在 YAML。變數多、或想把機密抽出 YAML 並 gitignore 時用。注意：專案根的 .env 是給 compose 檔本身插值用，env_file 才是注入容器。"
  }, {
    k: "volumes",
    label: "volumes",
    ex: "這裡有兩種。app-data:/var/lib/data 是具名 volume（Docker 管理、會持久化、最底下宣告）。./src:/app/src 是 bind mount（你的主機路徑）。要保留的資料用具名 volume，開發即時同步用 bind mount。"
  }, {
    k: "depends_on",
    label: "depends_on",
    ex: "啟動順序。condition: service_healthy 會等 db 通過 healthcheck 才啟動 web，避免應用搶在資料庫還沒 ready 時連線失敗。service_started 只等容器啟動、不保證 ready。"
  }, {
    k: "healthcheck",
    label: "healthcheck",
    ex: "Compose 判斷服務健康的依據。test 指令定時跑；depends_on: service_healthy 就是接這個。start_period 給容器一段啟動緩衝期，期間失敗不計。"
  }, {
    k: "networks",
    label: "networks",
    ex: "Compose 預設建一個 bridge 網路（myapp_default）讓所有服務加入。自己宣告網路可以隔離分層：把 db 只放在 backend，外界就無法直接連到它。"
  }, {
    k: "deploy",
    label: "deploy / GPU",
    ex: "resources.reservations.devices 配 driver: nvidia、count: all、capabilities: [gpu] 是官方給服務分配 GPU 的寫法（主機需裝 NVIDIA Container Toolkit）。resources.limits（cpus、memory）在 standalone 模式也生效；placement、replicas 屬 Swarm 專用。"
  }];
  const [sel, setSel] = useState("services");
  const cur = KEYS.find(x => x.k === sel) || KEYS[0];
  const css = `
  .ca-root{--ca-bg:#FAF8F3;--ca-surface:rgba(0,0,0,0.025);--ca-border:rgba(0,0,0,0.09);--ca-text:#2b2722;--ca-dim:#6f6a62;--ca-faint:#8a8378;--ca-accent:#bf7551;--ca-scr:#1c1815;--ca-scr-fg:#e9e1d4;--ca-scr-dim:#8d8478;border:1px solid var(--ca-border);border-radius:14px;background:var(--ca-bg);color:var(--ca-text);overflow:hidden;font-family:ui-sans-serif,system-ui,"Noto Sans TC",sans-serif;}
  .dark .ca-root{--ca-bg:#1b1a18;--ca-surface:rgba(255,255,255,0.03);--ca-border:rgba(255,255,255,0.08);--ca-text:#e7e3da;--ca-dim:#a8a299;--ca-faint:#8a8378;--ca-accent:#cf8a68;}
  .ca-head{padding:13px 18px 11px;border-bottom:1px solid var(--ca-border);font-size:12.5px;color:var(--ca-dim);display:flex;align-items:center;gap:8px;}
  .ca-head-ic{color:var(--ca-accent);flex-shrink:0;}
  .ca-chips{display:flex;gap:6px;padding:12px 14px 4px;flex-wrap:wrap;}
  .ca-chip{border:1px solid var(--ca-border);background:transparent;border-radius:20px;padding:4px 11px;font-size:12px;font-weight:600;cursor:pointer;color:var(--ca-dim);font-family:ui-monospace,"Cascadia Code","Consolas",monospace;transition:all .15s;}
  .ca-chip:hover{border-color:var(--ca-accent);color:var(--ca-accent);}
  .ca-chip-on{background:var(--ca-accent);border-color:var(--ca-accent);color:#fff;}
  .dark .ca-chip-on{color:#1b1a18;}
  .ca-grid{display:flex;gap:0;align-items:stretch;flex-wrap:wrap;}
  .ca-code{flex:1 1 340px;min-width:0;background:var(--ca-scr);padding:12px 4px 14px;overflow-x:auto;}
  .ca-line{display:block;font-family:ui-monospace,"Cascadia Code","Consolas",monospace;font-size:12.5px;line-height:1.55;white-space:pre;padding:0 14px;border-left:3px solid transparent;color:var(--ca-scr-fg);}
  .ca-line-k{cursor:pointer;}
  .ca-line-k:hover{background:rgba(255,255,255,.05);}
  .ca-line-on{background:rgba(191,117,81,.18);border-left-color:var(--ca-accent);}
  .ca-line-empty{color:var(--ca-scr-dim);}
  .ca-panel{width:300px;flex:1 1 250px;border-left:1px solid var(--ca-border);background:var(--ca-surface);padding:16px 18px;}
  .dark .ca-panel{border-left-color:var(--ca-border);}
  .ca-panel-k{font-size:14px;font-weight:700;color:var(--ca-accent);font-family:ui-monospace,"Cascadia Code","Consolas",monospace;margin-bottom:8px;}
  .ca-panel-ex{font-size:13px;line-height:1.65;color:var(--ca-dim);}
  @media (max-width:640px){.ca-panel{width:auto;border-left:none;border-top:1px solid var(--ca-border);}}
  `;
  return <div className="ca-root">
      <style>{css}</style>
      <div className="ca-head">
        <svg className="ca-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></svg>
        {t.lead}
      </div>
      <div className="ca-chips">
        {KEYS.map(x => <button key={x.k} type="button" className={"ca-chip" + (x.k === sel ? " ca-chip-on" : "")} onClick={() => setSel(x.k)}>
            {x.label}
          </button>)}
      </div>
      <div className="ca-grid">
        <div className="ca-code">
          {LINES.map((ln, i) => {
    if (!ln.t) return <span className="ca-line ca-line-empty" key={i}> </span>;
    const clickable = !!ln.k;
    const on = ln.k === sel;
    return <span key={i} className={"ca-line" + (clickable ? " ca-line-k" : "") + (on ? " ca-line-on" : "")} onClick={clickable ? () => setSel(ln.k) : undefined}>
                {ln.t}
              </span>;
  })}
        </div>
        <div className="ca-panel">
          <div className="ca-panel-k">{cur.label}</div>
          <div className="ca-panel-ex">{cur.ex}</div>
        </div>
      </div>
    </div>;
};

`Docker Compose`

單一 `docker run` 帶一堆旗標難維護，多個容器更是。Docker Compose 用一份 `compose.yaml` 把整套應用（多服務、網路、volume）宣告起來，一個指令拉起整組。

<Warning>
  現行版本是 `docker compose`（v2，空格，無連字號），已內建於 Docker CLI。舊的 `docker-compose`（v1，Python 版）**已 EOL、不再維護，不要再用**。`compose.yaml` 也**不需要也不要寫 `version:` 頂層欄位**，它在 Compose Specification 已 obsolete，寫了只會收到過時警告。
</Warning>

點 `compose.yaml` 裡的鍵，看每個區塊在做什麼：

<ComposeAnatomy lang="zh" />

## 頂層結構

```yaml theme={null}
name: myapp          # 專案名（可選）

services:            # 必要，各個容器
  web:
    image: nginx
networks:            # 可選，自訂網路
volumes:             # 可選，具名 volume
configs:             # 可選，非敏感設定檔
secrets:             # 可選，敏感資料
```

## service 常用鍵

| 鍵                          | 用途                                                               |
| -------------------------- | ---------------------------------------------------------------- |
| `image`                    | 拉現成映像（固定 tag，別 latest）                                           |
| `build`                    | 從 Dockerfile build（`context` / `dockerfile` / `args` / `target`） |
| `ports`                    | 發布埠，`主機:容器`                                                      |
| `environment` / `env_file` | 環境變數（內聯 / 從檔案）                                                   |
| `volumes`                  | bind mount 與具名 volume                                            |
| `depends_on`               | 啟動順序（配 `condition: service_healthy`）                             |
| `healthcheck`              | 健康檢查指令                                                           |
| `restart`                  | 重啟策略（`no` / `always` / `on-failure` / `unless-stopped`）          |
| `networks`                 | 加入哪些網路                                                           |
| `command` / `entrypoint`   | 覆蓋映像的預設指令 / 進入點                                                  |
| `profiles`                 | 把服務歸入 profile，預設不啟動                                              |
| `deploy`                   | 資源限制與 GPU                                                        |

## 建立網路

`docker compose up` 會自動建一個專案專屬的 bridge 網路（命名 `<專案名>_default`），所有服務自動加入。**服務名就是它在這個網路上的 DNS hostname**，所以 `web` 服務直接用 `db:5432` 連資料庫，不必管 IP。要分層隔離就自己宣告網路：

```yaml theme={null}
services:
  proxy:
    networks: [frontend]
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]          # 只在 backend，proxy 連不到

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
```

`proxy` 與 `db` 不同網路無法直接通訊，只有 `app` 能同時接觸兩者。

## 容器群組（專案）名稱

Compose 用「專案名」把這組資源綁在一起，並當命名前綴：

| 資源     | 命名格式                | 範例（專案 `myapp`、服務 `web`） |
| ------ | ------------------- | ----------------------- |
| 容器     | `<專案>-<服務>-<index>` | `myapp-web-1`           |
| 網路     | `<專案>_<網路>`         | `myapp_default`         |
| Volume | `<專案>_<volume>`     | `myapp_db-data`         |

優先序（高到低）：`docker compose -p <name>` > `COMPOSE_PROJECT_NAME` 環境變數 > 頂層 `name:` > `compose.yaml` 所在目錄名。

## 環境設定參數

| 寫法             | 適用                                   |
| -------------- | ------------------------------------ |
| `environment:` | 少量、可見的變數，直接寫在 YAML，支援 `${VAR}` 插值    |
| `env_file:`    | 變數多、或機密想抽出 YAML 並 gitignore          |
| 專案根的 `.env`    | 給 `compose.yaml` 本身插值用（**不是**自動注入容器） |

插值語法：`${VAR}` 取值、`${VAR:-default}` 未設定或為空用預設、`${VAR:?msg}` 未設定就報錯中止。容器內環境變數的優先序（高到低）：`docker compose run -e` > `environment:` > `env_file:` > 宿主 shell 繼承 > Dockerfile `ENV`。

## 使用 GPU

官方寫法是在服務的 `deploy.resources.reservations.devices` 宣告（主機需先裝 **NVIDIA Container Toolkit**）：

```yaml theme={null}
services:
  trainer:
    image: nvidia/cuda:12.9.0-base-ubuntu22.04
    command: nvidia-smi
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all              # 或 count: 1 / device_ids: ['0','3']
              capabilities: [gpu]     # 必填，缺了部署會報錯
```

## 效能與啟動順序

* **`depends_on` 配 `healthcheck`**：用 `condition: service_healthy` 等依賴服務真的 ready 才啟動，避免應用搶在資料庫還沒起來時連線失敗。
* **資源限制**：`deploy.resources.limits` 的 `cpus` / `memory` 在 standalone（非 Swarm）模式也生效。
* **`pull_policy`** 與 build cache：控制要不要每次都 pull、build 重用快取。
* 啟動相關：`up -d`（背景）、`--build`（先重 build）、`--scale web=3`（擴成 3 份）、`--wait`（等到全部 healthy）。

```yaml theme={null}
services:
  app:
    depends_on:
      db:
        condition: service_healthy
  db:
    image: postgres:18
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
```

## volume 與 bind mount 映射

Compose 的 `volumes:` 同時管 bind mount 與具名 volume，靠來源寫法區分（差別見 [資料落點](/notes/docker/guide/storage/)）：

```yaml theme={null}
services:
  app:
    volumes:
      - ./src:/app/src              # bind mount：主機相對路徑
      - ./config.yaml:/app/config.yaml:ro   # 唯讀
      - app-data:/var/lib/data      # 具名 volume（需頂層宣告）

volumes:
  app-data:
```

要保留的資料用具名 volume，開發即時同步用 bind mount。不在 `volumes:` 指定的路徑，寫進去就只在容器可寫層，容器一刪就沒。

## 一份完整的 `compose.yaml`

```yaml theme={null}
# compose.yaml — 不需要也不要寫 version 欄位（已 obsolete）
name: myapp

services:
  web:
    build:
      context: .
      target: production
    ports:
      - "8080:80"
    environment:
      DATABASE_URL: "postgresql://db:5432/${DB_NAME:-mydb}"
    env_file:
      - path: .env
        required: false
    volumes:
      - ./static:/app/static:ro
      - upload-data:/app/uploads
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped
    networks: [frontend, backend]

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME:-mydb}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks: [backend]

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

volumes:
  db-data:
  upload-data:
```

## `docker compose` 常用指令

```bash theme={null}
docker compose up -d            # 背景拉起整組
docker compose up -d --build    # 先重 build 再起
docker compose ps               # 看這個專案的容器
docker compose logs -f web      # 即時看 web 服務日誌
docker compose exec web sh      # 進 web 服務的 shell
docker compose config           # 展開並驗證 compose 檔（成功回 exit 0）
docker compose build --no-cache # 不用快取重 build
docker compose down             # 停止並移除容器與網路
```

<Note>
  上面的指令都假設當前目錄有預設檔名。`docker compose` 預設依序尋找 `compose.yaml`（首選）、`compose.yml`、`docker-compose.yaml`、`docker-compose.yml`（從工作目錄逐層往上找）。**檔名不是預設時（例如 `docker-compose.dev.yml`、`my-stack.yaml`），就要用 `-f` 明確指定**。`-f` 屬頂層選項，放在子指令（`up` / `down` …）之前；多個 `-f` 依宣告順序合併（後者覆寫前者）。也可改用 `COMPOSE_FILE` 環境變數。官方 CLI 文件即如此定義。
</Note>

```bash theme={null}
docker compose -f my-stack.yaml up -d                     # 指定非預設檔名
docker compose -f compose.yaml -f compose.dev.yaml up -d  # 多檔依序合併（後者覆寫）
```

一份 `compose.yaml` 起整組長這樣：

<TerminalDemo
  lang="zh"
  title="Ubuntu (WSL 2)"
  prompt="user@wsl:~/myapp$"
  commands={[
{
  cmd: "docker compose up -d",
  output: [
    "[+] Running 4/4",
    " ✔ Network myapp_default     Created",
    " ✔ Volume myapp_db-data      Created",
    " ✔ Container myapp-db-1      Started",
    " ✔ Container myapp-web-1     Started",
  ],
},
{
  cmd: "docker compose ps",
  output: [
    "NAME           IMAGE          STATUS                   PORTS",
    "myapp-db-1     postgres:18    Up (healthy)             5432/tcp",
    "myapp-web-1    myapp-web      Up 5 seconds             0.0.0.0:8080->80/tcp",
  ],
},
]}
/>

<Warning>
  **`docker compose down -v` 會刪掉具名 volume**（`compose.yaml` 的 `volumes:` 區段宣告的那些），資料庫資料常在裡面，下去就沒了。只想停服務、保留資料用不帶 `-v` 的 `docker compose down`。
</Warning>

## 接下來

* [資料落點](/notes/docker/guide/storage/)：volume 與 bind mount 的差別。
* [如何撰寫 Dockerfile](/notes/docker/dockerfile/basics/)：`build:` 指向的 Dockerfile 怎麼寫。

官方參考：[docs.docker.com/reference/compose-file](https://docs.docker.com/reference/compose-file/)、[Compose GPU 支援](https://docs.docker.com/compose/how-tos/gpu-support/)
