🧊

[はじめての開発]マイクラサーバの自動起動・サーバステータスページをGPT君と実装した話

に公開

1. 自動起動スクリプト実装背景

Linux上でMinecraftサーバを動かしているが、Linuxもセキュリティ対応などで再起動が必要になることがある。その際、現在はマイクラサーバを手動で停止してから再起動を行っているが、システム起動後にまた手動でサーバを起動しなければならないのが煩わしいと感じていた。
そこで、システム起動時にサーバを自動実行できないかと思い、GPT君に聞いてみた。

回答は、「簡単なスクリプトでできます」だった。
どうやらcronに@rebootという実行オプションがあるらしく、システム起動時に実行してくれるらしい。


2. サーバ自動起動スクリプトの実装

そこで、GPT君の協力で再起動スクリプトを完成させてみた。

#!/bin/bash

SESSION="minecraft"
SERVER_DIR="/home/ユーザ名/minecraft/minecraftsv"
JAR="paper.jar"
MEMORY="8G"
SCREEN_CMD="/usr/bin/screen"

cd "$SERVER_DIR"

# すでにscreenセッションが存在していないかチェック
if ! $SCREEN_CMD -list | grep -q "$SESSION"; then
    echo "[$(date)] Minecraftサーバを起動します..."
    $SCREEN_CMD -dmS "$SESSION" bash -c "java -Xms$MEMORY -Xmx$MEMORY -jar $JAR nogui; exec bash"
else
    echo "[$(date)] すでにMinecraftは起動中です。"
fi

うん、シンプルでかつ変更もしやすいスクリプトである。
これに権限を与え、cronに設定する。

chmod +x /home/ユーザ名/restartserver.sh

cronには以下を追記。

@reboot /home/ユーザ名/restartserver.sh >> /home/ユーザ名/restartserver.log 2>&1

これでシステムを再起動した際に、マイクラサーバが自動起動してくれるようになった。

※ちなみに当初GPT君と作ったスクリプトにはexec bashという記述がなく、前回実装した自動バックアップスクリプトを実行した際にscreenが消滅してしまい、再起動ができないという問題があったが、それに対処している。


3. サーバステータスページの実装

3-1. 実装背景

ここからが本題である。
現時点でサーバが無事に動いているか確認するにはSSH接続してscreenセッションに入り確認するか、ゲームクライアントを起動してサーバに接続できるか確認する必要があった。これは非常に煩わしい。なぜなら、ゲーム起動は外出先ではバッテリーを消耗するしファンが爆音を奏でるし、SSH接続はわざわざサーバに接続してscreenセッションを直接確認しなければならない。

※もっと確認しやすいサーバ構築方法もあったかもしれないが、今回はAPIを構築し、専用サイトでサーバのステータスを確認できるようにすることを目指す。

というわけでサーバのステータスページを実装してみたいと考えた。

3-2. 準備

さて、ステータスページを作りたいのだが、Webページは直接ホストしたくない。なぜならセキュリティとかHTTPS対応とかが大変だからだ。したがってフロントエンドにはGitHub Pagesを利用するとともに、バックエンドはNode.jsで実装する。

GPT君に聞いてみたところ、可能だとのことで以下の構成で進めることとした。

  • Web: GitHub Pages
  • API: Node.js

3-3. スクリプトを書く

HTML及びCSSは以前ちょっとだけ触っていたのであまり心配はしていなかったが、Node.jsは触れたことなかったのでまず何者なのかを調べるところからであった。

まず、サーバにNode.js及びMinecraftプロトコルを使用するためにインストールする。

sudo apt update
sudo apt install nodejs npm

ディレクトリmc-statusを作成し、ここにjsスクリプトや実行に必要なパッケージを保存する。

mkdir mc-status
cd mc-status
npm init -y
npm install express minecraft-server-util cors

サーバ側で動かすjsコード(index.js)を記述。
当初統合版から見た情報も取得しようとしていたため一部使用しないデータも含まれているがとりあえず放置している。

const express = require('express');
const cors = require('cors');
const { status, statusBedrock } = require('minecraft-server-util');

const app = express();
const PORT = 3000;

// CORS許可
app.use(cors());

// サーバの状態を返すAPI
app.get('/status', async (req, res) => {
  let javaData = { online: false };
  let bedrockData = { online: false };

  try {
    const result = await status('localhost', 25565);
    javaData = {
      online: true,
      players: result.players,
      motd: result.motd.clean,
      version: result.version.name,
      favicon: result.favicon || null,
    };
  } catch {}

  try {
    const result = await statusBedrock('localhost', 19132);
    bedrockData = {
      online: true,
      players: result.players || {},
      motd: typeof result.motd === 'string' ? result.motd : result.motd.clean,
    };
  } catch {}

  res.json({ java: javaData, bedrock: bedrockData });
});

const http = require('http');
http.createServer(app).listen(PORT, () => {
  console.log(`HTTPサーバ起動中: http://localhost:${PORT}/status`);
});

GitHub Pagesで動かすindex.htmlを記述する。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="icon" href="images/favicon/favicon.ico">
  <link rel="apple-touch-icon" sizes="180x180" href="images/favicon/apple-touch-icon-180x180.png">
  <link rel="icon" type="image/png" sizes="256x256" href="images/favicon/android-chrome-256x256.png">
  <title>MinakaMinecraft Server Status</title>
  <link href="style.css" rel="stylesheet">
</head>
<body>
  <h1>MINAKAMinecraft Server Status</h1>

  <div id="java-status" class="status">読み込み中...</div>

  <script>
    const apiUrl = 'リンクを記載';

    fetch(apiUrl)
      .then(response => response.json())
      .then(data => {
        const java = data.java;
        const javaDiv = document.getElementById('java-status');

        if (java.online) {
          javaDiv.innerHTML = `
            <p class="server-name">${java.motd}</p>
            <p class="online">🟢 オンライン</p>
            <img src="${java.favicon}" alt="サーバアイコン" class="server-icon">
            <p class="player-count">プレイヤー数: ${java.players.online} / ${java.players.max}</p>
            <p class="version">version: ${java.version}</p>
          `;
        } else {
          javaDiv.innerHTML = `
          <p class="offline">🔴 オフライン</p>
          `;
        }
      })
      .catch(error => {
        document.getElementById('java-status').innerHTML = '❌ 状態取得に失敗しました';
        console.error(error);
      });
  </script>


  <footer>
    <p class="copyright">&copy; 2025 水上イリス / ICCHAMA</p>
  </footer>
</body>
</html>

3-4. systemdを用いて起動する

/etc/systemd/system/mcstatus.serviceファイルを作成し有効化する。

[Unit]
Description=Minecraft Status API Server
After=network.target

[Service]
ExecStart=/usr/bin/node /home/ユーザ名/mc-status/index.js
WorkingDirectory=/home/ユーザ名/mc-status
Restart=always
RestartSec=5
User=ユーザ名
Environment=NODE_ENV=production
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

デーモンを読み込み有効化、開始

sudo systemctl daemon-reexec
sudo systemctl enable mcstatus
sudo systemctl start mcstatus

3-5. fetchにおけるHTTPS対応方針

これまでの手順で完成!のはずだったが、結局ここにぶち当たった。
GitHub Pages及びブラウザは、HTTPSを推奨している。したがって、fetchする側もされる側もHTTPSに対応しなければfetchが失敗するらしい。
最初はLet's Encryptで証明書を発行しようと試みたが、どうやらうまくいかなかったようでエラーになってしまった。
したがって代替案として提案された、Cloudflared Tunnelを用いることにした。

3-6. Cloudflared Tunnelを用いてHTTPSを実装する

Cloudflared Tunnelとは無料のHTTPSトンネルサービスでポート開放不要・任意のサブドメイン付与などの特徴があります。

3-6-1. 準備

まずサーバ側にインストールを行う。

wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared-linux-amd64.deb

同時にCloudflareのアカウント登録を行う。
登録後、サーバでアカウントにログインする。表示されるURLにアクセスする。

cloudflared login

3-6-2. トンネルを作成する

永続トンネルの作成を行う。(mcstatusはトンネル名)

cloudflared tunnel create mcstatus

トンネルの設定ファイルを作成する。(/home/ユーザ名/.cloudflared/config.yml)

tunnel: mcstatus
credentials-file: /home/ユーザ名/.cloudflared/"生成されるトンネルID".json

ingress:
  - hostname: mcstatus.自身のドメイン名
    service: http://localhost:3000
  - service: http_status:404

3-6-3. Cloudflareにドメインを登録する

次にCloudflareアカウントに自身のドメイン名を登録する。
以下の手順に沿って進めていく。

  1. 自身のDDNSドメインを登録する
  2. 所有権を証明するためDDNSサービス側のNSレコード欄に、Cloudflareから指定されたNSレコードを追加

以上の手順でCloudflare側から所有権が確認される。
時間がかかる場合もあるため、すぐに実行するボタンを推奨。

3-6-4. トンネル用のCNAMEを追加

登録が完了したらCNAMEを追加する。
以下の内容を記述。

タイプ: CNAME
名前: mcstatus
コンテンツ: "トンネルID".cfargotunnel.com
プロキシ:有効

※Aレコードは削除しない!また、マイクラサーバへの接続にAレコード情報を用いている場合はプロキシを無効にする

3-6-5. Cloudflared Tunnelをサービス化する

Cloudflared Tunnelをsystemdサービス化する。(/home/ユーザ名/systemd/system/cloudflared.service)

[Unit]
Description=Cloudflare Tunnel (mcstatus)
After=network.target mcstatus.service

[Service]
ExecStart=/usr/local/bin/cloudflared tunnel run mcstatus
Restart=on-failure
RestartSec=5
User=ユーザ名

[Install]
WantedBy=multi-user.target

サービスを適用する

sudo systemctl daemon-reload
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

※After欄にmcstatus.serviceと書いてあるが、これはNode.jsが起動した後にトンネルを確立するということである。

ちなみにこのサービスファイルにてExecStartの欄に、config.ymlをオプションに指定していたため、沼っていた。ここに指定はできないようだ。また、mcstatusとトンネル名も忘れず記載しなければいけない。

3-6-6. 動作確認

以上の作業を終え、以下のURLにブラウザでアクセスしてjsonが返れば正常である。

https://mcstatus."自身のドメイン名"/status

index.htmlのapiUrlにこのURLを設定すれば完了である。

3-7. [おまけ]GPT君にデザインをより良くしてもらった

自身でいい感じにしようと頑張ったものの、全然かっこいいページにならなかったのでCSSを書いてもらった。

@charset "utf-8";

body {
  font-family: 'Segoe UI', sans-serif;
  background: linear-gradient(to bottom, #cde7ec, #eaf6f8);
  margin: 0;
  padding: 0;
  text-align: center;
}

h1 {
  color: #ffffff;
  background-color: #2c8eb2;
  font-size: 36px;
  padding: 20px;
  margin: 0;
  border-bottom: 4px solid #1c657f;
  text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
}

.status {
  background: white;
  margin: 40px auto;
  padding: 30px;
  border-radius: 16px;
  box-shadow: 0 8px 16px rgba(0,0,0,0.1);
  max-width: 400px;
}

.online {
  color: green;
  font-size: 18px;
  font-weight: normal;
  margin-top: -10px;
  margin-bottom: 20px;
  opacity: 0.8;
}

.offline {
  color: red;
  font-weight: normal;
  font-size: 18px;
  margin-top: -10px;
  margin-bottom: 20px;
  opacity: 0.8;
}

.player-count {
  font-size: 20px;
  margin: 10px 0;
  font-weight: bold;
}

.server-name {
  font-size: 26px;
  font-weight: bold;
  color: #2e5de3;
  margin-bottom: 12px;
  text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
}

.version {
  font-size: 16px;
  color: #ff8a00;
  font-weight: bold;
  margin: 8px 0;
}

.server-icon {
  margin: 10px auto;
  width: 64px;
  height: 64px;
  border-radius: 8px;
  box-shadow: 0 2px 6px rgba(0,0,0,0.2);
}

footer {
  margin-top: 40px;
  padding: 10px;
  font-size: 12px;
  color: #666;
}

ちなみに、掲載したindex.htmlはかなり改良した後のものであり、サーバアイコンも取得し表示することができる。

完成したもの↓

4. まとめ

ステータスページの実装はかなりしんどかった。初めての開発である上、Cloudflared Tunnel等の初めて触れたツールを多数使用したため、思わぬ壁に何度もぶつかった。特に、systemdのサービスファイルでconfig.ymlを指定してトンネル名を指定し忘れていた時はサービスがすぐに落ちてしまい、ずっと接続エラーになっていた。
原因が分かるまでGPTと試行錯誤したが、向こうも万能ではなくコロコロと回答が変化するため、誤っているのかあっているのか分からずに作業することもあった。AI駆動開発と言っていいのか分からないが、初めての開発経験としてはまずまずのものが作成できたと思う。

今後も保守管理の利便性向上ツールを発案・実装していきたい。

Discussion