はじめに
「OpenAI 互換 API をセルフホストで動かしたい」というニーズに対する一番素直な答えが vLLM です。OpenAI SDK の base_url を差し替えるだけで叩けて、PagedAttention のおかげで ollama 系より同時実行に強い、というのが採用理由です。
本記事では、vLLM (OpenAI 互換 LLM 推論サーバー) を ConoHa VPS3 の NVIDIA L4 GPU インスタンスに conoha-cli でほぼワンコマンドでデプロイし、ブラウザの Swagger UI で実機検証するまでを通しで紹介します。
ファイル一式は PR crowdy/conoha-cli-app-samples#93 にコミット済みで、本文中のコードはすべてそのまま動く現物の抜粋です。完全版が必要な場合は vllm-gpu/ を参照してください。
構成
2 コンテナ構成です。
| サービス | 公開ポート | 役割 |
|---|---|---|
| vllm | 8000 (内部のみ) | OpenAI 互換推論サーバー |
| caddy | 80 / 443 (ホスト直結) | リバースプロキシ + 自動 TLS |
L4 24GB に対し、デフォルトモデルは Qwen/Qwen2.5-7B-Instruct-AWQ (AWQ INT4 で約 9GB)。MAX_MODEL_LEN=8192 + --gpu-memory-utilization 0.90 で KV キャッシュにも余裕があります。
クライアント → Caddy(:80, :443) → vLLM(:8000) → NVIDIA L4
compose.yml の押さえどころ
完全版は vllm-gpu/compose.yml にあります。要は以下の形です。
services:
vllm:
image: vllm/vllm-openai:v0.20.1
command:
- --model
- ${MODEL_NAME:-Qwen/Qwen2.5-7B-Instruct-AWQ}
- --gpu-memory-utilization
- "${GPU_MEMORY_UTILIZATION:-0.90}"
- --quantization
- ${QUANTIZATION:-awq_marlin}
- --served-model-name
- default
- --api-key
- ${VLLM_API_KEY:-}
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
start_period: 1200s # 初回モデル DL 猶予
caddy:
image: caddy:2-alpine
ports: ["80:80", "443:443"]
depends_on:
vllm: { condition: service_healthy }
押さえどころ:
-
vllmはホストにポートを公開しない (外向きは Caddy のみ) -
--api-keyは空文字なら認証 OFF (vLLM が空文字を falsy と判定) -
--served-model-name defaultで固定。OpenAI SDK からはmodel="default"で叩く -
start_period: 1200s(= 20 分) は必須。9GB の Qwen 7B AWQ をダウンロード中に healthcheck が走り出すと再起動ループに入る -
image: ...:v0.20.1のようにタグを切る (:latestは再現性が壊れる)
Caddyfile
{$DOMAIN_NAME} {
reverse_proxy vllm:8000 {
flush_interval -1
}
# stream=true 時のみ Accept: text/event-stream を見て gzip を外す
@no_sse {
not header Accept *text/event-stream*
}
encode @no_sse gzip
}
stream=true の SSE 配信を壊さないために、
-
flush_interval -1で reverse_proxy のレスポンスバッファを切る (write 即 flush) -
Accept: text/event-streamリクエストには gzip を外す
encode の名前付きマッチャはレスポンスではなく リクエストヘッダ側に効きます。レスポンス側の Content-Type で振り分けたくなりますが、Caddyfile では混同しやすい挙動なので、Accept ヘッダで判定するのが素直です。
DOMAIN_NAME を :80 のままにすれば HTTP-only、vllm.example.com のような実 FQDN を入れれば Caddy が Let's Encrypt の証明書を自動発行します。
.env (主な変数)
MODEL_NAME=Qwen/Qwen2.5-7B-Instruct-AWQ
QUANTIZATION=awq_marlin # MODEL_NAME に揃える (FP16 モデルなら none)
VLLM_API_KEY= # 空なら認証 OFF。本番では必ず設定
HF_TOKEN= # Llama 系などゲートモデル時のみ
DOMAIN_NAME=:80 # 実 FQDN を入れると Caddy が自動 TLS
MODEL_NAME を非 AWQ モデル (Llama 3.1 8B FP16 など) に切り替える時は QUANTIZATION=none に 必ず変えます。awq_marlin のままだと vLLM 起動時にコケます。
デプロイ手順
1. セキュリティグループ
ConoHa VPS3 の default SG は 22 番もデフォルトでは開いていないので、22 と 80 だけ通したグループを作ります。
SG_ID=$(conoha network sg create --name vllm-gpu-test-sg --no-input \
--format json | jq -r '.id')
for port in 22 80; do
conoha network sgr create --security-group-id $SG_ID \
--direction ingress --protocol tcp \
--port-min $port --port-max $port --remote-ip 0.0.0.0/0 --no-input
done
2. cloud-init で NVIDIA セットアップ
vmi-docker-29.2-ubuntu-24.04-amd64 イメージは Docker は入っていますが NVIDIA driver と Container Toolkit は入っていません。--user-data に渡す cloud-init で初回ブート時にまとめて済ませます。要点は 4 行です。
apt-get install -y nvidia-container-toolkit ubuntu-drivers-common
ubuntu-drivers install --gpgpu # headless GPU driver
nvidia-ctk runtime configure --runtime=docker
shutdown -r +1 # kernel module 反映のため再起動
3. サーバー作成
conoha server create \
--name vllm-gpu-test \
--flavor 1ff846c5-... \ # g2l-t-c4m16g1-l4 (4 vCPU / 16GB / L4)
--image 722c231f-... \ # vmi-docker-29.2-ubuntu-24.04-amd64
--key-name tkim-cli-test-key \
--security-group vllm-gpu-test-sg \
--user-data /tmp/vllm-gpu-cloudinit.sh \
--no-input --wait
L4 GPU の小さいフレーバーで Qwen 7B AWQ なら十分動きます。
4. アプリデプロイ
cloud-init 完了後、compose.yml のあるディレクトリで:
conoha app deploy <SERVER_ID> --app-name vllm-gpu \
--no-proxy --no-input --insecure
このとき --no-proxy が 必須 です。詳細は後述のハマりポイントで。
ログに vllm-gpu-vllm-1 Healthy が出れば OK。start_period: 1200s のため、初回はここまで 5–15 分かかります。
ハマりポイント
実機で踏んだ落とし穴 3 つ。
1. L4 GPU 在庫切れ → --volume で既存ボリュームを再利用
ConoHa3 の L4 GPU は人気が高く、時間帯によって HTTP 500: No compute resource in stock で蹴られます。困るのは API の仕様上、ブートボリュームはサーバー作成 API 呼び出しの前に作られる ため、失敗時はボリュームだけ orphan として残る点です。
boot volume 3a156cca-... was created but server creation failed.
You can delete it with: conoha volume delete 3a156cca-...
API error (HTTP 500): {"code": 500, "error": "No compute resource in stock."}
conoha server create --help を見直すと --volume <既存ボリューム ID> フラグがありました。リトライ時はこれを付けるだけで残った orphan を再利用でき、別フレーバーでも attach 可能でした。L4 の小さい方が在庫切れなら大きい方 (g2l-t-c20m128g1-l4) で同じボリュームを使い回せます。
conoha server create --name vllm-gpu-test \
--flavor b5d0e377-... \ # 別フレーバー
--image 722c231f-... \ # 元と同じ image (CLI 上必須)
--volume 3a156cca-... \ # ← orphan を再利用
--key-name tkim-cli-test-key --security-group vllm-gpu-test-sg \
--user-data /tmp/vllm-gpu-cloudinit.sh --no-input --wait
なお --volume 指定時も --image は CLI のバリデーション上必須です。同パターンを SKILL に追記する issue #13 を conoha-cli-skill に立てています。
2. cloud-init 後に nvidia-smi が見つからない
ubuntu-drivers install --gpgpu が入れる nvidia-headless-no-dkms-595-server-open には nvidia-smi が同梱されていません。確認用には別途 nvidia-utils-XXX-server をインストールします (XXX は driver シリーズ番号、検証時は 595)。
apt-get install -y nvidia-utils-595-server
nvidia-smi
# NVIDIA-SMI 595.58.03 Driver Version: 595.58.03 CUDA Version: 13.2
# 0 NVIDIA L4 23034 MiB / 23034 MiB
3. conoha app deploy の --no-proxy
compose.yml だけのレシピで conoha.yml を持たないものは、明示的に --no-proxy を付けないと
read conoha.yml: open conoha.yml: no such file or directory
で蹴られます。本サンプルは Caddy が直接 80/443 を握る構成のため conoha.yml を持たず、conoha-proxy の blue/green 配置にも乗りません。--no-proxy で「flat single-slot 模式」に切り替わり、docker compose up -d だけが実行されます。hunyuan3d-gpu のような GPU + 内蔵 reverse proxy 構成のレシピでは、今後もこのパターンが続くはずです。
動作確認 — Swagger UI でブラウザ検証
CI 用の smoke-test.sh (/v1/models, /v1/chat/completions, /v1/completions への 3 アサーション) は当然パスしますが、ブラウザでも触れます。vLLM は内部で FastAPI を使っており、Swagger UI が /docs に自動マウントされているからです。
$ curl -s -o /dev/null -w "HTTP %{http_code} (%{content_type})\n" \
http://160.251.238.117/docs
HTTP 200 (text/html; charset=utf-8)
ブラウザで http://<サーバー IP>/docs を開くと、いつもの Swagger UI が出ます。
認証
VLLM_API_KEY を設定している場合、画面右上の Authorize ボタンに値を入れます。Bearer プレフィックスは Swagger 側が自動付与するので、トークン本体だけで OK。これで /v1/* の各エンドポイントを 「Try it out」 で対話的に試せます。
/v1/chat/completions を叩く
リクエストボディ:
{
"model": "default",
"messages": [{"role": "user", "content": "こんにちは。あなたは何ができますか?"}],
"max_tokens": 200,
"stream": false
}
Execute でちゃんと OpenAI 互換のフォーマットが返ります (choices[0].message.content, usage.prompt_tokens / completion_tokens 付き)。
SSE ストリーミングの確認
stream を true にすると、Server-Sent Events で逐次 chunk が返ります。Swagger UI 上で chunk が連続して表示されれば OK — 前述の Caddyfile の flush_interval -1 と @no_sse マッチャが効いている証拠です。逆に chunk が一気にまとめて出てきた場合は、reverse_proxy のバッファか gzip のいずれかが残ってしまっています。
このように smoke-test (CI 用) と Swagger UI (人間の検証用) の二段で動作確認できるのは、FastAPI ベースの vLLM ならではのメリットです。
認証なしで開いている公開エンドポイント
| パス | 認証 | 用途 |
|---|---|---|
/health |
不要 | liveness probe |
/docs |
UI 表示は不要 / 呼び出しは要 | Swagger UI |
/openapi.json |
不要 | OpenAPI 3.1 スキーマ |
/v1/* |
必要 (Bearer) | 推論エンドポイント |
/openapi.json は LangChain や TypeScript の OpenAPI クライアント生成にそのまま流せるので、フロントや別サービスから vLLM を呼ぶ場合の手間が減ります。
後片付け
conoha server delete <SERVER_ID> --yes
conoha volume delete <VOLUME_ID> --yes
conoha network sg delete <SG_ID> --yes
L4 は単価が高いので、検証で立てたら必ず畳みます。server delete だけでは volume / SG は別 API リソースとして残るので、それぞれ消す必要があります。
まとめ
- vLLM は OpenAI 互換 API + PagedAttention の LLM 推論サーバー
- ConoHa L4 GPU +
conoha-cliで 1 セッションでデプロイ可能 -
compose.yml+Caddyfileの 2 ファイルで HTTPS 終端 + SSE 配慮も済む -
--volumeフラグは在庫切れ時の orphan ボリューム再利用に効く地味な切り札 -
conoha app deployはconoha.yml不在のレシピで--no-proxy必須 - vLLM は FastAPI ベースなので
/docsでブラウザから対話検証できる
サンプル一式は crowdy/conoha-cli-app-samples の vllm-gpu にあります。