DockerでRomMを構築:自宅ラボ(HomeLab)向けのWebベース・レトロゲームライブラリを作成する

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

レトロゲームの散乱問題

複数のマシンでレトロゲームを管理していると、コレクションはあっという間に収拾がつかなくなります。私の場合、15年間で集まったROMファイルは1,800本以上、合計140 GBにまで膨れ上がっていました。それらは2台の外付けHDD、古いSynology NAS、そしてデスクトップPCのセカンダリNVMeドライブにバラバラに散らばっていたのです。ファイル名も整理されておらず、きれいなNo-Intro規約に沿ったものもあれば、2004年当時の謎のリリースタグが付いたままのものもあり、パッケージ画像やマニュアル、発売日などのメタデータはほとんど存在しない状態でした。

プレイするのも一苦労でした。クライアントマシンごとにエミュレータのフロントエンドが必要で、ノートPCごとにRetroArchのコントローラー設定を手動で行わなければなりませんでした。BIOSパスは頻繁に壊れ、リビングのミニPCと携帯ゲーム機の間でセーブデータが同期されることもありません。友人にコントローラーを渡して一緒に遊ぼうとするだけでも、ボタンの割り当てやオーディオドライバのトラブルシューティングに15分も費やす羽目になっていたのです。

私が求めていたのは、レトロゲーム版「Jellyfin」のような体験でした。unRAIDやUbuntuサーバー上でファイルを一元管理し、メタデータを自動でカタログ化して、ローカルにエミュレータをインストールすることなく、あらゆるブラウザ上で直接ゲームをストリーミングプレイできる環境です。

基本アーキテクチャ:RomMのエミュレーションの仕組み

RomM(ROM Manager)は、まさにその理想のワークフローを実現してくれます。FastAPIを採用したPythonベースで構築されており、レスポンシブなWebフロントエンドと、自動スクレイパー、ブラウザベースのエミュレータを巧みに組み合わせています。スタックを構成する4つのコアコンポーネントは以下のとおりです。

  • FastAPIバックエンド & データベース:RomMはMariaDBまたはPostgreSQLをバックエンドとする軽量なPython APIを実行します。データベースはROMのカタログ、スクレイピング状態、カスタムタグ、マルチユーザー認証を管理します。メモリフットプリントは小さく、アイドル時のRAM使用量は通常250 MB未満に収まります。
  • IGDB & MobyGames メタデータスクレイパー:スキャナーがROMファイル名を検査し、Twitchが提供するIGDB APIに問い合わせを行います。手動でタグ付けすることなく、高解像度のパッケージ画像、裏表紙、ゲームの説明、開発者情報、ジャンル、発売日を自動で取得します。
  • EmulatorJSによるクライアントレスエミュレーション:RomMはインターフェースにEmulatorJSを直接組み込んでいます。「Play」をクリックするだけで、RomMがROMと適切なlibretroコアをクライアントに直接ストリーミングします。ブラウザはWebAssemblyキャンバス内でゲームをローカル実行し、標準のブラウザAPIを介してコントローラー入力やオーディオを処理します。
  • 決定的なディレクトリSlug:RomMはフォルダ名によってコンソール機種を識別します。ディレクトリには、snes、gba、megadrive、psxのような標準化されたSlug識別子を使用する必要があります。ディレクトリ名がこの規約に沿っていれば、スキャン時に手動で仕分ける必要は一切ありません。

Docker Composeを使ったRomMのデプロイ

今回のデプロイでは、Ubuntu 24.04 LTSホスト上でRomMとMariaDB 11.4を組み合わせます。ファイルシステムのレイアウトを作成し、自動スクレイピング用のTwitch API認証情報を生成して、Docker Composeでスタックを起動します。

ステップ1:ホストディレクトリの準備と権限設定

アプリケーションの状態、データベースストレージ、ゲームライブラリ用のディレクトリ構造を作成します。

mkdir -p ~/homelab/romm/{config,database,resources}
mkdir -p ~/homelab/romm/library/roms/{snes,gba,megadrive,psx,n64}
mkdir -p ~/homelab/romm/library/bios
cd ~/homelab/romm

RomMはデフォルトでUID 1000として実行されます。カバー画像のダウンロード時やセーブデータの書き込み時にコンテナの権限エラーが発生しないよう、あらかじめストレージフォルダの所有権を設定しておきます。

chown -R 1000:1000 ~/homelab/romm/config ~/homelab/romm/resources

それぞれのサブフォルダにROMファイルを配置します。たとえば、ゲームボーイアドバンスの.gbaファイルはlibrary/roms/gba/に、スーパーファミコンの.sfcや.smcファイルはlibrary/roms/snes/に配置します。必要なBIOSイメージ(PS1用のscph5501.binなど)はlibrary/bios/に直接コピーしてください。

ステップ2:IGDB用のTwitch開発者認証情報を取得する

IGDBは充実したゲームメタデータを提供してくれますが、リクエストにはTwitchの開発者認証情報が必要です。

  1. 通常のTwitchアカウントでTwitch Developer Consoleにサインインします。
  2. Register Your Application(アプリケーションの登録)を選択します。
  3. わかりやすい名前(例:my-homelab-romm)を入力します。
  4. OAuth Redirect URLにhttp://localhostを設定します。
  5. Category(カテゴリ)でApplication Integrationを選択し、Create(作成)をクリックします。
  6. 表示されたClient IDをコピーし、New SecretをクリックしてClient Secretを生成します。

ステップ3:Composeスタックの定義

Webセッションの暗号化に使用する、32文字のランダムな認証キーを生成します。

openssl rand -hex 32

次に、生成したキーとTwitchの認証情報を反映してdocker-compose.ymlファイルを作成します。

services:
  romm-db:
    image: mariadb:11.4
    container_name: romm-db
    restart: unless-stopped
    environment:
      - MARIADB_ROOT_PASSWORD=SetAStrongRootPasswordHere456
      - MARIADB_DATABASE=romm
      - MARIADB_USER=romm_user
      - MARIADB_PASSWORD=SetAStrongDatabaseUserPassword123
    volumes:
      - ./database:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 5

  romm:
    image: rommapp/romm:latest
    container_name: romm
    restart: unless-stopped
    depends_on:
      romm-db:
        condition: service_healthy
    environment:
      - DB_HOST=romm-db
      - DB_NAME=romm
      - DB_USER=romm_user
      - DB_PASSWD=SetAStrongDatabaseUserPassword123
      - ROMM_AUTH_SECRET_KEY=YOUR_OPENSSL_HEX_KEY_HERE
      - IGDB_CLIENT_ID=YOUR_TWITCH_CLIENT_ID_HERE
      - IGDB_CLIENT_SECRET=YOUR_TWITCH_CLIENT_SECRET_HERE
    ports:
      - "8080:8080"
    volumes:
      - ./resources:/romm/resources
      - ./config:/romm/config
      - ./library/roms:/romm/library/roms
      - ./library/bios:/romm/library/bios

ステップ4:コンテナの起動と初回のスキャン実行

デタッチモードでデータベースとアプリケーションのコンテナを起動します。

docker compose up -d

起動ログを監視し、データベースのスキーママイグレーションが正常に実行されることを確認します。

docker logs -f romm

ログにApplication startup completeと表示されたら、ブラウザを開いてhttp://<サーバーのIPアドレス>:8080にアクセスします。初期設定画面が表示されるので、管理者ユーザー名とパスワードを設定してください。

ライブラリを登録するには、ナビゲーションバーのSettingsをクリックし、ScanからScan Libraryを選択します。RomMがディレクトリ内を走査し、ファイル拡張子を識別して、IGDBと照合しながらカバー画像をダウンロードします。一般的な4コアのホームサーバーであれば、350本のスーパーファミコンおよびGBAタイトルのインデックス処理は約90秒で完了します。

実践ノートとトラブルシューティング

Webベースのゲームサーバーを運用するにあたり、命名規則、ディスクフォーマット、ブラウザのセキュリティポリシーなど、いくつか特有の注意点があります。

  • No-IntroやRedumpの命名規則を使用する:RomMはファイル名の類似性スコアに基づいて照合を行います。The Legend of Zelda - A Link to the Past (USA).sfcのような名前のファイルであれば、高い信頼度でIGDBと即座にマッチします。一方、zelda_lttp_v1.smcのような適当にリネームされたダンプファイルではマッチングに失敗し、手動検索が必要になります。
  • 複数ディスクのタイトルはM3Uプレイリストに変換する:『メタルギアソリッド』や『ファイナルファンタジーVII』のようなPS1タイトルは複数枚のディスクで構成されています。ディスクイメージは圧縮された.chdファイルとしてゲーム専用フォルダに保存してください。そして、各ディスクのファイル名を1行ずつ記述した.m3uプレイリストファイルを同じ場所に作成します。RomMにはこの.m3uファイルを認識させます。これにより、ライブラリ内で重複して登録されるのを防ぎつつ、EmulatorJS内でスムーズなディスク交換が可能になります。
  • HTTPS対応のリバースプロキシ配下で配信する:近年のChromiumやWebKitベースのブラウザは、localhost以外の非保護HTTPオリジンにおいて、HTML5 Gamepad APIやFullscreen APIの動作を制限します。そのため、ローカルサブネット経由でRomMにHTTPアクセスすると、Bluetoothコントローラーが認識されない場合があります。Traefik、Caddy、あるいはNginx Proxy Managerなどのリバースプロキシの配下にRomMを配置し、Let’s Encryptやプライベートなstep-caによる内部TLS証明書を適用してください。
  • ブラウザのセーブデータと同期:ゲーム内のSRAMバッテリーセーブは、ブラウザのIndexedDBストレージ内に保持され、ゲーム終了時にRomMホスト側へ同期されます。ブラウザで強力なプライバシー拡張機能を使用していたり、タブを閉じた際にサイトストレージを自動消去する設定にしていると、同期が完了する前に進行状況が失われてしまう恐れがあります。uBlock OriginやPrivacy Badgerなどで、RomMのドメインをホワイトリストに登録しておきましょう。

次のステップ

これで、ネットワーク内の誰もが数秒で名作レトロゲームを楽しめる、すっきりとしたセルフホスト型Webゲームライブラリが完成しました。ここからは、AuthentikやKeycloakと連携したOIDCシングルサインオンの設定、databaseおよびresourcesフォルダの夜間自動バックアップのスケジューリング、あるいはHomepageなどのダッシュボードへのRomMの追加を検討してみるとよいでしょう。

Share: