はじめに

単ノードの PostgreSQL をローカル基盤で安定して使う前提で組み直すにあたって、今回は PostgreSQL 18 を Podman + Quadlet の rootless 構成で動かす形に整理した。狙いは単に DB を起動することではなく、LLVM JIT を有効にしたうえで pgvector にも対応し、設定ファイルとデータ配置を含めて再現しやすい構成に寄せることだった。

特に重視したのは、コンテナ定義と実データの責務を分けること、そして起動管理を systemd –user に寄せることだった。そうしておくと、DB のチューニングとコンテナの運用を同じメモの中で扱っても破綻しにくい。

背景・目的

今回の目的はかなり明確で、高性能な単ノード PostgreSQL 環境を構築しながら、次の 2 点を両立させることだった。

  • LLVM JIT によるクエリ最適化
  • pgvector 拡張でのベクトル検索対応

そのうえで、コンテナ運用は Podman + Quadlet(rootless)に寄せ、設定とデータは NVMe 側の /mnt/data へ永続化する。つまり、性能面の準備だけでなく、日常運用で崩れにくい配置も同時に決める方針だった。

ディレクトリ構成

まず構成ファイルとイメージ定義の置き場を次のように分けた。

  /opt/containers/runtime/postgresql/
 ├── etc/
 │    ├── postgresql.conf
 │    ├── pg_hba.conf
 │    └── docker-entrypoint-initdb.d/
 │         └── 010-create-vector.sql
 ├── Dockerfile
 └── postgresql.container
  

データディレクトリは別にして、永続化先を NVMe 側へ寄せる。

  /mnt/data/postgresql/data/
  

この切り方にしている理由は単純で、実行時に差し替えたいものと、消えてはいけないものを混ぜたくないからだ。Dockerfile と postgresql.container は構成定義として管理し、postgresql.conf と pg_hba.conf はコンテナ外で保持する。DB 本体は /mnt/data/postgresql/data/ に固定する。こうしておくと、イメージを更新してもデータを巻き込まず、設定変更もファイル単位で追いやすい。

Dockerfile(pg18 + LLVM + pgvector)

ベースイメージは postgres:18-trixie を使い、そのうえで pgvector をビルドできるように開発系パッケージを追加した。

  FROM postgres:18-trixie

RUN set -eux; \
    apt-get update; \
    apt-get install -y --no-install-recommends \
      build-essential clang llvm-dev git ca-certificates pkg-config make postgresql-server-dev-18; \
    rm -rf /var/lib/apt/lists/*

ARG PGVECTOR_VERSION=v0.8.1
RUN git clone --depth 1 --branch "$PGVECTOR_VERSION" https://github.com/pgvector/pgvector.git /tmp/pgvector && \
    make -C /tmp/pgvector && make -C /tmp/pgvector install && rm -rf /tmp/pgvector

RUN sed -i 's/#jit = off/jit = on/' /usr/share/postgresql/postgresql.conf.sample \
    && printf '\njit_above_cost = 10000\n' >> /usr/share/postgresql/postgresql.conf.sample

COPY 010-create-vector.sql /docker-entrypoint-initdb.d/010-create-vector.sql
  

pgvector は v0.8.1 を指定して浅い clone にしている。ここでバージョンを固定しているのは、後からビルド結果が変わるのを防ぐためだ。postgresql-server-dev-18 を入れているので、PostgreSQL 18 向けの拡張ビルドに必要なヘッダ類も揃う。

JIT については、サンプル設定の #jit = off を書き換えたうえで jit_above_cost = 10000 を追記している。つまりイメージのデフォルト時点で JIT を有効にしておき、後段の外部 postgresql.conf でも明示的に揃える形になる。

初期化 SQL はこれだけだ。

  CREATE EXTENSION IF NOT EXISTS vector;
  

この一行だけでもかなり意味がある。DB 初期化直後に vector 拡張を作っておけば、アプリ側のマイグレーションや接続テストで「拡張が無いから失敗する」という初期段階のズレを減らせる。

postgresql.conf(メモリ 4GB 想定)

設定はメモリ 4GB 想定で切っている。

  listen_addresses = '*'
port = 5432
max_connections = 200
shared_buffers = 1GB
effective_cache_size = 3GB
maintenance_work_mem = 256MB
work_mem = 32MB
random_page_cost = 1.1
effective_io_concurrency = 200
max_worker_processes = 8
max_parallel_workers_per_gather = 4
max_parallel_workers = 8

# WAL
wal_buffers = 16MB
min_wal_size = 512MB
max_wal_size = 2GB
checkpoint_timeout = 15min
checkpoint_completion_target = 0.9
log_checkpoints = on

# ログ
log_destination = 'stderr'
logging_collector = on
log_min_duration_statement = 200ms
log_line_prefix = '%m [%p] %q%u@%d '

# JIT
jit = on
jit_above_cost = 10000
jit_optimize_above_cost = 50000
jit_inline_above_cost = 100000

# ロケール
lc_messages = 'C'
lc_monetary = 'C'
lc_numeric = 'C'
lc_time = 'C'
  

shared_buffers = 1GB、effective_cache_size = 3GB として、ホストに 4GB 相当を想定した配分にしている。work_mem = 32MB と maintenance_work_mem = 256MB は、単ノードの開発基盤としては無難な線だと思っている。

ストレージまわりでは random_page_cost = 1.1 と effective_io_concurrency = 200 を入れている。NVMe 前提であることがここに表れている。WAL も checkpoint_completion_target = 0.9 を入れ、チェックポイントの書き込みをできるだけ平準化するつもりで置いている。

ログは stderr と logging_collector = on に寄せ、200ms 以上のクエリを拾う。JIT 関連は jit = on に加えて、最適化や inline の閾値も明示しているので、単に JIT を有効にするだけでなく、どのコスト帯から効かせるかまで含めて意図がある。

pg_hba.conf

認証設定はかなりシンプルにしている。

  # TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all                                     trust
host    all             all             127.0.0.1/32            scram-sha-256
host    all             all             ::1/128                 scram-sha-256
host    all             all             192.168.0.0/16          scram-sha-256
host    all             all             10.10.0.0/16            scram-sha-256
  

UNIX ソケットのローカル接続は trust、TCP 接続は scram-sha-256 に分けている。ローカル開発用途ならこの割り切りは扱いやすい。一方で 192.168.0.0/16 と 10.10.0.0/16 を許可しているので、どのセグメントから接続するかは運用側で把握しておきたい。

環境変数ファイル

環境変数は ~/.config/containers/systemd/.postgresql.env にまとめた。

  POSTGRES_USER=postgres
POSTGRES_PASSWORD=localdev
POSTGRES_DB=appdb
  

権限は次の通りに絞る。

  chmod 600 ~/.config/containers/systemd/.postgresql.env
  

ここで大事なのは、EnvironmentFile の置き場所と Quadlet の読み取り位置をずらさないことだ。あとで出てくるトラブルシュートにもある通り、これを [Service] に置いてしまうと期待通りに反映されない。

Quadlet: postgresql.container

ユニット定義は次のようにした。

  [Unit]
Description=PostgreSQL 18 (LLVM/JIT + PG Vector)
Wants=network-online.target
After=network-online.target

[Container]
Image=compute.home.arpa/pg18-jit-vec:latest
ContainerName=postgresql
Network=slirp4netns:allow_host_loopback=true
PublishPort=5432:5432
Tmpfs=/dev/shm:size=4g

Volume=/mnt/data/postgresql/data:/var/lib/postgresql/data:rw
Volume=/opt/containers/runtime/postgresql/etc/postgresql.conf:/etc/postgresql/postgresql.conf:ro
Volume=/opt/containers/runtime/postgresql/etc/pg_hba.conf:/etc/postgresql/pg_hba.conf:ro

EnvironmentFile=%h/.config/containers/systemd/.postgresql.env

Exec=postgres -c config_file=/etc/postgresql/postgresql.conf -c hba_file=/etc/postgresql/pg_hba.conf

Ulimit=nofile=1048576:1048576

[Service]
Restart=always

[Install]
WantedBy=default.target
  

rootless でも PublishPort=5432:5432 を使ってホスト側から到達できるようにしつつ、ネットワークは slirp4netns:allow_host_loopback=true を選んでいる。これでホストから 127.0.0.1 へ向けた確認もしやすい。

Tmpfs=/dev/shm:size=4g を切っているのは、共有メモリまわりで窮屈にならないようにするためだ。さらに Ulimit=nofile=1048576:1048576 も入れておくことで、ファイルディスクリプタ上限で妙な詰まり方をしにくくしている。

設定ファイルを /etc/postgresql/ に RO マウントして、Exec=postgres -c config_file=… -c hba_file=… で明示しているのも重要だった。イメージ内のデフォルト配置に寄せるより、どの設定ファイルで起動したかがユニット定義だけで追える。

起動手順

起動前にデータディレクトリを作り、所有者を 999:999 に合わせる。

  mkdir -p /mnt/data/postgresql/data
sudo chown -R 999:999 /mnt/data/postgresql/data

systemctl --user daemon-reload
systemctl --user enable --now postgresql.container
systemctl --user status postgresql
  

PostgreSQL コンテナ内のユーザーに合わせて所有権を事前に揃えておかないと、初回起動時の初期化や書き込みでこける。rootless だから何でも自動で吸収してくれるわけではないので、永続ディレクトリの ownership は最初に合わせておくのが安全だった。

また、enable すべきなのは生成された .service ではなく .container 側である。ここは Quadlet を使い始めたときに引っかかりやすい。

動作確認

起動確認は最小限に絞っている。

  psql -h 127.0.0.1 -U postgres -d appdb -c "SHOW jit;"
psql -h 127.0.0.1 -U postgres -d appdb -c "SELECT * FROM pg_extension;"
  

期待している出力は次の通り。

  jit
-----
on
  
   name   | version | schema     | description
--------+---------+------------+----------------------------------------
 plpgsql | 1.0    | pg_catalog | PL/pgSQL procedural language
 vector  | 0.8.1  | public     | vector data type and ivfflat and hnsw access methods
  

JIT が有効かどうかと、vector 拡張が本当に入っているかの 2 点だけ見ている。まずこの 2 つが通れば、イメージビルド、初期化 SQL、起動時設定の流れは大きく外していないと判断できる。

チューニング・トラブルシュート

実際に詰まりやすい点を表でまとめている。

事象原因対処
chown: Read-only file systemconf を /var/lib/postgresql/… に RO マウントしている/etc/postgresql/ へ移動または :rw 指定
.env が反映されない[Service] に置いている[Container] EnvironmentFile= に移動
Unit is transient or generated.service は生成物.container を enable
/etc/containers/systemd/users/1000 が無い通常無視可
initdb 変数が効かない既に DB 初期化済みdata 削除して再起動 or SQL で変更

chown: Read-only file system は、設定ファイルをデータディレクトリ寄りに RO マウントしたときに起きやすい。今回 /etc/postgresql/ へ寄せているのは、その衝突を避ける意味もある。

.env が効かない問題も、Quadlet では見落としやすい。[Service] ではなく [Container] EnvironmentFile= に置く必要があるので、ユニット構成の理解がそのまま動作に直結する。

さらに initdb 系の変数は、既にデータディレクトリが初期化済みだと期待通りに効かない。初回の初期化フェーズでしか拾われないものは多いので、試行錯誤するときは既存データの有無を先に疑ったほうが早い。

結果

この構成で到達したかった状態は、「PostgreSQL 18 + LLVM JIT + pgvector」が rootless Quadlet 経由で安定して稼働することだった。イメージビルド、設定の外出し、NVMe 永続化、起動管理、検証手順、トラブルシュートまで一本につながっている。

特に良いのは、単なる Dockerfile メモで終わっていないことだ。postgresql.conf と pg_hba.conf の設計意図、環境変数ファイルの配置、Quadlet 側の落とし穴まで含めてあるので、次に見返したときにも再構築しやすい。

今後の作業

今後詰めるなら、まず初期化スクリプトを COPY のみで完結させるのか、コメントアウトされている Volume マウントも使うのかを運用ルールとして固定したい。両方の余地がある状態だと、再ビルド時と初回起動時で挙動を読み違えやすい。

次に、認証設定の許可範囲は環境に応じて見直したい。192.168.0.0/16 と 10.10.0.0/16 を広く許可しているので、将来的に接続元が増えるならセグメント単位でもう少し絞ったほうが安全だと思う。

最後に、POSTGRES_PASSWORD=localdev は明らかにローカル開発用の値なので、共有サーバーや長期運用の環境に持ち込むなら秘密情報管理を別に切る必要がある。とはいえ、ローカル基盤でまず再現性ある構成を固めるという目的に対しては、今回のメモは十分に実践的だった。