> ## 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

> Declare a multi-container app in one compose.yaml: services, networks, project name, env vars, GPUs, performance, volume mapping, and the 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`

A single `docker run` with a pile of flags is hard to maintain, and multiple containers more so. Docker Compose declares the whole app (services, networks, volumes) in one `compose.yaml` and brings it all up with one command.

<Warning>
  The current version is `docker compose` (v2, a space, no hyphen), built into the Docker CLI. The old `docker-compose` (the Python v1) is **EOL, no longer maintained, do not use it**. A `compose.yaml` also **does not need and should not include a `version:` top-level field**; it is obsolete in the Compose Specification and only produces an obsolescence warning.
</Warning>

Click the keys in `compose.yaml` to see what each block does:

<ComposeAnatomy lang="en" />

## Top-level structure

```yaml theme={null}
name: myapp          # project name (optional)

services:            # required, the containers
  web:
    image: nginx
networks:            # optional, custom networks
volumes:             # optional, named volumes
configs:             # optional, non-sensitive config
secrets:             # optional, sensitive data
```

## Common service keys

| Key                        | Purpose                                                                |
| -------------------------- | ---------------------------------------------------------------------- |
| `image`                    | Pull a prebuilt image (pin a tag, not latest)                          |
| `build`                    | Build from a Dockerfile (`context` / `dockerfile` / `args` / `target`) |
| `ports`                    | Publish ports, `HOST:CONTAINER`                                        |
| `environment` / `env_file` | Env vars (inline / from file)                                          |
| `volumes`                  | bind mounts and named volumes                                          |
| `depends_on`               | Startup order (with `condition: service_healthy`)                      |
| `healthcheck`              | Health-check command                                                   |
| `restart`                  | Restart policy (`no` / `always` / `on-failure` / `unless-stopped`)     |
| `networks`                 | Which networks to join                                                 |
| `command` / `entrypoint`   | Override the image's default command / entry point                     |
| `profiles`                 | Group a service into a profile, off by default                         |
| `deploy`                   | Resource limits and GPUs                                               |

## Creating networks

`docker compose up` automatically creates a project-specific bridge network (named `<project>_default`) and joins every service. **A service name is its DNS hostname on that network**, so `web` reaches the database at `db:5432`, no IP needed. To isolate tiers, declare your own networks:

```yaml theme={null}
services:
  proxy:
    networks: [frontend]
  app:
    networks: [frontend, backend]
  db:
    networks: [backend]          # backend only, proxy cannot reach it

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

`proxy` and `db` are on different networks and cannot talk directly; only `app` touches both.

## Container group (project) name

Compose ties the set together with a "project name" and uses it as a naming prefix:

| Resource  | Naming format                 | Example (project `myapp`, service `web`) |
| --------- | ----------------------------- | ---------------------------------------- |
| Container | `<project>-<service>-<index>` | `myapp-web-1`                            |
| Network   | `<project>_<network>`         | `myapp_default`                          |
| Volume    | `<project>_<volume>`          | `myapp_db-data`                          |

Precedence (high to low): `docker compose -p <name>` > `COMPOSE_PROJECT_NAME` env var > top-level `name:` > the directory containing `compose.yaml`.

## Environment configuration

| Form               | Use for                                                                                 |
| ------------------ | --------------------------------------------------------------------------------------- |
| `environment:`     | A few visible variables written inline, supports `${VAR}` interpolation                 |
| `env_file:`        | Many variables, or secrets you keep out of the YAML and gitignore                       |
| The project `.env` | Interpolation in `compose.yaml` itself (it does **not** auto-inject into the container) |

Interpolation: `${VAR}`, `${VAR:-default}` (fall back when unset or empty), `${VAR:?msg}` (error out when unset). In-container env var precedence (high to low): `docker compose run -e` > `environment:` > `env_file:` > host shell inheritance > Dockerfile `ENV`.

## Using GPUs

The official way is to declare it under a service's `deploy.resources.reservations.devices` (the host needs the **NVIDIA Container Toolkit** first):

```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              # or count: 1 / device_ids: ['0','3']
              capabilities: [gpu]     # required, deployment errors without it
```

## Performance and startup order

* **`depends_on` with `healthcheck`**: use `condition: service_healthy` to wait until a dependency is actually ready, so the app does not race a database that is not up yet.
* **Resource limits**: `deploy.resources.limits` `cpus` / `memory` also take effect in standalone (non-Swarm) mode.
* **`pull_policy`** and build cache: control whether to pull every time and reuse build cache.
* Startup: `up -d` (background), `--build` (rebuild first), `--scale web=3` (run 3 copies), `--wait` (wait until everything is 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 and bind mount mapping

Compose's `volumes:` handles both bind mounts and named volumes, distinguished by how the source is written (see [Where data lives](/en/notes/docker/guide/storage/)):

```yaml theme={null}
services:
  app:
    volumes:
      - ./src:/app/src              # bind mount: relative host path
      - ./config.yaml:/app/config.yaml:ro   # read-only
      - app-data:/var/lib/data      # named volume (declared at the top level)

volumes:
  app-data:
```

Use named volumes for data that must persist and bind mounts for live-syncing source. A path with no mount declared only writes to the container writable layer and is gone when the container is removed.

## A complete `compose.yaml`

```yaml theme={null}
# compose.yaml — no version field needed or wanted (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:
```

## Common `docker compose` commands

```bash theme={null}
docker compose up -d            # bring the whole set up in the background
docker compose up -d --build    # rebuild then start
docker compose ps               # containers in this project
docker compose logs -f web      # follow the web service logs
docker compose exec web sh      # shell into the web service
docker compose config           # expand and validate the compose file (exit 0 on success)
docker compose build --no-cache # rebuild without cache
docker compose down             # stop and remove containers and networks
```

<Note>
  The commands above all assume a default filename in the current directory. `docker compose` looks for `compose.yaml` (preferred), `compose.yml`, `docker-compose.yaml`, then `docker-compose.yml`, searching the working directory and walking up to parent directories. **When the filename is not a default (for example `docker-compose.dev.yml` or `my-stack.yaml`), you must name it explicitly with `-f`.** `-f` is a top-level option and goes before the subcommand (`up` / `down` ...); multiple `-f` flags merge in order (later overrides earlier). The `COMPOSE_FILE` environment variable does the same. This is exactly how the official CLI docs define it.
</Note>

```bash theme={null}
docker compose -f my-stack.yaml up -d                     # name a non-default file
docker compose -f compose.yaml -f compose.dev.yaml up -d  # merge in order (later wins)
```

Bringing a `compose.yaml` up looks like this:

<TerminalDemo
  lang="en"
  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` deletes named volumes** (the ones declared in the `volumes:` section), and database data often lives there: once gone, it is gone. To only stop services and keep data, use `docker compose down` without `-v`.
</Warning>

## Next

* [Where data lives](/en/notes/docker/guide/storage/): the difference between volumes and bind mounts.
* [How to write a Dockerfile](/en/notes/docker/dockerfile/basics/): writing the Dockerfile that `build:` points to.

Reference: [docs.docker.com/reference/compose-file](https://docs.docker.com/reference/compose-file/), [Compose GPU support](https://docs.docker.com/compose/how-tos/gpu-support/)
