目錄

用 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.1PostgreSQL 內建的檢查工具。一定要加 -h 127.0.0.1,原因見下方實測
shm_size: 256mbDocker 預設的共享記憶體 /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 existpsql 沒加 -U,預設使用目前系統使用者的名稱(容器內是 root)
role "postgres" does not exist設定了 POSTGRES_USER,預設的 postgres 使用者就不存在
database "shop" does not existPOSTGRES_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 -TCompose 的 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日