目錄
Docker Volume 教學:資料持久化與 Bind Mount
容器內寫入的檔案存在容器的可寫層,容器一刪除,資料就跟著消失。資料庫、使用者上傳的檔案這類需要保存的資料,必須放在容器外面。Docker 提供三種掛載方式:
| 類型 | 資料存在哪裡 | 適合 |
|---|---|---|
| Volume | Docker 管理的區域 | 資料庫、正式環境的持久資料 |
| Bind mount | 主機上你指定的資料夾 | 開發時同步程式碼、讀取主機設定檔 |
| tmpfs | 記憶體 | 暫存、不想寫入硬碟的資料 |
Volume:刪掉容器,資料還在
用 PostgreSQL 實際驗證。啟動資料庫,把資料存在名為 pgdata 的 Volume:
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
-v pgdata:/var/lib/postgresql 的意思是:把名為 pgdata 的 Volume 掛到容器內的 /var/lib/postgresql。Volume 不存在時,Docker 會自動建立。
建立一張表、寫入一筆資料,然後刪除容器,再用同一個 Volume 啟動一個全新的容器:
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
119ed37ba5c6dc12a773a32d4ef296523529552f94a2ab1ee56113c1b990d7ae
$ docker ps --filter name=db
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
119ed37ba5c6 postgres:18 "docker-entrypoint.s…" 8 seconds ago Up 8 seconds 5432/tcp db
$ docker exec db psql -U postgres -c "CREATE TABLE notes (id serial, body text); INSERT INTO notes (body) VALUES ('hello volume');"
CREATE TABLE
INSERT 0 1
$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
id | body
----+--------------
1 | hello volume
(1 row)
$ docker rm -f db
db
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql postgres:18
d94fb2966b3dc5790239b4a48d180aedd193f6fc2c846d6878a7c5fcc450f7cd
$ docker exec db psql -U postgres -c "SELECT * FROM notes;"
id | body
----+--------------
1 | hello volume
(1 row)
新容器讀到了舊容器寫入的資料:資料存在 Volume 裡,和容器的生命週期無關。
PostgreSQL 18 的資料目錄
很多舊教學寫的是
-v pgdata:/var/lib/postgresql/data。這是 PostgreSQL 17 以前的寫法,在postgres:18會直接啟動失敗:
$ docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:18
c1cec1074a4d440c1f6d106d994171d290cd8ea9da3266d798c0c81662bd8281
$ docker ps -a --filter name=db
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c1cec1074a4d postgres:18 "docker-entrypoint.s…" 8 seconds ago Exited (1) 7 seconds ago db
$ docker logs db
Error: in 18+, these Docker images are configured to store database data in a
format which is compatible with "pg_ctlcluster" (specifically, using
major-version-specific directory names). This better reflects how
PostgreSQL itself works, and how upgrades are to be performed.
See also https://github.com/docker-library/postgres/pull/1259
Counter to that, there appears to be PostgreSQL data in:
/var/lib/postgresql/data (unused mount/volume)
This is usually the result of upgrading the Docker image without
upgrading the underlying database using "pg_upgrade" (which requires both
versions).
The suggested container configuration for 18+ is to place a single mount
at /var/lib/postgresql which will then place PostgreSQL data in a
subdirectory, allowing usage of "pg_upgrade --link" without mount point
boundary issues.
See https://github.com/docker-library/postgres/issues/37 for a (long)
discussion around this process, and suggestions for how to do so.
$ docker rm -f db
db
$ docker volume rm pgdata
pgdata
PostgreSQL 18 的官方映像檔改把資料放在 /var/lib/postgresql/18/docker 這種「依主要版本分開」的子目錄,以便日後用 pg_upgrade 升級。新的做法是掛載上一層的 /var/lib/postgresql:
$ docker volume ls
DRIVER VOLUME NAME
local 5775c1d90defc3dc23a92cdde10cd62b182d9c774ae43f0a40595fe62131a63c
local pgdata
$ docker volume inspect pgdata
[
{
"CreatedAt": "2026-10-13T00:09:44Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
"Name": "pgdata",
"Options": null,
"Scope": "local"
}
]
$ docker exec db sh -c 'echo $PGDATA; ls /var/lib/postgresql'
/var/lib/postgresql/18/docker
18
PGDATA現在是/var/lib/postgresql/18/docker。docker volume inspect中的Mountpoint是 Volume 在 Docker 主機上的實際位置。在 Docker Desktop 上,它位於 Docker 的 Linux 虛擬機裡,無法直接從 Windows / macOS 的檔案總管看到,所以備份要用下面介紹的方法。
管理 Volume
docker volume ls # 列出所有 Volume
docker volume inspect pgdata # 查看詳細資訊
docker volume rm pgdata # 刪除(沒有容器使用時才能刪)
docker volume prune # 刪除未被使用的匿名 Volume
docker volume prune -a # 連具名 Volume 也一起刪(小心!)
docker volume prune -a和docker system prune --volumes會刪掉資料庫資料。執行前請先確認,或先做備份。
匿名 Volume 從哪裡來
沒有用 -v 指定時,很多資料庫映像檔仍然會建立 Volume,因為它們的 Dockerfile 裡有 VOLUME 指令:
$ docker volume ls
DRIVER VOLUME NAME
$ docker run -d --name anon -e POSTGRES_PASSWORD=secret postgres:18
1db583d1b34d0482cfa458a5fe4a7fc7c1320633bde0339407a0c24fe62ad630
$ docker volume ls
DRIVER VOLUME NAME
local 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{println}}{{end}}' anon
volume 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8 -> /var/lib/postgresql
$ docker rm -f anon
anon
$ docker volume ls
DRIVER VOLUME NAME
local 0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
$ docker volume prune -f
Deleted Volumes:
0bbe1b19b9c02afcc2619b031ceddb1af1b4b75c3068f28eb64642973b3fd6e8
Total reclaimed space: 40.49MB
- 只執行了
docker run,docker volume ls就多了一個名稱是一長串亂碼的匿名 Volume,掛在/var/lib/postgresql。 docker rm -f刪除容器後,匿名 Volume 還留著,佔著 40 MB。很多人的 Docker 越用越大,原因之一就是這些被遺忘的匿名 Volume。docker volume prune會清掉沒有被使用的匿名 Volume。刪除容器時加上-v(docker rm -fv anon)也會一併刪除它的匿名 Volume。
建議總是使用具名 Volume,一看就知道是做什麼用的。
Bind mount:把主機的資料夾掛進容器
docker run -d --name site -p 8080:80 -v "$(pwd)/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "${PWD}/site:/usr/share/nginx/html:ro" nginx:1.30
docker run -d --name site -p 8080:80 -v "%cd%/site:/usr/share/nginx/html:ro" nginx:1.30

在主機上用編輯器修改 site/index.html 的標題並存檔,重新整理瀏覽器:

這就是開發時最常見的用法:程式碼留在主機上用習慣的編輯器修改,容器負責執行。
注意事項:
- 主機路徑要寫絕對路徑。
$(pwd)會換成目前目錄(Windows PowerShell 用${PWD})。 - 如果寫的是名稱而不是路徑(例如
site:/data),Docker 會當成 Volume 名稱。 - 在 Linux 上,容器以 root 寫入的檔案,在主機上也屬於 root。可以用
-u $(id -u):$(id -g)讓容器以你的身分執行。 - 唯讀掛載加上
:ro。
-v 與 --mount 遇到不存在的路徑
$ ls -d nothere
ls: nothere: No such file or directory
$ docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
total 8
drwxr-xr-x 2 root root 4096 Oct 13 00:09 .
drwxr-xr-x 1 root root 4096 Oct 13 00:09 ..
$ ls -ld nothere
drwxr-xr-x 2 root root 4096 Oct 13 00:09 nothere
$ docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker: Error response from daemon: invalid mount config for type "bind": bind source path does not exist: /work/nothere2
Run 'docker run --help' for more information
對應 Windows 的寫法(點選分頁切換):
docker run --rm -v "$(pwd)/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="$(pwd)/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "${PWD}/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="${PWD}/nothere2",dst=/data alpine:3.23 ls /data
docker run --rm -v "%cd%/nothere:/data" alpine:3.23 ls -la /data
docker run --rm --mount type=bind,src="%cd%/nothere2",dst=/data alpine:3.23 ls /data
-v遇到不存在的主機路徑,會自動建立一個空資料夾(而且屬於 root)。容器啟動成功,但掛進去的是空的,很容易讓人以為「明明掛了卻沒有檔案」。--mount type=bind遇到不存在的路徑會直接報錯,比較容易發現打錯路徑。
--mount 寫法比較長,但更明確:
docker run -d \
--mount type=volume,src=pgdata,dst=/var/lib/postgresql \
--mount type=bind,src="$(pwd)/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly \
-e POSTGRES_PASSWORD=secret postgres:18
docker run -d `
--mount type=volume,src=pgdata,dst=/var/lib/postgresql `
--mount type=bind,src="${PWD}/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly `
-e POSTGRES_PASSWORD=secret postgres:18
docker run -d ^
--mount type=volume,src=pgdata,dst=/var/lib/postgresql ^
--mount type=bind,src="%cd%/init.sql",dst=/docker-entrypoint-initdb.d/init.sql,readonly ^
-e POSTGRES_PASSWORD=secret postgres:18
tmpfs:存在記憶體裡
$ docker run --rm --tmpfs /cache:size=64m alpine:3.23 df -h /cache
Filesystem Size Used Available Use% Mounted on
tmpfs 64.0M 0 64.0M 0% /cache
資料只存在記憶體中,容器停止就消失,適合暫存檔案(僅限 Linux 容器)。
備份與還原 Volume
Volume 的位置由 Docker 管理,最通用的備份方式是啟動一個臨時容器,把 Volume 打包成 tar 檔:
$ docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
$ ls -lh pgdata.tar.gz
-rw-r--r-- 1 root root 4.6M Oct 13 00:10 pgdata.tar.gz
$ docker volume create pgdata-restore
pgdata-restore
$ docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
$ docker run --rm -v pgdata-restore:/data alpine:3.23 ls /data/18/docker
PG_VERSION
base
global
pg_commit_ts
pg_dynshmem
…(以下省略)
$ rm pgdata.tar.gz
對應 Windows 的寫法(點選分頁切換):
docker run --rm -v pgdata:/data:ro -v "$(pwd):/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "$(pwd):/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "${PWD}:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "${PWD}:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
docker run --rm -v pgdata:/data:ro -v "%cd%:/backup" alpine:3.23 tar czf /backup/pgdata.tar.gz -C /data .
docker run --rm -v pgdata-restore:/data -v "%cd%:/backup" alpine:3.23 tar xzf /backup/pgdata.tar.gz -C /data
- 第一個指令:把
pgdata以唯讀方式掛在/data,目前資料夾掛在/backup,用 tar 把/data打包成pgdata.tar.gz,存到主機的目前資料夾。 - 還原時建立一個新的 Volume,用同樣的方式解壓縮進去。還原後的 Volume 裡可以看到
18/docker底下的 PostgreSQL 資料檔。
直接打包執行中資料庫的檔案,可能得到不一致的資料。上面的例子是在資料庫停止後備份。資料庫建議使用它自己的匯出工具,例如
pg_dump或mysqldump,詳見 用 Docker 跑 PostgreSQL。
該用哪一種?
- 資料庫、需要長期保存的資料 → Volume
- 開發時想即時看到程式碼修改 → Bind mount
- 把主機的設定檔交給容器讀取 → Bind mount(唯讀)
- 暫存、不需要保存 → tmpfs 或直接寫在容器內
重點回顧
- 資料庫等需要保存的資料用具名 Volume。
- 開發時用 Bind mount 讓容器直接使用主機上的程式碼。
-v遇到不存在的主機路徑會建立空資料夾;--mount會直接報錯。- 備份 Volume:用臨時容器同時掛載 Volume 與主機資料夾,用 tar 打包。
動手練習
練習 1:建立 Volume notes,用一個容器寫入檔案,刪除容器後用另一個容器讀出來。
答案
docker volume create notes
docker run --rm -v notes:/data alpine:3.23 sh -c "echo hello > /data/a.txt"
docker run --rm -v notes:/data alpine:3.23 cat /data/a.txt
輸出 hello。Volume 的管理指令見 docker volume。
練習 2:docker run -v data:/app 和 docker run -v ./data:/app 有什麼不同?
答案
data 不含斜線,被當成 Volume 名稱;./data 是路徑,是 Bind mount,掛載主機目前資料夾下的 data。
常見問題
Volume 的資料存在電腦的哪裡?
Linux 上在 /var/lib/docker/volumes/名稱/_data。Docker Desktop 的 Volume 在它的 Linux 虛擬機中,Windows 檔案總管或 Finder 看不到,可以用 Docker Desktop 的 Volumes 畫面查看。
Windows 上 -v 的主機路徑怎麼寫?
PowerShell 用 "${PWD}/site:/app",CMD 用 "%cd%/site:/app",本章的指令都附有三種寫法的分頁。
Bind mount 的檔案權限不對?
Linux 上容器以 root 寫入的檔案,在主機上也屬於 root。用 -u $(id -u):$(id -g) 讓容器以你的身分執行。
更新於 2026年10月14日