容器化技術 ch06 資料持久化:容器刪了,資料要怎麼活下來
下一章→
CH 06 日常必備

資料持久化:容器刪了,資料要怎麼活下來

Volume vs Bind Mount vs tmpfs掛載會遮住原有內容UID 權限地雷SQL Server 資料的正確保存與備份

ch03 已經證明了一件殘酷的事:容器裡寫的所有東西都在可寫層,容器一刪就跟著消失。但資料庫的資料顯然不能這樣——所以你 compose 檔裡才有那行 mssql-data:/var/opt/mssql。

這一章把「把資料放到容器外面」的三種方式講清楚,以及一堆會讓人卡整個下午的細節:為什麼掛載上去之後容器裡原本的檔案不見了、為什麼容器寫出來的檔案在你電腦上改不動也刪不掉、docker compose down -v 那個 -v 為什麼是核彈級指令。

ch03 的結論再複習一次:容器所有的寫入都在「可寫層」,而可寫層屬於那個容器,docker rm 就一併消失。

但這不是缺陷,而是刻意的:正因為每個容器都從乾淨的映像出發,才有「壞了不修、重建就好」的自由。問題只在於——有些資料真的必須活下來。

三類資料,三種處理

  • ① 必須永久保存(狀態):資料庫檔案、使用者上傳的檔案、產生的報表。→ 必須放到容器外面,這章的主題。
  • ② 需要即時同步(開發便利):你正在改的原始碼、想快速調整的設定檔。→ Bind mount,讓宿主機的改動立刻反映到容器裡。
  • ③ 用完即丟(暫存):快取、臨時檔、解密後的秘密。→ 留在可寫層沒關係,或用 tmpfs 放在記憶體裡(更快、更安全)。
💡 心智模型:容器是「執行程式的地方」,不是「存放資料的地方」。把容器想成一台隨時可能被回收的計程車——你的行李(資料)不能放在車上,要放在自己的行李箱裡(volume),換車的時候拎著走。

Docker 提供三種把資料放到容器外的方式

方式資料實際存在哪一句話定位
VolumeDocker 管理的區域
(/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/mssql
🚨 怎麼區分 volume 和 bind mount?看冒號左邊:
• mssql-data:/var/opt/mssql → 左邊是名字(沒有 / 或 ./)→ Volume
• ./data:/var/opt/mssql → 左邊是路徑(有 / 或 ./)→ Bind mount
就差一個符號,行為完全不同,這是新手最常搞混的地方。

Volume 的四個好處

  1. 生命週期獨立於容器:容器砍掉重建幾百次,volume 都還在。這正是你想要的——docker compose down 再 up,資料庫內容原封不動。
  2. 繞過 overlay 檔案系統,效能好:ch03 說過 Copy-on-Write 對大檔案頻繁寫入很不友善。Volume 直接寫到宿主機檔案系統,沒有 CoW 開銷。資料庫必須用 volume,效能是和持久性同等重要的理由。
  3. 跨平台一致:不用煩惱 Windows 的 C:\ 路徑、macOS 的權限差異——名字在哪都一樣。
  4. 可管理、可備份:有專門的一組指令可以操作它。

Volume 的日常指令

docker volume ls                    # 列出所有 volume
docker volume inspect mssql-data    # 看它實際在宿主機的哪個目錄
docker volume create my-data        # 手動建立
docker volume rm my-data            # 刪除(有容器在用會拒絕)
docker volume prune                 # 清掉所有沒被使用的 ⚠️危險

具名 vs 匿名 volume

  • 具名(named):-v mssql-data:/var/opt/mssql —— 有名字,你找得到、管得到。正式用法。
  • 匿名(anonymous):-v /var/opt/mssql(只有容器內路徑)—— Docker 自動產生一個亂碼名字。很容易變成孤兒垃圾佔滿磁碟,而且下次啟動不會自動接回去。避免使用。
⭐ 有些映像的 Dockerfile 裡有 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 的習慣,避免容器裡的程式意外改壞你的檔案。
  • 可以掛單一檔案,不一定要整個目錄。

典型用途

  • 開發熱更新:改了程式碼不用重建映像,容器裡立刻看到(配合 Spring Boot DevTools 或前端的 dev server)。
  • 注入設定檔:同一個映像,掛不同的設定檔就變成不同環境。
  • 取出產物:把容器產生的報表、日誌寫到掛出來的目錄,方便你直接用本機工具打開。

四個真實會卡住你的地雷

  1. 權限與 UID 對不上(最常見)
    容器內的程式若以 root(uid 0)執行,它寫出來的檔案在宿主機上擁有者就是 root——你的一般帳號可能改不動也刪不掉,得用 sudo。反過來,容器內的非 root 使用者(例如 uid 1000)可能沒有權限寫入你掛進去的目錄,於是應用啟動就報 Permission denied。
    解法:docker run -u $(id -u):$(id -g) ... 讓容器以你的 UID 執行,或事先把目錄權限設好。這是 Linux 上做 bind mount 最常見的卡關點——記住這個症狀,省你半天。
  2. Windows/macOS 上的效能懸崖
    Docker Desktop 底下是一台 Linux 虛擬機,宿主機的檔案要透過檔案共享層轉進去,大量小檔案的存取會慢到不可思議(Java 專案的 target/、Node 的 node_modules/ 是重災區)。解法:只掛真正需要同步的目錄;把相依套件目錄改用 volume(見下一張卡的技巧)。
  3. 路徑不存在會被「自動建立成目錄」
    你想掛一個設定檔卻打錯路徑,Docker 不會報錯,而是在宿主機建一個空目錄掛進去——於是應用讀到的是個目錄不是檔案,錯誤訊息還很莫名其妙。掛單一檔案前先確認檔案真的存在。
  4. 可攜性差
    Bind mount 綁死了宿主機的目錄結構,別人 clone 你的專案不一定有那個路徑。所以正式環境幾乎不用 bind mount,開發環境才用。

這是最反直覺、也最容易讓人卡住一整個下午的行為。

核心規則:掛載是「蓋住」,不是「合併」

把東西掛到容器內的路徑 /app/data 上,映像裡原本 /app/data 的內容就完全看不到了——被遮住,不是合併。(原理跟 ch03 的 OverlayFS 上層覆蓋下層是同一回事:掛載點永遠贏。)

經典災難現場:Node 的 node_modules

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,可以讓每次建置不用重新下載相依套件。

那個很重要的特例:空的具名 volume 會先「複製」映像內容進去

當你把一個全新、空的具名 volume 掛到容器內某個已經有檔案的路徑時,Docker 會先把映像裡那個路徑的內容複製到 volume 裡,然後才掛上去。
  • 這就是為什麼你第一次跑 mssql-data:/var/opt/mssql,資料庫的初始檔案結構能正常建立——volume 不是空空地把它們遮掉,而是先繼承了一份。
  • 但這個貼心行為只發生在「volume 是空的」的第一次。之後 volume 有內容了,就純粹是覆蓋。
  • 而且 bind mount 完全沒有這個行為——bind mount 永遠是無情覆蓋。這正是 node_modules 災難只發生在 bind mount 的原因。
🚨 由此推出一個超容易踩的坑:你改了映像裡 /var/opt/mssql 的初始內容(例如換了設定檔),重新 build 後啟動,卻發現完全沒生效——因為 volume 已經有內容了,Docker 不會再複製一次。你得先 docker volume rm 掉那個 volume(資料會消失!)才會重新初始化。同理,PostgreSQL/MySQL 映像的初始化腳本(/docker-entrypoint-initdb.d/)也只在資料目錄是空的時候才會執行一次。(注意:SQL Server 官方映像沒有這個機制,初始化要另外處理——見 ch12。)

把前面的知識套到你實際在用的情境。

正確的 compose 設定

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 檔和設定——整個目錄掛出來最安全,不要只挑子目錄。
  • 密碼絕對不要寫死在 compose 檔裡(這份檔案會進版控)。用 .env,並把 .env 加進 .gitignore。

備份 volume 的通用技巧(任何 volume 都適用)

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 只是一塊可以被任何容器掛載的儲存空間,誰掛它就能讀寫它。

💡 但對資料庫而言,更推薦用資料庫自己的備份機制(SQL Server 的 BACKUP DATABASE):直接複製資料檔在資料庫執行中可能拿到不一致的狀態。要用檔案層備份,請先停掉容器。

誰擁有這些檔案:UID 的真相

  • SQL Server 官方映像內部以 uid 10001 的使用者執行。你在宿主機上看 volume 目錄,會發現檔案的擁有者顯示成 10001(一個你機器上不存在的使用者)。
  • 這是正常的——UID 是純數字,容器內外共用同一組數字空間,只是名字對不上(因為 Docker 預設沒開 user namespace,見 ch02)。
  • 由此推出一個實務原則:不要用 bind mount 給資料庫存資料。權限問題會沒完沒了,用 volume 讓 Docker 處理。

那個核彈級指令

docker compose down       # 停止並刪除容器與網路,volume 保留 ✅
docker compose down -v    # 連 volume 一起刪除 💣 資料全毀
🚨 -v 是「重置整個開發環境」用的,執行前先問自己:裡面的測試資料我重建得回來嗎?這個指令沒有確認提示、沒有回收桶。

tmpfs:只存在記憶體的掛載

docker run --tmpfs /app/tmp:size=100m myapp

# compose
services:
  app:
    tmpfs:
      - /app/tmp:size=100m
  • 資料完全不落地,容器一停就徹底消失。
  • 用途一:速度——記憶體比磁碟快幾個數量級,適合大量暫存檔。
  • 用途二:安全——解密後的秘密、暫時的 token 放這裡,不會留在磁碟上被鑑識。
  • 注意它吃的是宿主機記憶體,一定要設 size 上限,否則寫爆記憶體。

唯讀根檔案系統:進階的安全實務

docker run --read-only --tmpfs /tmp myapp
  • --read-only 讓整個容器檔案系統唯讀,攻擊者即使進來也無法植入或修改檔案。
  • 再用 tmpfs 開放少數真的需要寫入的路徑(/tmp、JVM 的暫存目錄)。
  • 這是容器安全強化的標準作法之一,K8s 裡對應 readOnlyRootFilesystem: true(ch14)。

怎麼確認到底掛了什麼?

docker inspect -f '{{json .Mounts}}' myapp | jq

會列出每個掛載的類型(volume/bind)、來源、目標、是否唯讀。「我明明改了檔案怎麼沒生效」的第一個排查點就是這裡——先確認你以為的掛載真的存在、而且掛在你以為的位置。

⭐ 三選一的決策口訣:
• 資料要活下來 → Volume(正式環境唯一解)
• 開發時要即時同步宿主機檔案 → Bind mount(記得 :ro 和 UID 問題)
• 暫存或機敏、不想落地 → tmpfs(記得設 size)
三種掛載方式全對照
面向VolumeBind Mounttmpfs
資料放哪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 執行,寫出來的檔案屬於 rootdocker run -u $(id -u):$(id -g),或事先設好權限
容器啟動噴 Permission denied容器內非 root 使用者對掛入目錄沒有寫入權調整宿主機目錄權限,或改用 volume
掛設定檔卻讀到一個空目錄宿主機路徑打錯,Docker 自動建了空目錄掛單一檔案前先確認檔案存在

練習題 點選選項查看解析

0 / 8
01 / 8
compose 檔中 mssql-data:/var/opt/mssql 和 ./data:/var/opt/mssql 的差別是?
A 沒有差別,只是寫法不同
B 前者冒號左邊是「名稱」→ Docker 管理的 volume;後者左邊是「路徑」→ bind mount,直接對應宿主機目錄
C 前者是唯讀,後者可寫
D 前者用於 Linux,後者用於 Windows
解析
判斷法則很簡單:冒號左邊沒有 / 或 ./ 就是 volume 名稱,有就是 bind mount 路徑。只差一個符號,但行為天差地遠——包括「空的時候會不會從映像複製內容」「權限誰負責」「換台機器還能不能用」。
02 / 8
為什麼資料庫的資料目錄「必須」用 volume,而不是留在容器可寫層?
A 只是為了方便備份
B 兩個理由都成立:① 可寫層隨容器刪除,資料會不見;② volume 繞過 overlay 的 Copy-on-Write,避免大檔案頻繁寫入的效能懲罰
C 因為可寫層有 1 GB 上限
D 因為資料庫不支援 overlay
解析
持久性和效能是同等重要的兩個理由。ch03 說過 CoW 修改大檔案時得先複製整個檔案,資料庫這種頻繁隨機寫入的負載走 overlay 又慢又浪費空間。Volume 直接寫宿主機檔案系統,兩個問題一次解決。
03 / 8
你把宿主機專案目錄整個掛到容器的 /app,結果應用噴「找不到模組」。原因是?
A 映像建置失敗
B 掛載是「蓋住」不是「合併」——映像裡 /app 下已裝好的相依套件目錄被宿主機的內容遮住了
C 檔案權限不足
D 容器記憶體不足
解析
掛載點永遠贏(原理同 ch03 的上層覆蓋下層)。解法是再疊一個 volume 把那個子路徑蓋回來,例如 - /app/node_modules。Java 專案的等價情境是 target/ 或把 ~/.m2 做成 volume 以免每次重新下載相依套件。
04 / 8
把一個「全新的空具名 volume」掛到容器內已有檔案的路徑,會發生什麼?
A 映像裡的檔案被遮住,看不到了
B Docker 會先把映像裡該路徑的內容複製進 volume,然後才掛上——但這只發生在 volume 是空的那一次
C 會直接報錯
D 兩邊內容自動合併
解析
這個貼心的特例讓資料庫第一次啟動能正常建立初始結構。但要記住兩件事:一是它只在 volume 為空時發生,之後就純粹覆蓋(所以改了映像裡的初始檔案不會生效,得先刪掉 volume);二是 bind mount 完全沒有這個行為,永遠是無情覆蓋。
05 / 8
Linux 上用 bind mount 時,容器寫出來的檔案在宿主機上你改不動也刪不掉。為什麼?
A Docker 鎖住了那些檔案
B 容器內的程式以 root(uid 0)執行,寫出的檔案擁有者就是 root;Docker 預設沒開 user namespace,容器內外共用同一組 UID 數字
C 檔案系統損毀
D 需要重新啟動 Docker
解析
這是 Linux 上 bind mount 最常見的卡關點。反方向也會發生:容器內的非 root 使用者對你掛進去的目錄沒有寫入權,啟動就噴 Permission denied。解法是 docker run -u $(id -u):$(id -g) 讓容器以你的 UID 執行,或事先把目錄權限設好。也因此,資料庫請一律用 volume 而不是 bind mount。
06 / 8
docker compose down 和 docker compose down -v 的差別?
A 沒有差別
B down 會刪除容器與網路但保留 volume;加上 -v 連 volume 一併刪除,資料全毀且沒有回收桶
C -v 表示顯示詳細訊息
D -v 會保留容器
解析
-v 是「重置整個開發環境」用的核彈級指令,沒有確認提示。執行前先問自己:裡面的測試資料重建得回來嗎?同理,docker system prune --volumes 也是同等危險的操作。
07 / 8
什麼情況下適合使用 tmpfs 掛載?
A 存放資料庫的資料檔
B 存放不想落地的機敏暫存資料(解密後的秘密、臨時 token)或需要極高速的暫存檔——資料只存在記憶體,容器一停就徹底消失
C 存放使用者上傳的檔案
D 存放應用程式的原始碼
解析
tmpfs 的兩個賣點是速度(記憶體)與安全(不留在磁碟上被鑑識)。務必設 size 上限,因為它吃的是宿主機記憶體。進階用法是搭配 --read-only 讓整個容器檔案系統唯讀,只用 tmpfs 開放少數需要寫入的路徑——這是容器安全強化的標準作法。
08 / 8
你修改了資料庫映像裡的初始化腳本並重新 build,啟動後卻發現完全沒生效。最可能的原因是?
A 映像沒有 build 成功
B volume 裡已經有舊資料了——初始化腳本只在資料目錄為空時執行一次,而且空 volume 的內容複製行為也只發生在第一次
C 腳本語法錯誤
D 需要重新啟動 Docker 服務
解析
這是 ch12 還會再遇到的頭號困惑。資料庫映像的 /docker-entrypoint-initdb.d/ 腳本只在資料目錄是空的時候跑一次;同理 volume 只在為空時從映像複製內容。要重新初始化就得先 docker volume rm 掉那個 volume——注意這會刪掉所有資料。

點擊卡片翻面查看答案,共 10 張。

QUESTION
三種把資料放到容器外的方式?
點擊翻面
ANSWER
Volume:Docker 管理區 → 正式資料的標準答案
Bind mount:你指定的宿主機路徑 → 開發同步/注入設定
tmpfs:宿主機記憶體 → 機敏或高速暫存
點擊翻回
QUESTION
怎麼一眼分辨 volume 和 bind mount?
點擊翻面
ANSWER
看冒號左邊:
mssql-data:/path → 是名字 → Volume
./data:/path → 有 / 或 ./ → Bind mount
點擊翻回
QUESTION
Volume 的四個好處?
點擊翻面
ANSWER
① 生命週期獨立於容器(down/up 資料還在)
② 繞過 overlay CoW,效能好
③ 跨平台一致(不用管 Windows 路徑)
④ 可管理可備份(volume 指令組)
點擊翻回
QUESTION
掛載的核心規則?
點擊翻面
ANSWER
掛載是「蓋住」不是「合併」
映像裡原路徑的內容會被完全遮住
(掛載點永遠贏,同 ch03 上層覆蓋下層)
點擊翻回
QUESTION
空的具名 volume 有什麼特例?
點擊翻面
ANSWER
掛到已有檔案的路徑時,Docker 會先把映像內容複製進 volume
⚠️ 只發生在 volume 為空的那一次
⚠️ bind mount 完全沒有這個行為(永遠無情覆蓋)
點擊翻回
QUESTION
node_modules / target 被遮住怎麼解?
點擊翻面
ANSWER
在後面再疊一個 volume 蓋回來:
- ./:/app
- /app/node_modules
Java:把 ~/.m2 做成 volume 避免每次重下載
點擊翻回
QUESTION
bind mount 的四個地雷?
點擊翻面
ANSWER
① UID 對不上 → 檔案改不動 / Permission denied
② Win/macOS 大量小檔效能懸崖
③ 路徑打錯會被自動建成空目錄
④ 可攜性差 → 正式環境不用
點擊翻回
QUESTION
怎麼備份一個 volume?
點擊翻面
ANSWER
開臨時容器同時掛 volume 和目標目錄,用 tar 打包:
docker run --rm -v mssql-data:/source:ro -v $(pwd):/backup alpine tar czf /backup/x.tar.gz -C /source .
(資料庫更建議用它自己的 BACKUP 機制)
點擊翻回
QUESTION
docker compose down -v 的殺傷力?
點擊翻面
ANSWER
down:刪容器與網路,volume 保留 ✅
down -v:連 volume 一起刪 💣 資料全毀、無確認、無回收桶
點擊翻回
QUESTION
三選一的決策口訣?
點擊翻面
ANSWER
資料要活下來 → Volume
開發要即時同步宿主機檔案 → Bind mount(記得 :ro 與 UID)
暫存/機敏不落地 → tmpfs(記得設 size)
點擊翻回