Skip to content

Commit b01a73b

Browse files
committed
スキーマ適用をアプリへ寄せ、travel-log-db-init を廃止する
アプリが同じDBへpgで繋いでいる以上、psqlを積んだイメージをもう1つ公開・pullする 理由が無い。db/entrypoint.sh を scripts/migrate.mjs へ移植し、appが待ち受けを 始める前に当てる形にした。イメージ2→1、サービス4→3、Actionsのジョブが1つ減る。 - scripts/migrate.mjs: advisory lock・SQLと適用記録を1トランザクション・ 「既にテーブルがあるDBは流さず適用済みとして記録」の分岐・DB起動待ちは 旧スクリプトのまま移植した - psql は PGSSLMODE を自分で読むが pg は読まない(既定は ssl: false)ので、 psql と同じ意味に翻訳する(require は検証なし、verify-* は検証あり)。 これが無いと .env.remote の外部DB(Supabase)へ繋がらない - scripts/migrate-remote.sh はアプリのイメージで同じスクリプトを走らせる。 Docker運用とホスティング先とで当たるSQLがずれない、という元の設計は保つ - 失敗の見え方が変わる。ワンショットの非ゼロ終了ではなく、appが起動途中で 落ちて再起動を繰り返す形になる(DBがまだ起きていないだけなら次の回で通る) - ディレクトリを準備するワンショットは data-init から init へ改名。 スキーマ適用の init が消えて名前が空いたため - GHCR に残る travel-log-db-init パッケージは手で削除する。掃除のジョブからは 名前を外した(存在しないパッケージを指すとジョブが落ちるため) 動作確認: 空のDBへ本番イメージから適用(schema + 14本)、再実行が冪等、既存DBで applied=0、開発スタックの起動、そして scripts/bootstrap-sql_test.sh の突き合わせ (列・索引・トリガー・適用記録が206項目一致)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 102169c commit b01a73b

19 files changed

Lines changed: 262 additions & 292 deletions

.github/workflows/docker-publish.yml

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# mainへのpushで本番用Dockerイメージをビルドし、GHCRへ公開する。
22
# 本番環境の更新はこれらをpullするだけでよい(README「本番運用」参照)。
3-
# ghcr.io/rtcode337/travel-log … アプリ本体(Dockerfileのprodステージ、appサービス)
4-
# ghcr.io/rtcode337/travel-log-db-init … スキーマ・マイグレーション適用
5-
# (db/Dockerfile、initサービス。イメージ名は旧構成の名残)
3+
# ghcr.io/rtcode337/travel-log … アプリ本体(Dockerfileのprodステージ、appサービス)
4+
# スキーマとマイグレーションSQLもこれに焼き込まれ、
5+
# 起動時に scripts/migrate.mjs が当てる
66
#
77
# アプリ本体は amd64/arm64 を「それぞれのアーキのネイティブランナー」で並列ビルドし、
88
# ダイジェストで push したものを最後の merge ジョブでマニフェストにまとめる。
@@ -133,68 +133,31 @@ jobs:
133133
- name: Inspect
134134
run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
135135

136-
# DBの準備・マイグレーション適用用イメージ(db/Dockerfile)。マイグレーションSQLを
137-
# 焼き込むため、db/migrations/ を足したときもこのイメージのpullだけで本番に反映される。
138-
# 小さいイメージ(Alpine + SQLコピー)でハングの実績も無いため、こちらは従来どおり
139-
# QEMUマルチアーキビルドのまま(アプリ本体とは独立して並列に走る)
140-
build-and-push-db-init:
141-
runs-on: ubuntu-latest
142-
steps:
143-
- uses: actions/checkout@v7
144-
145-
- uses: docker/setup-qemu-action@v4
146-
147-
- uses: docker/setup-buildx-action@v4
148-
149-
- uses: docker/login-action@v4
150-
with:
151-
registry: ghcr.io
152-
username: ${{ github.actor }}
153-
password: ${{ secrets.GITHUB_TOKEN }}
154-
155-
- id: meta
156-
uses: docker/metadata-action@v6
157-
with:
158-
images: ghcr.io/${{ github.repository }}-db-init
159-
tags: |
160-
type=raw,value=latest,enable={{is_default_branch}}
161-
type=sha
162-
163-
- uses: docker/build-push-action@v7
164-
with:
165-
# マイグレーションSQLとentrypoint.shだけを含む小さなコンテキスト
166-
context: ./db
167-
push: true
168-
platforms: linux/amd64,linux/arm64
169-
tags: ${{ steps.meta.outputs.tags }}
170-
labels: ${{ steps.meta.outputs.labels }}
171-
cache-from: type=gha,scope=db-init
172-
cache-to: type=gha,mode=max,scope=db-init
173-
174136
# 古い版を消して直近 10 世代だけ残す。
175137
#
176138
# 公開パッケージなので GHCR の容量・転送は無料枠を消費しないが、版は放っておくと
177139
# 際限なく積み上がる(`latest` を付け替えても前の版は SHA タグ付きで残る)。
178140
# 一覧が読めなくなるのと、どれが生きているか分からなくなるのを防ぐための掃除。
179141
#
180-
# **専用のジョブにして、両方の push が終わってから走らせる。** db-init のジョブは
181-
# 本体のビルドと並行に走るので、そちらに相乗りさせると「新しい版が出来る前に掃除する」
182-
# 順序になりうる。
142+
# **専用のジョブにして、push が終わってから走らせる。** merge に相乗りさせると
143+
# 「新しい版が出来る前に掃除する」順序になりうる。
183144
#
184145
# **actions/delete-package-versions は使わない。** このリポジトリはマルチアーキで、
185146
# 1 つのタグが manifest list + アーキごとの子イメージで構成される。あちらは
186147
# 子イメージを「タグ無しの版」として消してしまい、残したはずのタグが壊れる。
187148
# この action は manifest list を理解して親子まとめて扱う。
188149
cleanup:
189-
needs: [merge, build-and-push-db-init]
150+
needs: [merge]
190151
runs-on: ubuntu-latest
191152
permissions:
192153
packages: write
193154
steps:
194155
- name: 古いイメージを削除して直近 10 世代だけ残す
195156
uses: dataaxiom/ghcr-cleanup-action@v1
196157
with:
197-
packages: travel-log,travel-log-db-init
158+
# travel-log-db-init は廃止(スキーマ適用をアプリへ寄せた)。GHCR に残っている
159+
# 古いパッケージは手で削除する —— ここに残すと存在しない扱いでジョブが落ちる
160+
packages: travel-log
198161
keep-n-tagged: 10
199162
delete-untagged: true
200163
token: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ yarn-error.log*
3232

3333
# データの置き場(composeのbindマウント先)。この下に db(Postgresの実データ)・
3434
# photos(訪問記録の写真)・exports(エクスポートのZIP)が入る。どれも個人情報を
35-
# 含むため絶対にコミットしない。ディレクトリ自体はdata-initサービスが起動時に
35+
# 含むため絶対にコミットしない。ディレクトリ自体はinitサービスが起動時に
3636
# 作るので.gitkeepは不要。中身ではなくディレクトリごと無視することで、
3737
# postgresユーザー所有で権限が絞られた中身をgitが読みに行かず
3838
# 「could not open directory」警告が出ないようにする

CLAUDE.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55
## コマンド
66

77
```bash
8-
docker compose -f docker-compose.dev.yml up --build # 開発用: アプリ(localhost:7040, next dev+ホットリロード)+Postgres。スキーマ作成・未適用マイグレーションはinitサービスが自動で行う
9-
docker compose pull && docker compose up -d # 本番用: GHCRのビルド済みイメージ(mainへのpushでGitHub Actionsが自動ビルド)で起動。未適用のマイグレーションはinitサービスが自動で当てる。SESSION_SECRET環境変数が必須(.env可)
8+
docker compose -f docker-compose.dev.yml up --build # 開発用: アプリ(localhost:7040, next dev+ホットリロード)+Postgres。スキーマ作成・未適用マイグレーションはアプリが起動時に自動で行う
9+
docker compose pull && docker compose up -d # 本番用: GHCRのビルド済みイメージ(mainへのpushでGitHub Actionsが自動ビルド)で起動。未適用のマイグレーションはアプリが起動時に自動で当てる。SESSION_SECRET環境変数が必須(.env可)
1010
npm run dev # Next.js開発サーバー(ローカルPostgresを直接使う場合のみ)
1111
npm run build # 本番ビルド(型チェック込み)
1212
```
@@ -24,36 +24,39 @@ LAN内の別端末から開発サーバを開くときは`ALLOWED_DEV_ORIGINS`(`
2424

2525
`docker-compose.standalone.example.yml`は、`.env`もリポジトリのクローンも置けない環境(NASのコンテナマネージャー等、管理画面にYAMLを貼り付けて起動するタイプ)向けの単体定義の雛形。`docker-compose.yml`との違いは「`${...}`を使わず値を直書きする」「bindマウントを絶対パスで書く」の2点だけで、サービス構成・起動順は同じ。**`docker-compose.yml`側のサービス・環境変数を変えたら、standalone側にも同じ変更を反映すること**(値の直書きぶん古くなりやすい)。**リポジトリに置くのは`.example`の付いた雛形だけ**で、実値を入れてコピーした`docker-compose.standalone.yml``.gitignore`してある(`.env.example``.env`の関係と同じ。この形式は`SESSION_SECRET`等を直書きするので、雛形を直接編集すると秘密がコミット対象に入る)。
2626

27-
このプロジェクトにアプリコードのテストスイート/テストコマンドは存在しない(唯一のテストは`scripts/bootstrap-sql_test.sh`で、Supabase向けの一括SQLが`init`サービスと同じスキーマを作るかを突き合わせるもの`db/migrations/README.md`参照)。リンターも未導入(Next.js 16で`next lint`が廃止された際、代替のESLint導入は見送った — eslint-config-nextの依存チェーンに未修正のbrace-expansion脆弱性(GHSA-mh99-v99m-4gvg)が含まれ、導入するとDependabotの高深刻度アラートが解消不能な形で付くため。エコシステム側の修正後に導入を検討する)。型チェックは`next build`が行う。
27+
このプロジェクトにアプリコードのテストスイート/テストコマンドは存在しない(唯一のテストは`scripts/bootstrap-sql_test.sh`で、Supabase向けの一括SQLがアプリの起動時の適用と同じスキーマを作るかを突き合わせるもの`db/migrations/README.md`参照)。リンターも未導入(Next.js 16で`next lint`が廃止された際、代替のESLint導入は見送った — eslint-config-nextの依存チェーンに未修正のbrace-expansion脆弱性(GHSA-mh99-v99m-4gvg)が含まれ、導入するとDependabotの高深刻度アラートが解消不能な形で付くため。エコシステム側の修正後に導入を検討する)。型チェックは`next build`が行う。
2828

2929
### スキーマ変更のルール
3030

3131
DB定義は`db/init/01_schema.sql`の1ファイルにすべてまとまっている(テーブル・索引・トリガー・既定のスポット種別の投入まで)。**このファイルが「現在あるべきスキーマの唯一の定義」**で、追加分を`02_...`のような別の初期化ファイルに切り出す方式は取らない。スキーマを変えるときは常にこのファイルだけを編集すること。
3232

3333
テーブル定義の読める形の一覧とER図は[docs/database.md](docs/database.md)にまとめてある。**DBに変更を入れたら、同じコミットでこの文書も更新すること**(README等と同じく実装に追従させる対象)。
3434

35-
あわせて、**テーブルに変更を加えた場合は同じコミットで`db/migrations/`に移行スクリプトを追加し、本番DBを既存データを保持したまま移行可能にすること**(本番には利用者の訪問記録・写真が入るため、`data/`を捨てる運用はできない)。ファイル名は`<連番>_<内容>.sql`で、ファイル名がそのまま`schema_migrations.version`になる。**`begin`/`commit``schema_migrations`へのinsertはスクリプトに書かない**(どちらも`db/entrypoint.sh`が受け持つ)。全文idempotentにすること — 新規DBに対しても一度は実行される。詳細は`db/migrations/README.md`
35+
あわせて、**テーブルに変更を加えた場合は同じコミットで`db/migrations/`に移行スクリプトを追加し、本番DBを既存データを保持したまま移行可能にすること**(本番には利用者の訪問記録・写真が入るため、`data/`を捨てる運用はできない)。ファイル名は`<連番>_<内容>.sql`で、ファイル名がそのまま`schema_migrations.version`になる。**`begin`/`commit``schema_migrations`へのinsertはスクリプトに書かない**(どちらも`scripts/migrate.mjs`が受け持つ)。全文idempotentにすること — 新規DBに対しても一度は実行される。詳細は`db/migrations/README.md`
3636

3737
適用は`docker compose up`で自動的に行われる(手で流す必要はない。下記「DBの初期化・マイグレーションの流れ」参照)。
3838

3939
移行スクリプトを書いたら、**旧スキーマのダンプに当てた結果が新規作成したDBと一致することを確認する**(`information_schema.columns``pg_trigger``pg_indexes`を新旧で突き合わせる。手順は`db/migrations/README.md`)。列の並び順だけはPostgresでは既存テーブルに対して変更できないため一致しないが、アプリは常に列名で読み書きしているため影響しない。
4040

4141
### DBの初期化・マイグレーションの流れ
4242

43-
composeは`db``init``app`の順に起動する。
43+
composeは`init``db``app`の順に起動する。スキーマの適用は`app`自身が起動時に行う
4444

4545
| サービス | 役割 | タイミング |
4646
|---|---|---|
47-
| `data-init` | `data/`の下に`db``photos``exports`を作り、`photos``exports`の所有者を実行ユーザーに合わせるワンショット(appと同じイメージをrootで起動) | 最初 |
48-
| `db` | Postgres本体(空のDBができるだけ。スキーマは作らない)。実データは`PGDATA``data/db/18/docker`に置く。所有者はpostgresのエントリポイントが自分で揃える | `data-init`の正常終了後 |
49-
| `init` | スキーマ本体(`/init/01_schema.sql`)と`/migrations`の未適用SQLを適用し`schema_migrations`に記録するワンショット(`db/Dockerfile``db/entrypoint.sh`) | dbのhealthcheck通過**** |
50-
| `app` | Next.js。`init`が正常終了するまで起動しない | 最後 |
47+
| `init` | `data/`の下に`db``photos``exports`を作り、`photos``exports`の所有者を実行ユーザーに合わせるワンショット(appと同じイメージをrootで起動) | 最初 |
48+
| `db` | Postgres本体(空のDBができるだけ。スキーマは作らない)。実データは`PGDATA``data/db/18/docker`に置く。所有者はpostgresのエントリポイントが自分で揃える | `init`の正常終了後 |
49+
| `app` | 待ち受けの前にスキーマ本体(`db/init/01_schema.sql`)と`db/migrations`の未適用SQLを適用し`schema_migrations`に記録し(`scripts/migrate.mjs`)、そのあとNext.jsを起動する | dbのhealthcheck通過**** |
5150

52-
**`data-init`は一度廃止して、別の理由で戻したもの** かつて同じ位置にいた`db-init`(prepareサブコマンド)は「ディレクトリの作成とchown」が仕事で、postgresのエントリポイントが同じことを自分でやるため不要になって消した。GHCRのイメージ名(`travel-log-db-init`)はその名残で、`init`サービスが使い続けている
51+
**スキーマの適用に専用のイメージとサービスは持たない** かつては`postgres`イメージにSQLとシェルスクリプトを焼いた`travel-log-db-init`を作り、**`init`という名前のワンショット**がdbとappの間で1回走っていた(**いまの`init`は名前が同じだけの別物** —— 下記)。**アプリが同じDBへ`pg`で繋いでいる以上、psqlを積んだイメージをもう1つ公開・pullする理由が無い**ので、`scripts/migrate.mjs`としてアプリ側へ寄せた(イメージ1つ・サービス1つ・Actionsのジョブ1つが減る)。**適用の中身は1か所**で、外部DBへ当てる`scripts/migrate-remote.sh`も同じスクリプトをアプリのイメージで走らせる —— Docker運用とホスティング先とで当たるSQLがずれないため
5352

54-
いま`data-init`が要るのは**`app`が非rootになったから** —— postgresが面倒を見てくれるのは`db/`だけで、`photos/``exports/`の所有者は誰も揃えない。bindマウント先がホストに無いとDockerがroot所有で作るので、何もしないと写真の保存で必ず落ちる。**当時の「自動作成に頼れない環境ほどそれが必要」という反省は今も有効**で、だから**ホストに用意してもらうのは親の`data/`1つだけ**にしてある(その下の3つは`data-init`が作る)。編集する場所も、standaloneのYAMLでパスを3つ書いていたのを`x-data-dir`1つに減らした
53+
**引き換えに、失敗の見え方が変わった。** ワンショットの非ゼロ終了ではなく、`app`が起動途中で落ちて再起動を繰り返す形になる(理由は`docker compose logs app``migrate:`の行)。DBがまだ起きていないだけなら次の回で通るので、再試行になること自体は望ましい
5554

56-
スキーマ本体もマイグレーションSQLも`travel-log-db-init`イメージに焼き込まれるため、本番ホストのリポジトリの新旧に関わらず、pullしたイメージの中身がそのまま適用される。マイグレーションが失敗すると`init`が非ゼロ終了し、`app`も起動しないため、古いスキーマのままアプリが動くことはない。
55+
**いまの`init`の仕事はディレクトリの準備で、これも一度廃止して戻したもの。** 昔いた`db-init`(prepareサブコマンド)がまさにそれをやっていたが、postgresのエントリポイントが同じことを自分でやるため不要になって消した(GHCRのイメージ名`travel-log-db-init`もその名残だった)。**スキーマ適用の`init`が消えて名前が空いた**ので、そちらの名前を引き継いでいる。
56+
57+
戻す理由は当時と違って**`app`が非rootになったから** —— postgresが面倒を見てくれるのは`db/`だけで、`photos/``exports/`の所有者は誰も揃えない。bindマウント先がホストに無いとDockerがroot所有で作るので、何もしないと写真の保存で必ず落ちる。**当時の「自動作成に頼れない環境ほどそれが必要」という反省は今も有効**で、だから**ホストに用意してもらうのは親の`data/`1つだけ**にしてある(その下の3つは`init`が作る)。編集する場所も、standaloneのYAMLでパスを3つ書いていたのを`x-data-dir`1つに減らした。
58+
59+
スキーマ本体もマイグレーションSQLもアプリのイメージに焼き込まれる(`Dockerfile`のprodステージが`db/init``db/migrations`をコピーする)ため、本番ホストのリポジトリの新旧に関わらず、pullしたイメージの中身がそのまま適用される。マイグレーションが失敗すると待ち受けに進まないため、古いスキーマのままアプリが動くことはない。
5760

5861
`01_schema.sql``schema_migrations`上では`000_init_schema`という名前の「一番先頭のマイグレーション」として扱う。空のDBには実行し、既にテーブルがあるDB(旧方式でinitdbが作ったもの)には実行せず適用済みとして記録するだけにするので、既存の本番DBをそのまま引き継げる。
5962

0 commit comments

Comments
 (0)