自分の分身AIと音声会話できるアプリ⑤
Cloudflare Tunnelを使い、Vercelで公開するWebアプリから自宅PCの文字起こしAPIへ安全に接続する環境を構築します。
はじめに
前回の記事では、GPT-SoVITSを使って自分の声を再現し、入力した文章を自分の声で読み上げられるところまで確認しました。
今回は、Vercelで公開するWebアプリから自宅PCで動作するWhisper、LM Studio、GPT-SoVITSのAPIへ接続するため、Cloudflare Tunnelを構築します。各APIをインターネットへ直接公開せず、Cloudflare Accessの認証を通して接続できる構成を目指します。
Cloudflare Tunnelへ独自のホスト名を割り当てるには、Cloudflareで管理するドメインが必要です。まずはCloudflare Registrarでドメインを登録します。すでにCloudflareで管理しているドメインがある場合、この手順は読み飛ばしてください。
Cloudflareでドメインを登録する
Cloudflareダッシュボードへログインし、左側のメニューから「ドメイン」→「登録」を開きます。続いて、画面右上の「ドメインを購入」を選択します。

希望するドメインを検索する
検索欄へ取得したいドメイン名を入力し、「検索」を選択します。完全なドメイン名だけでなく、キーワードを入力して候補を探すこともできます。
検索結果に希望するドメインが表示されたら、ドメイン名と初年度・更新時の料金を確認して「購入」を選択します。検索結果へ表示されていても、購入時の最終確認で取得できないと判定される場合があります。その場合は別の候補を検索します。
Cloudflare Registrarでは、Cloudflareのネームサーバーを使用します。登録後に別のDNS事業者のネームサーバーへ変更することはできないため、ほかのDNSサービスを利用する予定がある場合は事前に構成を確認してください。また、日本語ドメインなどの国際化ドメイン名には対応していません。
購入を完了する
画面に表示されるドメイン名、登録期間、料金、連絡先、自動更新の設定を確認します。利用規約とプライバシーポリシーへ同意し、「購入を完了」を選択します。
登録が完了するとドメインの管理画面へ移動し、Cloudflareから確認メールが届きます。「ドメイン」→「登録」の一覧に取得したドメインが表示され、ステータスが「アクティブ」になっていることを確認します。これで、Cloudflare Tunnelへ割り当てるドメインを用意できました。
詳しい要件や画面が変わった場合の最新手順は、Cloudflare Registrar公式ドキュメントを参照してください。
Named Tunnelを作成する
続いて、Cloudflare Zero TrustでNamed Tunnelを作成します。Named Tunnelには名前と固定のIDが割り当てられるため、トンネルを停止・再起動しても同じ設定を継続して使用できます。
Cloudflareダッシュボードの左側にある「ネットワーク」→「コネクタ」を開き、画面右上の「トンネルを作成」を選択します。中央に「トンネルを追加」が表示されている場合は、こちらから進んでも同じです。

トンネルの種類と名前を設定する

コネクタにはcloudflaredを選択します。次の画面でトンネルを識別する名前を入力し、「トンネルを保存」を選択します。今回は音声AI用であることが分かるように、voice-aiなどの名前を設定します。
トンネル名はCloudflareダッシュボードで管理するための名前です。外部からアクセスするURLには使用されません。
cloudflaredを接続する
トンネルを保存すると、cloudflaredを実行する環境の選択画面と起動コマンドが表示されます。今回は文字起こしAPIと同じDockerネットワークでcloudflaredを起動するため、オペレーティングシステムに「Docker」を選択します。
表示されるコマンドには、このトンネルをCloudflareへ接続するためのTunnel Tokenが含まれています。トークンをGitへコミットしたり、記事やスクリーンショットへ掲載したりしないでください。具体的な保存方法と起動手順は、後述する「Tunnel TokenをPCへ設定する」で説明します。
公開ホスト名を設定する
「パブリックホスト名」へ、外部公開するホスト名と接続先サービスを設定します。

今回の設定内容は次のとおりです。
| 項目 | 設定値 | 説明 |
|---|---|---|
| サブドメイン | whisper | 音声文字起こしAPIへ割り当てる名前 |
| ドメイン | 登録したドメイン | 前の手順でCloudflareへ登録したドメイン |
| パス | 空欄 | ホスト名以下のすべてのパスを転送する |
| サービスのタイプ | HTTP | Dockerネットワーク内のHTTPサービスへ接続する |
| URL | transcription:8000 | 文字起こしコンテナのサービス名とポート番号 |
サブドメインとドメインを組み合わせた完全なホスト名が、外部からアクセスするURLになります。たとえばドメインがexample.comの場合は、https://whisper.example.comです。
transcription:8000はDocker Composeのサービス名とポート番号です。cloudflaredと文字起こしAPIを同じDockerネットワークで起動することで、コンテナ間をこの名前で接続できます。
入力内容を確認して「セットアップを完了」を選択します。Cloudflare DNSには、このNamed Tunnelへ向けたDNSレコードが自動的に作成されます。
LM StudioとGPT-SoVITSの公開ホスト名を追加する
Whisper用の設定が完了したら、同じNamed TunnelにLM Studio用とGPT-SoVITS用の公開ホスト名も追加します。トンネルをサービスごとに作成する必要はなく、1つのトンネルに3つのルートを設定します。
Cloudflare Zero Trustの「ネットワーク」→「コネクタ」から作成したトンネルを開き、「パブリックホスト名を追加」を選択します。Whisperと同じ手順で、次のサブドメインと接続先を設定します。
| 用途 | サブドメイン | サービスURLの例 |
|---|---|---|
| 文字起こし | whisper | http://transcription:8000 |
| 返答生成 | lmstudio | http://host.docker.internal:1234 |
| 音声合成 | sovits | http://host.docker.internal:9880 |
cloudflaredをDockerで起動し、LM StudioとGPT-SoVITSをWindows上で起動する場合は、コンテナからホストPCへ接続するためにhost.docker.internalを使用します。ポート番号は各APIの起動設定に合わせて変更してください。また、LM Studioはローカルネットワークからの接続を許可した状態でAPIサーバーを起動します。
ドメインがexample.comの場合、追加したURLはそれぞれhttps://lmstudio.example.comとhttps://sovits.example.comになります。
これでNamed Tunnelと公開ホスト名の作成は完了です。ただし、この時点ではURLを知っている第三者からも接続できるため、続けてCloudflare Accessを設定してAPIを保護します。
画面構成が変わった場合は、Cloudflare Tunnelの公式セットアップガイドも参照してください。
Tunnel TokenをPCへ設定する
cloudflaredをDockerコンテナとして起動し、Named Tunnelへ接続するためのTunnel TokenをPCへ設定します。トークンをcompose.yamlや起動コマンドへ直接書かず、環境変数としてDockerへ渡します。
Cloudflare Zero Trustの「ネットワーク」→「コネクタ」を開き、作成したトンネルを選択します。「コネクター」タブからコネクターの追加画面を開き、オペレーティングシステムに「Docker」を選択します。

表示されたDockerコマンドのうち、--tokenの後ろにある文字列がTunnel Tokenです。コピーしたトークンを、PowerShellからWindowsユーザーの環境変数TUNNEL_TOKENへ保存します。
[Environment]::SetEnvironmentVariable(
"TUNNEL_TOKEN",
"<TUNNEL_TOKEN>",
[EnvironmentVariableTarget]::User
)
<TUNNEL_TOKEN>はCloudflareダッシュボードからコピーした実際の値へ置き換えます。設定後に新しいPowerShellを開き、環境変数が読み込まれていることを確認します。トークンそのものを画面へ表示しないよう、値の有無だけを判定します。
if ([string]::IsNullOrWhiteSpace($env:TUNNEL_TOKEN)) {
throw "TUNNEL_TOKENが設定されていません。"
}
Write-Host "TUNNEL_TOKENを読み込みました。"
環境変数を使ってcloudflaredを起動する
Dockerでは、-e TUNNEL_TOKENを指定するとPCに設定した環境変数をコンテナへ引き渡せます。
docker run --rm `
--name cloudflared `
--network <DOCKER_NETWORK_NAME> `
-e TUNNEL_TOKEN `
cloudflare/cloudflared:latest `
tunnel --no-autoupdate run
<DOCKER_NETWORK_NAME>は、文字起こしAPIのtranscriptionコンテナが参加しているDockerネットワーク名へ置き換えます。cloudflaredとtranscriptionを同じネットワークへ参加させることで、公開ホスト名に設定したhttp://transcription:8000へサービス名で接続できます。
ログにCloudflareへの接続完了を示すメッセージが表示され、ダッシュボード上でコネクターが接続済みになれば設定完了です。常時稼働させる場合は、Docker Composeのcloudflaredサービスへ同じ環境変数とネットワークを設定し、restart: unless-stoppedなどの再起動ポリシーを指定します。
Tunnel Tokenを知っている人は、そのトンネルへ新しいコネクターを接続できます。トークンをGit、スクリーンショット、ログ、チャットへ貼り付けないでください。同じWindowsユーザーで実行されるプロセスからはユーザー環境変数を読み取れるため、PCのアカウント自体も適切に保護します。
トークンが漏えいした可能性がある場合は、画像にある「トークンを更新」から新しいトークンを発行します。更新すると旧トークンでは新しい接続を開始できなくなりますが、すでに確立している接続は直ちには切断されません。新しいトークンをPCへ設定し直したあと、cloudflaredを再起動します。
利用できる起動パラメーターと環境変数は、Cloudflare公式のTunnel run parametersを参照してください。
Accessコントロールで認証を適用する
作成したwhisper、lmstudio、sovitsの公開ホスト名へCloudflare Accessを適用し、サービス・トークンを持つVercelアプリだけが各APIへ接続できるようにします。サービス・トークンは、ブラウザでログインできないバックエンド同士の通信を認証するための資格情報です。
サービス・トークンを作成する
Cloudflare Zero Trustの左側にある「Accessコントロール」→「サービス資格情報」を開き、「サービス・トークンを作成」を選択します。サービス・トークン名には、用途が分かるvercel-voice-aiなどの名前を入力します。
有効期間を選択してトークンを作成すると、クライアントIDとクライアントシークレットが表示されます。

クライアントシークレットを確認できるのは作成直後だけです。画面を閉じる前に、次の2つの値をパスワードマネージャーなどの安全な場所へ保存します。
| HTTPヘッダー | 保存する値 | Vercelの環境変数名 |
|---|---|---|
CF-Access-Client-Id | クライアントID | CF_ACCESS_CLIENT_ID |
CF-Access-Client-Secret | クライアントシークレット | CF_ACCESS_CLIENT_SECRET |
画像では検証用に有効期間を「無期限」としています。本番運用では有効期限を設定し、期限が近づいたら新しいトークンへローテーションする運用が安全です。これらの値はソースコードや.envファイルへ直接記載してGitへコミットしないでください。
セルフホストアプリケーションを作成する
Cloudflare Zero Trustの左側にある「Accessコントロール」→「アプリケーション」を開きます。既存のアプリケーションが一覧表示されたら、画面右上の「新規アプリケーションを作成」を選択します。

アプリケーションの種類から「セルフホスト」を選択し、アプリケーション名には用途が分かるvoice-ai-apiなどの名前を入力します。「アプリケーションの詳細」にある「宛先」では、「パブリックホスト名」にNamed Tunnelで作成した各API用のホスト名を設定します。
今回の設定内容は次のとおりです。
| サブドメイン | 用途 | パス | 説明 |
|---|---|---|---|
whisper | 文字起こし | 空欄 | Whisper APIのすべてのパスを保護する |
lmstudio | 返答生成 | 空欄 | LM Studio APIのすべてのパスを保護する |
sovits | 音声合成 | 空欄 | GPT-SoVITS APIのすべてのパスを保護する |
3つの宛先には、いずれもCloudflareで管理している同じドメインを指定します。画面の「パブリックホスト名を追加」から宛先を追加し、すべてのAPIに同じAccessポリシーを適用します。
ブラウザーレンダリングはRDP、SSH、VNCなどをブラウザーから利用する場合の機能です。今回はHTTPで文字起こしAPIへ接続するため、無効のままにします。
「Accessポリシー」では、Vercelからの通信を許可するサービス認証ポリシーを設定します。すでにAllow Vercelポリシーを作成している場合は一覧から追加できます。まだ作成していない場合は「新しいポリシーを作成」を選択し、次の手順で設定します。

サービス認証ポリシーを設定する
同じ画面の「Accessポリシー」で、Vercelからのリクエストを許可するポリシーを作成します。「新しいポリシーを作成」を選択し、ポリシー名とアクション、対象にするサービス・トークンを設定します。

ポリシーには次の内容を設定します。
| 項目 | 設定値 |
|---|---|
| ポリシー名 | Allow Vercel |
| アクション | サービス認証 |
| Includeのセレクター | Service Token |
| Includeの値 | vercel-voice-ai |
アクションには必ず「サービス認証」を選択します。「許可」を選択するとIDプロバイダーによるユーザーログインが要求され、Vercelのバックエンドから接続できません。入力内容を確認してセルフホストアプリケーションを保存します。
Vercelから認証ヘッダーを送信する
Accessで保護したURLへリクエストするときは、保存したクライアントIDとクライアントシークレットを次のHTTPヘッダーへ設定します。
const response = await fetch("https://whisper.example.com/transcribe", {
method: "POST",
headers: {
"CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID ?? "",
"CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET ?? "",
},
body: formData,
});
whisper.example.comは実際に設定したホスト名へ置き換えます。VercelプロジェクトにもCF_ACCESS_CLIENT_IDとCF_ACCESS_CLIENT_SECRETを環境変数として登録します。
これらは秘密情報のため、ブラウザー側のコードやNEXT_PUBLIC_で始まる環境変数へ設定してはいけません。必ずVercelのサーバー側だけで参照し、環境変数を追加または更新したあとは再デプロイして反映します。
正しいヘッダーを持たないリクエストはCloudflare Accessで拒否され、文字起こしAPIまで到達しません。これにより、文字起こしAPIをインターネットへ直接公開せず、Vercelアプリからの通信だけを許可できます。
接続を確認する
最後に、認証情報がないリクエストは拒否され、正しいサービス・トークンを付けたリクエストだけが文字起こしAPIへ到達することを確認します。テストには短い音声ファイルsample.wavを使用します。
認証ヘッダーなしで拒否されることを確認する
PowerShellで次のコマンドを実行します。whisper.example.comは実際のホスト名へ置き換えてください。
curl.exe -i -X POST "https://whisper.example.com/transcribe" `
-F "file=@sample.wav"
Cloudflare Accessから401 Unauthorizedまたは403 Forbiddenが返り、文字起こし結果が取得できなければ保護されています。レスポンスコードはAccessアプリケーションの設定によって異なります。
サービス・トークンを付けて接続する
新しいPowerShellを開き、クライアントIDとクライアントシークレットを環境変数へ設定します。値をコマンド履歴へ残したくない場合は、PowerShellの入力ダイアログから設定します。
$env:CF_ACCESS_CLIENT_ID = Read-Host "Cloudflare Access Client ID"
$secret = Read-Host "Cloudflare Access Client Secret" -AsSecureString
$env:CF_ACCESS_CLIENT_SECRET = [System.Net.NetworkCredential]::new("", $secret).Password
認証ヘッダーを付けて、同じ音声ファイルを送信します。
curl.exe -i -X POST "https://whisper.example.com/transcribe" `
-H "CF-Access-Client-Id: $env:CF_ACCESS_CLIENT_ID" `
-H "CF-Access-Client-Secret: $env:CF_ACCESS_CLIENT_SECRET" `
-F "file=@sample.wav"
2xxのレスポンスと文字起こし結果が返れば、Cloudflare Access、Cloudflare Tunnel、文字起こしAPIまでの経路を利用できる状態です。最後にVercelアプリを再デプロイし、実際の画面から音声ファイルを送信して同じ結果が返ることも確認します。
502 Bad Gatewayが返る場合は、cloudflaredとtranscriptionが同じDockerネットワークに参加していること、文字起こしAPIがポート8000で起動していること、サービスURLがhttp://transcription:8000になっていることを確認します。
確認後は、現在のPowerShellから一時的な認証情報を削除します。
Remove-Item Env:CF_ACCESS_CLIENT_ID
Remove-Item Env:CF_ACCESS_CLIENT_SECRET
最新の設定項目やトークンの更新方法は、Cloudflare公式のサービス・トークンドキュメントを参照してください。
次回予告
次回の記事では、分身AIの性格や話し方をpersona.mdへ記載し、Next.jsのAPI RouteからLM Studioへシステムプロンプトとして送信します。
「自分の分身AIと音声会話できるアプリ」全6回
- 開発のきっかけ・全体構成
- Whisperの環境構築
- LM Studioの導入
- GPT-SoVITSで自分の声を再現
- Cloudflare Tunnelの設定(この記事)
- persona.mdで性格・話し方を設定