📝

TailscaleでローカルのViteサーバーに繋がらなかった時のメモ

に公開

iPhoneで開発中のWebアプリを確認したくて、Tailscale使ったらデータ表示されなかった(CORSエラーで詰まった)のでメモです。

(この記事はClaudeCodeがほぼ書きました。
正直、iPhoneでSSHをしてTailscaleでMacにつないでClaudeCodeで開発していると、とても見通しが悪いため、自分用にmdドキュメントをzennにプッシュして、自分で内容を確認する意味合いが強い記事です💦 AI駆動開発をスマホで行うと、見通しが悪いので苦肉の策として..)

ローカル環境

  • Mac (M2)
  • iPhone (Tailscale経由)
  • Vite dev server (port 5180)
  • PHP API (port 8181)

問題

tailsclaeでMacにiPhoneからSSHを通してchromeで開発中のwebアプリにアクセスすると403エラー:

Request failed with status code 403

APIは動いてるのに、なぜかデータが表示されない。

原因

CORSの許可リストにTailscaleのIPが入ってなかった。

$allowedOrigins = [
    'http://localhost:5173',
    'http://localhost:3000',
    // ← ここに http://[TAILSCALE_IP]:5180 がない
];

解決方法

1. Viteの設定

vite.config.ts:

server: {
  host: true,  // これ重要!
  port: 5180,
  proxy: {
    '/api': {
      target: 'http://localhost:8181',
      changeOrigin: true,
      rewrite: (path) => path.replace(/^\/api/, '')
    }
  }
}

2. ❌ 危険な一時対応(やってはいけない)

最初はこんな風に書いてしまった(ClaudeCodeがw 「そりゃあかんでしょ」とツッコミ入れました):

// 絶対にこのまま本番に上げてはいけない
if (true) { // TODO: 本番では戻す
    header("Access-Control-Allow-Origin: *");
}

TODOで「本番では戻す」なんて、確実に忘れる。危険すぎる。(これは人間のコメント笑)

3. ✅ 環境変数による適切な設定

.env の設定

# 環境設定
APP_ENV="development"
TAILSCALE_IP="1xx.1xx.1xx.3x"  # MacのTailscale IP

cors_header.php の改善

// 環境変数による適切な分岐
$isDevelopment = $_ENV['APP_ENV'] === 'development' || $_ENV['APP_ENV'] === 'local';

if ($isDevelopment) {
    // 開発環境: Tailscale IPを動的に追加
    $tailscaleIp = $_ENV['TAILSCALE_IP'] ?? '';
    if ($tailscaleIp) {
        $allowedOrigins[] = "http://{$tailscaleIp}:5180";
        $allowedOrigins[] = "https://{$tailscaleIp}:5180";
    }
    
    // 開発用のlocalhostも許可
    if (isset($_SERVER["HTTP_ORIGIN"]) && 
        (in_array($_SERVER["HTTP_ORIGIN"], $allowedOrigins) ||
         strpos($_SERVER["HTTP_ORIGIN"], 'http://localhost:') === 0)) {
        header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]);
    } else {
        header("HTTP/1.1 403 Forbidden");
    }
} else {
    // 本番環境: 厳格なチェック
    if (isset($_SERVER["HTTP_ORIGIN"]) && 
        in_array($_SERVER["HTTP_ORIGIN"], $allowedOrigins)) {
        header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]);
    } else {
        header("HTTP/1.1 403 Forbidden");
    }
}

デバッグに使ったテストページ

シンプルなHTMLで問題の切り分け:

<!DOCTYPE html>
<html>
<body>
<button onclick="test()">Test API</button>
<div id="result"></div>
<script>
async function test() {
  const res = await fetch('/api/basics.php?timestamp=958550400');
  const data = await res.json();
  document.getElementById('result').innerText = JSON.stringify(data);
}
</script>
</body>
</html>

環境ごとの設定例

開発環境

APP_ENV="development"
TAILSCALE_IP="1xx.1xx.1xx.3x"

ステージング環境

APP_ENV="staging"
TAILSCALE_IP=""

本番環境

APP_ENV="production"
TAILSCALE_IP=""

メモ

  • host: trueを忘れると外部からアクセスできない
  • 開発環境でも完全に無制限にはしない
  • TODOコメントに頼らず、環境変数で確実に分岐させる
  • Tailscale IPは.envで管理すれば、リポジトリに含めなくて済む

まとめ

以上、次に同じことで詰まらないための備忘録。

Discussion