roji はコンテナを自動で検出する。一方、ホスト上で直接起動した開発サーバー (vite dev、wrangler dev、rails s など)はコンテナではないので検出 されない。roji は、こうしたサーバーにもコンテナと同じようにホスト名と HTTPS を付けられる。

方法は 2 つある。

  • roji run は、空きポートを選んで開発サーバーを起動する。設定は要らず、 ポートを決める必要も無い
  • 設定ファイルの proxy_sites は、自分で決めたポートにホスト名を付ける。 サーバーが起動していなくてもホスト名は残る

roji run: ポートを決めずに起動する

roji run app -- npx vite --port {port}
# roji: https://app.dev.localhost -> localhost:41873

roji run <name> -- <command> は、空きポートを選び、https://<name>.<domain> を そこへ向けてからコマンドを実行する。ルートはコマンドが動いている間だけ有効。

どのプロジェクトも、ほかが使っていないポートを受け取る。5173 や 8787 を 既定にする開発サーバーを同時に動かしても衝突しないし、プロジェクトの設定に ポートを書く必要も無い。

コマンドへのポートの渡し方

コマンドは、roji が選んだポートで待ち受ける必要がある。ポートは 2 通りの 方法でコマンドに渡る。

  • 引数の中の {port} は、すべてポート番号に置き換わる
  • 環境変数 PORT にポート番号が入る

どちらを読むかはツールによる。

ツール書き方
Viteroji run app -- npx vite --port {port}
Wranglerroji run api -- npx wrangler dev --port {port}
Next.js、Rails など PORT を読むものroji run web -- npm run dev

Vite と Wrangler は PORT を読まない。--port {port} を付けずに起動すると いつものポートで立ち上がり、roji が表示した URL はエラーページになる。 roji run は 20 秒待っても選んだポートで何も待ち受けていなければ、その旨を 表示する。

コマンドへのホスト名の渡し方

自分のアドレスを知らないと動かない開発サーバーがある(OAuth のメタデータ、 絶対 URL へのリダイレクト、許可するホストの一覧など)。 ホスト名はベースドメインで決まり、ベースドメインはマシンごとの設定なので、 リポジトリに入れるスクリプトには書けない。ホスト名も、ポートと同じ 2 通りの 方法でコマンドに渡る。

引数の中環境変数値
{host}ROJI_HOSTapp.dev.localhost
{url}ROJI_URLhttps://app.dev.localhost

roji の https_port が 443 でないとき、URL にはポートが付く。どちらの値も、 稼働中の roji が返したもの。

環境変数の名前を HOST にしていないのは、HOST を待ち受けるアドレスとして 読む開発サーバーが多いため。

リクエストのオリジンを見る Worker には、ホスト名とプロトコルの両方を渡す。 渡さないと、wrangler dev はリクエストを http のものとして Worker に見せる。

roji run api -- npx wrangler dev --port {port} --host {host} --upstream-protocol https

-- は省略できない。それより後ろはすべてコマンドのもので、コマンドの フラグが roji のフラグとして解釈されるのを防いでいる。

roji run がすること、しないこと

  • roji が動いている必要がある。roji run は最初にルートを登録し、登録 できなければコマンドを起動しない
  • 名前の規則は、下の host と同じ。設定ファイルの proxy_sites か static_sites が宣言している名前と、別の roji run が使用中の名前は 拒否する
  • 同じホスト名にコンテナがルーティングされている場合、コマンドの実行中は コンテナが隠れる。roji run がその旨を表示する
  • コマンドの終了コードが、そのまま roji run の終了コードになる。シグナルで 止まったときは、シェルと同じく 128 にシグナル番号を足した値を返す (Ctrl-C なら 130)。Ctrl-C と kill は、コマンドとそこから起動された プロセスのすべてに届く。npx を挟んでもサーバーは残らない
  • ルートを登録できるのは、roji が動いているマシンからだけ。bind の設定に よらない
  • roji run 自体を強制終了(kill -9)した場合、ルートは 30 秒以内に消える。 起動済みの開発サーバーは止まらない
  • トンネルでは公開されない。公開したい場合は proxy_sites に tunnel: true を書く

proxy_sites: 自分で決めたポートを使う

~/.config/roji/config.yaml にエントリを追加する。

proxy_sites:
  - host: app                       # -> app.dev.localhost
    target: http://localhost:5173   # 開発サーバーの待ち受け先
  - host: api
    target: http://127.0.0.1:8787

開発サーバーをいつもどおり起動し、https://app.dev.localhost を開く。

ホスト名

host の規則は 静的サイトと同じ。

  • ドットを含まない名前は {host}.{domain} に展開する(app → app.dev.localhost)
  • ドットを含む名前は書いたとおりに使う

ホスト名はベースドメインの 1 階層下に収める。roji の証明書が対応するのは *.{domain} までで、a.b.dev.localhost のような 2 階層の名前は、別ドメインの 名前と同じくブラウザに拒否される。外れているホスト名は roji doctor が報告する。

ダッシュボードのホスト名(既定では roji.dev.localhost)とベースドメイン そのものは使えない。ダッシュボードが先に応答するので、そこに宣言したサイトは 警告を出して登録しない。

target

target はスキームまで含めた URL で書く。スキームは http のみ。TLS は roji が終端する。

  • target: 8787 や target: 127.0.0.1:8787 は受け付けない。port: の ような略記も無い。1 つのサイトの書き方を 1 通りにするため
  • target にパスを書くと、すべてのリクエストの先頭に付く。 target: http://localhost:3000/base なら /items は /base/items に届く
  • サーバーがどのアドレスで待ち受けるか分からないときは http://localhost:ポート と書く。ツールや Node.js の版によって 「localhost」は 127.0.0.1 にも ::1 にもなり、roji は両方を試す
  • 別のマシンを指す target も使えるが、バリデーションが警告を出す。その リクエストは暗号化されずにこのマシンの外へ出る

開発サーバーが受け取るもの

コンテナの場合と同じ。

  • Host ヘッダーは、ブラウザが要求したホスト名(app.dev.localhost)の まま渡る。target のホスト名にはならない。Vite の server.allowedHosts の ようにホスト名を検査する開発サーバーには、この名前が見える。Vite は .localhost の名前を既定で許可している
  • X-Forwarded-Proto: https、X-Forwarded-For、X-Real-IP
  • WebSocket(Vite の HMR)、Server-Sent Events、gRPC はそのまま通る

サーバーが起動していないとき

開発サーバーをまだ起動していない状態はふつうにあるので、roji は target に 接続できるかを確かめずにサイトを登録する。その状態でアクセスすると、 target のアドレスと、起動すべきものを案内する 502 のページが返る。 Compose プロジェクトと違い、roji はそのプロセスのことを何も知らないので、 roji からは起動できない。

roji doctor は、待ち受けているプロセスが無い proxy サイトを警告として 一覧する。

Docker ルートとの優先順位

proxy サイトは、静的サイトと同じくホスト名を保持する。同じホスト名に コンテナがルーティングされている場合は proxy サイトが応答し、どの Docker ルートが隠れたかを roji がログに出す。設定ファイルの中で同じホスト名を 2 回宣言した場合(proxy_sites 同士、static_sites 同士、両方に 1 つずつの いずれでも)、後に書いた宣言が有効になる。proxy_sites は static_sites の 後に読まれる。

proxy サイトはホスト名全体を受け持つ。パスプレフィックスは指定できない。

変更の適用

再起動は不要。次のコマンドを実行するか、ダッシュボードの Reload Config ボタンをクリックする。

roji config reload

roji を動かしたまま target のポートを変えたいときも、この手順を使う。

コマンドは、宣言したサイトをすべて一覧し、どれが登録されたかを表示する。 登録されなかったサイト(target が URL でない、ダッシュボードのホスト名を 使っている、など)には理由が付く。その場合も、残りのサイトは反映される。

認証

proxy_sites:
  - host: api
    target: http://127.0.0.1:8787
    auth:
      basic:
        user: admin
        pass: secret
        realm: API   # 任意

詳細は 認証のガイドを参照。

トンネルで外部公開する

開発サーバーで webhook を受けるのは、 トンネルの 主な用途の 1 つ。tunnel: true でサイトを公開対象にする。

proxy_sites:
  - host: hook
    target: http://127.0.0.1:8787
    tunnel: true        # https://hook.example.com で到達できる

公開用の名前は、ローカルの名前のうちベースドメインの部分をトンネルの ドメインへ入れ替えて作る。そのため、公開できるのはベースドメインの下のホスト名だけ。

このキーが無いサイトは非公開のまま。開発サーバーが起動していないときに トンネル経由で来たリクエストには、上の案内ページではなく、トンネルの ほかの拒否と同じ素の 404 を返す。