Cloudflare Tunnelで自宅サーバーをポート開放なしで公開する手順

自宅のLinux機で動かしているWebアプリを、ルーターのポート開放なしで独自ドメインに公開したい人向けの手順です。
私は自作ECサイト(Next.js App Router + Prisma + SQLite)をこの構成で運用していて、Nodeアプリをsystemdで常駐させ、Cloudflare Tunnel経由で外部に出しています。ポート開放は一切していません。
cloudflaredの導入からingress設定、systemd化、そして構成上つまずきやすいポイント(クライアントIPの扱いなど)まで、上から順に追える形で書きます。
ポート開放なしで公開できる仕組みと前提条件
Cloudflare Tunnelは、サーバー側で動くcloudflaredというデーモンがCloudflare側へ外向きの接続を張り続ける方式です。外部からの着信を受けるわけではないので、ルーターのポートフォワーディングもグローバルIPの固定も不要です。訪問者のリクエストはCloudflareのエッジに届き、そこから既存のトンネルを通ってローカルのhttp://localhost:3000へ渡されます。
必要なものは次の3つです。
- Cloudflareアカウントと、ネームサーバーをCloudflareに向けたドメイン(ここが済んでいないとDNSルーティングができません)
- ローカルで動いているHTTPサービス(例:
localhost:3000) - サーバーへのsudo権限
設定方式は2通りあり、最初にどちらで行くか決めておくと後が楽です。
| 方式 | 設定場所 | 向いているケース |
|---|---|---|
| ローカル管理(config.yml) | サーバー上のYAMLファイル | 複数ホスト名・細かいingress制御をGit等で管理したい |
| リモート管理(ダッシュボード) | Cloudflare Zero Trustの画面 | 1サービスだけ手早く公開したい、CLIを触りたくない |
この記事はローカル管理(config.yml)で進めます。設定がファイルとして手元に残るためです。
手順1〜4: cloudflared の導入とトンネル作成
1. cloudflaredをインストールする
Debian/Ubuntu系(amd64)なら公式配布の.debが最短です。
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb
cloudflared --version
ARM機(Raspberry Piなど)ではcloudflared-linux-arm64.debに読み替えます。uname -mでアーキテクチャを確認してください。
2. Cloudflareアカウントに紐付ける
cloudflared tunnel login
表示されたURLをブラウザで開き、対象ドメインを選んで認可します。成功すると~/.cloudflared/cert.pemが作られます。このファイルはトンネル作成とDNSルーティングに使う証明書なので、消さないでください。
3. トンネルを作る
cloudflared tunnel create homeserver
トンネルのUUIDと、~/.cloudflared/<UUID>.jsonという認証情報ファイルが出力されます。このJSONが実質的な鍵なので、リポジトリにコミットしないよう注意します。
4. 作成されたか確認する
cloudflared tunnel list
手順5〜7: ingress 設定と DNS ルーティング
5. config.ymlを書く
~/.cloudflared/config.ymlを作成します。
tunnel: 0000aaaa-1111-2222-3333-444455556666
credentials-file: /home/youruser/.cloudflared/0000aaaa-1111-2222-3333-444455556666.json
ingress:
- hostname: shop.example.com
service: http://localhost:3000
- service: http_status:404
ここで重要なのは、公式ドキュメントでも明記されているとおり、最後の catch-all ルール(service: http_status:404)が必須という点です。hostnameを持たない受け皿ルールが無いと設定エラーになります。ルールは上から順に評価されるので、複数サービスを出す場合は具体的なホスト名を先に書きます。
書けたら、起動前に構文と経路を検証できます。
cloudflared tunnel ingress validate
cloudflared tunnel ingress rule https://shop.example.com/products
オリジンが自己署名証明書のHTTPSの場合は、該当ルールにoriginRequest: { noTLSVerify: true }を付けます。
6. DNSレコードを作る
cloudflared tunnel route dns homeserver shop.example.com
Cloudflare DNS側に<UUID>.cfargotunnel.comを指すCNAME(プロキシ有効)が自動作成されます。ダッシュボードのDNS画面で1件増えていることを確認してください。
7. 手動起動でまず動作確認
cloudflared tunnel run homeserver
この状態で外部端末からhttps://shop.example.comにアクセスできれば、経路は完成です。プロキシ有効なホスト名であれば公開側のTLS終端はCloudflare側で行われるため、この構成ではオリジン側でLet's Encryptを回さずに運用できます(オリジンとの区間も暗号化したい場合は、別途Origin証明書などを検討します)。
手順8〜9: systemd で常駐させる
8. アプリ側をsystemd常駐にする
トンネルだけ常駐してもアプリが落ちていれば502になります。私はNodeアプリをsystemdで常駐させる構成にしています。ユニットファイルは、たとえば次のような形になります。
[Unit]
Description=Next.js app
After=network.target
[Service]
Type=simple
User=app
WorkingDirectory=/srv/shop
Environment=NODE_ENV=production
ExecStart=/usr/bin/npm run start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now shop
next startは事前にnext buildが済んでいることが前提です。デプロイ手順にビルドを組み込んでおきます。
9. cloudflaredをサービス化する
sudo cloudflared service install
sudo systemctl status cloudflared
サービスとしてインストールした場合、参照される設定パス(/etc/cloudflared/config.ymlなど)や実行ユーザーは環境やバージョンによって異なることがあります。systemctl cat cloudflaredで実際のExecStartと設定パスを必ず確認し、必要ならconfig.ymlと認証情報JSONを/etc/cloudflared/配下に配置し直してください。起動後はjournalctl -u cloudflared -fでログを追えます。
つまずきやすい5つのポイント
クライアントIPが全部ローカルアドレスになる
この構成で影響が広いのがこの点です。アプリから見た接続元は同一ホスト上のcloudflaredなので、ソケットのリモートアドレスは実質使い物になりません。Cloudflareの公式ドキュメントでは、実際の訪問者IPはCF-Connecting-IPヘッダで渡されるとされているため、IPを条件に使うロジック(自動BAN、レート制限、ログイン失敗回数によるロックなど)はこのヘッダを参照する設計にする必要があると考えられます。私は自作のWAF層(Next.jsのproxy.ts)でIPの自動BANとレート制限(300req/分)、ログイン5回失敗で15分ロックを実装していますが、こうしたIPベースの制御はトンネル構成では特にこの点を踏まえる必要があります。
const ip = req.headers.get("cf-connecting-ip") ?? "unknown";
このヘッダを信用してよいのは、オリジンへの経路がトンネル1本に限定されていて、外部から直接オリジンに到達できない構成だからです。ポート開放を併用している環境ではヘッダ偽装が成立しうるので、その前提が崩れていないか確認してください。
アップロードサイズの上限
公式ドキュメントによると、執筆時点でFreeプランのリクエストボディ上限は100MBです。画像や動画を扱うサイトでは、この上限を超える経路を作らない設計(分割アップロードや外部ストレージ)が必要になります。
長時間レスポンスは524になる
公式ドキュメントによると、オリジンからの応答が既定のタイムアウト(Free/Proでは100秒)を超えるとCloudflareは524を返します。重いバッチをHTTPリクエスト内で同期実行する作りだと詰まるので、非同期化を検討します。サーバー側でバッチを回す構成の例はClaude Code CLIで生成AIブログ記事を自動生成する手順にも書いています。
開発サーバーをそのまま公開しない
私はNext.jsのdevサーバーにLAN内のスマホからアクセスした際、画面は表示されるのにonClickが発火しない現象に遭遇しました。同じ経路でも本番ビルド(next build && next start)では正常に動作しています。原因を特定できたわけではありませんが、公開するのは本番ビルドに限る、という運用が安全だと考えています。
トンネルが上がっているのに502/1033が出る
502はオリジン側に原因があるケースが多いと考えられます。curl -I http://localhost:3000でアプリ自体が応答するか、config.ymlのポート番号がアプリのListenポートと一致しているかを順に確認します。1033(トンネル未接続)が出る場合はsystemctl status cloudflaredとcloudflared tunnel info homeserverでコネクタの接続状況を見ます。
まとめ
Cloudflare Tunnelは、ポート開放も固定IPも公開側の証明書運用もなしで自宅サーバーを公開できる構成です。手順としては「cloudflared導入 → login → tunnel create → config.yml → route dns → 手動run → systemd化」の7段階で、詰まりやすいのは catch-all ルールの書き忘れと、公開後のクライアントIP取得あたりだと考えられます。
次に取る行動を1つだけ挙げるなら、cloudflared tunnel run で手動起動して外部端末から疎通確認するところまでを先に済ませることをおすすめします。ここが通っていれば、systemd化は残りの作業を切り分けやすくなります。逆にsystemd化を先にやると、切り分けるべき変数が2つ同時に増えて原因追跡が難しくなります。
よくある質問
Cloudflare Tunnelは無料で使えますか?
公式ドキュメントによると、執筆時点でCloudflare Tunnel自体はFreeプランでも利用できます。ただしリクエストボディ100MBなどプラン依存の制限があるため、扱うコンテンツに合うか事前に確認してください。
独自ドメインがなくても使えますか?
DNSルーティングにはCloudflareにネームサーバーを向けたドメインが必要です。ドメインなしで試すクイックトンネル機能もありますが、URLが毎回変わるため常時公開の用途には向きません。
アプリ側で取得できるIPが127.0.0.1になります
cloudflaredが同一ホストからオリジンに接続するためです。実際の接続元は`CF-Connecting-IP`ヘッダから取得します。レート制限やIP BANを実装している場合はこのヘッダ基準に直す必要があります。
ポート開放と比べてセキュリティ面はどう違いますか?
ルーター側に着信ポートを開けないため、直接スキャンされる面が減ります。ただしアプリ自体の脆弱性は防げないので、アプリ層のレート制限や入力検証は別途必要です。