sandbox.tunnels 名前空間は、Sandbox 内で動いているサービスを Cloudflare Tunnel 経由でインターネットに公開します。SDK はコンテナ内で cloudflared を動かし、Cloudflare のエッジへの永続的な QUIC 接続を開きます。
次の 2 種類があります。
- クイックトンネル(
sandbox.tunnels.get(port)) — 設定は不要です。Cloudflare が新しいcloudflaredプロセスごとにランダムな*.trycloudflare.comホスト名を割り当てます。Cloudflare アカウント、API トークン、DNS レコード、カスタムドメインは不要です。コンテナを再起動するたびに URL は変わります。 - 名前付きトンネル(
sandbox.tunnels.get(port, { name })) — 管理下のゾーン上の安定したホスト名<name>.<your-zone>に紐づけます。ホスト名はコンテナ再起動後も残り、同じnameを要求する Sandbox 間で共有されます。Cloudflare API トークン、アカウント、ゾーンが必要です。
どちらのトンネルも次が必要です。
- RPC トランスポート。 HTTP / Websocket トランスポートで
sandbox.tunnelsを呼ぶと"RPC transport required"がスローされます。トランスポートの設定 を参照してください。
名前付きトンネルには、さらに Cloudflare API トークン、アカウント、ゾーンが必要です。名前付きトンネル: 前提条件 を参照してください。
port のトンネルレコードを返します。まだ動いていなければ、SDK はコンテナ内で新しい cloudflared プロセスを起動します。このメソッドは冪等です。同じ (port, options) で繰り返すと、同じレコードを返します。
const tunnel = await sandbox.tunnels.get(
port: number,
options?: { name?: string }
): Promise<TunnelInfo>パラメーター:
port— Sandbox 内で公開するポート番号です(1024–65535。予約済みポートは除きます)。トンネル先のサービスは、コンテナ内の0.0.0.0:<port>ですでに待ち受けている必要があります。options.name(任意) — 単一の DNS ラベルです(小文字、数字、内部のハイフン。1–63 文字。ドットなし)。設定すると、<name>.<your-zone>に紐づく 名前付きトンネル をプロビジョニングします。省略すると、クイックトンネルをプロビジョニングします。
戻り値: Promise<TunnelInfo> — トンネルレコードです。TunnelInfo を参照してください。
すでにトンネルがあるポートに対して、異なる options で get(port) を呼ぶとスローされます。先に destroy(port) を呼んでください。
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080);
console.log(tunnel.url);
// → https://random-words-here.trycloudflare.com
// Repeated calls for the same port return the same record.
const same = await sandbox.tunnels.get(8080);
console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url });
},
};import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
await sandbox.startProcess("python -m http.server 8080");
const tunnel = await sandbox.tunnels.get(8080);
console.log(tunnel.url);
// → https://random-words-here.trycloudflare.com
// Repeated calls for the same port return the same record.
const same = await sandbox.tunnels.get(8080);
console.log(same.url === tunnel.url); // true
return Response.json({ url: tunnel.url });
},
};この Sandbox で現在追跡しているトンネルをすべて返します。
const tunnels = await sandbox.tunnels.list(): Promise<TunnelInfo[]>戻り値: Promise<TunnelInfo[]> — TunnelInfo レコードの配列です。アクティブなトンネルがないときは空です。
const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}const tunnels = await sandbox.tunnels.list();
for (const tunnel of tunnels) {
console.log(`port ${tunnel.port} → ${tunnel.url}`);
}トンネルを破棄します。ポート番号、または get() が返した TunnelInfo レコードのいずれかを受け付けます。冪等です。未知のポートを破棄しても成功として解決します。
await sandbox.tunnels.destroy(portOrInfo: number | TunnelInfo): Promise<void>パラメーター:
portOrInfo— ポート番号、またはget()が返したTunnelInfoレコードのいずれかです。
const tunnel = await sandbox.tunnels.get(8080);
// Tear down by port number...
await sandbox.tunnels.destroy(8080);
// ...or by the record.
await sandbox.tunnels.destroy(tunnel);const tunnel = await sandbox.tunnels.get(8080);
// Tear down by port number...
await sandbox.tunnels.destroy(8080);
// ...or by the record.
await sandbox.tunnels.destroy(tunnel);クイックトンネルには name がありません。名前付きトンネルは、options.name で渡したラベルを持ちます。
| フィールド | 型 | 説明 |
|---|---|---|
id |
string |
トンネル識別子です。クイックトンネルは quick-<random>、名前付きトンネルは Cloudflare Tunnel の UUID です。 |
port |
number |
トンネルがプロキシする Sandbox 内のポート番号です。 |
url |
string |
公開 URL です。クイックは https://<random>.trycloudflare.com、名前付きは https://<name>.<your-zone> です。 |
hostname |
string |
url のホスト名部分です。 |
createdAt |
string |
トンネル作成時の ISO-8601 タイムスタンプです。 |
name |
string |
名前付きトンネルのみ。 options.name で渡したラベルです。クイックトンネルにはありません。 |
type TunnelInfo = QuickTunnelInfo | NamedTunnelInfo;
interface QuickTunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
name?: never;
}
interface NamedTunnelInfo {
id: string;
port: number;
url: string;
hostname: string;
createdAt: string;
name: string;
}名前付きトンネルは、ユーザーが管理するホスト名 <name>.<your-zone> を、マネージドの Cloudflare Tunnel とゾーン上のプロキシ済み CNAME レコードで支えます。クイックトンネルと違い、URL は コンテナ再起動をまたいで安定 し、同じ name で get(port, { name }) を呼ぶ Sandbox 間で共有 されます。
| 項目 | クイックトンネル | 名前付きトンネル |
|---|---|---|
| ホスト名 | Cloudflare が割り当てるランダムな *.trycloudflare.com |
自分で選ぶ <name>.<your-zone> |
| 安定性 | コンテナを再起動するたびに変わる | 安定。再起動と Sandbox のライフサイクルをまたいで残ります |
| Cloudflare アカウント | 不要 | 必要(API トークン + ゾーン) |
| Cloudflare 側のリソース | なし | マネージドの Cloudflare Tunnel + プロキシ済み DNS CNAME |
| 稼働保証 | なし(デバッグ用) | ゾーンの標準 Cloudflare SLA が適用されます |
| TLS 証明書 | Cloudflare 所有のワイルドカード | <name>.<your-zone> の Universal SSL(単一 DNS ラベルのみ) |
| Server-Sent Events | 非対応(エッジが text/event-stream をバッファします) |
対応 |
名前付きトンネルをプロビジョニングするには、次が必要です。
- ゾーン(Cloudflare DNS で管理するドメイン)を持つ Cloudflare アカウント。
- 適切なスコープを持つ Cloudflare API トークン。
- アカウント ID と ゾーン ID — トークンのスコープがそれぞれちょうど 1 つなら、SDK がトークンから両方を推定できます。
My Profile > API Tokens > Create Token > Custom token から、次の権限でトークンを作成します。
| スコープ | 用途 |
|---|---|
| Account · Cloudflare Tunnel · Edit | トンネルの作成、参照、削除。 |
| Zone · DNS · Edit | <name>.<your-zone> のプロキシ済み CNAME の作成または更新と削除。 |
| Zone · Zone · Read | ゾーン名を参照し、<name>.<your-zone> を導出する。 |
| Account · Account Settings · Read (任意) | 明示的に設定しないとき、SDK がトークンからアカウント ID を推定できるようにします。 |
Account Resources では、トンネルを所有するアカウントにトークンをスコープします。Zone Resources では、紐づけたい特定のゾーンにスコープします。
User API Tokens と Account API Tokens(シークレットの接頭辞は cfat_)の両方に対応しています。SDK はトークンの種類を検出し、適切なイントロスペクションエンドポイントを使います。
ダッシュボードを使わずにトークンを作成できます。権限グループ ID は安定しています。次のスニペットはプレースホルダーを使っています。現在の ID は GET /user/tokens/permission_groups から取得してください。
curl -X POST "https://api.cloudflare.com/client/v4/user/tokens" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "sandbox-named-tunnels",
"policies": [
{
"effect": "allow",
"resources": { "com.cloudflare.api.account.<ACCOUNT_ID>": "*" },
"permission_groups": [{ "id": "<TUNNEL_EDIT_GROUP_ID>" }]
},
{
"effect": "allow",
"resources": { "com.cloudflare.api.account.zone.<ZONE_ID>": "*" },
"permission_groups": [
{ "id": "<DNS_EDIT_GROUP_ID>" },
{ "id": "<ZONE_READ_GROUP_ID>" }
]
}
]
}'SDK は Worker 環境から CLOUDFLARE_API_TOKEN を読み、トークンからアカウント ID とゾーン ID の推定を試みます。トークンが複数のアカウントまたはゾーンに関連付いていて SDK が 1 つに決められない場合は、CLOUDFLARE_ACCOUNT_ID や CLOUDFLARE_ZONE_ID を明示的に設定する必要があります。
| 変数 | 必須? | 備考 |
|---|---|---|
CLOUDFLARE_API_TOKEN |
はい | wrangler secret put でシークレットとして保存します。 |
CLOUDFLARE_ACCOUNT_ID |
トークンが複数アカウントを見る場合のみ | それ以外はトークンから推定します。 |
CLOUDFLARE_ZONE_ID |
トークンが複数ゾーンを見る場合のみ | それ以外はトークンから推定します。 |
npx wrangler secret put CLOUDFLARE_API_TOKENローカル開発では、変数を .dev.vars(gitignore 済み)に置きます。本番では、シークレットではない ID(必要なとき)を Wrangler 設定の vars に置きます。
{
"vars": {
"CLOUDFLARE_ACCOUNT_ID": "<account-id>",
"CLOUDFLARE_ZONE_ID": "<zone-id>"
}
}推定に失敗すると、SDK は設定すべき変数名を明示した分かりやすいエラーをスローします。
import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request, env) {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
// Reuse an existing app process across container restarts, or start it.
let proc = await sandbox.getProcess("app");
if (!proc) {
try {
proc = await sandbox.startProcess("python -m http.server 8080", {
processId: "app",
});
} catch (err) {
if (err?.code !== "PROCESS_ALREADY_EXISTS") throw err;
proc = await sandbox.getProcess("app");
}
}
// Provision (or reuse) https://app.example.com pointing at port 8080.
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
return Response.json({ url: tunnel.url });
},
};import { getSandbox } from "@cloudflare/sandbox";
export { Sandbox } from "@cloudflare/sandbox";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const sandbox = getSandbox(env.Sandbox, "my-sandbox");
// Reuse an existing app process across container restarts, or start it.
let proc = await sandbox.getProcess('app');
if (!proc) {
try {
proc = await sandbox.startProcess('python -m http.server 8080', { processId: 'app' });
} catch (err) {
if ((err as { code?: string })?.code !== 'PROCESS_ALREADY_EXISTS') throw err;
proc = await sandbox.getProcess('app');
}
}
// Provision (or reuse) https://app.example.com pointing at port 8080.
const tunnel = await sandbox.tunnels.get(8080, { name: "app" });
console.log(tunnel.url); // → https://app.example.com
return Response.json({ url: tunnel.url });
},
};名前付きトンネルは、プロビジョニングしたコンテナより長く残るように設計されています。
sandbox.tunnels.get(port, { name })の 最初の呼び出し:- 設定したゾーン ID から
<name>.<your-zone>を解決します。 - Sandbox ID でタグ付けした Cloudflare Tunnel リソース
sandbox-<sandbox-id>-<name>を作成します。 <name>.<your-zone>から<tunnel-id>.cfargotunnel.comへのプロキシ済みCNAMEを作成または更新します。- トンネルのトークンを使って、コンテナ内で
cloudflaredを起動します。
- 設定したゾーン ID から
- 同じ
(port, name)での 以降の呼び出し は、Cloudflare に問い合わせず、キャッシュしたレコードを返します。 - コンテナ再起動(Durable Object の退避、デプロイ、クラッシュ):
cloudflaredはコンテナとともに終了しますが、Cloudflare Tunnel と DNS レコードは残ります。- 次の
get(port, { name })で、SDK は Cloudflare API 経由でタグ付きトンネルを再発見し、cloudflaredを再起動します。ホスト名は変わりません。
sandbox.tunnels.destroy(port)による 明示的な破棄:- コンテナ内の
cloudflaredを停止します。 - Cloudflare Tunnel リソースを削除します。
- プロキシ済み
CNAMEレコードを削除します。
- コンテナ内の
sandbox.destroy()による Sandbox の破棄 は、コンテナを止める前に、その Sandbox がプロビジョニングしたトンネル(Cloudflare 側のリソースを含む)をすべて破棄します。
destroy() が Cloudflare API に届かない場合(たとえば get() と destroy() の間にトークンが取り消された場合)、SDK は孤立した tunnelId と dnsRecordId を警告としてログに出し、ダッシュボードから手動で片付けられるようにします。
名前付きトンネルは、Sandbox コンテナの外、自分の Cloudflare アカウント上 にリソースを作ります。Durable Object ストレージには保存されず、Sandbox のクォータにも計上されません。ただし Cloudflare ダッシュボードに表示され、アカウントのトンネルと DNS のクォータは消費します。
各 (sandbox, name) の組に対して、SDK は次を作成します。
| リソース | 名前 / 場所 | 識別子 |
|---|---|---|
| Cloudflare Tunnel | Networking > Tunnels | sandbox-<sandbox-id>-<name> |
| プロキシ済み DNS レコード | 自分のゾーン、DNS > Records | CNAME <name>.<zone> → <tunnel-id>.cfargotunnel.com |
どちらのリソースもタグ付けされているため、ダッシュボードまたは API から監査、照会、一括削除できます。
- トンネルのメタデータ:
{ sandboxId, createdBy: 'sandbox-sdk', name, port } - DNS レコードのコメント:
sandbox-<sandbox-id> - リソースタグ (Enterprise プランのみ):
sandboxId:<sandbox-id>
Enterprise 以外のプランでは、Cloudflare はリソースタグを拒否します。SDK はこれを検出し、タグなしでリクエストを再試行します。DNS コメントとトンネルメタデータは引き続き付くため、リソースを Sandbox までたどることは常にできます。
指定したアカウントで SDK が作成したトンネルをすべて一覧するには:
curl "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/cfd_tunnel?name=sandbox-" \
-H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"どちらのトンネルにも共通:
- WARP / Zero Trust の egress。 ローカルマシンで Cloudflare WARP または別の Zero Trust egress ポリシーが動いていると、
api.trycloudflare.comと cloudflared エッジへの送信がブロックされることがあります。その場合、tunnels.get()はエッジのハンドシェイクで止まり、最終的にタイムアウトします。WARP を無効にするか、これらの宛先に egress の例外を追加してください。 - 短い DNS のウォームアップ。 作成直後の URL への最初のリクエストは、
get()が解決したあとでも、DNS が伝播するまで数秒かかることがあります。
クイックトンネルのみ:
- URL はコンテナ再起動後に残りません。 Cloudflare は
cloudflaredの起動ハンドシェイク中にホスト名を割り当てるため、再起動のたびに新しい URL になります。SDK はコンテナ起動時にトンネルキャッシュを消すため、次のtunnels.get(port)は新しいレコードを返します。安定したホスト名には 名前付きトンネル を使ってください。 - 稼働保証はありません。 Cloudflare は
trycloudflare.comをデバッグ用として位置づけており、本番向けではありません。 - Server-Sent Events は使えません。
trycloudflare.comのエッジはtext/event-streamレスポンスをバッファするため、SSE イベントはクライアントに届きません。WebSocket は通常どおり動きます。サービスが SSE をストリーミングする場合は 名前付きトンネル を使ってください。
名前付きトンネルのみ:
- 単一の DNS ラベル。
nameにドットを含められません。Universal SSL がカバーするのは<name>.<your-zone>だけです。 - ゾーンのクォータに計上されます。 名前付きトンネルは、アカウント上に Cloudflare Tunnel と DNS レコードを 1 つずつ作ります。Cloudflare Tunnel の制限 を参照してください。
- クリーンアップには API トークンが必要です。 トークン取り消し後に
destroy()が走ると、Cloudflare 側のリソースは孤立します。SDK は孤立 ID をログに出すので、手動で削除できます。
- プレビュー URL の概念 — Worker が手前に立つプレビュー URL と、クイックトンネルとの違い。
- Ports API —
exposePort()と、Worker が手前に立つプレビュー URL の流れ。 - サービス公開ガイド — 本番でサービスを公開する一連の手順。
- トランスポートの設定 — RPC とルートベースのトランスポート。