- 借りた GPU に手元から届かせる道は 3 本あり、外に見えてしまう範囲がまったく違うこと
ssh -Lでトンネルを 1 本通し、ポートを 1 つも開けずに手元から使う手順- API クライアントに書く 3 つの値(ベース URL・キー・モデル名)が、インスタンス側の何に対応しているか
- Open WebUI(= ブラウザで動くチャット画面)を手元に置いて、トンネル越しに使う手順
- 「繋がらない」を上から 4 つの順で潰す切り分け
届かせる道は 3 本ある
第 12〜14 章で、インスタンスの中では LLM が動く状態になりました。しかしそのサーバが待ち受けているのはインスタンスの内側のアドレスで、手元の Mac / PC のブラウザやコードからは、何もしなければ 1 バイトも届きません。届かせる道は次の 3 本です。
- SSH トンネル(
ssh -L)— SSH(= 遠くのコンピューターに安全にログインする仕組み)の暗号化された接続の中に、手元のポートとインスタンスのポートを繋ぐ通り道を 1 本作る。外にはポートを 1 つも開けない - ポート公開(
-p)— インスタンスのポートを共有の公開 IP 上に出す。インターネットの誰からでも到達できる状態になる - 画面ごとインスタンス上に立てる — Open WebUI のようなチャット画面をインスタンス側で動かし、Instance Portal(= vast.ai の base image を積んだテンプレートで
Openボタンを押すと最初に出る、インスタンス内のサービス一覧ページ。第 10 章)のトンネル(トークン付きの URL)でブラウザから開く
| 道 | 手元に要るもの | 外から見えるもの | 向いている用途 |
|---|---|---|---|
| ① SSH トンネル | ターミナルと SSH 鍵 | なし(SSH ポートのみ) | 自分だけで使う。本書の既定 |
| ② ポート公開 | なし(ブラウザだけでも可) | 公開 IP 上の外部ポート | 他のマシンやサービスから叩く |
| ③ 画面をインスタンス上に | ブラウザだけ | トークン付きのトンネル URL | ターミナルを使いたくないとき |
vast.ai の公式ドキュメントは、道 ① を「公開インターフェースにポートを開けることの安全な代替。すべてのデータは SSH 接続の中を運ばれる」と説明しています。本章はこれを既定として進めます。ポートを 1 つも開けなければ、外から踏まれる事故がそもそも起こりようがないからです。
道 ③ には 1 つ留保があります。Instance Portal が自動で作るのは Cloudflare の quick トンネル(trycloudflare.com の、ランダムに選ばれた 4 単語のサブドメイン)で、vast.ai の移行ガイドはこれを「ベストエフォート。常に利用できるとは限らない」と注記しています。トンネルの準備が間に合わないときは Instance Portal 自体が公開 IP のリンクに切り替わります。確実さが要る場面で頼り切らないでください。
道 ①: トンネルを 1 本通す
ssh -L は、SSH 接続の中にもう 1 本の通り道を作る機能です。手元のポート宛ての通信を、繋いだ先のマシンの中で受け直してくれます。公式ドキュメントはこれを「しばしば見落とされる SSH の機能」と呼んでいます。
ssh -p <ポート> root@<ホスト> -L 8000:localhost:8000
<ポート> と <ホスト> には、インスタンスカードの SSH ボタンが表示する接続情報を入れます。CLI なら vastai ssh-url <インスタンス ID> が ssh://root@ssh5.vast.ai:33426 の形で返すので、@ の後ろがホスト、: の後ろがポートです。ユーザー名は常に root です。なお SSH ボタンが出す既定のコマンド文字列には、最初から -L 8080:localhost:8080 が付いています(例: ssh -p 20544 root@142.214.185.187 -L 8080:localhost:8080)。そのままコピペすると 8080 番のトンネルしか通らないので、自分が使いたい番号に書き換えるか、後ろに -L をもう 1 本足してください。
読み方で唯一まぎらわしいのが -L 8000:localhost:8000 の部分です。左の 8000 が手元のポート、localhost:8000 はインスタンス側から見た宛先です。つまりこの localhost は自分の Mac ではなく、繋いだ先のインスタンスを指しています。トンネルの出口はインスタンスの内側なので、サーバが 127.0.0.1(= そのマシン自身だけを指すアドレス。外からは直接届かない)でしか待っていなくても届きます。
覚えておく性質が 3 つあります。第一に、-L は必要な数だけ並べられます(-L 8000:localhost:8000 -L 3000:localhost:3000)。第二に、トンネルは SSH 接続が生きている間だけ通ります。そのターミナルを閉じると通信は止まります。第三に、繋いだ先は既定で tmux(= 1 つのターミナルの中に複数の画面を持ち、接続が切れても中の処理を生かしておく仕組み。第 9 章)のセッションの中です。画面下に緑のバーが出ますが、これはプロセスを守るための仕組みなので消さないでください。
ここから手を動かします。前提は「第 13 章か第 14 章で OpenAI 互換 API(= OpenAI の API と同じ URL の形・同じ JSON の形で呼べる、という約束事)のサーバが動いている」ことです。ポート番号は章によって違うので、読み替えを先に決めておきます。第 13 章の vLLM は vllm serve の既定どおり 8000 番、第 14 章の llama-server は vast.ai の Open WebUI テンプレートが http://localhost:20000 を見に行く設定に合わせて 20000 番で起動しました。本章の例はすべて 8000 番で書きます。llama.cpp を使っている読者は、以下に出てくる 8000 をすべて 20000 に読み替えてください(-L 20000:localhost:20000、http://localhost:20000/v1 のように、左右とも同じ番号に揃えます)。
第 13 章の vllm serve(8000 番)または第 14 章の llama-server(20000 番)が動いている状態から始めます。所要 5 分、課金は約 0.03 USD(1×RTX 5090 の on-demand 価格は 2026-08-24 時点で中央値 0.40 USD/時。1 USD = 150 円として約 60 円/時 × 5 分 = 約 5 円)。
- 手元のターミナルで、トンネルを付けて接続する。
- そのウィンドウは繋いだまま放置する。閉じるとトンネルも消える。
- 手元の別のターミナルを開き、自分の
localhost:8000を叩く。
ssh -p <ポート> root@<ホスト> -L 8000:localhost:8000
curl http://localhost:8000/v1/models
サーブ中のモデル ID が入った JSON が返れば成功です。Connection refused が返るときは、トンネルが切れているか、サーバがまだ起動しきっていないかのどちらかです。本章末の「届かないときは上から 4 つ潰す」の順に確認してください。vLLM は初回起動時にモデルのダウンロードで API がすぐ使えないと公式が明記しているので、インスタンス側のログ(vastai logs <インスタンス ID> --tail 50)で進み具合を見てから再試行します。
クライアントに書く値は 3 つだけ
OpenAI 互換の API を使うツールに入れる値は、名前が違うだけでどれも同じ 3 つです。どこへ送るか(ベース URL)・誰として送るか(API キー)・何に送るか(モデル名)。トンネルを張った後は、この 3 つがインスタンス側の実体とこう対応します。
OPENAI_API_BASE_URL と書かせるものもある)が、右の実体は変わりません。ベース URL の末尾に /v1 が要る点は、本書で扱うツールではどれも共通です。キーについて補足します。vLLM は --api-key token-abc123 のようにキーを指定して起動でき、環境変数 VLLM_API_KEY でも設定できます。キーを設定していないサーバに対しては、vLLM 公式のサンプルは api_key="EMPTY" という文字列を渡しています(Open WebUI の公式ページは同じ場面で none と書いています)。どちらでも通ります。ここで大事なのは「キーは要らない」ではなく「何か文字列を入れておく」のが公式サンプルの作法だ、という点です(Open WebUI の API Key 欄については、公式は空欄でもよいと書いています)。
モデル名には、サーブしているモデルの ID(Hugging Face のリポジトリ名)をそのまま入れます。vllm serve はモデル名を位置引数で 1 つ取る形で、llama-server の /v1/models も公式ドキュメントが「返るリストは常に単一要素」と書いています。つまりどちらも 1 プロセス 1 モデルなので、迷ったら /v1/models が返した id をそのまま写せば確実です。
同じ 3 つの値が、ツールごとにどの欄に化けるかを並べておきます。
| ツール | ベース URL | キー | モデル名 |
|---|---|---|---|
curl | URL に直接書く(http://localhost:8000/v1/chat/completions) | -H "Authorization: Bearer <キー>"(サーバにキーを設定していなければ不要) | JSON の "model" |
| OpenAI の Python ライブラリ | base_url | api_key | model |
| Open WebUI の接続設定 | URL | API Key | Model IDs (Filter)(任意。手で足す欄) |
| Open WebUI の環境変数 | OPENAI_API_BASE_URL | OPENAI_API_KEY | — |
やってみる 15-1 のトンネルを張ったまま進めます。所要 10 分、課金は約 0.07 USD(2026-08-24 時点の中央値 0.40 USD/時 × 10 分)。手元に pip install openai で OpenAI の Python ライブラリを入れておきます。
- まず
curlで 1 往復させて、経路が生きていることを確かめる。 - 同じ 3 つの値を Python のクライアントに書き写す。
<モデル ID>には/v1/modelsが返した ID を入れる。
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "<モデル ID>",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Who won the world series in 2020?"}
]
}'
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="EMPTY",
)
completion = client.chat.completions.create(
model="<モデル ID>",
messages=[{"role": "user", "content": "Hello!"}],
)
print(completion.choices[0].message)
返答が表示されれば、手元のコードから借りた GPU を呼べる状態です。OpenAI の API に無いパラメータ(top_k など vLLM 独自のもの)は extra_body={"top_k": 50} の形で渡します。
ブラウザの画面から使う — Open WebUI
ターミナルではなくチャット画面から使いたいときは Open WebUI を使います。公式によれば Docker・Kubernetes・pip・ベアメタルのどこでも動き、接続先として Ollama・OpenAI 互換・Open Responses を標準で選べます。ログイン情報を含むすべてのデータは既定でその端末にローカル保存され、既定で外部へのリクエストを行いません。
置き場所は 2 つあります。手元の Mac / PC に置くなら、トンネルの出口(手元の localhost:8000)を見に行かせるだけで、外に何も開けずに済みます。インスタンス上に置くなら、vast.ai のテンプレート Open Webui (Ollama)(公式ドキュメント内の別表記は Ollama + Open WebUI)を使い、Open ボタン → Instance Portal に並ぶ Open WebUI のリンクから開きます。本章の趣旨(公開せずに自分だけで使う)に沿うのは前者です。
Docker で手元に置く場合の起動コマンドはこれです。
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:main
-p 3000:8080 は「コンテナの中は 8080、手元からは 3000」という意味なので、ブラウザで開くのは http://localhost:3000 です。-v open-webui:/app/backend/data は保存領域の指定で、これを付けないとコンテナを作り直したときに設定もチャット履歴もユーザーも全部消えます。Docker を使わない場合は pip install open-webui の後に open-webui serve で、こちらは 8080 番で待ちます(対応 Python は 3.11 と 3.12 で、3.13 は未対応)。
初回アクセスで最初に作られたアカウントが管理者(Administrator)になります。2 人目以降は Pending 状態になり、管理者の承認が要ります。インスタンス上に立てた場合、これは「URL を先に他人に踏まれたら、その人が管理者になる」ことを意味します。起動したら他の作業より先に自分でアクセスしてアカウントを作ってください。なお認証を丸ごと切る WEBUI_AUTH=False という設定もありますが、公式が「シングルユーザーモードとマルチアカウントモードは、この変更後に切り替えられない」と警告している片道切符です。
接続の登録は Admin Settings → Connections → OpenAI → Add Connection で行います。入力欄は 3 つで、URL・API Key・Model IDs (Filter)。ここで最も多い失敗は URL の末尾です。公式が感嘆符 2 つで「Note the /v1 at the end!!」と書いているとおり、http://localhost:8000 ではなく http://localhost:8000/v1 と書きます。
もう 1 つの落とし穴が localhost の意味です。Docker コンテナの中の localhost はコンテナ自身を指すので、手元のトンネル出口には届きません。公式の指示は「モデルサーバがホストマシン上にある場合は、URL の localhost を host.docker.internal に置き換える」です。
接続を保存したのにモデルが出てこないときの公式の説明も押さえておきます。Open WebUI は接続先の /models エンドポイントを呼んで検証しますが、それを実装していないサーバでは検証がエラーになります。それでも chat completions は動きます。その場合は Model IDs (Filter) にモデル ID を手で入力して + を押し、保存します。サーバの起動が遅くて検証がタイムアウトするなら、既定 10 秒の待ち時間を AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=30 で延ばせます。モデルを追加したのにセレクタに出ないときは、まずページをリロードしてください。
やってみる 15-1 のトンネルを張ったまま進めます。所要 20 分、課金は約 0.13 USD(2026-08-24 時点の中央値 0.40 USD/時 × 20 分)。手元に Docker が入っている前提です。
- Open WebUI を起動する。初回はイメージの取得に数分かかる。
- ブラウザで
http://localhost:3000を開き、最初のアカウントを作る(これが管理者になる)。 - Admin Settings → Connections → OpenAI → Add Connection を開き、URL に
http://host.docker.internal:8000/v1(llama.cpp ならhttp://host.docker.internal:20000/v1)、API Key にサーバ起動時の--api-keyの値(付けていなければnone)を入れて保存する。 - ページをリロードし、チャット画面上部のモデルセレクタに ID が出ることを確認して会話する。
- 終わったらコンテナを止める。インスタンス側は第 16 章の手順で destroy する。
docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui ghcr.io/open-webui/open-webui:main
docker stop open-webui
モデルが出ない場合は Model IDs (Filter) に ID を手で足します。ここまで通れば、手元のブラウザだけで借りた GPU と会話できる状態です。外に開いたポートは 1 つもありません。
どうしても外に開けるとき
別のマシンや外部サービスから叩く必要があるなら道 ② です。テンプレート編集画面の Docker オプション欄(CLI なら vastai create instance の --env "-p 8000:8000")に -p 8000:8000 と書くと、インスタンスの 8000 番が共有の公開 IP 上のランダムな外部ポートにマップされます。番号は自分では決められません。実際の番号は、インスタンス上部の IP Port Info ボタンで 65.130.162.74:33526 -> 8000/tcp(左が公開 IP と外部ポート、右が内部ポート)の形で読むか、次のコマンドで確認します。インスタンスの中からなら、第 10 章で扱った環境変数 VAST_TCP_PORT_8000 でも同じ番号が取れます。
vastai show instance <インスタンス ID> --raw | jq '.ports'
ここで押さえるべき事実は 3 つです。第一に、ポート番号がランダムなのはセキュリティではありません。公式のポートマッピング確認手順が「ブラウザで開けばディレクトリ一覧が見える」と書いている以上、他人のブラウザからも同じものが見えます。第二に、vast.ai の base image 系テンプレートは、この危険を認証トークンで塞ぐ設計になっています。直接アクセスには Basic 認証(ユーザー名 vastai、パスワードは OPEN_BUTTON_TOKEN の値)、API アクセスには Bearer トークンが要ります。トークンはインスタンス内で echo $OPEN_BUTTON_TOKEN で取れます。vast.ai は自己署名証明書を使うため、curl には -k が必要です。裏を返せば、この認証層があるのは base image 系テンプレートを使っている場合だけです。素の Docker イメージを自分で指定して立てたインスタンスに -p でポートを開けると、認証も TLS も無い http:// のまま誰でも叩ける状態になります。
curl -k https://<インスタンスの IP>:<外部ポート>/v1/models \
-H "Authorization: Bearer <OPEN_BUTTON_TOKEN>"
第三に、vLLM の --api-key だけを頼りに公開してはいけません。公式が明示的に警告しているとおり、このオプションが認証するのは /v1・/v2・/inference の配下だけで、同じ推論機能を持つ /invocations は認証されません。公式の対策はリバースプロキシ(= 外から来た通信を受けて中のアプリへ取り次ぐ役。第 10 章で出てきた Caddy がこれ)の配下に置くことです。ポートを開けるなら、トークンで守るテンプレートに乗るのが最短の安全策です。
なお、1 インスタンスあたりのオープンポートは合計 64 個までです。-p を書き忘れて後から必要になった場合は、インスタンスを作り直さなくても Instance Portal の Tunnels ページで http://localhost:7860 のように入力して Create New Tunnel を押せば、そのポート用のトンネルが作られます(Instance Portal を持つ base image 系テンプレートを使っている場合に限ります。作成直後は使えるようになるまで少し待ちます)。
届かないときは上から 4 つ潰す
「手元から叩いたが返ってこない」の原因はほぼこの 4 つに収まります。順番に確認してください。
curl http://localhost:8000/v1/models が通るかどうかで、経路と宛先のどちら側かを 1 回で切り分けられます。それぞれの潰し方を具体的に書きます。① SSH が生きているかは、トンネルを張ったターミナルを見れば分かります。プロンプトが出ていて画面下に tmux の緑のバーが見えていれば生きています。ウィンドウを閉じてしまっていたら ssh -p <ポート> root@<ホスト> -L 8000:localhost:8000 を打ち直します。② サーバが起動し終えているかは、インスタンス側のログで見ます。手元から vastai logs <インスタンス ID> --tail 50 を打つか、インスタンスカードの Logs ボタンでライブ出力を眺めます。vLLM は初回起動時にモデルのダウンロードで API がすぐ使えないと公式が明記しているので、ログが進んでいるなら待つのが正解です。③ URL の末尾は、ツールの設定欄なら http://localhost:8000/v1、curl なら http://localhost:8000/v1/models です。④ Docker の中から呼んでいないかは、Open WebUI をコンテナで動かしているかどうかで決まります。動かしているなら localhost を host.docker.internal に置き換えます。
4 つとも潰しても届かないときは、経路ではなくインスタンスそのものの状態を疑います。インスタンスカードのメインボタンが Inactive(停止中。データは残っている)や Offline(マシンが vast のサーバから切断された)になっていないかを見てください。状態が exited / unknown / offline まで落ちたインスタンスは running には二度と戻らないと公式が明記しています。粘らずに destroy して別の offer で借り直すのが正解です。待っている間もディスクの課金は続きます。
つまずきポイント
「接続できたから」と ssh のウィンドウを閉じると、その瞬間にトンネルも消えます。curl は Connection refused を返しますが、サーバ側は無事です。ウィンドウは開いたまま置いておき、作業は別のターミナルで行ってください。SSH で入った先は tmux なので、Ctrl+b → c で新しいウィンドウを作ってインスタンス側の作業を進める手もあります。
/v1 を付け忘れる
Open WebUI の接続設定でも、OpenAI の Python ライブラリでも、入れるのは http://localhost:8000/v1 です。公式が「Note the /v1 at the end!!」と感嘆符 2 つで書いているほど頻出します。症状は「保存はできたがモデルが 1 つも出てこない」。逆に curl で叩くときは http://localhost:8000/v1/models のようにパスまで書くので、ここに /v1 を二重に入れないよう注意します。同じ欄でポート番号を取り違えるのも同じ症状になります。第 14 章の llama-server は 20000 番で起動しているので、http://localhost:20000/v1 です。
localhost を指す
コンテナの中の localhost はコンテナ自身です。手元で動かしているトンネルの出口には届きません。公式の指示どおり host.docker.internal に置き換えます。Docker を使わず open-webui serve で直接起動している場合は、この置き換えは不要で localhost のままで届きます。
--api-key を付けたから公開しても安全だと思う
vLLM の --api-key が守るのは /v1・/v2・/inference の配下だけで、同じ推論機能を持つ /invocations は認証されません。公式は「--api-key だけに頼って vLLM を守るな」と明記しています。ポートを開けるなら、トークンで守る vast.ai のテンプレートに乗るか、リバースプロキシを前に置いてください。開ける必要がないなら ssh -L のままにするのが最も安全です。
理解度チェック
Q1. ssh -p 20544 root@142.214.185.187 -L 8000:localhost:8000 の localhost は、どちらのマシンを指しますか?
インスタンス側です。-L の左の 8000 が手元で待つポート、右の localhost:8000 は「トンネルを抜けた先=インスタンスの中」から見た宛先です。だからサーバが 127.0.0.1 でしか待っていなくても届きます。
Q2. Open WebUI の接続設定に http://localhost:8000 と入れたらモデルが 1 つも出ません。最初に疑うのはどこですか?
末尾の /v1 です。http://localhost:8000/v1 が正しい形で、公式も感嘆符 2 つで強調しています。加えて、Open WebUI を Docker で動かしているなら localhost を host.docker.internal に置き換える必要があります。
Q3. 接続の検証が赤く失敗しました。この接続は使えませんか?
使えることがあります。Open WebUI は /models エンドポイントを呼んで検証しますが、公式は「一部のプロバイダはそれを実装しておらず検証は失敗する。しかし chat completions は動く」と説明しています。Model IDs (Filter) にモデル ID を手で入力して + を押せば使えます。
Q4. Open WebUI をインスタンス上に立てて外からアクセスできる状態にしました。真っ先にやることは何ですか?
自分でアクセスして最初のアカウントを作ることです。最初に作られたアカウントが管理者になり、2 人目以降は承認待ちになります。URL を先に他人に踏まれると、その人が管理者になってしまいます。
Q5. 外部ポートの番号がランダムなので、URL を知られなければ安全と言えますか?
言えません。ランダムな番号はセキュリティ機構ではなく、割り当ての都合です。公式のポート確認手順自体が「ブラウザで開けば中身が見える」ことを前提にしています。認証トークンを掛けるか、そもそもポートを開けずに ssh -L を使ってください。
まとめ
- 手元から借りた GPU に届かせる道は 3 本。初心者の既定は
ssh -Lのトンネルで、外にポートを 1 つも開けずに済む -L 8000:localhost:8000のlocalhostはインスタンス側。トンネルは SSH 接続が生きている間だけ通る。番号はサーバの待ち受けポートに合わせる(第 13 章の vLLM は 8000、第 14 章のllama-serverは 20000)- クライアントに書くのはベース URL・キー・モデル名の 3 つだけ。ベース URL の末尾には必ず
/v1、キー未設定ならEMPTY(Open WebUI の欄はnone)、モデル名は/v1/modelsが返したidを写す - Open WebUI は
-p 3000:8080なので手元ではlocalhost:3000。最初に作ったアカウントが管理者で、Docker の中からホストを見るにはhost.docker.internal - ポートを開けるなら認証は必須。
OPEN_BUTTON_TOKENで守るテンプレートに乗る。--api-keyだけでは/invocationsが素通りになる
ここまでで「借りて・動かして・手元から使う」が一周しました。次章では、使い終わった後にお金を確実に止める手順を扱います。
参考リンク
- Connect via SSH(vast.ai 公式) —
ssh -Lの書式とトラブルシュート - Networking(vast.ai 公式) —
-pで開いたポートが外からどう見えるか - Instance Portal(vast.ai 公式) — トンネルとトークン、Tunnels ページ
- Open WebUI Quick Start(公式) — Docker 起動と初回アカウント
- Starting With OpenAI-Compatible Servers(Open WebUI 公式) — 接続設定と
host.docker.internal - OpenAI-Compatible Server(vLLM 公式) — エンドポイント一覧と
--api-keyの警告 - vLLM テンプレート(vast.ai 公式) —
OPEN_BUTTON_TOKENを使った API アクセス