ch03 已經證明了一件殘酷的事:容器裡寫的所有東西都在可寫層,容器一刪就跟著消失。但資料庫的資料顯然不能這樣——所以你 compose 檔裡才有那行 mssql-data:/var/opt/mssql。
這一章把「把資料放到容器外面」的三種方式講清楚,以及一堆會讓人卡整個下午的細節:為什麼掛載上去之後容器裡原本的檔案不見了、為什麼容器寫出來的檔案在你電腦上改不動也刪不掉、docker compose down -v 那個 -v 為什麼是核彈級指令。
ch03 的結論再複習一次:容器所有的寫入都在「可寫層」,而可寫層屬於那個容器,docker rm 就一併消失。
但這不是缺陷,而是刻意的:正因為每個容器都從乾淨的映像出發,才有「壞了不修、重建就好」的自由。問題只在於——有些資料真的必須活下來。
| 方式 | 資料實際存在哪 | 一句話定位 |
|---|---|---|
| Volume | Docker 管理的區域 ( /var/lib/docker/volumes/) | 正式資料的標準答案 |
| Bind mount | 你指定的宿主機路徑 | 開發時同步程式碼/設定檔 |
| tmpfs | 宿主機的記憶體(不落地) | 敏感或高速暫存資料 |
接下來三張卡逐一拆解。它們的共同機制其實一模一樣:都是 mount,把容器裡的某個路徑「換成」外面的某個東西。
Volume 就是一塊由 Docker 建立與管理的儲存空間,掛載到容器裡的某個路徑。你不需要知道它實際在磁碟的哪裡,只要記得名字。
# 指令列
docker run -v mssql-data:/var/opt/mssql mcr.microsoft.com/mssql/server:2022-latest
# compose 檔(你已經在用的寫法)
services:
sqlserver:
volumes:
- mssql-data:/var/opt/mssql # ← 冒號左邊沒有斜線 = volume 名稱
volumes:
mssql-data: # ← 這裡宣告,Docker 幫你建
# 明確版寫法(可讀性最好,推薦用在正式設定)
volumes:
- type: volume
source: mssql-data
target: /var/opt/mssqlmssql-data:/var/opt/mssql → 左邊是名字(沒有 / 或 ./)→ Volume./data:/var/opt/mssql → 左邊是路徑(有 / 或 ./)→ Bind mountdocker compose down 再 up,資料庫內容原封不動。C:\ 路徑、macOS 的權限差異——名字在哪都一樣。docker volume ls # 列出所有 volume
docker volume inspect mssql-data # 看它實際在宿主機的哪個目錄
docker volume create my-data # 手動建立
docker volume rm my-data # 刪除(有容器在用會拒絕)
docker volume prune # 清掉所有沒被使用的 ⚠️危險-v mssql-data:/var/opt/mssql —— 有名字,你找得到、管得到。正式用法。-v /var/opt/mssql(只有容器內路徑)—— Docker 自動產生一個亂碼名字。很容易變成孤兒垃圾佔滿磁碟,而且下次啟動不會自動接回去。避免使用。VOLUME /var/lib/mysql 宣告,意思是「這個路徑應該被持久化」。你沒指定時 Docker 會自動建立匿名 volume——這就是為什麼你的 docker volume ls 裡常常有一堆看不懂的亂碼名字,而且刪容器後它們還留著。定期 docker volume ls -f dangling=true 檢查一下。Bind mount 是把宿主機上一個你指定的路徑,直接掛到容器裡的某個路徑。兩邊看到的是同一份檔案——任何一邊改動,另一邊立刻看到。
# 開發時同步原始碼
docker run -v /home/user/myapp/src:/app/src myapp:dev
# compose(相對路徑以 compose 檔所在位置為基準)
services:
app:
volumes:
- ./src:/app/src # 程式碼同步
- ./config/app.yml:/app/config/app.yml:ro # 單一檔案,唯讀:ro = read-only,容器只能讀不能改。掛設定檔時養成加 :ro 的習慣,避免容器裡的程式意外改壞你的檔案。sudo。反過來,容器內的非 root 使用者(例如 uid 1000)可能沒有權限寫入你掛進去的目錄,於是應用啟動就報 Permission denied。docker run -u $(id -u):$(id -g) ... 讓容器以你的 UID 執行,或事先把目錄權限設好。這是 Linux 上做 bind mount 最常見的卡關點——記住這個症狀,省你半天。target/、Node 的 node_modules/ 是重災區)。解法:只掛真正需要同步的目錄;把相依套件目錄改用 volume(見下一張卡的技巧)。這是最反直覺、也最容易讓人卡住一整個下午的行為。
/app/data 上,映像裡原本 /app/data 的內容就完全看不到了——被遮住,不是合併。(原理跟 ch03 的 OverlayFS 上層覆蓋下層是同一回事:掛載點永遠贏。)volumes:
- ./:/app # 把整個專案目錄掛進去/app/node_modules 裝好了所有套件。/app 上——而你本機沒有 node_modules(或是給另一個作業系統用的版本)。node_modules 被遮住了,應用啟動就噴 Cannot find module。解法:再疊一個 volume 把那個子路徑「蓋回來」
volumes:
- ./:/app # 專案程式碼同步
- /app/node_modules # ← 匿名 volume 蓋在上面,保住映像裡那份Java 專案的等價情境是 /app/target 或 Maven 的 ~/.m2——把 .m2 做成 volume,可以讓每次建置不用重新下載相依套件。
mssql-data:/var/opt/mssql,資料庫的初始檔案結構能正常建立——volume 不是空空地把它們遮掉,而是先繼承了一份。/var/opt/mssql 的初始內容(例如換了設定檔),重新 build 後啟動,卻發現完全沒生效——因為 volume 已經有內容了,Docker 不會再複製一次。你得先 docker volume rm 掉那個 volume(資料會消失!)才會重新初始化。同理,PostgreSQL/MySQL 映像的初始化腳本(/docker-entrypoint-initdb.d/)也只在資料目錄是空的時候才會執行一次。(注意:SQL Server 官方映像沒有這個機制,初始化要另外處理——見 ch12。)把前面的知識套到你實際在用的情境。
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: 'Y'
MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD} # ← 從 .env 讀,不要寫死
ports:
- '1433:1433'
volumes:
- mssql-data:/var/opt/mssql # 資料(必須)
- ./backup:/backup # 備份檔交換區
volumes:
mssql-data:/var/opt/mssql 底下同時放了資料檔、log 檔和設定——整個目錄掛出來最安全,不要只挑子目錄。.env,並把 .env 加進 .gitignore。Volume 沒有內建的匯出指令,標準作法是開一個臨時容器,同時掛上 volume 和你的目錄,用 tar 打包:
# 備份
docker run --rm \
-v mssql-data:/source:ro \
-v $(pwd):/backup \
alpine tar czf /backup/mssql-backup.tar.gz -C /source .
# 還原
docker run --rm \
-v mssql-data:/target \
-v $(pwd):/backup \
alpine sh -c 'cd /target && tar xzf /backup/mssql-backup.tar.gz'看懂這兩行,你就真的理解 volume 了:volume 只是一塊可以被任何容器掛載的儲存空間,誰掛它就能讀寫它。
BACKUP DATABASE):直接複製資料檔在資料庫執行中可能拿到不一致的狀態。要用檔案層備份,請先停掉容器。10001 的使用者執行。你在宿主機上看 volume 目錄,會發現檔案的擁有者顯示成 10001(一個你機器上不存在的使用者)。docker compose down # 停止並刪除容器與網路,volume 保留 ✅
docker compose down -v # 連 volume 一起刪除 💣 資料全毀-v 是「重置整個開發環境」用的,執行前先問自己:裡面的測試資料我重建得回來嗎?這個指令沒有確認提示、沒有回收桶。docker run --tmpfs /app/tmp:size=100m myapp
# compose
services:
app:
tmpfs:
- /app/tmp:size=100msize 上限,否則寫爆記憶體。docker run --read-only --tmpfs /tmp myapp--read-only 讓整個容器檔案系統唯讀,攻擊者即使進來也無法植入或修改檔案。/tmp、JVM 的暫存目錄)。readOnlyRootFilesystem: true(ch14)。docker inspect -f '{{json .Mounts}}' myapp | jq會列出每個掛載的類型(volume/bind)、來源、目標、是否唯讀。「我明明改了檔案怎麼沒生效」的第一個排查點就是這裡——先確認你以為的掛載真的存在、而且掛在你以為的位置。
:ro 和 UID 問題)| 面向 | Volume | Bind Mount | tmpfs |
|---|---|---|---|
| 資料放哪 | Docker 管理區 | 你指定的宿主機路徑 | 宿主機記憶體(不落地) |
| compose 寫法 | mssql-data:/path(左邊是名字) | ./data:/path(左邊有 / 或 ./) | tmpfs: - /path:size=100m |
| 容器刪掉後 | 資料保留 | 資料保留(本來就在你機器上) | 立刻消失 |
| 效能 | 好(繞過 overlay CoW) | Linux 好;Win/macOS 大量小檔很慢 | 最快(記憶體) |
| 空目錄掛上去時 | 會先把映像內容複製進來(僅第一次) | 無情覆蓋,映像內容被遮住 | 無情覆蓋 |
| 權限問題 | 少(Docker 處理) | 多!UID 對不上是頭號卡關點 | 少 |
| 可攜性 | 高(換機器只要名字一樣) | 低(綁死宿主機目錄結構) | 高 |
| 主要用途 | 資料庫、上傳檔案等正式資料 | 開發時同步程式碼/注入設定檔 | 機敏暫存、高速暫存 |
| 症狀 | 真正原因 | 解法 |
|---|---|---|
| 重建容器後資料全沒了 | 資料寫在可寫層,隨容器刪除 | 掛 volume 到資料目錄 |
| 掛上去後容器裡原本的檔案不見了 | 掛載是「蓋住」不是「合併」 | 改掛子路徑,或再疊一個 volume 蓋回來 |
| 改了映像裡的初始檔案卻沒生效 | volume 已有內容,不會再從映像複製 | 先 docker volume rm(會刪資料)再重啟 |
| 宿主機上的檔案改不動、刪不掉 | 容器以 root 執行,寫出來的檔案屬於 root | docker run -u $(id -u):$(id -g),或事先設好權限 |
| 容器啟動噴 Permission denied | 容器內非 root 使用者對掛入目錄沒有寫入權 | 調整宿主機目錄權限,或改用 volume |
| 掛設定檔卻讀到一個空目錄 | 宿主機路徑打錯,Docker 自動建了空目錄 | 掛單一檔案前先確認檔案存在 |
點擊卡片翻面查看答案,共 10 張。