データベースはDockerとホストのどちらで動かす?
PostgreSQL、MySQL、MongoDB、Redisを本番でDocker運用するのは問題ありません。注意すべき点はボリューム、所有者、メジャーバージョンアップグレード、復元を確認したバックアップ、メモリ制限です。
データベースは Docker で実行すべきですか、それともホストで実行すべきですか?
データベースは Docker で実行します。1 台の VPS で 1 つのアプリケーションスタックを運用する場合、コンテナ化した PostgreSQL、MySQL、MongoDB、Redis は一般的な本番環境の選択肢です。この点について議論される内容は、通常、論点がずれています。コンテナは、namespace と cgroup で囲まれた Linux プロセスであり、仮想マシンではありません。そのため、データベースとディスクの間にハイパーバイザーはありません。bind mount またはローカルの named volume を使用すれば、読み書きはホストのファイルシステムに反映されます。これは、パッケージをインストールした場合に使用されるファイルシステムと同じです。
本当のコストは運用にあります。この構成が問題ないか、遅い障害につながるかを決めるのは、次の 4 点です。データの保存場所、そのディレクトリの所有者、メジャーバージョンアップグレードの手順、そしてバックアップを実際にリストアした経験の有無です。これらを適切に管理できれば、コンテナは細部にすぎません。管理を誤ると、原因としてコンテナを責めることになります。
この判断は、サーバー上で実行するすべてのデータベースに共通します。以下の例では PostgreSQL、MySQL、MongoDB、Redis を使用し、重要な製品固有の違いについては該当箇所で説明します。
コンテナによって実際に変わるもの
ボリュームをマウントしている限り、ストレージパスは変わりません。カーネル、ページキャッシュ、ファイルシステムも同じです。
実際の性能上の注意点は 1 つだけで、何もマウントしない場合です。ボリュームがないと、データディレクトリはイメージ上に重ねられた overlay filesystem である、コンテナの書き込み可能レイヤーに置かれます。そこへの書き込みは遅く、コンテナを削除するとレイヤー全体も削除されます。「今朝、データベースが空になっていた」という事象は、ここから発生します。
実際に変わるものは次のとおりです。
- ライフサイクル。
docker compose downはコンテナを破棄します。ボリュームに保存されていないデータは、すべて失われます。 - バージョン。イメージタグがバージョンです。データベースコンテナ内の
apt upgradeは、次のdocker compose pull後も保持されません。 - メモリの計上。cgroup の制限はカーネルが適用する厳格な上限であり、データベースはその存在を認識しません。
- ユーザー。プロセスはコンテナ内の数値ユーザー ID で実行されます。この ID は、ホスト上ではどのファイルも所有していない可能性があります。
データの保存場所がすべてを決めます
適切な選択肢は2つあり、よくある誤りが1つあります。
- 名前付きボリューム:
pgdata:/var/lib/postgresql/data。Docker は/var/lib/docker/volumes/<project>_pgdata/_dataにディレクトリを作成し、イメージの entrypoint が初回起動時に所有者を設定します。通常はこれを選択します。 - bind mount:
/srv/appname/pg:/var/lib/postgresql/data。パスを自分で指定するため、権限の問題も自分で解決する必要があります。 - マウントしない。上記を参照してください。データはコンテナ内に保存されます。
詳細なトレードオフは別のテーマであり、bind mount と名前付きボリュームの比較で説明しています。データベースについて簡潔に言えば、ホスト上のパスを把握する必要がある明確な理由がない限り、名前付きボリュームを使用してください。bind mount を使用する場合は、git clean がアクセスできるプロジェクトディレクトリ内ではなく、/srv/appname/pg のような安定した場所に配置します。
厳守すべき制限が1つあります。データベースのデータディレクトリを NFS(network file system)や、ロックおよび fsync の動作をテストしていないネットワークマウント上に配置しないでください。データベースは、成功した fsync によってバイト列が安定したストレージに保存されたと想定します。この想定が成り立たない場合、数週間後に破損が発生します。
ボリュームが見えなくなる前に、ボリューム名を固定する
Compose はボリュームに <project>_<volume> という名前を付け、プロジェクト名はデフォルトでディレクトリ名になります。そのため、ボリュームの識別情報は、何気なく変更されることがあるディレクトリ名に依存します。
/srv/app を /srv/app-old に移動するか、compose ファイルの pgdata キーの名前を変更すると、次回の docker compose up -d で新しい空のボリュームが作成されます。Postgres はそのボリュームに新しいクラスタを初期化します。コンテナは正常で、アプリケーションも起動しますが、すべてのテーブルが消えます。古いボリュームは古い名前でディスク上に残っています。これは幸いな点です。
docker volume ls
docker volume inspect app_pgdataこの問題が起きないように、名前を固定します。プロジェクト名とボリューム名を明示的に設定します。
name: myapp
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
name: myapp_pgdata既存のボリュームにデータが残っている場合は、データベースを停止してからコピーします。
docker compose stop db
docker run --rm -v app_pgdata:/from -v myapp_pgdata:/to alpine sh -c 'cp -a /from/. /to/'
docker compose start dbデータベースの実行中にコピーすると、書き込み中のファイルが不完全な状態でコピーされます。先に停止してください。
データディレクトリの所有者
公式の Postgres、MySQL、MongoDB イメージは、通常 999 などの非特権ユーザー ID でサーバーを実行します。コンテナが root で起動すると、entrypoint はデータディレクトリの所有権をそのユーザーに変更してから、権限を降格します。そのため、空の bind mount であれば通常は最初から動作します。
compose ファイルで user: を設定すると、entrypoint に修正に必要な権限が残らないため、すぐに失敗します。Postgres はこの点を明確に示します。
initdb: error: could not change permissions of directory "/var/lib/postgresql/data": Operation not permitted誤ったモードで存在するデータディレクトリでは、別のメッセージが表示されます。このメッセージは、修正に必要なのが chmod であり、chown ではないことを示すため、覚えておく価値があります。
FATAL: data directory "/var/lib/postgresql/data" has invalid permissions
DETAIL: Permissions should be u=rwx (0700) or u=rwx,g=rx (0750).MongoDB は、root が所有する bind mount ではロックファイルの作成時に失敗します。
Unable to create/open the lock file: /data/db/mongod.lock (Permission denied). Ensure the user executing mongod is the owner of the lock file and has the appropriate permissions.修正するには、ホスト上のディレクトリを名前ではなく数値 ID の所有に変更します。
sudo chown -R 999:999 /srv/appname/pg
sudo chmod 700 /srv/appname/pg
ls -ldn /srv/appname/pgls -ldn は名前ではなく数値を表示します。結果は 999 999 になるはずです。ホスト上の postgres というアカウントと、イメージ内の postgres というアカウントは関係ありません。カーネルが比較するのは数値であり、名前はホスト側とコンテナ側で別々に参照されます。PUID と PGID がホストのユーザーをコンテナに対応付ける方法では、この対応関係を詳しく説明しています。rootless Docker または user namespace remapping を使用すると数値はさらに変わるため、999 と仮定せず、実行中のコンテナから ID を確認してください。
名前付きボリュームを使用すると、初回起動時にはこの問題全体を回避できます。Docker が空のディレクトリを作成し、entrypoint がその所有者になるためです。
アップグレード: パッケージのアップグレードとイメージタグの変更
ホスト上では、apt upgrade によりマイナーバージョンを順に更新できます。ディストリビューションがデータベースのメジャーバージョンを自動的に上げることはありません。自分でメジャーバージョンを上げる場合も、2 つのバージョンのバイナリを同時にインストールできます。これは、pg_upgrade に必要な動作です。
コンテナでは、タグがバージョンを表します。そのため、アップグレードは 1 行の編集で済みます。マイナーアップグレードは簡単ですが、メジャーアップグレードには手順が必要です。
postgres:16 を postgres:17 に変更して docker compose up -d を実行すると、コンテナはすぐに終了します。
PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.データが破損したわけではありません。メジャーバージョン間で変更されたディスク上のカタログ形式を、新しいバイナリが読み取れないためです。タグを postgres:16 に戻すと、再び起動します。このロールバックが、コンテナでアップグレードを行う際の唯一の明確な利点です。
サポートされている方法は、dump と restore です。PostgreSQL では、新しいクライアントで dump を取得することが推奨されます。そのため、compose ネットワーク上で稼働中の古いサーバーに対し、新しいイメージから実行します。
docker run --rm --network myapp_default -e PGPASSWORD="$POSTGRES_PASSWORD" \
postgres:17 pg_dumpall -h db -U postgres > /srv/backups/all.sql
ls -lh /srv/backups/all.sql
tail -n 2 /srv/backups/all.sqlファイルは少なくとも数十キロバイトのサイズになり、最後に PostgreSQL database cluster dump complete と記された行が含まれている必要があります。数百バイトしかないファイルは dump に失敗したことを示します。そのままでは、何も得られないまま volume を削除することになります。この確認が済んでから、次を実行します。
docker compose down
docker volume rm myapp_pgdata
# edit the compose file: image: postgres:17
docker compose up -d db
docker compose exec -T db psql -U postgres -f /dev/stdin < /srv/backups/all.sql他のエンジンでは動作が異なります。
- MySQL 8 は起動時に独自のデータディクショナリをアップグレードするため、マイナータグの更新は通常、再起動だけで済みます。リリース系列をまたいで更新する場合は、事前にリリースノートを確認し、どちらの場合でも先に dump を取得してください。
- MariaDB では、新しいバージョンでサーバーが起動した後に
mariadb-upgradeを実行する必要があります。 - MongoDB は一度に 1 つのメジャーバージョンだけアップグレードする必要があります。各段階の後、次に進む前に feature compatibility version を設定します。バージョンを飛ばすと、
mongodは起動を拒否し、featureCompatibilityVersionの名前を示すUPGRADE PROBLEM行をログに記録します。MongoDB 7.0 以降では、コマンドに明示的な確認フラグが必要です:db.adminCommand({ setFeatureCompatibilityVersion: "8.0", confirm: true })。 - Redis は古い snapshot ファイルを問題なく読み込みますが、新しい形式のファイルは読み込めません。そのため、アップグレードは再起動で済みますが、ダウングレードではデータの読み込みに失敗することがあります。
一般則は次のとおりです。コンテナではダウングレードは簡単になりますが、アップグレードが簡単になるわけではありません。
データベースコンテナが終了コード 137 で終了するのはなぜですか?
カーネルの Out of Memory(OOM)キラーがコンテナを強制終了したためです。137 は 128 にシグナル 9 を加えた値です。
docker compose ps
docker inspect myapp-db-1 | grep -i oomkilled
journalctl -k | tail -n 20docker compose psには Exited (137) と表示され、inspect の行には "OOMKilled": true と記録されます。カーネルログにも対応するエントリがあります。
Memory cgroup out of memory: Killed process 4711 (postgres) total-vm:2170416kB仕組みは次のとおりです。これは誤解されやすい点です。PostgreSQL と MySQL は、ホストが報告する総メモリ量を基にバッファーサイズを決めます。cgroup の制限は、その値を変更しません。16 GB のホストで制限が 2 GB の場合、データベースは 16 GB あるものとして計画します。そのため、ホスト自体にメモリ不足の兆候が現れるよりはるか前に、cgroup がデータベースを強制終了します。したがって、メモリ制限を設定するだけでは不十分です。データベースが使用できるメモリ量も指定する必要があります。
- PostgreSQL:
shared_buffersを設定し、work_memにも注意してください。work_memは接続ごと、ソート処理ごとに割り当てられます。そのため、大きな値に 50 接続を掛けた状態が、起動時ではなく負荷時にコンテナが終了する一般的な原因です。 - MySQL と MariaDB: デフォルトが 128M の
innodb_buffer_pool_sizeを設定してください。コンテナではinnodb_dedicated_serverを無効にしてください。これは検出したマシンのメモリ量を基に自身のサイズを決めるための機能だからです。 - MongoDB: ホストのメモリ量から推測させず、WiredTiger のキャッシュサイズを明示的に設定してください。
- Redis:
maxmemoryのデフォルトは無制限です。そのため、cgroup に停止させられるまで Redis がメモリを使い続けます。maxmemoryはコンテナの制限より十分小さい値に設定し、maxmemory-policyを選択してください。
Postgres 側でもこのイベントが記録されます。ログには次の 2 行が表示されます。
LOG: server process (PID 123) was terminated by signal 9: Killed
LOG: terminating any other active sessions due to crash of another server process1 つのバックエンドが強制終了されると、共有メモリが不整合になる可能性があるため、他のすべてのバックエンドも再起動します。これはアプリケーションにとって静かなイベントではなく、接続の集中発生です。Docker Compose でメモリ制限を設定する方法では、構文と mem_limit形式と deploy.resources形式の違いを説明しています。
この問題がホスト上でなくなるわけではありません。発生場所が移るだけです。cgroup がなければ、データベースはホスト上の他のすべてのプロセスとメモリを競合します。その場合、ホストの OOM キラーはスコアに基づいて強制終了する対象を選ぶため、sshd が対象になることもあります。データベースを予測可能な形で停止させる制限のほうが、ホストの OOM によって接続不能になる状態より運用しやすくなります。
バックアップ: 内部では dump、外部へバックアップ
稼働中のデータベースのデータディレクトリをコピーしてバックアップしないでください。サーバーが書き込み中に取得したファイル単位のコピーは不完全なコピーになり、復元時になって初めて問題が判明します。
確実な方法は2つあります。データベース固有のツールで稼働中に dump を取得し、その dump をバックアップする方法です。もう1つは、コンテナを停止して volume を停止状態でコピーする方法です。
docker compose exec -T db pg_dump -U postgres -Fc appdb > /srv/backups/appdb.dump
docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases > /srv/backups/mysql.sql
docker compose exec -T db mongodump --archive --gzip --db appdb > /srv/backups/appdb.archive.gz
docker compose exec -T redis redis-cli BGSAVE-Tが重要です。これがないと、docker compose execがコマンドに端末を接続でき、端末層が出力ストリームに復帰改行を追加します。すると、テキスト dump は復元時に不自然なエラーになり、バイナリ dump は単純に破損します。バックアップ時には何も報告されず、1か月後に大きなエラーとして発覚します。
--single-transactionにより、mysqldumpはサーバー全体をロックせずに InnoDB テーブルの一貫したスナップショットを取得できます。
これらのコマンドは、それぞれ1つのファイルを書き出します。これらはバックアップシステムではありません。保持期間の管理、外部へのコピー、検証がないためです。3つすべてを実行するツールに dump ディレクトリを渡してください。VPS からの restic バックアップはそのためのものです。バックアップするのは/srv/backupsであり、/var/lib/docker/volumesではありません。
次に復元を実行してください。一度も復元していないバックアップは、バックアップとはいえません。
docker compose exec -T db createdb -U postgres restore_test
docker compose exec -T db pg_restore -U postgres -d restore_test < /srv/backups/appdb.dump
docker compose exec -T db psql -U postgres -d restore_test -c '\dt'\dtには、アプリケーションのテーブルが一覧表示されるはずです。結果が空、またはDid not find any relations.の場合、dump の内容が想定と異なります。完了したらrestore_testを削除してください。
すべてを削除するコマンド
docker compose down -v.
通常の down は、コンテナとネットワークを削除します。-v は、その Compose ファイルで宣言された名前付きボリュームに加え、それらのコンテナに接続されたすべての匿名ボリュームも削除します。確認プロンプトは表示されず、元に戻すこともできません。これは self-hosted データベースが破壊される最も一般的な原因です。通常は、無関係な問題のトラブルシューティング中に、フォーラムの回答に従って実行してしまうことで発生します。
影響範囲を抑える方法は 4 つあります。
- データベースのボリュームを
external: trueとして宣言します。Compose は自身が所有していないボリュームを削除しないため、-vからそのボリュームに到達できません。docker volume create myapp_pgdataで 1 回作成します。 - 通常の再起動には
docker compose stopとdocker compose startを使用します。Compose における down と stop の違い で、それぞれが削除する対象を説明しています。 - ダンプは、Compose が管理するすべてのボリュームの外側にあるホストパスへ保存します。
- 重要なデータを保持する stack に、トラブルシューティングの回答から
-vを貼り付けて実行してはいけません。
データベースのポートを公開しない
この行により、データベースがパブリックインターネットに公開されます。
ports:
- "5432:5432"すべてのインターフェースで待ち受ける設定です。Docker は、ファイアウォールの input ルールがパケットを確認する前に、宛先を書き換えてポートを公開します。ufw のルールは input chain にあるため、ufw deny 5432 はまったく機能しません。Docker の公開ポートが ufw をバイパスする理由で、chain の通過順序を確認できます。
同じ compose project 内のアプリケーションは、compose network 上で service name を使ってデータベースに接続できます。そのため、公開ポートは不要です。このブロックを削除してください。ホスト上のクライアントから接続する場合は、loopback のみにバインドします。
ports:
- "127.0.0.1:5432:5432"実際に待ち受けているポートを確認します。
sudo ss -ltnp | grep 5432127.0.0.1:5432 であれば適切です。0.0.0.0:5432 の場合は、誰でもパスワードを試せます。
どこで何を実行するか
1 台の VPS で 1 つのアプリケーションを運用する場合。 コンテナを使用します。名前を固定した名前付きボリュームを割り当て、ポートは公開しません。メモリ上限を設定し、データベース側にも対応する設定を適用します。毎晩、ホスト上のパスへダンプを取得し、restic で収集します。VPS にクリーンな Docker 環境を構築するから始め、スタックは 1 つの compose ファイルにまとめてコミットします。データベースのバージョンを git でレビュー可能な行として管理できる点が大きな利点です。
1 台のホストで複数のサービスを運用する場合。 コンテナを使用し、アプリケーションごとに 1 つのデータベースを用意します。すべてのアプリケーションで 1 つの共有サーバーを使う構成にはしません。共有サーバーでは、すべてのアプリケーションが 1 つのアップグレードスケジュールに依存し、1 つの暴走したクエリが全アプリケーションの障害になります。各コンテナに個別のメモリ上限を設定し、問題のあるクエリをそれを発行したアプリケーション内に封じ込めます。小規模な Postgres インスタンスを複数運用するほうが、ディスクコストは少し増えますが、調整作業は大幅に減ります。
データベース自体が製品である場合。 ベンダーのパッケージリポジトリからホストにインストールするか、マネージドサービスを利用します。pg_upgrade ではバイナリの 2 つのメジャーバージョンを同時にインストールする必要があります。これはパッケージで実現できますが、単一バージョンのイメージでは実現できません。WAL(先行書き込みログ)のアーカイブを使用したレプリケーションとポイントインタイムリカバリは、データベースがマシンとディスクを専有しているほうが、どちらも容易です。03:00 に自分へ通知が届くシステムには、堅実な方法を選択します。
アプリケーションが小規模な場合。 サーバーデータベースを使わない構成を検討します。1 台の VPS 上で単一の書き込み元を持つ Web アプリケーションでは、VPS の本番環境で SQLite を使用するほうが適していることがよくあります。この場合、バックアップ対象は 1 つのファイルで、アップグレード経路はライブラリのバージョン更新になります。
FAQ
本番データベースを Docker で実行しても安全ですか?
単一サーバーのアプリケーション構成であれば安全です。コンテナは、namespace と cgroup で分離された Linux プロセスです。そのため、volume をマウントすると、パッケージからインストールした場合と同じホストのファイルシステムにデータベースが書き込みます。リスクは速度ではなく、運用に関するものです。volume 名を固定していないこと、bind mount の所有者が誤った user id であること、リストアを一度もテストしていないこと、そして docker compose down -v です。この4点を解消すれば、コンテナで問題ありません。データベースが主なワークロードで、pg_upgrade、レプリケーション、または point-in-time recovery が必要な場合は、ホストへの直接インストールに移行します。
データベースのデータには bind mount と named volume のどちらを使うべきですか?
ホスト上のパスを把握する明確な理由がない限り、named volume を使います。Docker がディレクトリを作成し、イメージの entrypoint が初回起動時に所有権を設定するため、権限の問題が発生しません。明示的な name: で volume を固定するか、external: true としてマークしてください。そうしないと、プロジェクトディレクトリの名前を変更したときに、新しい空の volume と空のデータベースが暗黙に作成されます。bind mount を使う場合は、ホスト上のディレクトリを、イメージが実行する数値 user id に対して chown します。公式の Postgres、MySQL、MongoDB イメージでは 999 です。ls -ldn で確認してください。ls -l はその番号に対応するホスト側の名前を表示しますが、その名前はコンテナ内では意味を持ちません。
docker compose down -v は何を削除しますか?
通常の down と同様にコンテナとネットワークを削除します。さらに -v は、その compose ファイルで宣言されたすべての named volume と、対象コンテナに接続されたすべての anonymous volume を削除します。データベースも含まれます。確認プロンプトは表示されず、復旧もできません。external: true としてマークされた volume は削除されません。データベースの volume を external としてマークする主な理由はこれです。通常の再起動には、代わりに docker compose stop と docker compose start を使います。
Docker で PostgreSQL を新しい major version にアップグレードするにはどうすればよいですか?
ダンプしてリストアします。postgres:16 を postgres:17 に変更して再起動すると、DETAIL に両方のバージョンを示す行が表示された FATAL: database files are incompatible with server になります。新しいバイナリは古い catalog レイアウトを読み込めないためです。データが破損したわけではありません。古い tag に戻せば起動します。稼働中の古いコンテナに新しいバージョンの client を使って pg_dumpall を取得し、ファイルが PostgreSQL database cluster dump complete で終わっていることを確認します。その後、空の volume で新しい tag を起動し、ダンプを読み込みます。同じ major version 内の minor upgrade では、pull と再起動だけで済みます。
データベースコンテナが終了コード 137 で終了するのはなぜですか?
137 は 128 に signal 9 を加えた値なので、何かがプロセスを強制終了しています。docker inspect <container> | grep -i oomkilled を実行してください。true の値は、コンテナが cgroup のメモリ上限に達したことを示します。通常の原因は、PostgreSQL と MySQL がホスト全体のメモリ量を読み取り、コンテナの上限を認識しないことです。そのため、2 GB のコンテナ内で 16 GB を前提に動作しようとします。shared_buffers と work_mem、または innodb_buffer_pool_size を設定し、コンテナに指定した上限に合わせます。journalctl -k で対応する Memory cgroup out of memory 行を確認し、kernel がどのプロセスを選択したかを確認します。