4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Minecraft工業サーバ建立奮闘記

Last updated at Posted at 2024-10-29

更新

  • 2024年11月2日
    • Minecraftサーバの起動引数がおかしかったので修正
      • Log4Shellの対策設定がおかしかったので修正(-Dlog4j.configurationFile)
      • GC作動時の停止時間が0.2秒が長かったので短縮(MaxGCPauseMillis)
    • 修正前:
      ENTRYPOINT ["./run.sh", "-XX:+AlwaysPreTouch", "-XX:+DisableExplicitGC", "-XX:+ParallelRefProcEnabled", "-XX:+PerfDisableSharedMem", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseShenandoahGC", "-XX:+UseTransparentHugePages", "-XX:+UseNUMA", "-XX:InitiatingHeapOccupancyPercent=20", "-XX:MaxGCPauseMillis=200", "-XX:MaxTenuringThreshold=1", "-XX:SurvivorRatio=32", "-Xmx48G", "-Xms48G", "-jar", "-server", "-Dlog4j2_112-116.xml", "forge-1.12.2-14.23.5.2860.jar", "--nogui", "--universe", "/minecraft/."]
      
    • 修正後
      ENTRYPOINT ["./run.sh", "-XX:+AlwaysPreTouch", "-XX:+DisableExplicitGC", "-XX:+ParallelRefProcEnabled", "-XX:+PerfDisableSharedMem", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseShenandoahGC", "-XX:+UseTransparentHugePages", "-XX:+UseNUMA", "-XX:InitiatingHeapOccupancyPercent=20", "-XX:MaxGCPauseMillis=50", "-XX:MaxTenuringThreshold=1", "-XX:SurvivorRatio=32", "-Xmx48G", "-Xms48G", "-jar", "-server", "-Dlog4j.configurationFile=log4j2_112-116.xml", "forge-1.12.2-14.23.5.2860.jar", "--nogui", "--universe", "/minecraft/."]
      

きっかけ

友人に「Nomi CEU っていう Greg ベースのModpack やってみん?」と言われた。
それワタシがサーバを用意するやつよね?

Nomi CEuについてはこちら。

背景

E5-2697 v2とかDDR3 256GBとかがあったら流石にそうなる。
とりあえずポート開放が前提なので最低限やることやってかないとね。

構成

Proxmox VE8を動かしているので、その上にKVMとしてMinecraftサーバを建てる。
それだけだと面白味に欠けるので、Dockerの上でJVMを動かす。

マシン構成

↓構成はこんな感じか?
緑のgameserverがVM、紫枠のmc-forge-mainがDocker上のMinecraftサーバになる。

MC-Forge on Docker
gameserver VM resources
CPU 12 cores, 1 socket
RAM 64 GB
Storage 100 GB / SSD

OS/ミドルウェア

thrust2799@gameserver:~$ docker -v
Docker version 27.3.1, build ce12230
thrust2799@gameserver:~$ docker compose version
Docker Compose version v2.29.7
thrust2799@gameserver:~$ cat /etc/os-release
PRETTY_NAME="Ubuntu 24.04.1 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04.1 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo

構成の考え方

Docker上で動かすので、OpenJDKはDockerイメージに含めて環境が汚れないというメリットがある。また、インシデントが起こっても被害がコンテナ内に留まるのではないかという狙いもある。
さらに今回はdocker composeコマンドで全体を管理するので、デーモン化を含めて管理が非常に楽になる。
あとは、サーバにアクセスする人を制限したりしたいが...。

Velocity Serverが使えない!(敗北)

最近のマイクラにはVelocityというプロキシサーバがあり、これを公開して多段構成にしようとした。
Velocityは複数サーバ間で移動できるコマンドがあるので、まずmc-forge-lobbyサーバに入ってから権限がある人のみmc-forge-mainサーバに移動する構成を考えていた。
また、Velocity対応の権限集中管理ツールとしてLuckPermsがあるので、これも使いたかった。(サーバ間の権限同期にはRDBが使えるので、MariaDBも建てようと思っていた。)

ボツ案になってしまったマシン構成としてはこんな感じ。
ボツ構成

Velocityと連携できるのは、対応したプラグインサーバに加えForgeやFabricが導入されているサーバもある。
Forgeサーバで言うと、Minecraft 1.13以上だと専用Modで連携ができる。それ以下のバージョンだと Sponge ForgeというModを入れることで最低限は動くらしい。

今回入れるNomi CEuはMinecraft 1.12.2で動くForge用のModpack。
しかも、Sponge Forgeが微妙にサポートされていない

Sponge forgeを入れてもNomi CEuは動くけど、設定を変えたりしないといけない。
そのあたりはGithubのWikiページにまとまっている。

なお、LuckPermsについてはそもそもForgeに対応していない

結果

やりたいことが悉くできなかったので、諦めてMinecraft Forgeのサーバ1本で建てることにした。(その結果が最初の構成の話になる。)

Dockerfileで作るMinecraft Forgeなコンテナイメージ

まずはDockerfileでお手製のMinecraft Forgeなコンテナイメージを作成。ベースイメージはUbuntu 24.04を使っているが、多分busyboxとかでも行けるとは思う。
別の目的もあるためOpenJDKのイメージは使わない。(そもそも直近まで知らなかった。)

FROM ubuntu:noble

作成過程を試行錯誤した内容とともに振り返る。

パッケージ操作とcurl

RUN apt-get update \
    && apt-get install -y curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

この後Forgeのインストーラを取得するためにcurlを使うが、実はベースイメージにこのコマンドが含まれていないという。Ubuntu 24.04 LTS Serverには入っているので直観に反する。(n年ぶりm敗目)

なお、apt-get cleanrm -rf /var/lib/apt/lists/*はパッケージ操作で生じた不要ファイルを削除する目的で実行する。
Debian系のベースイメージを使うときはパターンとして入れるようにしている。

目的はインストーラの取得なので、curlの代わりにwgetを入れても良い。

Java環境をどうするかという問題 (with ShenandoahGC

COPY ./rh-openjdk /jre

今回サーバとして建てるベースはMinecraft 1.12.2なので、OpenJDK 8が必要になる。
通常なら先のパッケージ操作の段階でopenjdk-8-jre-headlessを入れるのだが、今回はRedHatがビルドしているOpenJDKを利用する。(ベースイメージがbusyboxでも良さそうな理由はここにある。)

というのも、OpenJDK 11.0.9から実装されているShenandoahGCを使いたくなったが、通常のOpenJDK 8ではもちろん使えない...。(1敗)
が、RedHat版ではバックポートされているらしい。ShenandoahGCは少ない停止時間で動くガベージコレクタとされているため、G1GCの代わりに使いたくなった。

RedHat版OpenJDK 8はダウンロードにRedHatのアカウントが必要となる。
アクセスが限られているので、このイメージは個人利用にとどめるべきだろう...。

ダウンロードしたJREはホストのrh-openjdkフォルダに展開した。このフォルダをコンテナイメージの/jreへコピーして利用する。

minecraftユーザと/minecraftフォルダ

RUN groupadd -g 500 -o minecraft
RUN useradd -u 500 -g 500 -M -o -r -s /sbin/nologin minecraft
RUN mkdir -p /minecraft/logs
RUN mkdir -p /minecraft/mods
RUN mkdir -p /minecraft/world && chown -R minecraft:minecraft /minecraft
USER minecraft:minecraft

WORKDIR /minecraft

このベストプラクティスでも示されている通り、コンテナ内とはいえrootユーザでプロセスを動かすのはリスクがある。そのためDockerイメージの作成においては専用ユーザと専用フォルダを作成し、それらを用いて動作するように構成を行う。

今回はユーザIDとグループIDが500:500minecraftユーザを作成する。この時、システムユーザかつ/sbin/nologinをログインシェルとして指定し、ホームディレクトリは作成しないでおく。(気休め)
また、/minecraftフォルダを作成し権限を与えておく。(logsmodsworldフォルダはMinecraft用の基本セットという意識で作成しているが、別になくても問題ない。)

最後にUSER命令とWORKDIR命令で作業ユーザと作業フォルダを変更しておく。

Forgeサーバをインストール

WORKDIR /minecraft
RUN curl -fOsSL https://maven.minecraftforge.net/net/minecraftforge/forge/1.12.2-14.23.5.2860/forge-1.12.2-14.23.5.2860-installer.jar \
    && /jre/bin/java -jar forge-1.12.2-14.23.5.2860-installer.jar --installServer
RUN curl -fSsSL https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml
RUN echo "eula=true" > ./eula.txt

今回はForgeを使ったMinecraftサーバを建てるため、ForgeインストーラのダウンロートURLを確認してコンテナ内で取得するよう構成する。ここで先ほど入れたcurlを使う。
インストーラの取得に併せてForgeサーバをインストールしておく。これでいつでもMinecraft + Forgeなサーバの準備が整う。

なお、ForgeインストーラはNomi CEuで指定されているバージョンをダウンロードする。

ここで、javaは先ほど展開した/jre配下のものを絶対パスで指定する。
間違えてjava -jar ...なんてするとパスが通ってないのでコンテナイメージのビルドに失敗する。

また、大騒動になったLog4Shellを回避するために公式が出しているXMLファイルを取得しておく。
Nomi CEuのサーバファイルにも含まれているが、多分どちらでもよい...? 教えて有識者

eula=trueをファイルに書き出しているが、これがないとサーバ起動に失敗する。
大体のMinecraftサーバ初回起動でつまずくポイントだが、今回はあらかじめ対応済みのコンテナイメージにすることで回避する。

Minecraftサーバなコンテナイメージを作って配布する人はeula.txtを削除して配布すべきである。なぜなら、このファイルをtrueにすることがその名の通りエンドユーザライセンスへの合意であるからだ。

エントリポイントに指定する起動用スクリプトの作成

RUN cp -r libraries libraries_orig
RUN echo '#!/bin/bash' > run.sh \
    && echo 'set -x' >> run.sh \
    && echo 'if [[ ! -e ./libraries/org ]]; then' >> run.sh \
    && echo '  cp -rf libraries_orig/* libraries/.' >> run.sh \
    && echo 'fi' >> run.sh \
    && echo '/jre/bin/java "$@"' >> run.sh
RUN chmod o+x run.sh

このバージョンのForgeはlibrariesファイルが生成される。この後docker composeコマンドでコンテナにマウントするホストディレクトリを指定していくが、ホストディレクトリをマウントするとマウント先に存在したコンテナ内のファイルが使えなくなる。
今回はコンテナ内のファイルを利用しなければいけないため、この部分に対処しなければいけない。別のMinecraftバージョンでやった時はこんなフォルダなかったのに...。(5敗)

回避策として、今回はイメージ内でlibraries_origへ移動しておくことにする。こうすることでdocker composeコマンドによってホストディレクトリがマウントされても削除されなくなる。
そして、この後起動用スクリプトでコピーしてこれば解決となる。

作った起動スクリプトはちゃんと動作するように実行権限を付与しておく。

シェルスクリプトについて

#!/bin/bash
set -x
if [[ ! -e ./libraries/org ]]; then
  cp -rf libraries_orig/* liberaries/.
fi
/jre/bin/java "$@"

突っ込みどころしかない部分。

  • [[ ! -e ./libraries/org ]]は正規表現を使っていないので[ ! -e ./libraries/org ]で良い
  • 条件文の-e(ファイル存在)は-d(ディレクトリ存在)の方が明示的
  • /jre/bin/java "$@"、ダブルクォートで囲ってなくて動作しなかった。(1敗)
  • /jre/bin/javaじゃなくてjavaにしたせいでコンテナ起動時にエラー(1敗)

この部分の方式について少し反省する点

librariesの扱いについて、コンテナイメージは最低限動作に必要なものが揃うのでコンテナイメージ側をベースにすべきだった。さらに、現状だとコンテナイメージ内のファイル内容がホスト側へ反映されるため「追加したファイルがどれかわからない!」という状態を引き起こしてしまう。

改善策としては、docker composeコマンドでlibraries_hostディレクトリとしてマウントしておき、起動用スクリプトでlibrariesに上書きする挙動が良さそう。

最後のほう

# RUN rm -f /minecraft/forge-1.12.2-14.23.5.2860-installer.jar /minecraft/forge-1.12.2-14.23.5.2860-installer.jar.log

EXPOSE 25565/tcp
STOPSIGNAL SIGTERM

ENTRYPOINT ["./run.sh", "-XX:+AlwaysPreTouch", "-XX:+DisableExplicitGC", "-XX:+ParallelRefProcEnabled", "-XX:+PerfDisableSharedMem", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseShenandoahGC", "-XX:+UseTransparentHugePages", "-XX:+UseNUMA", "-XX:InitiatingHeapOccupancyPercent=20", "-XX:MaxGCPauseMillis=50", "-XX:MaxTenuringThreshold=1", "-XX:SurvivorRatio=32", "-Xmx48G", "-Xms48G", "-jar", "-server", "-Dlog4j.configurationFile=log4j2_112-116.xml", "forge-1.12.2-14.23.5.2860.jar", "--nogui", "--universe", "/minecraft/."]
CMD ["--world", "world"]

コメントアウトしてるのはForgeインストーラとそのログを削除する行。最初は入れてたけどデバッグしてるうちにコメントアウトしていた。容量削減を目指すなら含めるべき。

EXPOSE命令でMinecraftサーバのデフォルトポートを開放する設定を入れている。ポート変更はDocker側で設定できるのでコンテナイメージであえて変更する必要はない。
SOTPSIGNAL命令でCtrl-Cを押したときにサーバを正常終了させれる方向に誘導する。

ENTRYPOINT命令で先に記載したスクリプトを指定しつつ、JVM引数とMinecraftサーバの引数を入れる。
ここで先ほど展開したXMLファイルの指定やガベージコレクタの指定、メモリ使用量の指定を行う。詳細は割愛。

CMD命令に--world引数を指定しているのは、ワールド名を変えられる余地を残すため。このあたりはお好み。

JVM引数をテキストファイルにまとめて@user_jvm_args.txtみたいな感じで指定するのはJava8ではできません!(1敗)
そのせいでENTRYPOINT命令がとんでもない長さになってる!

Dockerfile

最終的に出来上がったものはこちらになります。

FROM ubuntu:noble

RUN apt-get update \
    && apt-get install -y curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

COPY ./rh-openjdk /jre

RUN groupadd -g 500 -o minecraft
RUN useradd -u 500 -g 500 -M -o -r -s /sbin/nologin minecraft
RUN mkdir -p /minecraft/logs
RUN mkdir -p /minecraft/mods
RUN mkdir -p /minecraft/world && chown -R minecraft:minecraft /minecraft
USER minecraft:minecraft

WORKDIR /minecraft
RUN curl -fOsSL https://maven.minecraftforge.net/net/minecraftforge/forge/1.12.2-14.23.5.2860/forge-1.12.2-14.23.5.2860-installer.jar \
    && /jre/bin/java -jar forge-1.12.2-14.23.5.2860-installer.jar --installServer
RUN curl -fSsSL https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml
RUN echo "eula=true" > ./eula.txt
RUN cp -r libraries libraries_orig
RUN echo '#!/bin/bash' > run.sh \
    && echo 'set -x' >> run.sh \
    && echo 'if [[ ! -e ./libraries/org ]]; then' >> run.sh \
    && echo '  cp -rf libraries_orig/* libraries/.' >> run.sh \
    && echo 'fi' >> run.sh \
    && echo '/jre/bin/java "$@"' >> run.sh
RUN chmod o+x run.sh

# RUN rm -f /minecraft/forge-1.12.2-14.23.5.2860-installer.jar /minecraft/forge-1.12.2-14.23.5.2860-installer.jar.log

EXPOSE 25565/tcp
STOPSIGNAL SIGTERM

ENTRYPOINT ["./run.sh", "-XX:+AlwaysPreTouch", "-XX:+DisableExplicitGC", "-XX:+ParallelRefProcEnabled", "-XX:+PerfDisableSharedMem", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseShenandoahGC", "-XX:+UseTransparentHugePages", "-XX:+UseNUMA", "-XX:InitiatingHeapOccupancyPercent=20", "-XX:MaxGCPauseMillis=50", "-XX:MaxTenuringThreshold=1", "-XX:SurvivorRatio=32", "-Xmx48G", "-Xms48G", "-jar", "-server", "-Dlog4j.configurationFile=log4j2_112-116.xml", "forge-1.12.2-14.23.5.2860.jar", "--nogui", "--universe", "/minecraft/."]
CMD ["--world", "world"]

compose.yamlで作るNomi CEuサーバ

これでDockerイメージができるので、サーバを自動構成するためのcomposeファイルを作る。
ここでも試行錯誤した内容とともに振り返る。

ディレクトリツリー

ディレクトリ構成は最終的にこんな感じ。
「(空) or (多数)」になっているフォルダがあるのは、初回起動時は空にしておき本番起動時にデータを格納する運用のため。

/srv/minecraft/
├── compose.yaml
├── forge-12.2-14.23.5.2860
│   ├── Dockerfile
│   └── rh-openjdk
│       └── (略)
└── main
    ├── banned-ips.json
    ├── banned-players.json
    ├── backups
    │   └── (空) or (多数)
    ├── config
    │   └── (空) or (多数)
    ├── crash-reports
    │   └── (空) or (多数)
    ├── groovy
    │   └── (空) or (多数)
    ├── libraries
    │   └── (空) or (多数)
    ├── local
    │   └── (空) or (多数)
    ├── logs
    │   └── (空) or (多数)
    ├── mods
    │   └── (空) or (多数)
    ├── ops.json
    ├── resources
    │   └── (空) or (多数)
    ├── scripts
    │   └── (空) or (多数)
    ├── server-icon.png
    ├── server.properties
    ├── usercache.json
    └── whitelist.json

services定義

動かすコンテナを列挙し、それぞれの構成を記載していく。今回は1個のコンテナmainについて記載する。

コンテナイメージのビルドとコンテナの基本

services:
  main:
    build: ./forge-12.2-14.23.5.2860
    image: mc-forge:12.2-14.23.5.2860
    container_name: mc-forge-main
    hostname: mc-forge-main
    tty: true
    stdin_open: true
    user: 500:500
    # (略)

buildではコンテナイメージのビルド設定を記載する。この書き方だとビルドするDockerfileが存在するディレクトリを指定している。
先に作ったDockerfileとOpenJDKが入ったディレクトリを相対パスで指定する。

imageではコンテナ起動に用いるイメージ名を指定する。buildでイメージタグを指定していなければ、ビルドされるコンテナイメージはこのイメージ名/タグが設定される。

container_namehostnameでは個別のコンテナ名を指定する。
docker containerコマンドで指定する名前やDocker内でアクセスするときのホスト名に利用される。

tty: truestdin_open: truedocker runコマンドにおける-itに相当する。
今回Minecraftサーバをコンソールから操作できるようにあらかじめTTYと標準入力を有効化しておく。

userはコンテナの実行UID/GIDを指定する。
すでにDockerfileに指定しているが、こちらでも念のために記載する。

ホストファイルの連携

services:
  main:
    # (略)
    volumes:
      - type: bind
        source: ./main/backups
        target: /minecraft/backups
      - type: volume
        source: main_world
        target: /minecraft/world
      # (略)

ミソPoint。マウント方法によって挙動が異なるので、したいことと記述をしっかりと一致させる。「プログラムは書いた通りにしか動かない」と認識する。(連敗)

今回長い書き方(Long Syntax)をしている。1行で短く記載(Short Syntax)するとマウント方式を混同する要因になるので、面倒でも複数行に明示的に記載していくのが良さそう。

Bindマウント

ホストのフォルダを指定してマウントし、そこにデータを格納する。
コンテナ内で利用している状態そのままにホスト側に反映されるので管理に注意が必要となる。

パーミッションや所有権も同様にコンテナ内の状態がホスト側に染み出す。
例えば、コンテナ内のUID:GID500:500で動作する設定のため、コンテナを動かす際はホスト側で必ずchown -R 500:500 /srv/minecraft/main/*を実行する必要がある。

Volumeマウント

Docker側で確保する領域をマウントし、そこにデータを格納する。
最後のほうに記載するvolumesで領域設定を入れるので、そこに指定した名前を書く。

configsは使えないのか?

configsではコンテナで利用する構成ファイルを定義しDockerに管理を任せるもの。
例えば、server.propertiesファイルをconfigsに記載するとするとこのようになる。

services:
  main:
    configs:
      - source: server-properties
        target: /minecraft/server.properties
        uid: "500"
        gid: "500"
        mode: 0644
# (略)
configs:
  server-properties:
    file: ./files/create/server.propertie

この記載でポイントとなるのは、「UIDやGID、パーミッションの指定」と「そもそもconfigsの動作で良いのか」となる。

UIDやGID、パーミッションの指定

上記のように指定して起動すると起動時に以下のメッセージが表示される。
実際、これで動かそうとするとファイルがroot:rootで作成されるので、パーミッションエラーが出てサーバ起動に失敗する。(連敗)

WARN[0000] config `uid`, `gid` and `mode` are not supported, they will be ignored

一応公式ドキュメント上にはサポートされている記載があるが、実際には動いていないのが現状。

おかしいと思って古い環境(Docker Compose version V2.19.1)で確認したところ、ちゃんと動いていた。
どうやらバージョンアップに伴って何かしらあったようだ...。

そもそもconfigsの動作で良いのか

configsだと指定したファイルや環境変数などをコンテナ内に書き込む動作をする。Bindマウントと違うのは「コンテナ内で上書きされた際に、ホスト側へ反映されない」という点にある。
今回だと、ホストからマウントするファイルはほぼすべてで実行時に上書きされる可能性があるため、configsの動作は適さないことになる。

今回はサーバ側で動的に設定変更することを想定した。
server.propertiesops.jsonなどの設定を固定化したい場合は、むしろconfigsに設定すべきかもしれない。

ポート開放

Dockerfile内のEXPOSE命令で指定したポートをホスト外にも公開する設定。今回はポートのみ指定することで[::]:255650.0.0.0:25565といった形でポート開放する。

services:
  main:
    # (略)
    ports:
      - 25565:25565

volumes定義

volumes:
  main_world:

Minecraftのワールドデータ格納用のボリュームを定義する。ここで書いたボリューム名が先のVolumeマウントで指定されている。

何も指定していないためDocker側で適当に領域が生成されているが、NFSなどを指定することも可能らしい。

networks定義

networks:
  default:
    name: mc-servers

Docker内部の通信を構成する。(Velocity導入構想の名残)
設定しなくてもデフォルトで作成されるネットワークがある。
複数のネットワークを使いたい時やコンテナ同士で隔離したい時などに定義すると良い。

compose.yaml

最終的に出来上がったものはこちらになります。

services:
  main:
    build: ./forge-12.2-14.23.5.2860
    image: mc-forge:12.2-14.23.5.2860
    container_name: mc-forge-main
    hostname: mc-forge-main
    tty: true
    stdin_open: true
    user: 500:500
    volumes:
      - type: bind
        source: ./main/backups
        target: /minecraft/backups
      - type: volume
        source: main_world
        target: /minecraft/world
      - type: bind
        source: ./main/libraries
        target: /minecraft/libraries
      - type: bind
        source: ./main/logs
        target: /minecraft/logs
      - type: bind
        source: ./main/mods
        target: /minecraft/mods
      - type: bind
        source: ./main/config
        target: /minecraft/config
      - type: bind
        source: ./main/local
        target: /minecraft/local
      - type: bind
        source: ./main/groovy
        target: /minecraft/groovy
      - type: bind
        source: ./main/scripts
        target: /minecraft/scripts
      - type: bind
        source: ./main/resources
        target: /minecraft/resources
      - type: bind
        source: ./main/crash-reports
        target: /minecraft/crash-reports
      - type: bind
        source: ./main/banned-ips.json
        target: /minecraft/banned-ips.json
      - type: bind
        source: ./main/banned-players.json
        target: /minecraft/banned-players.json
      - type: bind
        source: ./main/ops.json
        target: /minecraft/ops.json
      - type: bind
        source: ./main/usercache.json
        target: /minecraft/usercache.json
      - type: bind
        source: ./main/whitelist.json
        target: /minecraft/whitelist.json
      - type: bind
        source: ./main/server-icon.png
        target: /minecraft/server-icon.png
      - type: bind
        source: ./main/server.properties
        target: /minecraft/server.properties
    ports:
      - 25565:25565
volumes:
  main_world:
networks:
  default:
    name: mc-servers

Minecraftサーバの起動

とりあえず初回起動ということで、この通りに空のフォルダ/ファイルを./main配下に作成してから起動する。

docker composeコマンドはカレントディレクトリのファイルを確認しに行く。
そのため、作成したcompose.yamlが存在するディレクトリで実行する。

thrust2799@gameserver:/srv/minecraft$ docker compose up

正常にワールドが生成されたら、一旦Ctrl+Cでサーバを閉じる。これでMinecraftサーバの起動に必要な最低限のものが揃うので、Nomi CEuの起動準備に入る。

詳細は省くが、./main配下に対してファイル/フォルダを配置する。配置後はここで触れた通り所有権の変更だけ確実に行う。
また、初回起動によりワールドデータが作成されてしまっているので削除しておく。

thrust2799@gameserver:/srv/minecraft$ docker volume ls
DRIVER    VOLUME NAME
local     minecraft_main_world
thrust2799@gameserver:/srv/minecraft$ docker volume rm minecraft_main_world
minecraft_main_world

次回以降の起動は末尾に-dを付けてデーモン化した状態で起動する。
管理はdocker attachコマンドやdocker logs -fコマンドで対応する。

thrust2799@gameserver:/srv/minecraft$ docker compose up -d # 起動時
thrust2799@gameserver:/srv/minecraft$ docker compose down  # 終了時

その他の設定

ここまででNomi CEuサーバはきちんと動作するが、どうせなら公開にあたってセキュリティ対策などを頑張っておきたい。
ということで、FWやウイルス対策ソフトなどを導入していく。

UFW

Ubuntu 24.04 LTSにはUFWがデフォルトで備わっている。
OSインストール時の状態だと無効化されているので、デフォルトポリシーと最低限の許可設定を投入して有効化しておく。

今回だと下記通り。< server ip ><local client pc ip>には適切なIPアドレスが表示される。

thrust2799@gameserver:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip

To                         Action      From
--                         ------      ----
<  server ip  > 22         ALLOW       <local client pc ip>
<  server ip  > 25565      ALLOW       Anywhere

clamav

LinuxにはClamAV(Clam Anti-Virus)というウイルス対策ソフトがあるので、これを入れる。

インストールと初期設定

インストールはいつものパッケージ操作で。

thrust2799@gameserver:~$ sudo apt install clamav clamav-daemon

インストール後はスキャン設定をしておく。
デフォルトだと全ディレクトリを検査するので、スキャン除外設定を入れておく。ここではLinuxの場合で一般的に入れておくべきものや、UbuntuやDockerに特有の除外項目を設定しておく。
また、スキャン時に利用するCPUコア数がデフォルトだとすべてになるので、Minecraftサーバが重くならないように念のため減らしておく。(今回だと12から8に減らす。)

ここでオンアクセス(リアルタイム)スキャンを有効化できるが、今回は負荷を考えて有効にはしない。

追加設定した行は次の通り。

ExcludePath ^/proc/
ExcludePath ^/sys/
ExcludePath ^/dev/
ExcludePath ^/snap/
ExcludePath ^/run/
ExcludePath ^/var/lib/clamav/quarantine/
ExcludePath ^/var/lib/docker/overlay2/
ExcludePath ^/var/lib/docker/volumes/backingFsBlockDev/
ExcludePath ^/var/snap/lxd/common/lxd/unix.socket
ExcludePath ^/var/snap/lxd/common/lxd-user/unix.socket
MaxThreads 8

/var/lib/clamav/quarantine/はウイルス検知後の隔離フォルダにするので、重複検知しないよう除外する。

定期スキャンの設定

このままだとスキャンはできるけど自動的に回してくれないので、1日1回はスキャンするように設定する必要がある。
そこで、大体の人はスキャンスクリプトを作ってcronジョブとして登録するが、せっかくSystemdがあるのでこちらを活用してみる。

スキャンスクリプト

ログ出力先とウイルス隔離先を定義して、ルートディレクトリ(/)配下をスキャンするようスクリプトにしている。
最後の2行は、存在するログからウイルスを検知した数をサマリとして書き出している(後で使う)。

  • /usr/local/bin/clamdscan-onschedule.sh
#!/bin/bash

SCAN_LOG_FILE=/var/log/clamav/clamdscan-`date +%Y%m%d`.log
QUARANTINE_DIR=/var/lib/clamav/quarantine/

/usr/bin/clamdscan -m --fdpass --log=$SCAN_LOG_FILE --move=$QUARANTINE_DIR /

SUM=$(grep Infected files: /var/log/clamav/clamdscan-*.log 2>/dev/null | awk '{s += $3} END {print s}')
echo `date +%Y/%m/%d`: $SUM infected files was found in this month. | tee /var/log/clamav/summary.txt
Systemd.ServiceとSystemd.Timer

Systemdでは/etc/systemd/system/配下にユニットファイルを作成すると管理対象として認識してくれる。
また、cronと同じようなものとしてSystemd.Timerという機能が存在している。.serviceファイルと同じ名前の.timerファイルを作成し設定を書き込むと有効なSystemd.Timerユニットとして認識される。

今回は1日1回スキャンスクリプトを回す(デーモンではない)ので、.serviceファイルのTypeにはonechotと指定しておく。
.timerファイルではカレンダー形式で開始時間を指定できる。cronよりも直観的に実行管理ができるが、もちろん、cronのような設定方法も存在している。
今回は毎日AM03:00に1回だけ実行するように記載する。

/etc/systemd/system/clamav-scan.service

[Unit]
Description = ClamAV OnSchedule Scan Service
RefuseManualStart = no
RefuseManualStop = yes
Requires = clamav-daemon.service
After = clamav-daemon.service

[Service]
Type = oneshot
ExecStart = /usr/local/bin/clamdscan-onschedule.sh
KillMode = process
KillSignal = SIGINT

[Install]
WantedBy = multi-user.target

/etc/systemd/system/clamav-scan.timer

[Unit]
Description = ClamAV OnSchedule Scan Timer

[Timer]
OnCalendar = *-*-* 03:00:00
Persistent = true

[Install]
WantedBy = timers.target

ファイルの作成が終わったら、最後にSystemdに各ユニットを認識させて有効化する。

thrust2799@gameserver:~$ sudo systemctl daemon-reload
thrust2799@gameserver:~$ sudo systemctl enable --now clamav-scan.timer

systemctl enable --nowで有効化と同時に起動ができるので便利。
systemctl enablesystemctl startを個別に実行しても同じように動く。

systemctl enableのみだと、次回OS起動時にスケジューリングされるだけで即時にスケジューリングしてくれない。ちゃんとsystemctl enable --nowとするかsystemctl startを実行するようにしよう。(1敗)

検知したことを知りたい

でもメールとか通知を送る仕組みは面倒。じゃあログインしたときにサマリが通知できれば良いよね!
ということで、先のサマリと隔離フォルダの内容を表示するように~/.bashrcに追記する。

thrust2799@gameserver:~$ tail -2 .bashrc
cat /var/log/clamav/summary.txt
ls -oa /var/lib/clamav/quarantine
thrust2799@gameserver:~$ . .bashrc
2024/10/29: 0 infected files was found in this month.
total 8
drwxr-xr-x 2 clamav 4096 Oct 29 02:51 .
drwxr-xr-x 3 clamav 4096 Oct 29 02:36 ..
thrust2799@gameserver:~$

いい感じ!

ログのローテーション

スキャンスクリプトを見返すと、このようなログ出力になっている。

SCAN_LOG_FILE=/var/log/clamav/clamdscan-`date +%Y%m%d`.log

これは、/var/log/clamav/配下にclamdscan-<YYYYMMDD>.logというファイルが毎日追加されることを意味している。
これではlogrotatedでも対応が難しいが、スキャン毎にログファイルは分けておきたい...。でも放置してると無限にログファイルが溜まっていく...。

ということで、この際ログの削除もSystemd.Timerで実現する。
実現することは下記通り。設定方法はスキャンスクリプトの時と同じなので詳細は割愛。

  • 毎月1日のAM00:10に前月分のログファイルを.tgzで固める
  • 同じくオリジナルの前月分ログファイルを削除する
  • 同じく13か月前のログファイルがあれば削除する

/usr/local/games/clamdscan-logarchive.sh

#!/bin/bash
set -x

tar -czvf ./clamdscan-`date --date '1 month ago' +%Y%m`.tgz ./clamdscan-`date --1 month ago +%Y%m`*
rm -f ./clamdscan-`date --date '1 month ago' +%Y%m`*.log
rm -f ./clamdscan-`date --date '13 month ago' +%Y%m`.tgz

/etc/systemd/system/clamav-scan.service

[Unit]
Description = ClamAV Scan Log Archiver
RefuseManualStart = no
RefuseManualStop = yes

[Service]
Type = oneshot
WorkingDirectory=/var/log/clamav
ExecStart = /usr/local/games/clamdscan-logarchive.sh
KillMode = process
KillSignal = SIGINT

[Install]
WantedBy = multi-user.target

/etc/systemd/system/clamav-scan.timer

[Unit]
Description = ClamAV Scan Log Archive Timer

[Timer]
OnCalendar = *-*-1 00:10:00
Persistent = true

[Install]
WantedBy = timers.target

サーバ側はこれでほぼ完了なのでは?

あとはルータ側で公開設定をするだけ。
どうせNAPT設定になっているので、ポート開放のついでにポート番号をランダムにずらして少しだけセキュアにすればいいかな?

この辺りの詳細は割愛。個別設定になるので自分のルータやネットワーク、ISPに応じて設定すれば良し!

友人からもインフラ力をお褒めいただけたので、今回はここまで。



P.S.
見た感じ敗北し続けてるな?

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?