PostgreSQLコンテナをDocker Composeで運用する設計|データ永続化と初期化スクリプト、ダンプ取得の実務

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Docker > PostgreSQLコンテナをDocker Composeで運用する設計|データ永続化と初期化スクリプト、ダンプ取得の実務
「docker compose down したらPostgreSQLのデータが全部消えた」
「コンテナを作り直すたびにデータベースが初期化されてしまう」
Dockerを使い始めたエンジニアがPostgreSQLを扱うとき、ほぼ必ずぶつかる壁です。

コンテナはそれ自体が使い捨て設計のため、ボリューム(永続化ストレージ)を明示しないと、コンテナを削除した瞬間にデータも一緒に消えます。
加えて「初期テーブルをどう投入するか」「バックアップをどうとるか」という運用の疑問も、最初はなかなかまとまった答えが見つかりません。

この記事では、PostgreSQLコンテナをDocker Composeで永続化する設計パターン、初期化スクリプト(/docker-entrypoint-initdb.d)の使い方、稼働中コンテナからpg_dumpでダンプを取得する手順まで、実務ですぐ使える形で解説します。

動作確認環境: Rocky Linux 9.4 / Ubuntu 24.04 LTS(Docker Engine 26.1.x・Docker Compose Plugin v2.27)

この記事のポイント

・docker composeのnamedボリュームでコンテナ削除後もDBデータが残る永続化を実現できる
・/docker-entrypoint-initdb.dにSQLファイルを置くと初回起動時だけ自動実行される
・pg_dumpはdocker compose exec経由で稼働中コンテナから直接取得できる
・ボリュームが既に存在すると初期化スクリプトが再実行されない点が最大のはまりどころ


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

コンテナを消すとデータが消える|PostgreSQL永続化が必要な理由

Dockerコンテナのファイルシステムは、コンテナのライフサイクルに依存しています。
docker compose down(またはdocker rm)でコンテナを削除すると、コンテナレイヤー上に書き込まれたデータはすべて破棄されます。
PostgreSQLのデータディレクトリ(デフォルトは/var/lib/postgresql/data)も、ボリュームを別途指定しない限りコンテナ内レイヤーに存在するため、削除とともに消えます。

この問題を解決するのが「ボリューム(Volume)」です。
ボリュームはコンテナの外側でDockerが管理する永続化領域であり、コンテナを削除しても残り続けます。
Docker Composeではvolumes:セクションにnamedボリュームを定義することで、コンテナ再作成後も同じデータに接続できる設計を実現します。

Docker ComposeでPostgreSQLのデータを永続化する設計

1. docker-compose.ymlの基本構成(postgres + named volume)

まずボリュームを宣言し、PostgreSQLサービスのデータディレクトリにマウントするのが基本の型です。

# docker-compose.yml(最小構成:PostgreSQL + namedボリューム) services: db: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: appdb POSTGRES_USER: appuser POSTGRES_PASSWORD: changeme volumes: - pgdata:/var/lib/postgresql/data ports: - "5432:5432" volumes: pgdata:

volumes.pgdata:がnamedボリュームの宣言です(値は空でよい)。
services.db.volumespgdata:/var/lib/postgresql/dataが、そのボリュームをPostgreSQLのデータディレクトリへマウントする指定です。

2. namedボリュームとバインドマウントの違い

永続化の手段は大きく2種類あります。
namedボリューム(推奨):Dockerが/var/lib/docker/volumes/配下に管理する領域。コンテナ初回起動時にPostgreSQLのinitdbが走り、正しい権限でファイルが作成される
バインドマウント:ホストの任意ディレクトリをそのままマウントする方法(例:./data:/var/lib/postgresql/data)。ホストのディレクトリが空でない場合や権限ミスが多く、PostgreSQLは初回起動時に「Permission denied」や「initdb: directory is not empty」で止まることがある

本番・開発どちらでも、PostgreSQLの永続化にはnamedボリュームを使うのが定石です。
バインドマウントはホスト側からファイルを直接確認・編集したい場合(設定ファイル・初期化SQLなど)にのみ限定して使います。

3. コンテナ削除後もデータが残ることを確認する

実際にデータが消えないことを確認しましょう。

# コンテナを起動してpostgresに接続し、テストデータを投入する $ docker compose up -d $ docker compose exec db psql -U appuser -d appdb -c \ "CREATE TABLE memo (id serial PRIMARY KEY, body text);" $ docker compose exec db psql -U appuser -d appdb -c \ "INSERT INTO memo (body) VALUES ('test record');" # コンテナを削除する(--volumes なしでボリュームは残る) $ docker compose down # 再作成してデータを確認する $ docker compose up -d $ docker compose exec db psql -U appuser -d appdb -c "SELECT * FROM memo;" id | body ----+------------- 1 | test record (1 row)

docker compose down--volumes(または -v)オプションを付けるとnamedボリュームも削除されます。本番運用ではこのオプションを誤って実行しないよう注意が必要です。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、PostgreSQLの永続化設計をはじめとするDockerの実践スキルを体系的に学べる講座を用意しています。
Dockerマスター講座の詳細はこちら >>

/docker-entrypoint-initdb.dで初期化スクリプトを実行する

4. 初期化スクリプトの仕組みと配置方法

PostgreSQLの公式イメージには「namedボリュームが空の状態で初回起動したとき/docker-entrypoint-initdb.d/内のSQLファイル・シェルスクリプトを自動実行する」仕組みがあります。
これを使うと、docker compose up一発で初期テーブル作成やマスタデータ投入まで完了できます。

配置方法はバインドマウントで実現するのが最もシンプルです。

# docker-compose.yml(initdb対応版) services: db: image: postgres:16 restart: unless-stopped environment: POSTGRES_DB: appdb POSTGRES_USER: appuser POSTGRES_PASSWORD: changeme volumes: - pgdata:/var/lib/postgresql/data - ./initdb:/docker-entrypoint-initdb.d:ro volumes: pgdata:

:roは読み取り専用マウントです。コンテナ内から誤って初期化スクリプトを書き換えられないよう付けておきます。

5. SQLファイルで初期テーブルとデータを投入する

ホスト側にinitdb/ディレクトリを作り、以下のようなSQLファイルを置きます。
ファイルは辞書順(アルファベット順)で実行されるため、実行順序が重要な場合は先頭に番号を付けます。

# initdb/01_create_tables.sql CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(200) UNIQUE NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS articles ( id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES users(id), title VARCHAR(200) NOT NULL, body TEXT, created_at TIMESTAMPTZ DEFAULT NOW() );

# initdb/02_seed_data.sql(マスタデータ投入) INSERT INTO users (name, email) VALUES ('山田 太郎', 'yamada@example.com'), ('鈴木 花子', 'suzuki@example.com') ON CONFLICT DO NOTHING;

初回起動時のログを確認することで、スクリプトが実行されたかどうかを検証できます。

$ docker compose logs db | grep -E 'initdb|running' db-1 | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/01_create_tables.sql db-1 | /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/02_seed_data.sql

6. 初期化が実行されない場合の注意点

初期化スクリプトは「ボリュームが空のとき=PostgreSQLのデータディレクトリが存在しないとき」にのみ実行されます。
一度でもコンテナを起動してデータが書き込まれると、次回以降は初期化スクリプトをスキップします(既存データを守るための設計)。

「スクリプトを修正したのに反映されない」という場合は、既存のnamedボリュームを削除して作り直す必要があります。

# 注意: 既存のボリューム(データ)を完全に削除してから再作成する # ※ 本番データがある場合は先にダンプを取得してから実行すること $ docker compose down -v $ docker compose up -d

pg_dumpで稼働中コンテナからダンプを取得する

7. docker compose execでpg_dumpを実行する

稼働中のPostgreSQLコンテナに対してdocker compose exec経由でpg_dumpを実行し、ホスト側にダンプファイルを出力するのが基本パターンです。

# プレーンSQLフォーマットでダンプを取得する(-Tでバイナリ混入を防ぐ) $ docker compose exec -T db pg_dump -U appuser -d appdb > appdb_$(date +%Y%m%d_%H%M%S).sql # 実行結果の確認 $ ls -lh appdb_*.sql -rw-r--r-- 1 user user 4.5K Sep 6 10:00 appdb_20260906_100000.sql # ダンプ内容の先頭を確認 $ head -5 appdb_20260906_100000.sql -- -- PostgreSQL database dump -- -- Dumped from database version 16.4 -- Dumped by pg_dump version 16.4

-Tオプション(TTY割り当てを無効にする)を付けることで、バイナリデータの混入を防ぎ、リダイレクトで正確にダンプを取得できます。

8. ダンプファイルからpsqlで復元する

# 別環境またはコンテナ再作成後のPostgreSQLに復元する $ docker compose exec -T db psql -U appuser -d appdb < appdb_20260906_100000.sql # 復元後のテーブル確認 $ docker compose exec db psql -U appuser -d appdb -c "\dt" List of relations Schema | Name | Type | Owner --------+----------+-------+--------- public | articles | table | appuser public | users | table | appuser (2 rows)

pg_dumpのカスタム形式(-Fc)でダンプを取得すると、並列リストアや選択的リストアが可能になります。
データ量が大きくなってきた環境では、カスタム形式への移行を検討してください。

# カスタム形式でダンプを取得する(.dumpファイル) $ docker compose exec -T db pg_dump -U appuser -d appdb -Fc > appdb_20260906.dump # カスタム形式の復元はpg_restoreを使う $ docker compose exec -T db pg_restore -U appuser -d appdb < appdb_20260906.dump

9. ダンプをcronで定期自動化するシェルスクリプト

本番運用ではスクリプトにまとめてcronで定期実行するのが実務の定石です。

#!/bin/bash # /opt/scripts/backup-postgres.sh COMPOSE_DIR="/opt/myapp" BACKUP_DIR="/var/backup/postgres" DB_SERVICE="db" DB_USER="appuser" DB_NAME="appdb" KEEP_DAYS=7 TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz" mkdir -p "${BACKUP_DIR}" cd "${COMPOSE_DIR}" && \ docker compose exec -T "${DB_SERVICE}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" \ | gzip > "${BACKUP_FILE}" # 7日より古いバックアップを削除する find "${BACKUP_DIR}" -name "*.sql.gz" -mtime +${KEEP_DAYS} -delete echo "Backup completed: ${BACKUP_FILE}"

# crontabへの登録例(毎日午前3時に実行) $ crontab -e 0 3 * * * /opt/scripts/backup-postgres.sh >> /var/log/postgres-backup.log 2>&1

トラブルシュート|よくあるエラーと原因の切り分け

10. 「データディレクトリのパーミッションエラー」が出る場合

バインドマウントでデータディレクトリを指定したとき、ホスト側のディレクトリ所有者がPostgresのUID(999)と一致しないと以下のエラーが出ます。

db-1 | initdb: error: directory "/var/lib/postgresql/data" exists but is not empty db-1 | If you want to create a new database system, either remove or empty db-1 | the directory "/var/lib/postgresql/data" or run initdb db-1 | with an argument other than "/var/lib/postgresql/data".

namedボリュームに切り替えることが根本解決です。バインドマウントを使い続ける場合は、ホスト側ディレクトリの所有者をUID 999に変更してください。

# ホスト側のディレクトリ所有者をPostgresコンテナのUID(999)に変更する $ sudo chown -R 999:999 ./data

11. 初期化SQLが実行されない・エラーになる場合

ボリュームに既存データがある:既存ボリュームがあると初期化スクリプトはスキップされる。docker volume lsでボリューム名を確認し、docker compose down -vでクリアしてから再起動する
SQLファイルの文字コードがUTF-8以外:WindowsでSQLを編集してBOMが混入していると構文エラーになる。エディタでBOMなしUTF-8で保存し直す
ファイル名の拡張子が.sqlでない:/docker-entrypoint-initdb.d.sql.shのみを実行対象とする

12. pg_dump実行時に「FATAL: role does not exist」が出る場合

$ docker compose exec db pg_dump -U wronguser -d appdb pg_dump: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "wronguser" does not exist

-Uで指定するユーザー名がPOSTGRES_USER環境変数と一致しているか確認してください。
スーパーユーザーのpostgresを使いたい場合は-U postgresと明示します。

本記事のまとめ

やりたいこと 設定・コマンド
コンテナ削除後もDBデータを残す compose.ymlでvolumes: pgdata:/var/lib/postgresql/dataとnamedボリュームを宣言する
初回起動時にテーブルを自動作成する ./initdb:/docker-entrypoint-initdb.d:roにSQLファイルを配置する
稼働中コンテナからダンプを取得する docker compose exec -T db pg_dump -U appuser -d appdb > dump.sql
ダンプファイルを復元する docker compose exec -T db psql -U appuser -d appdb < dump.sql
namedボリュームを削除してやり直す docker compose down -v(本番データは先にバックアップすること)
初期化スクリプトが実行されない 既存ボリュームにデータがあるため、down -vでボリュームを削除してから再起動する
PostgreSQLをDocker Composeで運用する設計の核心は「データとコンテナのライフサイクルを切り離す」ことです。
namedボリューム・初期化スクリプト・定期バックアップの3点を整備しておけば、コンテナの再作成・バージョンアップ・環境移行を安心して行える基盤が整います。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、本記事で紹介したDocker ComposeによるPostgreSQL永続化設計をさらに深掘りできる講座を用意しています。
Dockerマスター講座の詳細はこちら >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。