目錄
用 Docker 跑 PostgreSQL:安裝、連線、備份與升級
PostgreSQL 18 的重要變更:從
postgres:18開始,官方映像檔的資料目錄改為/var/lib/postgresql/18/docker,Volume 要掛載在/var/lib/postgresql。網路上大多數教學寫的/var/lib/postgresql/data是 17 版以前的路徑,用在 18 版會啟動失敗,見下方實測。
一行指令啟動
$ docker run -d --name pg -e POSTGRES_PASSWORD=secret -p 127.0.0.1:5432:5432 -v pgdata:/var/lib/postgresql --restart unless-stopped postgres:18
1b34d05a195f347c86e431441162b760167a5c04016a528e10342d0199be016d
$ docker logs pg 2>&1 | tail -n 3
2026-10-13 00:53:32.071 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2026-10-13 00:53:32.075 UTC [72] LOG: database system was shut down at 2026-10-13 00:53:31 UTC
2026-10-13 00:53:32.080 UTC [1] LOG: database system is ready to accept connections
$ docker exec pg psql -U postgres -c "SHOW data_directory;"
data_directory
-------------------------------
/var/lib/postgresql/18/docker
(1 row)
| 參數 | 說明 |
|---|---|
POSTGRES_PASSWORD | 超級使用者的密碼(必填) |
-p 127.0.0.1:5432:5432 | 只開放給本機 |
-v pgdata:/var/lib/postgresql | 資料存在 Volume pgdata。18 版要掛載這個路徑 |
SHOW data_directory 確認實際的資料目錄是 /var/lib/postgresql/18/docker,在我們掛載的 /var/lib/postgresql 底下。目錄名稱包含大版本號,未來升級時新舊版本的資料可以並存在同一個 Volume 中。
其他常用的環境變數:
| 環境變數 | 說明 | 預設值 |
|---|---|---|
POSTGRES_USER | 超級使用者名稱 | postgres |
POSTGRES_DB | 預設資料庫 | 與 POSTGRES_USER 相同 |
PGDATA | 資料目錄 | /var/lib/postgresql/18/docker(18 版) |
TZ | 時區 | UTC |
和 MySQL 一樣,這些變數只在資料目錄是空的時候生效。之後修改
POSTGRES_PASSWORD不會改變既有的密碼,要用ALTER USER ... PASSWORD '...'修改。
實測:18 版掛載舊路徑會失敗
照著舊教學把 Volume 掛載到 /var/lib/postgresql/data:
$ docker run -d --name pgold -e POSTGRES_PASSWORD=secret -v pgold:/var/lib/postgresql/data postgres:18
b480eb9c72b29fa99f1ed8df17972e2bfc2d6da4b86c058a36cf21fd63579bfa
$ docker ps -a --filter name=pgold --format "{{.Status}}"
Exited (1) 5 seconds ago
$ docker logs pgold 2>&1 | head -n 12
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
容器在幾秒內以 Exited (1) 結束。錯誤訊息說明 18 版改用依大版本命名的目錄,而它在 /var/lib/postgresql/data 發現了「未使用的掛載」。這個檢查是為了避免你以為資料有保存,其實全部寫在容器裡、刪除容器就消失。把掛載路徑改成 /var/lib/postgresql 即可。
用 Compose 管理
實際專案建議用 Compose。建立資料夾 pg-demo:
pg-demo/
├── compose.yaml
├── .env
└── initdb/
└── 01-init.sql
.env:
DB_PASSWORD=change-this-password
initdb/01-init.sql(第一次初始化時執行,建立擴充功能、資料表和範例資料;最後的 pg_sleep(8) 是故意模擬耗時的初始化,例如匯入大量資料,用來觀察健康檢查):
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE products (
id serial PRIMARY KEY,
name text NOT NULL,
price integer NOT NULL
);
INSERT INTO products (name, price) VALUES ('珍珠奶茶', 65), ('鳳梨酥', 45), ('牛肉麵', 180);
-- 模擬耗時的初始化(例如匯入大量資料)
SELECT pg_sleep(8);
compose.yaml:
services:
db:
image: postgres:18
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: app
TZ: Asia/Taipei
command: ["postgres", "-c", "max_connections=200", "-c", "shared_buffers=256MB"]
ports:
- "127.0.0.1:5432:5432"
volumes:
- pgdata:/var/lib/postgresql
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U app -d app"]
interval: 2s
timeout: 3s
retries: 30
shm_size: 256mb
restart: unless-stopped
volumes:
pgdata:
| 設定 | 說明 |
|---|---|
POSTGRES_USER: app | 超級使用者改名為 app,不會再有 postgres 這個使用者 |
command: ["postgres", "-c", ...] | 用 -c 傳入 PostgreSQL 設定,不需要另外準備設定檔 |
pg_isready -h 127.0.0.1 | PostgreSQL 內建的檢查工具。一定要加 -h 127.0.0.1,原因見下方實測 |
shm_size: 256mb | Docker 預設的共享記憶體 /dev/shm 只有 64 MB,較複雜的查詢可能出現 could not resize shared memory segment 錯誤 |
$ docker compose up -d --wait
[+] up 3/3
✔ Network pg-demo_default Created 0.0s
✔ Volume pg-demo_pgdata Created 0.0s
✔ Container pg-demo-db-1 Healthy 11.3s
$ docker compose exec db psql -U app -h 127.0.0.1 -c "SELECT * FROM products;"
id | name | price
----+----------+-------
1 | 珍珠奶茶 | 65
2 | 鳳梨酥 | 45
3 | 牛肉麵 | 180
(3 rows)
$ docker compose logs db 2>&1 | grep -E "running /docker-entrypoint-initdb.d|init process complete|listening on IPv4"
db-1 | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01-init.sql
db-1 | PostgreSQL init process complete; ready for start up.
db-1 | 2026-10-13 08:52:58.663 CST [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
$ docker compose exec db psql -U app -c "SHOW max_connections;" -c "SHOW shared_buffers;" -c "SELECT now();"
max_connections
-----------------
200
(1 row)
shared_buffers
----------------
256MB
(1 row)
now
-------------------------------
2026-10-13 08:53:01.293902+08
(1 row)
$ docker compose exec db df -h /dev/shm
Filesystem Size Used Avail Use% Mounted on
shm 256M 1.1M 255M 1% /dev/shm
$ docker compose exec db psql -U app -c "\dx"
List of installed extensions
Name | Version | Default version | Schema | Description
---------+---------+-----------------+------------+-------------------------------------------------------------------
pg_trgm | 1.6 | 1.6 | public | text similarity measurement and index searching based on trigrams
plpgsql | 1.0 | 1.0 | pg_catalog | PL/pgSQL procedural language
(2 rows)
Healthy花了 11.3 秒,正好是初始化加上pg_sleep(8)的時間。健康之後立刻用 TCP 連線,資料都在,中文也正常。- 日誌顯示初始化腳本有執行、初始化完成後才監聽
0.0.0.0:5432。 max_connections是 200、shared_buffers是 256MB,command的參數生效了;now()顯示+08時區。/dev/shm是 256M,shm_size生效了。\dx列出已安裝的擴充功能,pg_trgm已經由初始化腳本建立。
健康檢查的陷阱:pg_isready 不加 -h 會太早回報 healthy
網路上常見的寫法是 pg_isready -U app -d app(沒有 -h)。實測改成這個寫法:
$ docker compose up -d --wait
[+] up 3/3
✔ Network pg-demo_default Created 0.0s
✔ Volume pg-demo_pgdata Created 0.0s
✔ Container pg-demo-db-1 Healthy 3.2s
$ docker compose exec db psql -U app -h 127.0.0.1 -c "SELECT count(*) FROM products;"
psql: error: connection to server at "127.0.0.1", port 5432 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
$ docker compose logs db 2>&1 | grep -E "pg_sleep|init process complete|listening on IPv4"
只過了 3.2 秒 就回報 Healthy,但用 TCP 連線卻得到 Connection refused。
原因與 MySQL 一樣:初始化期間,啟動腳本會先開一個只接受 Unix socket 連線的暫時伺服器來執行 initdb 中的腳本。pg_isready 不加 -h 時走 socket,於是在初始化還沒完成時就回報可以連線。依賴它的應用程式(depends_on 的 condition: service_healthy)會在這時啟動,用 TCP 連線就失敗。加上 -h 127.0.0.1 強制走 TCP,就能等到正式伺服器啟動。
連線與 psql
用 psql 操作
容器內有 psql 用戶端。互動式使用 docker compose exec db psql -U app(單獨的容器則是 docker exec -it pg psql -U postgres),進入 app=# 提示字元後輸入 SQL 或下面的指令。也可以用 -c 直接執行:
$ docker compose exec db psql -U app -c "SELECT version();"
version
--------------------------------------------------------------------------------------------------------------------
PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
(1 row)
$ docker compose exec db psql -U app -c "\l"
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+-------+----------+-----------------+------------+------------+--------+-----------+-------------------
app | app | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
postgres | app | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
template0 | app | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/app +
| | | | | | | | app=CTc/app
template1 | app | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/app +
| | | | | | | | app=CTc/app
(4 rows)
$ docker compose exec db psql -U app -c "\dt"
List of tables
Schema | Name | Type | Owner
--------+----------+-------+-------
public | products | table | app
(1 row)
$ docker compose exec db psql -U app -c "\d products"
Table "public.products"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+--------------------------------------
id | integer | | not null | nextval('products_id_seq'::regclass)
name | text | | not null |
price | integer | | not null |
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
$ docker compose exec db psql -U app -c "\du"
List of roles
Role name | Attributes
-----------+------------------------------------------------------------
app | Superuser, Create role, Create DB, Replication, Bypass RLS
| 指令 | 作用 |
|---|---|
\l | 列出資料庫 |
\c 名稱 | 切換資料庫 |
\dt | 列出資料表 |
\d 資料表 | 查看資料表結構 |
\du | 列出使用者(角色) |
\dx | 列出已安裝的擴充功能 |
\q | 離開 |
\du 顯示 app 是 Superuser,\l 中所有資料庫的 Owner 也都是 app:設定了 POSTGRES_USER 後,它就取代了預設的 postgres。
為什麼容器內不用輸入密碼
上面的 psql 都沒有輸入密碼。查看資料目錄中的 pg_hba.conf(PostgreSQL 的連線驗證規則):
$ docker compose exec db grep -v "^#" /var/lib/postgresql/18/docker/pg_hba.conf | grep -v "^$"
local all all trust
host all all 127.0.0.1/32 trust
host all all ::1/128 trust
local replication all trust
host replication all 127.0.0.1/32 trust
host replication all ::1/128 trust
host all all all scram-sha-256
$ docker run --rm --network pg-demo_default -e PGPASSWORD=wrong postgres:18 psql -h db -U app -c "SELECT 1;"
psql: error: connection to server at "db" (172.19.0.2), port 5432 failed: FATAL: password authentication failed for user "app"
$ docker run --rm --network pg-demo_default -e PGPASSWORD=change-this-password postgres:18 psql -h db -U app -c "SELECT current_user, inet_server_addr();"
current_user | inet_server_addr
--------------+------------------
app | 172.19.0.2
(1 row)
- 官方映像檔設定:容器內的連線(Unix socket、
127.0.0.1、::1)都是trust,不檢查密碼。 - 其他來源的連線(最後一行
host all all all)使用scram-sha-256密碼驗證。 - 從同一個 Compose 網路中的另一個容器連線到
db,密碼錯誤時得到password authentication failed;密碼正確才能連線。
所以能進入容器的人就能直接操作資料庫,這也是為什麼要保護好 Docker 的權限(參考 Docker 安全性)。從主機透過 -p 對應的連接埠連線時,來源 IP 是 Docker 網路的閘道,同樣需要密碼。
從主機與其他容器連線
postgres://app:密碼@127.0.0.1:5432/app # 從主機(GUI 工具、應用程式)
postgres://app:密碼@db:5432/app # 從同一個 Compose 專案的其他服務
常見的連線錯誤
$ docker compose exec db psql -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "root" does not exist
$ docker compose exec db psql -U postgres -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "postgres" does not exist
$ docker compose exec db psql -U app -d shop -c "SELECT 1;"
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: database "shop" does not exist
$ docker compose exec -e PGPASSWORD=wrong db psql -h 127.0.0.1 -U app -c "SELECT 1;"
?column?
----------
1
(1 row)
$ docker compose exec db psql -U app -h 127.0.0.1 -p 5433 -c "SELECT 1;"
psql: error: connection to server at "127.0.0.1", port 5433 failed: Connection refused
Is the server running on that host and accepting TCP/IP connections?
| 錯誤 | 原因 |
|---|---|
role "root" does not exist | psql 沒加 -U,預設使用目前系統使用者的名稱(容器內是 root) |
role "postgres" does not exist | 設定了 POSTGRES_USER,預設的 postgres 使用者就不存在 |
database "shop" does not exist | POSTGRES_DB 只在初始化時建立;其他資料庫要自己 CREATE DATABASE |
Connection refused | 連接埠錯誤、資料庫還沒啟動完成,或連到錯誤的主機 |
password authentication failed | 密碼錯誤,或 Volume 是用其他密碼初始化的 |
注意第 4 個指令:在容器內用錯誤的密碼連 127.0.0.1 也成功了,這就是上面說的 trust 規則。
備份與還原
$ docker compose exec -T db pg_dump -U app app > app.sql
$ ls -lh app.sql && grep -E "^CREATE (TABLE|EXTENSION)" app.sql
-rw-r--r-- 1 root root 2.3K Oct 13 00:53 app.sql
CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;
CREATE TABLE public.products (
$ docker compose exec -T db pg_dump -U app -Fc app > app.dump
$ ls -lh app.dump
-rw-r--r-- 1 root root 3.1K Oct 13 00:53 app.dump
$ docker compose exec db psql -U app -c "DROP TABLE products;"
DROP TABLE
$ docker compose exec -T db pg_restore -U app -d app --clean --if-exists < app.dump
$ docker compose exec db psql -U app -c "SELECT count(*) FROM products;"
count
-------
3
(1 row)
$ rm app.sql app.dump
| 指令 | 說明 |
|---|---|
pg_dump app > app.sql | 匯出成 SQL 純文字,可以直接閱讀、用 psql 匯入 |
pg_dump -Fc app > app.dump | 自訂格式,已壓縮,用 pg_restore 還原,可以選擇只還原部分資料表 |
pg_restore --clean --if-exists | 還原前先刪除已存在的物件,避免重複 |
exec -T | Compose 的 exec 預設會配置 TTY,用重新導向時要加 -T 關閉 |
備份中包含 CREATE EXTENSION,還原到新資料庫時擴充功能會一併建立。這個範例資料很少,所以自訂格式(3.1K)反而比純文字(2.3K)大;資料量大時自訂格式會小很多。
其他常用指令:
# 備份所有資料庫與角色(使用者、密碼)
docker compose exec -T db pg_dumpall -U app > all.sql
# 還原 SQL 純文字
docker compose exec -T db psql -U app -d app < app.sql
可以用 cron 搭配上面的指令定期備份,並把檔案複製到其他機器或雲端儲存。
Windows 使用者的備份與還原
PowerShell 不支援 <,Windows PowerShell 5.1 的 > 會改變檔案編碼,-Fc 的二進位備份更會直接損壞。改成用 pg_dump -f 在容器內產生檔案、再用 docker compose cp 複製,這個寫法在所有系統都能用:
# 備份
docker compose exec db pg_dump -U app -Fc -f /tmp/app.dump app
docker compose cp db:/tmp/app.dump app.dump
# 還原
docker compose cp app.dump db:/tmp/restore.dump
docker compose exec db pg_restore -U app -d app --clean --if-exists /tmp/restore.dump
這組指令沒有用到 $(pwd)、<、>,bash、PowerShell、CMD 都能直接執行。實測(先建立資料表、備份、刪除、還原):
$ docker compose exec db psql -U app -c 'CREATE TABLE t (id int); INSERT INTO t VALUES (1),(2);'
CREATE TABLE
INSERT 0 2
$ docker compose exec db pg_dump -U app -Fc -f /tmp/app.dump app
$ docker compose cp db:/tmp/app.dump app.dump
pgwin-db-1 Copying pgwin-db-1:/tmp/app.dump to app.dump
pgwin-db-1 Copied pgwin-db-1:/tmp/app.dump to app.dump
$ ls -l app.dump
-rw-rw-rw- 1 root root 1251 Oct 13 05:30 app.dump
$ docker compose exec db psql -U app -c 'DROP TABLE t;'
DROP TABLE
$ docker compose cp app.dump db:/tmp/restore.dump
pgwin-db-1 Copying app.dump to pgwin-db-1:/tmp/restore.dump
pgwin-db-1 Copied app.dump to pgwin-db-1:/tmp/restore.dump
$ docker compose exec db pg_restore -U app -d app /tmp/restore.dump
$ docker compose exec db psql -U app -c 'SELECT count(*) FROM t;'
count
-------
2
(1 row)
大版本升級(例如 16 → 18)
小版本更新(例如 18.5 → 18.6)只要拉取新映像檔重新建立容器即可。大版本則不同:PostgreSQL 不同大版本的資料檔案格式不相容。
先用 16 版建立一些資料:
$ docker run -d --name pg16 -e POSTGRES_PASSWORD=secret -v pg16data:/var/lib/postgresql/data postgres:16
20691764a98828d24c958eaaa71e83c08ebff29b984350ff7f6782ba8d74b4ca
$ docker exec pg16 psql -U postgres -c "CREATE TABLE notes (id serial PRIMARY KEY, body text); INSERT INTO notes (body) VALUES ('written by 16');"
CREATE TABLE
INSERT 0 1
$ docker stop pg16
pg16
直接換映像檔會失敗
用 postgres:18 開啟 16 版的資料(這裡用 PGDATA 指定舊路徑,模擬直接把 tag 從 16 改成 18):
$ docker run --name pg18 -e POSTGRES_PASSWORD=secret -e PGDATA=/var/lib/postgresql/data -v pg16data:/var/lib/postgresql/data postgres:18 2>&1 | tail -n 3
2026-10-13 00:54:09.740 UTC [1] FATAL: database files are incompatible with server
2026-10-13 00:54:09.740 UTC [1] DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 18.6 (Debian 18.6-1.pgdg13+2).
$ docker rm pg18
pg18
database files are incompatible with server:資料目錄是 16 版初始化的,18.6 無法使用。
匯出再匯入
最單純安全的做法:用舊版 pg_dumpall 匯出全部資料,再匯入到新版的新 Volume。
$ docker start pg16
pg16
$ docker exec pg16 pg_dumpall -U postgres > upgrade.sql && ls -lh upgrade.sql
-rw-r--r-- 1 root root 3.4K Oct 13 00:54 upgrade.sql
$ docker run -d --name pg18 -e POSTGRES_PASSWORD=secret -v pg18data:/var/lib/postgresql postgres:18
ec651837b76a0c9cc8b2468ec5a2ff4002334dcf015514ef94490a6c38cfe73d
$ docker exec -i pg18 psql -U postgres -q < upgrade.sql
ERROR: role "postgres" already exists
set_config
------------
(1 row)
set_config
------------
(1 row)
setval
--------
1
(1 row)
$ docker exec pg18 psql -U postgres -c "SHOW server_version;" -c "SELECT * FROM notes;"
server_version
-------------------------------
18.6 (Debian 18.6-1.pgdg13+2)
(1 row)
id | body
----+---------------
1 | written by 16
(1 row)
$ rm upgrade.sql
- 匯入時的
ERROR: role "postgres" already exists可以忽略:新的 18 版容器已經建立了postgres使用者,pg_dumpall的輸出又要建立一次。 - 最後確認版本是 18.6,16 版寫入的
written by 16資料都在。 - 注意新容器的 Volume 掛載在
/var/lib/postgresql(18 版的新路徑)。
確認資料和應用程式都正常後,再刪除舊的容器和 Volume。資料量很大、無法接受長時間停機時,可以研究 pg_upgrade 或邏輯複寫(logical replication),但步驟複雜很多,請先在測試環境演練。
重點回顧
- PostgreSQL 18 的 Volume 掛載在
/var/lib/postgresql。 - 健康檢查用
pg_isready -h 127.0.0.1。 - 容器內連線是 trust,外部連線需要密碼。
- 大版本升級:舊版
pg_dumpall匯出,新版新 Volume 匯入。
動手練習
練習 1:用 psql 在 app 資料庫建立一個資料表 notes,再用 \dt 確認。
答案
docker compose exec db psql -U app -c "CREATE TABLE notes (id serial PRIMARY KEY, body text);"
docker compose exec db psql -U app -c "\dt"
練習 2:備份成自訂格式(-Fc)和純文字 SQL 有什麼差別?
答案
自訂格式經過壓縮,要用 pg_restore 還原,可以選擇只還原部分資料表;純文字可以直接閱讀,用 psql 匯入。
常見問題
postgres:18 啟動就退出?
18 版要掛載 /var/lib/postgresql,掛在舊的 /var/lib/postgresql/data 會因為偵測到不相容的目錄而退出,實測見本章。
小版本更新(18.5 → 18.6)怎麼做?
拉取新映像檔重建容器即可:docker compose pull db && docker compose up -d db。大版本才需要匯出再匯入。
Windows 上 pg_restore 失敗?
PowerShell 不支援 <,且 5.1 的 > 會破壞二進位備份。用 pg_dump -f 在容器內產生檔案、再用 docker compose cp 複製,本章附有寫法。
更新於 2026年10月14日