DB構築済みPostgreSQL Dockerイメージを作成する

# はじめに

postgresコンテナ起動時にDBを初期化する方法として、`/docker-entrypoint-initdb.d` 配下にSQLファイルを配置した状態でコンテナを起動する方法はよく知られています。

しかしながら、CIなど使い捨てコンテナを繰り返し起動する等、起動時のDB初期化に時間やコストを掛けたくない場合に、その方法は使用できません。本稿では、起動時に自動でDBを構築するのではなく、コンテナイメージ作成時点でDB構築が完了したカスタムpostgresイメージを作成する方法を紹介します。

# サンプルコード

まずは初期化対象のDB構築用サンプルSQLです。内容に意味はないので読み飛ばして大丈夫です。

```sql:sampledb.sql
CREATE DATABASE sampledb;

\c sampledb

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  email VARCHAR(100) UNIQUE
);

INSERT INTO users (name, email) VALUES
  ('Alpha', 'alpha@example.com'),
  ('Beta', 'beta@example.com'),
  ('Gamma', 'gamma@example.com');  
```

続いて、コンテナイメージ作成用のDockerfileです。

```dockerfile:Dockerfile
FROM postgres:17

ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=postgres
ENV POSTGRES_DB=postgres

COPY sampledb.sql /docker-entrypoint-initdb.d/

# DBを初期化する
RUN docker-ensure-initdb.sh && \
    rm -f /docker-entrypoint-initdb.d/sampledb.sql
```

重要なのは、末尾の `RUN docker-ensure-initdb.sh` です。

# docker-ensure-initdb.sh

`docker-ensure-initdb.sh` は、Docker公式のpostgresイメージが提供するヘルパースクリプトであり、 `/docker-entrypoint-initdb.d` 配下に保存されたSQL等のカスタムファイルを用いてDBを初期化してくれます。イメージ作成時にこのスクリプトを実行することで、DB初期化済みのイメージを作成できます。

カスタムファイルは複数配置可能で、アルファベット順に実行されます。なお、実行後カスタムファイルは不要となるのでイメージから削除するのがよいでしょう。

:::note info
`docker-ensure-initdb.sh` は、postgresイメージに組み込まれており、postgresをベースイメージとしていればデフォルトで利用可能です。[ソースコード](https://github.com/docker-library/postgres/blob/master/docker-ensure-initdb.sh)はGitHubで公開されています。`docker-ensure-initdb.sh` 自体はDB初期化方法の実例を示すリファレンス実装という役割もあるので、このソースコードを参考にして独自の初期化スクリプトをくみ上げてみるのも良いでしょう。
:::

## サポートするカスタムファイルフォーマット

`docker-ensure-initdb.sh` がサポートするカスタムファイルのフォーマット (拡張子) は以下のとおりです。これ以外のフォーマットは無視されます。

- `*.sql`

    そのまま `psql` で実行されます。

- `*.sql.gz`
- `*.sql.xz`
- `*.sql.zst`

    それぞれgzip, xz, zstdで圧縮されたSQLファイルです。自動で伸長され、`psql` で実行されます。

- `*.sh`

    シェルスクリプトです。以下、注意点です。
    
    - 実行可能な場合は別プロセスで実行され、実行可能でない場合は`source`でロードされます。
    - rootではなくpostgresユーザーで実行されます。権限に注意してください (例えば、そのまま `/docker-entrypoint-initdb.d` 配下でtarボールを展開しようとしても権限エラーになります)。


# イメージ作成と動作確認例

## イメージ作成

```console
$ docker image build -t postgres-db-initialized .
```

:::note info
なお、この方法でイメージを作成した場合、postgresコンテナ内部のDBディレクトリは揮発性となります。つまり、DBのデータを更新したあとコンテナを再起動すると、更新内容は失われます。これは一見デメリットのように見えるかもしれませんが、冒頭のユースケースで述べた通り、CIなどで使い捨てコンテナを繰り返し実行するような場合、副作用の影響を受けない再現可能なイメージが繰り返し利用できるという点で、むしろメリットになると言えます。
:::

## コンテナ起動

イメージ時点で既にDB初期化済みのため、起動時にDB構築処理が走っていないことが分かります。

```console
$ docker container run --name postgres --rm -it -d postgres-db-initialized

$ docker container logs postgres                                           

PostgreSQL Database directory appears to contain a database; Skipping initialization

2025-06-05 15:39:43.266 UTC [1] LOG:  starting PostgreSQL 17.5 (Debian 17.5-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
2025-06-05 15:39:43.271 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2025-06-05 15:39:43.271 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2025-06-05 15:39:43.603 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2025-06-05 15:39:43.610 UTC [29] LOG:  database system was shut down at 2025-06-05 14:04:23 UTC
2025-06-05 15:39:43.616 UTC [1] LOG:  database system is ready to accept connections
```


## 初期化済みDB確認

```console
$ docker container exec -it postgres psql -U postgres sampledb
psql (17.5 (Debian 17.5-1.pgdg120+1))
Type "help" for help.

sampledb=# SELECT * FROM users;
 id | name  |       email       
----+-------+-------------------
  1 | Alpha | alpha@example.com
  2 | Beta  | beta@example.com
  3 | Gamma | gamma@example.com
(3 rows)
```

DB初期化済みであることが確認できます。

# まとめ

Docker公式から提供される`docker-ensure-initdb.sh`スクリプトを用いて、コンテナイメージ作成時点でDB構築済み (初期化済み) のPostgreSQLコンテナイメージ作成方法を紹介しました。使い捨てコンテナを繰り返し実行するようなケースで、実行時のDB初期化コストを省力化したい場合等に役立つと思います。