手元で動いていたサービスが、公開した途端に動かない。予約サービスでは、決済のあとに戻ってくるURLがずれる、確認メールのリンクが違うサイトを指す、といった形で現れます。第10回までで予約・決済・メール・管理画面・自動テストがそろったので、最終回の今回は、Vercel(Next.jsの開発元が運営する公開サービス)に公開するための準備と、本番チェックをまとめます。
「開発会社から『本番に公開しました』と報告を受けたけれど、本当に本番で動く状態なのか、発注側では何を見て確認すればいいの?」
先に結論です。公開作業で一番事故が起きやすいのは、コードではなく「設定(環境変数)」と「定期実行」です。 本番で必要な設定を一覧にし、公開前に機械でチェックし、公開後に実際に1件予約して確かめる。この3つを手順にしておけば、「公開したら動かない」はほとんど防げます。
前回の第10回では、予約の通しテストを自動化しました。詳しくは次の記事で解説しています。


この回で作るもの:本番用の設定と公開前チェック
使ったバージョンは Next.js 16・PostgreSQL 16 で、コードは blogs/it_hacchu/series/next-yoyaku/code/ にあります。
| 作るもの | 役割 | 主なファイル |
|---|---|---|
| サイトURLの一本化 | メールのリンクや決済後の戻り先を、1か所の関数で決める | lib/site-url.ts |
| 本番の環境変数チェック | 未設定・テスト用の値の入れ忘れを、公開前に見つける | lib/env-check.ts、scripts/check-env.ts |
| 定期実行の設定 | 期限切れの仮予約の整理と、確認メールの再送を一定間隔で呼ぶ | vercel.json |
| DB接続数の調整 | Vercel上で接続数が増えすぎないようにする | lib/db.ts |
公開前に決めておくこと:環境変数の一覧
環境変数とは、パスワードやAPIキーのように「コードに書かず、動かす場所ごとに別の値を渡す設定」のことです。本番とテストで値が違うものは、すべてここに入ります。Vercelでは、プロジェクトの設定画面から環境(Production / Preview / Development)ごとに登録できます。
| 名前 | 本番での値 | 入れ忘れたときの症状 |
|---|---|---|
DATABASE_URL | 本番DBの接続先 | 画面が開かない |
APP_URL | 本番のURL(https:// で始まる) | メールのリンクや決済後の戻り先が違うサイトになる |
STRIPE_SECRET_KEY | 本番用の鍵(sk_live_) | テスト用の鍵のままだと、実際の決済ができない |
STRIPE_WEBHOOK_SECRET | 本番用Webhookの署名用シークレット(whsec_) | 支払いが済んでも予約が確定しない |
CRON_SECRET | 16文字以上のランダムな文字列 | 定期実行が断られる |
RESEND_API_KEY、MAIL_FROM | メール送信サービスの鍵と、認証済みの送信元 | 確認メール・ログインメールが届かない |
ADMIN_EMAILS | 管理者のメールアドレス | 管理画面に誰も入れない |
Stripeのテスト用と本番用は、鍵もWebhookの登録も別です。テストで通ったからといって、本番用の設定が済んだことにはなりません。
実装1:サイトのURLを1か所で決める
第5〜8回では、メールのリンクや決済後の戻り先のURLを、場所ごとに作っていました。ログインメールはリクエストのホスト名から作る書き方でした。この方式だと、偽のホスト名を付けて呼び出されたとき、他人のサイトへ誘導するリンクがメールに入るおそれがあります。そこで、URLを作る関数を1つにまとめ、本番では環境変数で固定しました。
// lib/site-url.ts
export function siteUrl(env: Record<string, string | undefined> = process.env): string {
const fixed = env.APP_URL?.trim();
if (fixed) return fixed.replace(/\/+$/, "");
// Vercel が自動で入れる本番ドメイン(スキームなし)。APP_URL を入れ忘れたときの保険
const vercelHost = env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
if (vercelHost) return `https://${vercelHost}`;
if (env.NODE_ENV === "production") {
throw new Error("APP_URL が設定されていません(本番では必須。.env.example を参照)");
}
return "http://localhost:3000";
}
VERCEL_PROJECT_PRODUCTION_URL は、Vercelがプロジェクトの本番ドメイン(スキームなし)を自動で入れてくれる環境変数です。確認用のプレビュー環境でも本番のドメインが入るため、あくまで入れ忘れたときの保険にとどめ、APP_URL を主にしています。どちらも無い本番では、間違ったリンクを作るより止まるほうがよいので、エラーにしています。
📰 出典:Vercel Docs「System environment variables」
ログインのServer Action(app/login/actions.ts)、予約のServer Action(app/spaces/[slug]/reserve/actions.ts)、Stripeの通知を受ける入口(app/api/stripe/webhook/route.ts)、定期実行の入口(app/api/cron/expire-holds/route.ts)の4か所を、この関数に置き換えました。
実装2:本番の環境変数を公開前にチェックする
環境変数の入れ忘れは、人の目では見落とします。そこで「そろっているか」と「本番向けの値か」を調べる関数を作り、コマンドで実行できるようにしました。値そのものは出力せず、名前と状態だけを返します(ログに秘密が残らないようにするため)。
// lib/env-check.ts(抜粋)
export function checkProductionEnv(env: Env = process.env): EnvIssue[] {
const issues: EnvIssue[] = [];
for (const name of REQUIRED) {
if (!env[name]?.trim()) issues.push({ name, problem: "missing" });
}
const has = (name: string) => !issues.some((i) => i.name === name);
// 本番の URL は https。http のままだと Cookie の Secure 属性やメールのリンクが崩れる
if (has("APP_URL") && !/^https:\/\//.test(env.APP_URL!.trim())) issues.push({ name: "APP_URL", problem: "invalid" });
// 本番にテスト用の鍵(sk_test_)を入れたまま公開すると、実際には誰からもお金を受け取れない
if (has("STRIPE_SECRET_KEY") && !/^sk_live_/.test(env.STRIPE_SECRET_KEY!.trim())) issues.push({ name: "STRIPE_SECRET_KEY", problem: "invalid" });
// (STRIPE_WEBHOOK_SECRET の形式、CRON_SECRET の長さも同様に確認)
return issues;
}
# 本番の値を一時ファイルに取り出して確認する(このファイルは git に入れない)
node --env-file=.env.production scripts/check-env.ts
何も設定していない状態で実行すると、8項目すべてが「未設定」と表示され、終了コード1で止まります。単体テスト(tests/env-check.test.ts、tests/site-url.test.ts)も追加し、npm test に含めています。
実装3:定期実行(Cron)を設定する
第7〜8回で、「期限切れの仮予約を片づける」「未送信の確認メールを送り直す」入口(/api/cron/expire-holds)を作りました。Vercelでは、vercel.json に書くだけで、この入口を一定間隔で呼んでくれます。
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"crons": [{ "path": "/api/cron/expire-holds", "schedule": "*/10 * * * *" }]
}
CRON_SECRET という名前の環境変数をVercelに登録しておくと、Vercelが呼び出すときに Authorization: Bearer <値> のヘッダーを自動で付けます。第7回の入口はすでにこの形で確認しているので、コードの変更は不要でした。Vercelの公式ドキュメントも、16文字以上のランダムな文字列を勧めています。
📰 出典:Vercel Docs「Managing Cron Jobs」
注意したいのは料金プランの制限です。 執筆時点(2026年10月)のドキュメントでは、無料のHobbyプランは1日1回までで、それより細かい間隔の設定は公開(デプロイ)の段階でエラーになります。10分ごとに回すには有料のProプラン以上が必要です。また、定期実行は取りこぼしや二重実行が起こりうるため、処理は「何度実行しても同じ結果になる」作りにしておく必要があります(本連載の仮予約の整理とメール再送は、その作りです)。
📰 出典:Vercel Docs「Usage & Pricing for Cron Jobs」
実装4:DB接続数をVercel向けに絞る
Vercelの関数は、アクセスが増えると同時にいくつも起動します。1つの関数が複数のDB接続を持つと、合計がDBの接続上限をすぐ超えます。そこで、Vercel上では接続を1本に絞りました。
// lib/db.ts(抜粋)
export function getDb() {
// Vercel の関数は同時にいくつも起動する。1つが5本ずつ接続を持つとDBの上限をすぐ超えるので、1本にする
const max = process.env.VERCEL ? 1 : 5;
// 接続プーラー(トランザクションモード)経由ではプリペアドステートメントが使えない
globalForDb.db ??= drizzle(postgres(connectionUrl(), { max, prepare: !process.env.VERCEL }), { schema });
return globalForDb.db;
}
マネージドDB(Neon、Supabaseなど)には、接続をまとめて受ける「接続プーラー」用のURLが別に用意されていることが多く、その場合は DATABASE_URL にそちらを入れます。どのURLを使うかは、DB事業者の案内を確認してください。マイグレーション(テーブルの作成・変更)は、プーラー経由ではなく直接つなぐURLで実行するのが一般的です。
公開後の本番チェック
公開できたら、次の順に確認します。1〜2は自動、3以降は人が1回ずつ確かめます。
- ☐
/api/healthが200で、データベースがupになっている - ☐ Vercelの設定画面の「Cron Jobs」に
/api/cron/expire-holdsが表示され、手動実行で401にならない - ☐ 会員ログインのメールが届き、リンクのURLが本番のドメインになっている
- ☐ Stripeの本番用Webhookに、本番URLの
/api/stripe/webhookを登録し、署名用シークレットをSTRIPE_WEBHOOK_SECRETに入れた - ☐ 少額のスペースで1件予約し、支払い後に予約が「確定」になり、確認メールが届く
- ☐ 同じ予約をキャンセルし、返金されることをStripeの管理画面で確かめる
- ☐ 管理者以外のアカウントで
/adminを開くと、404になる
動作確認でできたこと・できていないこと
開発環境で、次のことを確認しました。
npm run lint、npx tsc --noEmitが通った- 単体テスト(
npx vitest run)が通った。ただしDBにつなぐテスト45件は、この環境にPostgreSQLを起動できなかったため実行していない next build --webpackが成功した(この環境ではTurbopackのネイティブ版が動かないため、--webpackを付けて確認している)scripts/check-env.tsが、未設定の環境で8項目すべてを指摘して終了コード1になる
Vercelへの実際の公開、本番のStripe・メール送信・独自ドメインでの動作は未確認です。 この記事の手順は、公式ドキュメントとコードの確認にとどまります。実際の公開では、上のチェックリストで必ず確かめてください。
つまずきやすい点・セキュリティ上の注意
- 環境変数を変えたら、再デプロイが必要。 Vercelでは、変更後の値は新しく作った公開版から反映されます
NEXT_PUBLIC_で始まる環境変数は、ブラウザに公開される。 秘密の鍵には絶対に付けない- プレビュー環境に本番の鍵を入れない。 確認用の環境から本番の決済やメールが動いてしまう
- 本番DBへのマイグレーションは、公開の手順に組み込む。 順番を間違えると、新しいコードが存在しない列を読んで落ちる
- 本連載で省略したもの:監視・障害通知、バックアップと復旧の訓練、アクセス集中への備え、独自ドメインとメール認証(SPF・DKIMなど)の設定。実運用では必要です
発注者向けメモ:公開作業を頼むときの確認点
公開(リリース)は、機能の開発と違って地味ですが、事故が起きやすい工程です。開発会社に頼むときは、次を確認しておくと安心です。
- ☐ 本番で使う設定(環境変数)の一覧と、その管理者(誰がパスワードや鍵を持つか)を決めている
- ☐ Vercel・DB・Stripe・メール送信サービスなどのアカウントが、発注者の名義で作られている(開発会社の個人名義にしない)
- ☐ 公開前チェックのリストがあり、公開後に実際に1件予約して確かめた記録がある
- ☐ 定期実行に必要な料金プランを、月額の費用を含めて確認した
- ☐ 公開に失敗したときに前の版へ戻す方法(ロールバック)と、連絡先が決まっている
- ☐ 本番運用の月額(Vercel・DB・メール・決済手数料)の見込みを、見積もりとは別に聞いている
開発会社への質問例は、次のとおりです。
- 「本番に必要な設定の一覧と、公開前に機械で確かめる仕組みはありますか」
- 「テスト用の鍵と本番用の鍵は、誰がどこで管理しますか。担当が変わったときの引き継ぎはどうなりますか」
- 「定期実行が止まったり、二重に動いたりしたとき、予約や返金にどんな影響がありますか」
- 「公開後に問題が見つかったとき、どのくらいの時間で前の版へ戻せますか」
まとめと次回予告
最終回では、サイトのURLを1か所に決める、本番の環境変数を公開前に機械で確かめる、定期実行を設定する、DB接続数をVercel向けに絞る、という4点を足し、公開後のチェックリストにまとめました。
第0回の雛形づくりから、一覧・カレンダー・予約フォーム・二重予約の防止・会員ログイン・決済・Webhook・メールと返金・管理画面・テストを経て、貸し会議室のオンライン予約サービスを作る連載はここで完結です。この連載で一貫していたのは、「お金」と「枠」を扱うシステムでは、機能の数よりも、壊れたときに被害が広がらない作りが大切ということでした。
この連載の記事一覧
この記事は連載「Next.jsで作るオンライン予約サービス」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Next.jsで作るオンライン予約サービス 第0回】完成像とNext.js 16の雛形を動かす(App Router・PostgreSQL・Drizzle)
- 【Next.jsで作るオンライン予約サービス 第1回】スペース一覧と詳細ページを作る(Server Components・Drizzle・メタデータ)
- 【Next.jsで作るオンライン予約サービス 第2回】空き状況カレンダーを作る(時間枠・日本時間・Drizzle)
- 【Next.jsで作るオンライン予約サービス 第3回】予約フォームをServer Actionsで作る(Zodによる入力チェックとエラー表示)
- 【Next.jsで作るオンライン予約サービス 第4回】ダブルブッキングを防ぐ(PostgreSQLの排他制約と同時アクセスのテスト)
- 【Next.jsで作るオンライン予約サービス 第5回】会員ログインを付ける(メールのリンクで入る方式と予約履歴)
- 【Next.jsで作るオンライン予約サービス 第6回】Stripeで支払いを受ける(Checkoutとサーバー側で決める金額)
- 【Next.jsで作るオンライン予約サービス 第7回】Webhookで予約を確定する(署名の検証・二重処理の防止・仮予約の期限切れ)
- 【Next.jsで作るオンライン予約サービス 第8回】予約確認メールとキャンセル・返金(キャンセル規定・Stripeの返金・メール送信)
- 【Next.jsで作るオンライン予約サービス 第9回】管理画面を作る(管理者の権限・予約一覧・スペースの料金編集)
- 【Next.jsで作るオンライン予約サービス 第10回】テストを自動化する(VitestとPlaywrightで予約の通しを確かめる)
- 【Next.jsで作るオンライン予約サービス 第11回】Vercelに公開する(環境変数・DB接続・本番チェックで「公開したら動かない」を防ぐ)(この記事)









コメント
コメント一覧 (2件)
[…] 【Next.jsで作るオンライン予約サービス 第11回】Vercelに公開する(環境変数… […]
[…] 【Next.jsで作るオンライン予約サービス 第11回】Vercelに公開する(環境変数… […]