Skip to content

Commit b69ccdd

Browse files
committed
standalone用composeの追加・プロジェクト名の統一・PUBLIC_BASE_URL対応
- docker-compose.standalone.yml を追加。`.env`もクローンも置けない環境 (NASのコンテナマネージャー等、管理画面にYAMLを貼り付けるタイプ)向けの単体定義。 本番用との違いは「${...}を使わず値を直書き」「bindマウントを絶対パスで書く」の2点だけで、 サービス構成・起動順は同じ。編集する値はすべて冒頭の「ここだけ編集」にまとめた - composeのプロジェクト名を3ファイルとも travel-log に統一(旧: travel-log-prod / travel-log-dev)。元々ポート3000とdb/dataを共有していて同時起動はできなかったため、 名前を分ける意味が薄かった。旧名で動かしているホストは初回だけ旧スタックを down してから起動する必要がある(README参照) - 外向きURLの決定に PUBLIC_BASE_URL を追加(lib/auth/request-url.ts)。 優先順位は PUBLIC_BASE_URL → X-Forwarded-Proto/Host → リクエスト自身のURL。 forwardedヘッダを送らないリバースプロキシの背後では、GoogleログインのリダイレクトURIが http:// で組まれて認証に失敗するため、公開URLを明示できるようにした Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 9d92664 commit b69ccdd

7 files changed

Lines changed: 215 additions & 3 deletions

File tree

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ SESSION_SECRET=
1515
GOOGLE_CLIENT_ID=
1616
GOOGLE_CLIENT_SECRET=
1717

18+
# 公開URL(任意)。リバースプロキシ(HTTPS終端)の背後で動かす場合、プロキシが
19+
# X-Forwarded-Proto/X-Forwarded-Host を送らない構成だと、Googleログインの
20+
# リダイレクトURIが http:// で組まれて認証に失敗する。そのときだけ設定する。
21+
# 未設定ならリクエストから自動導出する(lib/auth/request-url.ts)。
22+
# PUBLIC_BASE_URL=https://travel.example.com
23+
1824
# 訪問記録の写真の保存先(lib/photoStorage.ts)。
1925
# fs … ローカルのファイルシステム(既定)。Docker運用はこちら。
2026
# docker-compose は ./photos を /app/photos にbindマウントする。

CLAUDE.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ npm run build # 本番ビルド(型
1515

1616
どちらにも`predev`/`prebuild``npm run copy-maplibre-worker`(`scripts/copy-maplibre-worker.mjs`)が付いており、MapLibreのワーカースクリプトを`node_modules`から`public/maplibre-gl/`へコピーする(生成物のためgit管理外。理由は下記「MapLibreのワーカースクリプト」)。`next dev`/`next build`を直接叩くとこのコピーが走らないため、地図が真っ白になったらまず`npm run copy-maplibre-worker`を実行すること。
1717

18-
`docker-compose.yml`(本番用)と`docker-compose.dev.yml`(開発用)はプロジェクト名を分けてある(`travel-log-prod`/`travel-log-dev`)ため、同一ホスト上で両方動かしてもコンテナ・イメージ・ボリュームは衝突しない。
18+
3つのcomposeファイル(`docker-compose.yml`=本番用 / `docker-compose.dev.yml`=開発用 / `docker-compose.standalone.yml`)はどれもプロジェクト名を`travel-log`に揃えてある。同じホスト上で本番用と開発用を**同時に**は動かせない(元々ポート3000も`db/data`も共有しているため、名前を分けても同時起動はできなかった)。切り替えるときは先に`docker compose -f <今動いている方> down`すること。
19+
20+
`docker-compose.standalone.yml`は、`.env`もリポジトリのクローンも置けない環境(NASのコンテナマネージャー等、管理画面にYAMLを貼り付けて起動するタイプ)向けの単体定義。`docker-compose.yml`との違いは「`${...}`を使わず値を直書きする」「bindマウントを絶対パスで書く」の2点だけで、サービス構成・起動順は同じ。**`docker-compose.yml`側のサービス・環境変数を変えたら、standalone側にも同じ変更を反映すること**(値の直書きぶん古くなりやすい)。
1921

2022
このプロジェクトにテストスイート/テストコマンドは存在しない。リンターも未導入(Next.js 16で`next lint`が廃止された際、代替のESLint導入は見送った — eslint-config-nextの依存チェーンに未修正のbrace-expansion脆弱性(GHSA-mh99-v99m-4gvg)が含まれ、導入するとDependabotの高深刻度アラートが解消不能な形で付くため。エコシステム側の修正後に導入を検討する)。型チェックは`next build`が行う。
2123

@@ -80,6 +82,8 @@ Next.jsのRoute Handlersのみで、別立てのAPIサーバーは存在しな
8082

8183
NextAuthではなく自前実装。`lib/auth/session.ts`がHMAC-SHA256で署名したCookie(Web Crypto APIのみ使用、外部依存なし)を発行し、`proxy.ts`(Next.js 16で`middleware.ts`から改名されたもの。Node実行)とRoute Handlersの両方で同じロジックにより検証できるようにしている。Cookieには`{ sub: userId, exp }`のみを持たせ、roleは意図的にCookieに含めていない — `lib/auth/current-user.ts`経由で毎リクエストDBから引き直すことで、管理者によるロール変更やDB作り直しが古いCookieのまま反映されない事態を防いでいる。`proxy.ts``/login``/api/**`、および`/manifest.webmanifest`(ブラウザのmanifest取得は既定でCookieを送らないため、ガードするとPWAとしてインストールできなくなる)以外の全ルートをガードする。
8284

85+
外向きのURL(GoogleログインのリダイレクトURIとCookieの`Secure`属性)は`lib/auth/request-url.ts`が組み立てる。優先順位は`PUBLIC_BASE_URL`(環境変数) → `X-Forwarded-Proto`/`X-Forwarded-Host` → リクエスト自身のURL。リバースプロキシがforwardedヘッダを送らない構成(NAS内蔵のリバースプロキシ等)では、`PUBLIC_BASE_URL`を設定しないとリダイレクトURIが`http://`で組まれてGoogleログインが失敗する。**外向きのURLを組む処理を増やすときは`request.url`を直接使わず、必ずこのモジュール経由にすること**
86+
8387
### PWA(インストール可能化)
8488

8589
manifest(`app/manifest.ts`、Next.jsのMetadata Files規約で`/manifest.webmanifest`として配信)+アイコン(`public/icons/`のmanifest用3枚と、`app/icon.png`・`app/apple-icon.png`のファビコン/apple-touch-icon)による最小構成のPWA対応で、Service Worker・オフライン対応は意図的に持たない(「デプロイしたのに古い画面が出る」系の問題を避けるため、必要になるまで導入しない方針)。インストール後も中身は同じWebアプリで、認証Cookieもそのまま使われる。iOSはmanifestの`display`/`icons`を見ないため、`app/layout.tsx`の`metadata.appleWebApp`と`app/apple-icon.png`で別途同等の設定をしている。ページ自体のズームは`app/layout.tsx`の`viewport`(`maximumScale: 1`+`userScalable: false`)で無効化してある — 検索窓等への入力フォーカス時の自動ズームで下のタブバーが隠れ、地図表示中はピンチが地図操作に取られてページのズームを戻せなくなるため(地図の拡大縮小はMapLibreのジェスチャなので影響しない)。`/manifest.webmanifest`は`proxy.ts`のガード対象から除外が必要(上記「認証」参照)。アイコンPNGは`scripts/generate-icons.mjs`(sharp使用、依存には含めない)で生成したものをコミットしてあり、デザイン変更時のみ再生成する。iOSのスタンドアロン起動では`target="_blank"`の外部リンクがアプリ内ブラウザ(オーバーレイ)で開かれてしまうため、外部サイトへのリンク(`SpotInfoModal`の「Wikipediaで続きを読む」)はiOS+スタンドアロンのときだけ`x-safari-https://`スキーム(未文書化だがiOSが解釈する)で本物のSafariに切り替えている(`isIosStandalone`)。

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@ http://localhost:3000 を開くと `/login` にリダイレクトされる。初
8989
既存のメールアカウントと同じメールアドレスでGoogleログインした場合は自動的に紐付く。
9090
自由なサインアップはできず、管理者が作成したアカウントのみログインできる。
9191

92+
**リバースプロキシ(HTTPS終端)の背後で動かす場合**、アプリが受け取るリクエストは
93+
プロキシからのプレーンHTTPになる。リダイレクトURIのスキームは`X-Forwarded-Proto` /
94+
`X-Forwarded-Host`から判断しているため、これらを送らないプロキシ(NAS内蔵の
95+
リバースプロキシ機能など、設定項目が無いものもある)ではリダイレクトURIが
96+
`http://`で組まれ、Googleコンソールに登録した`https://`のURIと一致せず認証に失敗する。
97+
その場合は`PUBLIC_BASE_URL`に公開URLを設定する(設定するとヘッダより優先される)。
98+
99+
```
100+
PUBLIC_BASE_URL=https://travel.example.com
101+
```
102+
103+
- `.env`(Docker Compose)/ `.env.local`(Dockerを使わない場合)/
104+
`docker-compose.standalone.yml`冒頭の`x-public-base-url` のいずれかに書く
105+
- パス部分は使わず、スキーム・ホスト・ポートだけを見る
106+
- `https://`を設定するとセッションCookieに`Secure`属性が付く。同じインスタンスに
107+
LAN内から`http://<ホスト>:3000`で直接アクセスしてもログインできなくなる点に注意
108+
(公開URL経由でアクセスすること)
109+
- 直接`http://<ホスト>:3000`で使う場合や、`X-Forwarded-*`を送るプロキシ(nginx等で
110+
設定済み)の場合は設定不要
111+
92112
</details>
93113

94114
### 本番運用
@@ -106,6 +126,15 @@ echo "SESSION_SECRET=$(openssl rand -base64 32)" >> .env
106126
docker compose pull && docker compose up -d
107127
```
108128

129+
- Composeのプロジェクト名は本番・開発・standaloneとも`travel-log`。以前は
130+
`travel-log-prod`/`travel-log-dev`に分けていたため、**それ以前から動かしている
131+
ホストでは初回だけ旧スタックを止めてから起動すること**(止めずに`up`すると、
132+
同じ`db/data`とポート3000を奪い合う新旧2つのスタックが並ぶ)
133+
134+
```bash
135+
docker compose -p travel-log-prod down # 開発機では -p travel-log-dev -f docker-compose.dev.yml
136+
docker compose pull && docker compose up -d
137+
```
109138
- 公開されるイメージは2つ。`ghcr.io/rtcode337/travel-log`(アプリ本体)と
110139
`ghcr.io/rtcode337/travel-log-db-init`(DBの準備とマイグレーション適用)
111140
- **DBスキーマの更新は自動**`docker compose up`すると`db-migrate`サービスが未適用の
@@ -128,6 +157,17 @@ docker compose pull && docker compose up -d
128157
イメージがいつのどのコミットのものか、pull後の反映確認に使える(ローカル開発時など
129158
埋め込みが無い場合は「開発ビルド」と表示される)
130159

160+
#### リポジトリを置けない環境(NASのコンテナマネージャー等)
161+
162+
`.env`もクローンも置けず、管理画面にYAMLを貼り付けて起動するタイプの環境向けに
163+
[docker-compose.standalone.yml](docker-compose.standalone.yml)を用意している。
164+
`${...}`を使わず値を直書きし、bindマウントを絶対パスで書いたもの(サービス構成・
165+
起動順は`docker-compose.yml`と同じ)。冒頭の「ここだけ編集」——3つの置き場
166+
(`db` / `db/data` / `photos`)の絶対パスと`SESSION_SECRET`——を書き換えて貼り付ければ起動する。
167+
168+
`SESSION_SECRET`は空のままだとログイン時に`SESSION_SECRET is not set`で失敗するので、
169+
必ず32バイト程度のランダム文字列を入れること。
170+
131171
## 画面
132172

133173
すべて`/[種別キー]/...`の形式(例: `/tourist/map`)で、種別ごとに切り替えて使う。

docker-compose.dev.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: travel-log-dev
1+
name: travel-log
22

33
services:
44
db:
@@ -49,6 +49,9 @@ services:
4949
SESSION_SECRET: ${SESSION_SECRET:-local-dev-secret-change-me}
5050
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
5151
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
52+
# 公開URL(任意)。本番と同じ挙動を手元で試したいときだけ設定する
53+
# (lib/auth/request-url.ts)
54+
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-}
5255
NODE_ENV: development
5356
ports:
5457
- "3000:3000"

docker-compose.standalone.yml

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# `.env` を置けず、リポジトリのクローンもできない実行環境向けの単体定義。
2+
# (NASのコンテナマネージャー等、管理画面にYAMLを貼り付けて起動するタイプの環境を想定)
3+
#
4+
# docker-compose.yml との違いは2点だけ:
5+
# 1. `${...}` を使わず、値をYAMLに直接書く(`.env`もシェルの環境変数も無い前提のため)
6+
# 2. bindマウントを相対パス(./db・./photos)ではなく絶対パスで書く
7+
# (管理画面から登録する環境では相対パスの基準が分かりにくいため)
8+
# サービス構成・起動順(db-init → db → db-migrate → app)は本番用と同じ。
9+
#
10+
# 使い方:
11+
# 1. 「ここだけ編集」の置き場を、そのホストの絶対パスに書き換える
12+
# 2. SESSION_SECRET に32バイト程度のランダム文字列を設定する(必須)
13+
# 3. その内容を管理画面に貼り付けて起動する
14+
# 4. http://<ホスト>:3000/ を開く(初回アクセス時のセットアップで管理者アカウントを作る)
15+
#
16+
# 更新は管理画面の「イメージを最新にして再作成」相当の操作(`docker compose pull` +
17+
# `up -d`)だけでよい。未適用のマイグレーションは db-migrate が自動で当てる。
18+
# 特定時点に戻したいときは、下の2つの image のタグを `latest` から
19+
# `sha-xxxxxxx`(Actionsが付けるコミットSHAタグ)に変える —— ただしマイグレーションは
20+
# 前進のみで巻き戻せないため、スキーマ変更を伴うリリースを戻すにはDBのリストアが要る。
21+
#
22+
# 通常の(リポジトリを配置できる)環境では docker-compose.yml を使う
23+
# ——こちらは値の直書きぶん古くなりやすい。
24+
25+
name: travel-log
26+
27+
# ---- ここだけ編集: データの置き場(すべてホストの絶対パス) ----
28+
# db … db-init が中に data/ を作る親ディレクトリ
29+
# db-data… Postgresの実データ。**必ず db の直下の data にすること**
30+
# (db-init が /db/data を作って postgres(uid/gid 70)に chown するため)
31+
# photos … 訪問記録の写真。DBには相対パスだけが入るので、DBと一緒にバックアップする
32+
# いずれも無ければDockerが自動作成するが、先に作っておくのが確実。
33+
x-db-dir: &db-dir "/path/to/travel-log/db"
34+
x-db-data-dir: &db-data-dir "/path/to/travel-log/db/data"
35+
x-photos-dir: &photos-dir "/path/to/travel-log/photos"
36+
37+
# ---- ここだけ編集: アプリの設定(.env の代わり) ----
38+
# セッションCookieの署名鍵(必須)。32バイト程度のランダム文字列を入れる
39+
# (生成例: openssl rand -base64 32)。空のままだとログイン時に
40+
# "SESSION_SECRET is not set" で失敗する。変更すると既存のログインセッションは
41+
# すべて無効になる(再ログインすれば直る)。
42+
x-session-secret: &session-secret ""
43+
# Googleでログイン(任意)。空のままならメールログインのみ。
44+
# 取得方法・リダイレクトURIの設定はREADMEの「Googleログインの設定」参照
45+
x-google-client-id: &google-client-id ""
46+
x-google-client-secret: &google-client-secret ""
47+
# 公開URL(任意。例: "https://travel.example.com")。
48+
# リバースプロキシ越し(HTTPS終端あり)で使う場合、プロキシが X-Forwarded-Proto /
49+
# X-Forwarded-Host を送らない構成だと、Googleログインのリダイレクトが http:// で
50+
# 組まれて認証に失敗する。そのときはここに公開URLを書いて明示する。
51+
# 直接 http://<ホスト>:3000 で使うなら空のままでよい。
52+
x-public-base-url: &public-base-url ""
53+
54+
services:
55+
# dbの起動前に、bindマウント先(db/data)の作成と所有者調整を行うワンショット
56+
# (詳細は db/entrypoint.sh の prepare 関数のコメント参照)。
57+
db-init:
58+
image: ghcr.io/rtcode337/travel-log-db-init:latest
59+
user: root
60+
volumes:
61+
- type: bind
62+
source: *db-dir
63+
target: /db
64+
command: ["prepare"]
65+
restart: "no"
66+
67+
db:
68+
image: postgres:16-alpine
69+
depends_on:
70+
db-init:
71+
condition: service_completed_successfully
72+
environment:
73+
# ホストにポート公開しないコンテナ間だけの接続。変えるなら db-migrate の
74+
# PG* と app の DATABASE_URL も同じ値に揃えること
75+
POSTGRES_USER: travel_log
76+
POSTGRES_PASSWORD: travel_log
77+
POSTGRES_DB: travel_log
78+
volumes:
79+
# db/init はマウントしない。スキーマ本体(01_schema.sql)は db-migrate が流す
80+
- type: bind
81+
source: *db-data-dir
82+
target: /var/lib/postgresql/data
83+
healthcheck:
84+
test: ["CMD-SHELL", "pg_isready -U travel_log"]
85+
interval: 5s
86+
timeout: 5s
87+
retries: 10
88+
restart: unless-stopped
89+
90+
# スキーマ本体(/init/01_schema.sql)と未適用のマイグレーション(/migrations/*.sql)を
91+
# 順に適用するワンショット。適用済みリビジョンは schema_migrations テーブルに記録され、
92+
# 2回目以降は何もせず終了する。app はこれが正常終了するまで起動しない
93+
# (古いスキーマのままアプリが動くのを防ぐ)。
94+
db-migrate:
95+
image: ghcr.io/rtcode337/travel-log-db-init:latest
96+
depends_on:
97+
db:
98+
condition: service_healthy
99+
environment:
100+
PGHOST: db
101+
PGUSER: travel_log
102+
PGPASSWORD: travel_log
103+
PGDATABASE: travel_log
104+
restart: "no"
105+
106+
app:
107+
image: ghcr.io/rtcode337/travel-log:latest
108+
depends_on:
109+
db-migrate:
110+
condition: service_completed_successfully
111+
environment:
112+
DATABASE_URL: postgres://travel_log:travel_log@db:5432/travel_log
113+
# 値は冒頭の「ここだけ編集」にまとめてある
114+
SESSION_SECRET: *session-secret
115+
GOOGLE_CLIENT_ID: *google-client-id
116+
GOOGLE_CLIENT_SECRET: *google-client-secret
117+
PUBLIC_BASE_URL: *public-base-url
118+
NODE_ENV: production
119+
volumes:
120+
# 訪問記録の写真ファイル置き場(lib/photoStorage.ts)。DBには相対パスのみ保存される
121+
- type: bind
122+
source: *photos-dir
123+
target: /app/photos
124+
ports:
125+
# host:container。LAN内からのみ見える状態にしておくこと
126+
- "3000:3000"
127+
restart: unless-stopped

docker-compose.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: travel-log-prod
1+
name: travel-log
22

33
services:
44
# dbの起動前に、bindマウント先(./db/data)の作成と所有者調整を行う
@@ -62,6 +62,10 @@ services:
6262
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required (openssl rand -base64 32)}
6363
GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-}
6464
GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-}
65+
# 公開URL(任意)。リバースプロキシがX-Forwarded-Proto/Hostを送らない構成では
66+
# これを設定しないとGoogleログインのリダイレクトURIがhttp://で組まれる
67+
# (lib/auth/request-url.ts)。例: https://travel.example.com
68+
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-}
6569
NODE_ENV: production
6670
volumes:
6771
# 訪問記録の写真ファイル置き場(lib/photos.ts)。DBには相対パスのみ保存される

0 commit comments

Comments
 (0)