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

# 資料落點：volume、bind mount、tmpfs 與不掛載

> 容器資料存在哪、容器刪了還在不在：可寫層、volume、bind mount、tmpfs 的差別，與 WSL 2 的掛載效能。

export const StorageMounts = ({lang = "zh"}) => {
  const t = lang === "en" ? {
    lead: "Pick a mount mode to see where the data lives.",
    loc: "Where data lives",
    survive: "Survives container rm",
    share: "Shareable across containers",
    perf: "Performance",
    use: "Best for",
    yes: "Yes",
    no: "No",
    cwrite: "Container"
  } : {
    lead: "撥一種掛載模式，看資料實際存在哪。",
    loc: "資料存哪",
    survive: "容器刪除後還在",
    share: "可跨容器共享",
    perf: "效能",
    use: "適用場景",
    yes: "在",
    no: "不在",
    cwrite: "容器"
  };
  const MODES = lang === "en" ? [{
    key: "none",
    name: "No mount",
    sub: "writable layer",
    loc: "Container writable layer (copy-on-write, on top of the image layers)",
    locTag: "Writable layer",
    survive: false,
    share: false,
    perf: "Slower for writes: copy-on-write copies the whole file even for a 1-byte change",
    use: "Throwaway data you never need to keep",
    note: "Anything written without a mount lives only inside the container. docker rm wipes it."
  }, {
    key: "volume",
    name: "Volume",
    sub: "named",
    loc: "/var/lib/docker/volumes/<name>/_data  (under the WSL 2 backend it sits inside the docker-desktop-data VM, not on the Windows filesystem)",
    locTag: "Docker-managed",
    survive: true,
    share: true,
    perf: "Same as native host filesystem; no cross-OS boundary",
    use: "Databases, persistent app data, backup and migration",
    note: "Docker manages it. Survives container deletion until you docker volume rm it. The recommended default for persistence."
  }, {
    key: "bind",
    name: "Bind mount",
    sub: "host path",
    loc: "Any host path you point at (absolute path)",
    locTag: "Host path",
    survive: true,
    share: true,
    perf: "Native on Linux; mounting from /mnt/c (Windows) under WSL 2 is noticeably slower and inotify is unreliable",
    use: "Live-syncing source code in dev, injecting config files",
    note: "The host file stays after the container is gone. With -v a missing relative path becomes a named volume, not a bind mount: use --mount type=bind to avoid the trap."
  }, {
    key: "tmpfs",
    name: "tmpfs",
    sub: "in memory",
    loc: "Host RAM only, never written to disk",
    locTag: "Host memory",
    survive: false,
    share: false,
    perf: "Fastest (RAM I/O)",
    use: "Secrets you do not want on disk, ephemeral scratch cache; Linux only",
    note: "Gone the moment the container stops. Cannot be shared between containers."
  }] : [{
    key: "none",
    name: "無掛載",
    sub: "可寫層",
    loc: "容器可寫層（writable layer，疊在映像層之上，走 copy-on-write）",
    locTag: "可寫層",
    survive: false,
    share: false,
    perf: "寫入較慢：改一個 byte 也要整檔複製（copy-on-write）",
    use: "用完即丟、完全不需要保留的資料",
    note: "沒掛載時寫的東西只活在容器裡，docker rm 就一起消失。"
  }, {
    key: "volume",
    name: "Volume",
    sub: "具名",
    loc: "/var/lib/docker/volumes/<name>/_data（WSL 2 後端下這條路徑在 docker-desktop-data VM 裡，不在 Windows 檔案系統）",
    locTag: "Docker 管理",
    survive: true,
    share: true,
    perf: "等同主機檔案系統原生速度，不跨 OS 邊界",
    use: "資料庫、需持久化的應用資料、備份與遷移",
    note: "由 Docker 管理。容器刪掉 volume 仍在，要明確 docker volume rm 才會消失。持久化的預設首選。"
  }, {
    key: "bind",
    name: "Bind mount",
    sub: "主機路徑",
    loc: "你指定的任一主機路徑（絕對路徑）",
    locTag: "主機路徑",
    survive: true,
    share: true,
    perf: "Linux 原生速度；WSL 2 下從 /mnt/c（Windows）掛載會明顯變慢，且 inotify 不可靠",
    use: "開發時即時同步程式碼、注入設定檔",
    note: "容器刪了主機檔案還在。用 -v 時，找不到的相對路徑會被當成具名 volume 而非 bind mount，改用 --mount type=bind 杜絕這個雷。"
  }, {
    key: "tmpfs",
    name: "tmpfs",
    sub: "記憶體",
    loc: "只在主機記憶體（RAM），不落磁碟",
    locTag: "主機記憶體",
    survive: false,
    share: false,
    perf: "最快（RAM I/O）",
    use: "不想落地的敏感憑證、暫存快取；僅限 Linux",
    note: "容器一停就消失，不能跨容器共享。"
  }];
  const [sel, setSel] = useState(1);
  const idx = Math.min(sel, MODES.length - 1);
  const cur = MODES[idx];
  const css = `
  .sm-root{--sm-bg:#FAF8F3;--sm-surface:rgba(0,0,0,0.025);--sm-border:rgba(0,0,0,0.09);--sm-text:#2b2722;--sm-dim:#6f6a62;--sm-faint:#8a8378;--sm-accent:#bf7551;--sm-ok:#5f8a52;--sm-warn:#c0792f;border:1px solid var(--sm-border);border-radius:14px;background:var(--sm-bg);color:var(--sm-text);overflow:hidden;font-family:ui-sans-serif,system-ui,"Noto Sans TC",sans-serif;}
  .dark .sm-root{--sm-bg:#1b1a18;--sm-surface:rgba(255,255,255,0.03);--sm-border:rgba(255,255,255,0.08);--sm-text:#e7e3da;--sm-dim:#a8a299;--sm-faint:#8a8378;--sm-accent:#cf8a68;--sm-ok:#86b274;--sm-warn:#d7a043;}
  .sm-head{padding:13px 18px 11px;border-bottom:1px solid var(--sm-border);font-size:12.5px;color:var(--sm-dim);display:flex;align-items:center;gap:8px;}
  .sm-head-ic{color:var(--sm-accent);flex-shrink:0;}
  .sm-tabs{display:flex;gap:8px;padding:14px 16px 4px;flex-wrap:wrap;}
  .sm-tab{flex:1 1 110px;min-width:104px;text-align:left;background:transparent;border:1px solid var(--sm-border);border-radius:11px;padding:10px 12px;cursor:pointer;color:inherit;font:inherit;transition:border-color .15s,background .15s,box-shadow .15s;}
  .sm-tab:hover{background:var(--sm-surface);}
  .sm-tab-on{border-color:rgba(191,117,81,.5);background:rgba(191,117,81,.07);box-shadow:0 0 0 3px rgba(191,117,81,.1);}
  .sm-tab-name{font-size:14px;font-weight:650;line-height:1.25;}
  .sm-tab-on .sm-tab-name{color:var(--sm-accent);}
  .sm-tab-sub{font-size:11.5px;color:var(--sm-faint);margin-top:2px;}
  .sm-diagram{display:flex;align-items:center;gap:10px;padding:14px 16px 4px;}
  .sm-node{flex:1 1 0;min-width:0;border:1px solid var(--sm-border);border-radius:10px;padding:10px 12px;background:var(--sm-surface);}
  .sm-node-l{font-size:11px;text-transform:uppercase;letter-spacing:.4px;color:var(--sm-faint);margin-bottom:3px;}
  .sm-node-v{font-size:13.5px;font-weight:600;}
  .sm-arrow{flex-shrink:0;display:flex;flex-direction:column;align-items:center;color:var(--sm-dim);font-size:10.5px;gap:1px;}
  .sm-arrow svg{width:26px;height:18px;}
  .sm-loc-ok{border-color:rgba(95,138,82,.4);background:rgba(95,138,82,.08);}
  .sm-loc-eph{border-color:rgba(192,121,47,.4);background:rgba(192,121,47,.08);}
  .sm-loc-ok .sm-node-v{color:var(--sm-ok);}
  .sm-loc-eph .sm-node-v{color:var(--sm-warn);}
  .sm-body{padding:8px 16px 16px;}
  .sm-specs{border:1px solid var(--sm-border);border-radius:10px;overflow:hidden;margin-bottom:11px;}
  .sm-row{display:flex;border-top:1px solid var(--sm-border);font-size:13px;}
  .sm-row:first-child{border-top:none;}
  .sm-k{width:34%;flex-shrink:0;padding:8px 12px;color:var(--sm-dim);background:var(--sm-surface);}
  .sm-v{flex:1 1 0;min-width:0;padding:8px 12px;font-weight:500;}
  .sm-pill{display:inline-flex;align-items:center;gap:5px;font-size:12.5px;font-weight:650;}
  .sm-pill-ok{color:var(--sm-ok);}
  .sm-pill-no{color:var(--sm-warn);}
  .sm-dot{width:8px;height:8px;border-radius:50%;display:inline-block;}
  .sm-dot-ok{background:var(--sm-ok);}
  .sm-dot-no{background:var(--sm-warn);}
  .sm-note{font-size:13px;line-height:1.6;color:var(--sm-dim);padding:0 2px;}
  @media (max-width:600px){.sm-diagram{flex-direction:column;align-items:stretch;}.sm-arrow{flex-direction:row;}.sm-arrow svg{transform:rotate(90deg);}.sm-k{width:42%;}}
  `;
  const Pill = ({ok}) => <span className={"sm-pill " + (ok ? "sm-pill-ok" : "sm-pill-no")}>
      <span className={"sm-dot " + (ok ? "sm-dot-ok" : "sm-dot-no")} />
      {ok ? t.yes : t.no}
    </span>;
  const locClass = cur.survive ? "sm-loc-ok" : "sm-loc-eph";
  return <div className="sm-root">
      <style>{css}</style>
      <div className="sm-head">
        <svg className="sm-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"><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14a9 3 0 0 0 18 0V5" /><path d="M3 12a9 3 0 0 0 18 0" /></svg>
        {t.lead}
      </div>
      <div className="sm-tabs">
        {MODES.map((m, i) => <button key={m.key} type="button" className={"sm-tab" + (i === idx ? " sm-tab-on" : "")} onClick={() => setSel(i)}>
            <div className="sm-tab-name">{m.name}</div>
            <div className="sm-tab-sub">{m.sub}</div>
          </button>)}
      </div>
      <div className="sm-diagram">
        <div className="sm-node">
          <div className="sm-node-l">{t.cwrite}</div>
          <div className="sm-node-v">docker run …</div>
        </div>
        <div className="sm-arrow">
          <svg viewBox="0 0 26 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="2" y1="9" x2="22" y2="9" /><polyline points="16,3 22,9 16,15" /></svg>
          <span>write</span>
        </div>
        <div className={"sm-node " + locClass}>
          <div className="sm-node-l">{t.loc}</div>
          <div className="sm-node-v">{cur.locTag}</div>
        </div>
      </div>
      <div className="sm-body">
        <div className="sm-specs">
          <div className="sm-row"><div className="sm-k">{t.loc}</div><div className="sm-v">{cur.loc}</div></div>
          <div className="sm-row"><div className="sm-k">{t.survive}</div><div className="sm-v"><Pill ok={cur.survive} /></div></div>
          <div className="sm-row"><div className="sm-k">{t.share}</div><div className="sm-v"><Pill ok={cur.share} /></div></div>
          <div className="sm-row"><div className="sm-k">{t.perf}</div><div className="sm-v">{cur.perf}</div></div>
          <div className="sm-row"><div className="sm-k">{t.use}</div><div className="sm-v">{cur.use}</div></div>
        </div>
        <div className="sm-note">{cur.note}</div>
      </div>
    </div>;
};

`Docker` `Volume` `bind mount`

容器一刪，寫進它「可寫層」的東西就沒了。要留住資料，得把資料放到容器之外。這頁把四種落點講清楚。撥下面四種模式，看資料實際落在哪、容器刪掉還在不在：

<StorageMounts lang="zh" />

## 不掛載時資料在容器可寫層

容器在唯讀映像層之上有一層「可寫層」。沒掛載時，寫進去的檔案都落在這層，走 **copy-on-write**（CoW）：改一個既有檔案時，先把**整個檔案**從唯讀層複製到可寫層，再改副本。

* 改一個 1 GB 檔案的一個 byte，也要先整檔複製，**寫入密集會明顯變慢**。
* 可寫層跟著容器生命週期，`docker rm` 就全沒。

所以資料庫、需要保留的資料、大量 I/O 的工作負載，都不該只待在容器可寫層。

## 三種掛載比較

| 維度        | Volume                                 | Bind mount    | tmpfs               |
| --------- | -------------------------------------- | ------------- | ------------------- |
| 定義        | Docker 管理的具名儲存區                        | 主機路徑直接映射進容器   | 存於主機記憶體，不落磁碟        |
| 資料存哪      | `/var/lib/docker/volumes/<name>/_data` | 你指定的主機路徑      | 主機 RAM              |
| 容器刪後資料在？  | 在                                      | 在（主機上）        | 不在                  |
| 跨容器共享     | 可                                      | 可             | 不可                  |
| 效能（Linux） | 等同主機檔案系統原生                             | 等同主機原生        | 最快（RAM I/O）         |
| 適用        | 資料庫、持久化資料、備份遷移                         | 開發同步程式碼、注入設定檔 | 敏感憑證、暫存快取（僅限 Linux） |

## 具名 volume 的實體位置

Linux 上：

```bash theme={null}
docker volume inspect my-vol
# "Mountpoint": "/var/lib/docker/volumes/my-vol/_data"
```

WSL 2 後端下，這條路徑在 **`docker-desktop-data`** 這個 WSL 虛擬機裡，**不在 Windows 檔案系統**，從檔案總管直接點不進去。要讀 volume 內容：

```bash theme={null}
docker run --rm -v my-vol:/data alpine ls /data    # 用容器掛上去看
```

## bind mount：`-v` 與 `--mount`

兩種語法做同一件事，但 `--mount` 語義明確、官方建議優先：

```bash theme={null}
# -v：簡短，但歧義多
docker run -v /host/data:/app/data:ro app

# --mount：明確指定 type，杜絕歧義
docker run --mount type=bind,source=/host/data,target=/app/data,readonly app
```

| 行為             | `--mount type=bind` | `-v`                  |
| -------------- | ------------------- | --------------------- |
| 主機路徑不存在        | 報錯，拒絕執行             | 自動建一個空目錄              |
| 相對路徑（`./data`） | 當 bind mount        | **可能被當成具名 volume 名稱** |

<Warning>
  **`-v` 的相對路徑雷**：`-v mydata:/app/data`（無斜線開頭）會被當成「建一個叫 `mydata` 的具名 volume」，不是 bind mount。要確定是 bind mount，用絕對路徑或明確的 `--mount type=bind`。
</Warning>

## 匿名 volume 與 Dockerfile `VOLUME` 的雷

`-v` 只給容器路徑、不給名稱，會建一個隨機 UUID 的**匿名 volume**。Dockerfile 的 `VOLUME` 指令在 `docker run` 時也會自動建匿名 volume 掛到那個路徑。

```dockerfile theme={null}
FROM node:20
WORKDIR /app
COPY . .
RUN npm install     # node_modules 裝進映像層
VOLUME /app         # 雷：run 時匿名 volume 掛上 /app，遮蔽掉 node_modules
```

`docker run` 時匿名 volume 掛到 `/app`，**遮蔽掉容器層裡 `npm install` 裝好的 `node_modules`**，應用就跑不起來。對策：`VOLUME` 別宣告在會被遮蔽的路徑，或乾脆不用 `VOLUME`，改在 `docker run` / Compose 顯式掛載。

容器不帶 `--rm` 刪掉後，匿名 volume 會殘留成 dangling volume，用 `docker volume prune` 清。

## docker volume 指令

```bash theme={null}
docker volume create my-vol
docker volume ls
docker volume inspect my-vol      # 看 Mountpoint
docker volume rm my-vol
docker volume prune               # 清未使用的 volume（預設只清匿名）
```

## WSL 2 的掛載效能（重要）

高 I/O 的資料（程式碼、`node_modules`、資料庫檔）要放在 **WSL 2 的 Linux 檔案系統**再 bind mount，**不要從 `/mnt/c`（Windows 路徑）掛載**：

```bash theme={null}
docker run -v ~/my-project:/sources app          # 正確：WSL 2 Linux 檔案系統
docker run -v /mnt/c/Users/me/project:/sources app   # 避免：跨 OS 邊界，慢
```

官方說明：從 Windows 檔案系統 bind mount 效能明顯較差，而且 **inotify 事件只在 Linux 檔案系統上有效**，掛 `/mnt/c` 會讓 hot reload（Vite、webpack、nodemon）收不到檔案變更而失效。高 I/O 場景優先用具名 volume 或從 WSL 2 Linux 側出發的 bind mount。

## 接下來

* [效能設定](/notes/docker/guide/config/)：`docker-desktop-data` VHDX 怎麼看大小、搬移。
* [Docker Compose](/notes/docker/guide/compose/)：在 compose.yaml 裡掛 volume 與 bind mount。

官方參考：[docs.docker.com/engine/storage](https://docs.docker.com/engine/storage/)
