vast.ai で RTX 5090 を借りて ローカル LLM を動かす|2026-08 版
第 15 章

手元の Mac / PC から使う

SSH トンネル・Open WebUI・API クライアント設定。公開せずに自分だけで使う

この章で分かること

届かせる道は 3 本ある

第 12〜14 章で、インスタンスの中では LLM が動く状態になりました。しかしそのサーバが待ち受けているのはインスタンスの内側のアドレスで、手元の Mac / PC のブラウザやコードからは、何もしなければ 1 バイトも届きません。届かせる道は次の 3 本です。

  1. SSH トンネル(ssh -L)— SSH(= 遠くのコンピューターに安全にログインする仕組み)の暗号化された接続の中に、手元のポートとインスタンスのポートを繋ぐ通り道を 1 本作る。外にはポートを 1 つも開けない
  2. ポート公開(-p)— インスタンスのポートを共有の公開 IP 上に出す。インターネットの誰からでも到達できる状態になる
  3. 画面ごとインスタンス上に立てる — Open WebUI のようなチャット画面をインスタンス側で動かし、Instance Portal(= vast.ai の base image を積んだテンプレートで Open ボタンを押すと最初に出る、インスタンス内のサービス一覧ページ。第 10 章)のトンネル(トークン付きの URL)でブラウザから開く
ここから右は公開インターネット 道 ① SSH トンネル — 何も開けない 手元の curl / SDK localhost:8000 SSH 接続の中を通る 外に開いたポートは 0 個 vLLM / llama-server 127.0.0.1:8000 で待つ 道 ② ポートを公開する — 世界に開く 手元の curl / SDK 共有 IP のランダムポート HTTP がそのまま外へ 8000 番が誰からでも見える vLLM / llama-server 0.0.0.0:8000 で待つ 道 ③ インスタンス上に画面を立てる 手元のブラウザ trycloudflare.com トークン付きの HTTPS URL を知る人だけが届く Open WebUI + モデルサーバ
図 15-1 破線が「公開インターネットとの境目」です。道 ① だけは境目を越えるのが SSH 接続 1 本で、サーバのポートは外に一切出ません。道 ② は境目の上に 8000 番が置かれるので、認証を自分で用意しない限り誰でも叩けます。
道手元に要るもの外から見えるもの向いている用途
① 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 のように、左右とも同じ番号に揃えます)。

やってみる 15-1|トンネルを張って、手元から /v1/models を見る

第 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 円)。

  1. 手元のターミナルで、トンネルを付けて接続する。
  2. そのウィンドウは繋いだまま放置する。閉じるとトンネルも消える。
  3. 手元の別のターミナルを開き、自分の 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 つがインスタンス側の実体とこう対応します。

手元のクライアント設定 インスタンス側の実体 base_url http://localhost:8000/v1 api_key EMPTY(キー未設定のとき) model Qwen/Qwen3-8B のような ID どこへ送るか 誰として送るか 何に送るか ssh -L 8000:localhost:8000 トンネルの出口 = サーバの 8000 --api-key に渡した値 付けていなければ何でもよい GET /v1/models が返す id サーブ中のモデルは 1 つだけ
図 15-2 左の 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キーモデル名
curlURL に直接書く(http://localhost:8000/v1/chat/completions)-H "Authorization: Bearer <キー>"(サーバにキーを設定していなければ不要)JSON の "model"
OpenAI の Python ライブラリbase_urlapi_keymodel
Open WebUI の接続設定URLAPI KeyModel IDs (Filter)(任意。手で足す欄)
Open WebUI の環境変数OPENAI_API_BASE_URLOPENAI_API_KEY—
やってみる 15-2|手元の Python から借りた GPU を呼ぶ

やってみる 15-1 のトンネルを張ったまま進めます。所要 10 分、課金は約 0.07 USD(2026-08-24 時点の中央値 0.40 USD/時 × 10 分)。手元に pip install openai で OpenAI の Python ライブラリを入れておきます。

  1. まず curl で 1 往復させて、経路が生きていることを確かめる。
  2. 同じ 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 に置き換える」です。

手元の Mac / PC インスタンス(vast.ai 側) SSH トンネルの出口 127.0.0.1:8000 で待っている Open WebUI コンテナ 中の localhost は自分自身 host.docker.internal で越える ここは届かない SSH の中 vLLM / llama-server 8000 番で待つ ブラウザで開くのは手元の localhost:3000
図 15-3 同じ「localhost」という綴りが 3 か所で別のマシンを指しています。コンテナの中=コンテナ自身、トンネル指定の右側=インスタンス、ブラウザの URL=手元の Mac。迷ったら「今その文字列を解釈するのは誰か」で判定します。

接続を保存したのにモデルが出てこないときの公式の説明も押さえておきます。Open WebUI は接続先の /models エンドポイントを呼んで検証しますが、それを実装していないサーバでは検証がエラーになります。それでも chat completions は動きます。その場合は Model IDs (Filter) にモデル ID を手で入力して + を押し、保存します。サーバの起動が遅くて検証がタイムアウトするなら、既定 10 秒の待ち時間を AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=30 で延ばせます。モデルを追加したのにセレクタに出ないときは、まずページをリロードしてください。

やってみる 15-3|手元に Open WebUI を立てて、トンネル越しに会話する

やってみる 15-1 のトンネルを張ったまま進めます。所要 20 分、課金は約 0.13 USD(2026-08-24 時点の中央値 0.40 USD/時 × 20 分)。手元に Docker が入っている前提です。

  1. Open WebUI を起動する。初回はイメージの取得に数分かかる。
  2. ブラウザで http://localhost:3000 を開き、最初のアカウントを作る(これが管理者になる)。
  3. 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)を入れて保存する。
  4. ページをリロードし、チャット画面上部のモデルセレクタに ID が出ることを確認して会話する。
  5. 終わったらコンテナを止める。インスタンス側は第 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 が失敗した — 上から順に潰す OK なら次へ OK なら次へ OK なら次へ NG NG NG NG そのターミナルで ssh はまだ繋がっているか サーバは起動し終えているか URL の末尾に /v1 を付けたか Docker の中から呼んでいないか 接続が切れるとトンネルも消える 繋ぎ直して -L を付け直す 初回はモデル DL で数分〜数十分 vastai logs で進捗を見る モデル一覧が空のまま 公式が !! で強調する最頻の失敗 コンテナの localhost は自分自身 host.docker.internal に置き換える
図 15-4 上の 2 つは「経路の問題」、下の 2 つは「宛先の書き方の問題」です。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 で新しいウィンドウを作ってインスタンス側の作業を進める手もあります。

ベース URL の末尾に /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 です。

Docker の中の Open WebUI から 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 を使ってください。

まとめ

ここまでで「借りて・動かして・手元から使う」が一周しました。次章では、使い終わった後にお金を確実に止める手順を扱います。