🖥️

完全無料で GPU 予約システムを作ってデプロイした話

に公開4件

はじめに

はじめまして、TransMedia Tech Lab に所属している B4 の山田 祐平です。

研究室で GPU サーバーを複数人で共有していると、こんな問題が起きがちです。

  • 「今誰が GPU 使ってる?」が分からない
  • Slack で毎回「今日の 15 時から GPU 使います!」と宣言するのが面倒
  • 予約がかぶって学習ジョブが落ちる悲劇

そこで、GPU の予約状況を可視化し、Webから簡単に予約できるシステム を作ることにしました。
また、研究室の予算は限られているため、完全無料 で運用できることを目指しました。

実際の画面は以下のようになっています。

システム構成図

今回構築したシステムの全体像です。

  • フロントエンド (React): Cloudflare Pages でホスティング(画面の表示)
  • バックエンド (FastAPI): Render.com でホスティング(データの処理)
  • データベース (PostgreSQL): Supabase でホスティング(データの保存)

これらすべて、各サービスの無料枠(Free Tier)の範囲内で運用しています。

技術スタック & 技術選定

レイヤー 技術 選定理由
フロントエンド React + Vite HMR (コード保存時に自動で画面が更新される機能)、シンプルな構成
バックエンド FastAPI + SQLModel Python 製でシンプル、型安全(変数の種類を明示してミスを防ぐ仕組み)
データベース PostgreSQL (Supabase) 無料枠あり、管理が楽
フロントホスティング Cloudflare Pages 無料、高速 CDN(世界中にサーバーを配置して近くから配信する仕組み)、デプロイが簡単
バックエンドホスティング Render.com 無料枠あり、GitHub 連携が楽
パッケージ管理 uv pip より爆速、ロックファイル対応

なぜ Vercel ではなく Cloudflare Pages?

Vercel も素晴らしいサービスですが、今回は以下の理由で Cloudflare Pages を選択しました。

  • 無料枠の制限が緩い: 商用利用の制限やビルド回数が Vercel より寛容
  • グローバル CDN: Cloudflare の強力なネットワークで、世界中どこからでも高速に表示される
  • シンプルな設定: 特別な設定ファイル (vercel.json など) なしで、Git に Push するだけでデプロイ可能

なぜ Supabase?

  • PostgreSQL をそのまま使える: 独自拡張が少なく、他の環境への移行もしやすい「普通の PostgreSQL」として使える
  • 無料枠が実用的: 500MB のストレージは、テキスト主体の予約データなら十分すぎる(1件の予約データは約0.5KB程度なので、100万件以上保存可能)
  • 接続プーリング対応: データベースへの接続を使い回して効率化する機能が標準でついており、サーバーレス環境(Render など)からの多数の接続をうまく捌いてくれる

使用技術の深掘り

FastAPI + SQLModel

FastAPI は近年人気の Python Web フレームワークです。「型ヒント」を使うことで、コードを書くだけで自動的に仕様書 ( Swagger UI ) が作られるのが最大の特徴です。

from fastapi import FastAPI
from sqlmodel import SQLModel, Field

# データベースのテーブル定義と、APIのデータ定義をこれ1つで兼ねる!
class GPU(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    model: str | None = None
    memory_gb: float | None = None

app = FastAPI()

@app.get("/api/gpus")
def get_gpus():
    # Pythonの関数を書くだけでAPIになる
    ...

SQLModel を使うと、通常は「データベース用」と「API用」で2回書かないといけない定義を1つのクラスでまとめて書けます。コードの重複が減り、「片方だけ修正して、もう片方を直し忘れる」というミスを防げます。

uv によるパッケージ管理

uv は Rust 製の非常に高速な Python パッケージマネージャーです。
従来の pip や poetry に代わるもので、インストールの待ち時間が劇的に短くなります。

# 依存関係を一瞬でインストール
uv sync

# 仮想環境を意識せずにスクリプトを実行
uv run main.py

pyproject.toml(プロジェクトの設定ファイル)で使うライブラリを管理し、誰が実行しても同じ環境が再現できるようにしています。

React + Vite

Vite (ヴィート) は、フロントエンドの開発ツールです。
コードを保存した瞬間にブラウザ上の画面が更新される HMR (Hot Module Replacement) という機能が非常に高速で、開発のストレスが全くありません。

// ReservationForm.jsx(予約フォームのコンポーネント)
// モダンな React の書き方でサクサク実装
const handleSubmit = async (e) => {
  e.preventDefault();
  await createReservation({
    gpu_id: selectedGpu,
    user: user,
    start_time: startTime,
    end_time: endTime,
  });
};

実装のポイント

今回のシステムの主要な実装部分をいくつか紹介します。

1. バックエンド: シンプルなAPI設計 (FastAPI + SQLModel)

FastAPI と SQLModel を組み合わせることで、非常に少ないコード量で DB 操作を含む API が実装できました。
例えば、予約を作成するエンドポイントは以下のようになります。

# backend/app/routes.py (予約作成のロジック)

@router.post("/reservations", response_model=ReservationRead)
def create_reservation(reservation: ReservationCreate, session: Session = Depends(get_session)):
    # 1. 重複チェック
    statement = select(Reservation).where(
        Reservation.gpu_id == reservation.gpu_id,
        Reservation.end_time > reservation.start_time,
        Reservation.start_time < reservation.end_time
    )
    if session.exec(statement).first():
        raise HTTPException(status_code=400, detail="この時間帯は既に予約されています")
    
    # 2. 予約の保存
    db_reservation = Reservation.from_orm(reservation)
    session.add(db_reservation)
    session.commit()
    session.refresh(db_reservation)
    return db_reservation

SQLModel のおかげで、ReservationCreate というバリデーション用のクラスと、DB保存用のモデルの変換が from_orm 一発で済むのが気持ちいいポイントです。

2. フロントエンド: 予約フォームの状態管理 (React)

フロントエンドでは、フォームの入力状態を useState で管理し、送信時に API を叩くだけのシンプルな構成にしました。
datetime-local 入力を ISO 形式に変換する処理などが地味に重要です。

// frontend/src/components/ReservationForm.jsx

const ReservationForm = ({ onReservationCreated }) => {
    const [formData, setFormData] = useState({
        gpu_id: '',
        user: '',
        purpose: '',
        start_time: '',
        end_time: '',
    });

    // ... (中略)

    const handleSubmit = async (e) => {
        e.preventDefault();
        
        // 日本時間を UTC ISO 文字列に変換して送信
        await createReservation({
            ...formData,
            gpu_id: parseInt(formData.gpu_id),
            start_time: new Date(formData.start_time).toISOString(),
            end_time: new Date(formData.end_time).toISOString(),
        });

        // 成功したら親コンポーネントに通知(リスト再読み込みのため)
        if (onReservationCreated) onReservationCreated();
    };

    return (
        <form onSubmit={handleSubmit}>
            {/* ... 入力フォーム ... */}
        </form>
    );
};

ハマったポイント

ここからが本題です。ローカルでは動いていたのに、デプロイしようとしたら動かない...という「デプロイあるある」なエラーとその解決策をまとめます。

1. ルートディレクトリの指定忘れ

エラー

error: No `pyproject.toml` found in current directory

(訳:設定ファイルが見つからないよ!)

原因
今回のプロジェクトは、1つのリポジトリの中に frontend/ フォルダと backend/ フォルダが入っている「モノレポ構成」でした。
しかし、Render はデフォルトでは 「一番上の階層(ルート)」 に pyproject.toml(Pythonの設定ファイル)があると思い込んで探してしまうため、エラーになっていました。

解決策
Render の Settings で Root Directory (作業場所)を backend に指定しました。
「一番上じゃなくて、backend フォルダの中を見てね」と教えてあげる設定です。

詳しい手順
  1. Render を開く
  2. Settings を開く
  3. Root Directory を backend に設定する

2. IPv6 接続エラー

エラー

sqlalchemy.exc.OperationalError: Network is unreachable

(接続先が IPv6 アドレス 2406:da14:... になっている)

原因
データベースの Supabase は最新の通信規格 IPv6 を使おうとしましたが、アプリを動かしている Render の一部のサーバーがまだ IPv6 に完全対応していませんでした。
「住所は分かってるけど、その道を通る手段がない」ような状態です。

解決策
Supabase には、従来の IPv4 でも接続できるようにする Connection Pooler という機能があります。
これを使うための設定(ポート番号 6543)に変更することで、Render からも無事に接続できるようになりました。

- postgres://user@db.xxx.supabase.co:5432/postgres
+ postgres://user@aws-0-region.pooler.supabase.com:6543/postgres
詳しい手順
  1. Supabase を開く
  2. 画面上部にある Connect を押す
  3. 赤枠の箇所を押す
  4. Transaction pooler を選択する
  5. 赤枠の値をコピーする
  6. Render を開く
  7. 画面左部にある Environment を開く
  8. 赤枠の箇所に 5 でコピーした値を貼り付ける
  9. Save する

3. API パスの不一致

エラー
フロントエンドからのリクエストがすべて 404 Not Found になる。

原因
PC間での手紙のやり取りで、宛先を間違えていたようなミスです。

  • フロントエンド:「/gpus さん、データください」
  • バックエンド:「私は /api/gpus で待ってます(/gpus なんて知らないよ)」

解決策
フロントエンドの通信用コードである api.js を修正し、必ず /api を付けてアクセスするようにしました。

// api.js (API通信を管理するファイル)
const API_URL = import.meta.env.VITE_API_URL || '';
const API_BASE = `${API_URL}/api`;  // ← 必ず /api を付けるように修正

4. render.yaml が無視される

現象
設定ファイル (render.yaml) を書き換えてコミットしたのに、Render 上の設定が変わらない。

原因
Render には 2 通りの作り方があります。

  1. Blueprint: render.yaml(インフラ構成を記述したファイル)で全自動管理
  2. Web Service: 画面上でポチポチ手動設定

今回は「2. 手動設定」で作っていたため、リポジトリ内の設定ファイルは完全に無視されていました。

解決策
Render ダッシュボードの Settings 画面から、Start Command を直接編集しました。

# データ投入(seed)をしてから、アプリを起動(main)する
uv run python -m app.seed && uv run main.py
詳しい手順
  1. Render を開く
  2. Settings を開く
  3. Build & Deploy セクションにある Start Command を以下のようにする

5. 無料プランのスリープ問題

現象
久しぶりにサイトを開くと、最初の表示に 1 分くらいかかる。

解説
これは Render の無料プランの 仕様(コールドスタート) です。
15 分間誰もアクセスしないと、サーバーは節約のために「スリープモード」に入ります。
叩き起こすのに少し時間がかかるため、最初の 1 人目だけ待たされてしまいます。

解決策(回避策)

  • UptimeRobot などの無料監視サービスを使って、5 分おきに自動でアクセスさせ、スリープを阻止する
    • ※ ただし、24 時間稼働させると月 744 時間を消費します。Render の無料枠は月 750 時間なので、同一アカウントで複数サービスを動かしている場合は枠を超える可能性があります
  • 「無料だし仕方ない」と割り切る

まとめ

今回、完全無料で GPU 予約システムを構築・デプロイしました。

学んだこと

  1. モノレポ構成の罠: ホスティングサービス側の「Root Directory」設定を忘れずに
  2. IPv6 の壁: クラウドサービス間の接続では、まだ IPv4 互換設定が必要なことがある
  3. 無料の力: Cloudflare Pages や Render の無料枠を組み合わせれば、十分実用的なアプリが作れる

今後の展望

  1. 認証機能: 研究室のメンバーだけが使えるようにログイン機能を付ける
  2. GPU 使用率の可視化: 単なる予約だけでなく、「今実際に動いているか」を nvidia-smi コマンドなどで取得して表示したい

学生エンジニアの方や、研究室の DX を進めたい方の参考になれば幸いです!

GitHubで編集を提案

Discussion

Hidden comment
junerjuner

pingを送ることで時間を消費していることは意識したほうがよさそう……?感

無料プランでは、アカウント内のすべての無料 Web サービスで 1 か月あたり 750 時間の実行時間と、無料サービスごとに 100 GB の送信帯域幅が許可されます。

https://zenn.dev/link/comments/9bd13388d71592

yuheiyuhei

ご指摘ありがとうございます!
確かに、24時間稼働させると月744時間となり、無料枠(750時間)をほぼ使い切りますね。
複数サービスを同一アカウントで運用している場合は特に注意が必要ですね。記事に補足を追加しようと思います。

2