サイトロゴ まいの雑記帳
Misskeyサーバー構築から爆破までのすべて

Misskeyサーバー構築から爆破までのすべて

投稿した日
2025/07/31
更新した日
2025/07/31
読了まで
43.12分で読み終われます (25,873文字)

はじめに

巷に溢れるMisskeyサーバーの構築記事、正直「うーん…」ってなるものが多くないですか?
xsnsなんかはそもそも話にならないとして、書かれているコマンドの意味もよくわからないままコピペするだけで、本当に大丈夫なのかなって、わたしは少し心配になってしまいます。
大した理解もないのに自信満々に記事を書けちゃうメンタル、どこから来るんでしょうね。

なので当エントリでは、サーバーの構築から、いつか来る爆破の時までを、私のわかる範囲でちゃんとマニュアル化してみようと思います。
ただ、手取り足取りって感じではないです。
「ちゃんと自分で調べて、理解してから触ろうね」というスタンスなので、その点だけご了承ください。

サーバーを用意する

Misskeyサーバーを構築するにあたり、まずそのアプリケーションが稼働するための計算基盤、すなわちサーバーを用意する必要があります。
そのアーキテクチャは、大別して二つの選択肢に集約されるでしょう。
物理的なサーバー実体を自らの管理下に置くか、専門事業者が提供する仮想化された計算資源を利用することです。

ここでまず、わたしの基本的なスタンスを明確にしておきます。 わたしはこれからMisskeyサーバーを運用しようとする方に対して、自宅サーバーという選択肢を絶対に推奨しません。

もちろん、技術的な探求や学習といった特定のコンテキストにおいては、自宅サーバーは依然として優れた選択肢です。
しかし、安定したサービス提供を目的とするならば、それは合理的な判断とは言えないでしょう。
物理的な障害対応や、見過ごされがちな電気代、そして何より人的な運用コストといったものを考慮すると、その経済的・時間的合理性は極めて限定的だからです。

ここでは、あなたが本来集中すべきアプリケーションの運用から乖離するような、不必要な複雑さからは距離を置きます。

ということで、いずれかのVPS事業者と契約することになります。(マネージドサービス系はお財布に優しくない)

正直なところ、特定の事業者を強く推奨したいのですが、現実問題として、どの事業者を選択しても何かしらの微妙なところが存在します。
そのため、ここでは比較的マシであり、特にこだわりがないのであれば大きな失敗には繋がりにくいであろう選択肢を挙げるに留めます。

  • さくらのVPS
  • Vultr

基本的には、何を契約しても構いません。

仮に公開サーバーとして運用するとしても、最初は2Core/2GiB程度のスペックを持ちインターネットへの疎通性のある月額1,500円前後のプランで十分です。
もちろん、これ以上のスペックがあればより快適ですが、最初から過剰な投資をする必要はありません。

また、契約時には必ずしもサーバーがグローバルなv4アドレスを持つプランである必要はありません。
Cloudflare Tunnelのようなものを用いれば、グローバルIPアドレスを持たない環境からでもサービスを公開することが可能です。たまに挙動が微妙なことがあるけど。

安定したサービス運用は、ある種のインフラ投資です。
無闇にコストを削ることが、結果として技術的負債や将来的な運用負荷に繋がる可能性を、常に念頭に置いておくべきでしょう。
あなたが合理的だと判断した事業者とプランを選択し、契約を進めてください。

今回は3Core/4GBの環境を利用しています。

Xserverの無料の子です。
検証用途であればきっと差し支えないでしょう。

ドメインを用意する

Misskeyのインスタンスを公開するにあたり、固有のドメインが必要になります。
既存のドメインがあり、サブドメインを利用する場合、この工程は飛ばして構いません。

ActivityPubの仕様上、ドメイン名を後から変更することができないので、ある程度は考えて選んでください。
.com.devといったgTLDであれば、基本的には何でも構いません。

ドメインはレジストラから取得しますが、どのレジストラを選ぶかは重要です。
わたしは、国内でよく名前が挙がる「お名前.com」のような事業者を絶対に推奨しません。

もし、あなたが特にこだわりがないのであれば、Cloudflare Registrar🔗をお勧めします。 Cloudflareはドメインを卸値で提供しており、余計な手数料がかからず安価です。
また、この後利用することになるCDNやセキュリティ機能との連携もスムーズで、管理画面もシンプルです。特別な理由がない限り、Cloudflareを選んでおけば間違いないでしょう。

Cloudflareのアカウントを作成し、希望のドメインを取得してください。

ドメインを取得できたら、Auto MinifyとRocket Loader™をすべて無効化しておきます。
この手順を飛ばすとMisskeyが正常に動作しない場合がありますので、必ず行ってください。

Spped > Optimization > Content Optimizationから設定が可能です。

Cloudflare Tunnelの下準備

今回の手順では、IPv6のみの環境など、より多くのケースで汎用的に利用できるようCloudflare Tunnel🔗を利用します。
これはCloudflareが提供するトンネリングサービスで、サーバーとCloudflareのデータセンター間を安全な経路で接続し、外部にポートを公開することなくウェブサイトを公開できるものです。
後述の手順でトークンが必要になるため、あらかじめ取得しておきます。

Cloudflareの管理画面は少し分かりにくいのですが、以下の手順で進めてください。

  1. Cloudflareのダッシュボードにログインし、サイドメニューからZero Trustを選択。
  1. Networks > Tunnels より Create a tunnelをクリック
  1. Cloudflaredを選択
  1. 任意の名称を入力し、Save Tunnelを押下
  1. トークンを控える

cloudflared.exe service install eyJhIjoiYThlODIxMWM2N...のような文字列がコピーできるので、eyJhIjoiYThlODIxMWM2N...のようなトークン部のみを控え、Nextを押下。

  1. FQDNとの紐付け

前項の手順で用意したドメインと、サービスを紐付けます。
このとき、Serviceは必ずhttp://app:3000となるようにしておいてください。

ちなみに、ここでServiceにループバックアドレスを指定しても疎通自体は可能です。
しかしながら、ループバックアドレスを設定してしまうとパフォーマンス上の問題が発生する可能性がでてきます。

Dockerは、起動時に各サービスが通信するための内部ネットワークを構築します。
このネットワーク内では、各コンテナにサービス名を使って直接アクセスできる、一種のDNS機能が提供されます。

Serviceの向き先として http://app:3000 を指定することで、CloudflaredのコンテナからMisskeyコンテナへの通信は、最適化されたDocker内部ネットワークで完結します。

一方、ループバックアドレスを指定した場合、通信は一度ホストOSのネットワークスタックを経由し、ポートマッピングの仕組みを介してコンテナへと届けられます。
リクエスト数が少ない場合、パフォーマンスへの影響は少ないのですが、大量のリクエストが発生した場合はdocker-proxyが大量のCPUリソースを消費してしまう現象があるようです。

無駄なことをするのは控えておきましょう。

Misskey本体の構築

サーバーとドメインの準備が整いました。
いよいよ、Misskey本体を構築していきます。

最新のイケイケな技術に惹かれる気持ちは、技術者であれば誰しもが抱くものかもしれません。
わたしも、その例外ではありません。
しかし、わたしたちの目的が技術的な探求ではなく、インスタンスの安定稼働であるならば、その技術選定はより現実的な視点で行う必要があります。

例えば、Kubernetesなんかがその典型例でしょう。
スケーラビリティがそこまで重要ではない小規模なインスタンスにおいて、Kubernetesの導入は無用な複雑さとオーバーヘッドを生み、管理コストを増大させるだけです。
期待するほどのメリットは、まず得られません。
新しい技術に挑戦する姿勢は評価できますが、目的と手段を見誤った技術選定は賢明と言えないでしょう。

結論から言えば、個人や小規模なコミュニティが運営するMisskeyインスタンスにおいて、その基盤はシングルノードDockerで十分である、とわたしは考えています。
できもしない無謀な試みをせず、身の丈に合った選択をしましょう。

ここでは、その思想に基づき、Dockerを利用してMisskeyを構築していきます。

Dockerの導入

契約したVPSにSSHを行い、以下のコマンドを実行します。

sudo apt install curl -y && curl https://get.docker.com/ | sudo sh -

現時点で最新のものです。

環境の用意

ディレクトリを作成し、必要なファイル類を作成します。

mkdir ./misskey ./misskey/config
cd ./misskey
touch ./compose.yaml ./config/.env
curl https://raw.githubusercontent.com/misskey-dev/misskey/refs/heads/develop/.config/docker_example.yml > ./config/default.yml

vimやらnanoやらを使って各ファイルを編集します。

./compose.yaml
services:
  app:
    image: misskey/misskey:latest
    restart: always
    links:
      - db
      - redis
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal_network
    volumes:
      - ./config/default.yml:/misskey/.config/default.yml:ro

  redis:
    restart: always
    image: eqalpha/keydb:alpine
    networks:
      - internal_network
    volumes:
      - ./data/keydb:/data
    healthcheck:
      test: "redis-cli ping"
      interval: 5s
      retries: 20

  db:
    restart: always
    image: groonga/pgroonga:latest-debian-16
    ports:
      - '5430:5432'
    networks:
      internal_network:
    env_file:
      - ./config/.env
    volumes:
      - ./data/db:/var/lib/postgresql/data
    healthcheck:
      test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
      interval: 5s
      retries: 20

  tunnel:
    restart: always
    image: cloudflare/cloudflared
    command: tunnel --no-autoupdate run
    profiles:
      - tunnel
    env_file:
      - ./config/.env
    networks:
      - internal_network

networks:
  internal_network:
./config/.env
POSTGRES_PASSWORD=データベースのパスワードとして利用する任意の英数字
POSTGRES_USER=misskey
POSTGRES_DB=mk1
DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}"

TUNNEL_TOKEN=Cloudflare Tunnelのトークン
./config/default.yml
#   ┌─────┐
#───┘ URL └─────────────────────────────────────────────────────

# Final accessible URL seen by a user.
# You can set url from an environment variable instead.
url: https://利用するドメイン/
./config/default.yml
#   ┌──────────────────────────┐
#───┘ PostgreSQL configuration └────────────────────────────────

db:
... 中略
  # Database name
  # You can set db from an environment variable instead.
  db: mk1

  # Auth
  # You can set user and pass from environment variables instead.
  user: misskey
  pass: データベースのパスワードとして利用する任意の英数字
./config/default.yml
#   ┌───────────────────────────────┐
#───┘ Fulltext search configuration └─────────────────────────────

# These are the setting items for the full-text search provider.
fulltextSearch:
... 中略
  provider: sqlPgroonga

./config/default.ymlはあまりにも長いので変更部位のみピックアップしています。

DBの準備

次のコマンドでデータベースの初期化と全文検索のためのPGroongaの有効化を行います。
これにはしばらく時間がかかります。

sudo docker compose run --rm app pnpm run init
sudo docker compose exec db psql -U misskey -d mk1 -c "create extension if not exists pgroonga;"
sudo docker compose exec db psql -U misskey -d mk1 -c "create index idx_note_text_with_pgroonga on note using pgroonga (text);"

起動

以下のコマンドでMisskeyを起動できます。

sudo docker compose --profile tunnel up -d

うまくいくとこんな感じ

ここでtunnelをprofileとして指定しているのは、misskeyを更新するたびにCloudflare Tunnelのコンテナまで再生成されないようにする意図があります。

設定

セットアップウィザード

これまでの手順が正しく完了していれば、取得したドメインにお手元のブラウザからアクセスすると、Misskeyの初期設定画面が表示されるはずです。

ここでは管理者ユーザーを作成するのですが、このアカウントは一般ユーザーとは扱いが異なり、削除等が通常できません。

あなたがこのサーバーを個人的なおひとり様サーバーとしてではなく、コミュニティとして運用するつもりなのであれば、ここで作成するアカウントを日常的に利用することは避けるべきでしょう。
普段使いのアカウントとは別に、管理作業のためだけの専用アカウント(例えば@adminといったユーザー)を作成することを、強く推奨します。

わたしはこれで後悔しました。

入力後、次を押下するとウィザードへ遷移します。

この項目をスキップすることもできますが、ここで片付けておきましょう。
基本的な設定はこれで完了します。

オブジェクトストレージ

Misskeyではドライブの格納先としてAWS S3または互換のオブジェクトストレージを使う設定ができます。
これを有効化しておかないと、files/ディレクトリ配下にファイルが蓄積されるのですが、運用上あまり好ましくないので、可能であればオブジェクトストレージの利用をしましょう。
ここではCloudflare R2を利用します。

Cloudflareのアカウントホームを開き、R2 Object Storage > Overviewを開き、新規bucketを作成します。

名称は何でもいいのですが、ここではmi-testとしています。

作成後、Settingsを開き、S3 APIのURLを控えておきます。
また、キャッシュを効かせるためカスタムドメインを追加します。

カスタムドメインは適当なサブドメインにでもしておきましょう。
ここではmedia-mi-test.mq1.devを指定しました。

R2のダッシュボードから、API > Manage API tokensを開きトークンを新規作成します。

権限はObject Read & Write、スコープを先ほど作成したbucketのみに絞っておきます。

Create Account API Tokenを押下し、Access Key IDとSecret Access Keyを控えておきます。

閉じてしまうと再表示できなくなるので注意してください。

Misskeyへ戻り、コントロールパネル > 設定 > オブジェクトストレージより、R2の情報を入力します

ここでの入力値は以下のものに対応します。

  • Base URL

    設定したカスタムドメイン(末尾に/を入れないでください)
    例: https://media-mi-test.mq1.dev

  • Bucket

    Bucket名
    例: mi-test

  • Prefix

    任意のPrefix名
    例: drive

  • Endpoint

    控えておいたS3 APIのURL
    例: https://a8e8211c674c2b00f3a8996b65b56447.r2.cloudflarestorage.com/mi-test

  • Region

    us-east-1

  • Access Key

    控えておいたAccess Key ID

  • Secret Key

    控えておいたSecret Access Key

すべての項目を入力し、保存を押下します。

メールサーバー

メールの配信を行うサーバーの設定を行います。
公開インスタンスとして運営するつもりであれば、必ず設定しておきましょう。

私は、鯖設立当初、SESを利用していましたが現在はResend🔗を利用しています。
メールの送信にTLSを強制している影響か、キャリアメール宛への送信がうまくいかない場合がありますが、ガラパゴスな仕様のサポートは切り落とすに限ります。

SMTPが利用できれば何でも構いません。

コントロールパネル > 設定 > メールサーバーより、SMTPのクレデンシャルを入力し保存します。

Resendを使う場合、必要なことはすべてここ🔗に書かれているので参照してください。
https://resend.com/docs/send-with-smtp🔗

ServiceWorker

通知の機能をユーザーが利用できるようにするため、ServiceWorkerの設定を行う必要があります。
以下のコマンドを、任意の環境で実行し、Public KeyとPrivate Keyを生成し、設定を行います。

sudo apt install nodejs npm -y && sudo npm install web-push -g && web-push generate-vapid-keys

コントロールパネル > 設定 > 全般 > ServiceWorkerより、生成したPublic KeyとPrivate Keyを入力します。

保存を押下し、完了です。

リレー

Misskeyにはリレーとよばれるサーバーによって投稿を中継配信する機能があります。

建てたての小規模インスタンスや、お一人様鯖であれば、GTLの流量をかさ増しするために設定することをおすすめします。
しかし、リレーに参加することでリレーからのノートが大量に発生するため、DBの容量などを考慮した上で設定を行ってください。

リレーサーバーはいくつかありますが、有名なものであれば。
このあたり🔗が参考になりそうです。
https://hisubway.online/blog/fediverse_relay/🔗

コントロールパネル > 設定 > リレーより追加が可能です。

Log IP address

有無を言わず有効化しておきましょう。

コントロールパネル > 設定 > セキュリティ > Log IP addressをEnable化するだけです。

ノート検索を許可する

デフォルトの状態では、ノート検索ができないようになっています。

今回構築した環境ではPGroongaと呼ばれる全文検索のためのPostgreSQLの拡張機能が導入されているので、ロールを変更するだけで高速な全文検索が利用可能です。

コントロールパネル > 管理 > ロール > ベースロールより、ノート検索の利用はいに変更します。

その他の諸々

ブランディングやBotプロテクションの設定など、Misskeyでは自由度が高い分弄れる項目がかなり多いです。
その他の細かい部分については、私が過去に書いたQiitaの記事🔗にも色々あるので、そちらも併せて参照してください。

ちなみに、2025年に入ってから行われたMisskey本体のアップデートでかなりの変更が入っているので、若干古い部分があったりします。許してください。

絵文字の追加

多くの人が知っているように、Misskeyにおけるコミュニケーションは、カスタム絵文字によってその豊かさと深さが定義されると言っても過言ではありません。
これは単なる装飾機能ではなく、リアクションやノートの文脈を補強し、時に言語以上に雄弁な非言語的コミュニケーションを可能にする、このプラットフォームの根幹を成す文化的なものです。

サーバー管理者は、この重要な資源を管理する権限を持ちます。

絵文字の管理は、コントロールパネル > 管理 > 絵文字より行います。

手動での追加

オリジナルの絵文字や、特にあなたのコミュニティで必要とされる特定の画像を追加する際の基本的な方法です。
カスタム絵文字の管理パネルを開き、右上の+から、画像をアップロードし、名前やカテゴリ、タグ、ライセンス等を割り当てます。

この過程ににおいて、いくつか留意しておくといい点があります。

ます、その絵文字に与える名前です。
この名前は絵文字ピッカーでの検索性に直結します。
わたしの運用例ですが、主要な名前をキャメルケース(例: shahuchan_always_watching_you)で統一し、エイリアスに、ひらがな、カタカナ、漢字といった様々な呼称とキーワードを複数登録しています。
登録の手間こそありますが、どのような単語で検索しても、目的の絵文字に辿り着ける可能性が高まり、しあわせになれるかもしれません。

そして、個々の絵文字の命名規則と同様に、全体を俯瞰した際の秩序を保つのがカテゴリ分類の役割です。
無秩序に追加された絵文字は、ピッカーの利便性を低下させますから、「リアクション」や「キャラクター」といった分類を設けるべきであると考えます。
さらにカテゴリが増えた際には、001_original, 002_blob/001_blobcatのように接頭詞として連番を割り当てたり、002_blob/001_blobcat, 002_blob/002_blobikaのように階層化したりすることで、ピッカー内での視認性と表示順を意図通りに制御できるでしょう。

最後に、これら全ての作業の前提として、権利者への配慮を忘れてはなりません。
安易な画像の使用が、様々なリスクに繋がる可能性を常に念頭に置いてください。

他インスタンスからのインポート

Misskeyでは、連合先に存在する観測された限りの絵文字をインポートする機能が備わっており、手動での追加とは対照的に、極めて効率的に思えます。
しかし、その手軽さは、あなたが予期しない種類の、そして極めて面倒なリスクを抱え込むことと表裏一体です。

あなたが他のインスタンスから絵文字をインポートする時、その絵文字のライセンスに関する一切の責任も同時にインポートしている、という事実を認識しなくてはなりません。
残念ながら、インターネット上に存在する多くの絵文字は、著作権を全く考慮されずに作成・登録されています。
アニメの一場面、企業のロゴ、ファンアート。
その出所は玉石混淆です。

あなたのサーバー単体で閉じていれば、それは大きな問題にはならないかもしれません。
しかし、あなたのサーバーが他のサーバーと連合を始めた瞬間、その絵文字は連合先のタイムラインにも表示されることになります。

そして、連合先のインスタンスには、時としてライセンスの扱いに極めて厳格な、気難しい連中が必ずと言っていいほど存在します。
彼ら彼女らは、あなたのサーバーのユーザーが付けた一つのリアクションからライセンスに反する絵文字を発見し、その出所であるあなたのサーバーを突き止め、そして、あなたに苦言を呈すでしょう。

このような外部からの指摘に対応する時間は、サーバー管理者にとって最も不毛な時間の一つです。
もちろん、ライセンスの問題に加え、ストレージやデータベースへの負荷、文化的な不一致といった技術的な問題も依然として存在します。

故に、インポート機能を利用する際は、その手軽さに惑わされてはなりません。
最も安全なのは、やはりあなた自身がライセンスをクリアできると確信した画像のみを、手動で追加していくことです。
遠回りに見えても、将来的な紛争の火種を自ら抱え込むよりは、遥かに賢明な選択と言えるでしょう。

ちょっとした小技

しかし、時に、一点ずつ手動で登録するのも、あるいは他のインスタンスからライセンスを確認しつつインポートするのも、煩雑で面倒だと感じることもあるでしょう。

そのような状況に対応するため、Misskeyには、特定の形式でパッケージ化されたZIPファイルから、絵文字を一括でインポートする機能が存在します。
これは、絵文字パックを配布・導入する際などに利用される、効率的な手段です。

パックのZIPファイルが要求する詳細な仕様については、各自で調べていただくとして、ここではいくつかの参考資料を提示するに留めます。

公式ドキュメント
カスタム絵文字の管理 一括インポート🔗

仕様解説といい感じのツール
Misskeyでカスタム絵文字を一気に入れる🔗
https://tools.e17.dev/emoji-manager/🔗
Misskey Emoji Archive Generator🔗

DBの定期バックアップ

この項目は、本エントリ全体で最も重要です。
もし、この先の内容を理解し、実行する自信がないのであれば、あなたは自前でMisskeyインスタンスを運用すべきではありません。
今すぐ、全てを諦めてお布団にはいりましょう。

万が一何かしらの問題が発生した場合、サーバーは再構築できます。
オブジェクトストレージに保存したファイルも、再設定すれば済む話です。
しかし、データベースだけは、失ってしまえば二度と元には戻りません。

そして、Misskeyが利用するActivityPubというプロトコルの仕様上、データベースに保存されている署名鍵を失うことは、そのドメインでサーバーを継続することの完全な終わりを意味します。
新しいサーバーを同じドメインで建て直しても、過去の投稿やフォロワーを引き継ぐことはできず、あなたのサーバーは連合ネットワークの中で完全に孤立した、全くの別物になってしまうのです。

何があっても、データベースだけは死守しなければなりません。

これは、脅しでも、単なる建前でもありません。
何を隠そう、わたしは過去に土砂崩れで旧自宅の半分とサーバー群を文字通り全て失い、ドメインを変えてゼロからサーバーを再構築した経験があります。
あの時の絶望感と、ユーザーに対する申し訳なさは、今でも忘れられません。

だからこそ、バックアップの重要性だけは、声を大にして、何度でも伝えたいのです。

バックアップの実行と自動化

バックアップを最もシンプルかつ確実な方法は、pg_dumpを用いたデータベースのフルバックアップを定期的に実行することです。
以下の様なスクリプトを用意し、cronで毎日深夜にでも実行する構成を推奨します。

backup.sh
#!/bin/sh

BACKUP_FILE="/misskey-data/backups/${POSTGRES_DB}_$(TZ='Asia/Tokyo' date +%Y-%m-%d_%H-%M).dump"
COMPRESSED="${BACKUP_FILE}.zst"

set -o errexit
set -o pipefail
set -o nounset

{
    # PostgreSQLのバックアップ
    pg_dump -Fc -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB > $BACKUP_FILE

    # ファイルが存在していれば圧縮
    if [ -f $BACKUP_FILE ]; then
        zstd -f $BACKUP_FILE
    else
        echo "Backup file not found"
        exit 1
    fi

    # ファイルが存在していればアップロード
    if [ -f $COMPRESSED ]; then
        rclone copy --s3-upload-cutoff=5000M --multi-thread-cutoff 5000M $COMPRESSED backup:${R2_PREFIX}
    else
        echo "Compressed file not found"
        exit 1
    fi

    echo "Backup succeeded"
} || {
    # 失敗時
    echo "Backup failed"
}

# バックアップファイルを削除
rm -rf $BACKUP_FILE
rm -rf $COMPRESSED

このスクリプトを、例えば /root/backup.sh として保存し、実行権限を与えます (chmod +x /root/backup.sh)。
次に、crontab -e コマンドでcronの設定ファイルを開き、以下の行を追記することで、毎日午前3時にスクリプトが自動実行されるようになります。

0 3 * * * /root/backup.sh

実行には以下の環境変数が必要です。

# postgres接続情報
POSTGRES_HOST=postgres
POSTGRES_USER=
POSTGRES_DB=mk1
PGPASSWORD=

# オブジェクトストレージ接続情報
RCLONE_CONFIG_BACKUP_ENDPOINT=
RCLONE_CONFIG_BACKUP_ACCESS_KEY_ID=
RCLONE_CONFIG_BACKUP_SECRET_ACCESS_KEY=
RCLONE_CONFIG_BACKUP_BUCKET_ACL=private

R2_PREFIX=backups

ちなみに、ホストOS上に直接cronを仕込み、シェルスクリプトを実行する、という素朴な手法も確実ではあります。
しかし、わたしはローカル環境にアプリケーションと管理タスクが混在する状態を、衛生的でない、ある種の気持ち悪さを感じます。
そのため、私がDockerで運用していた時代には、以下のリポジトリで提供されているコンテナを利用していました。

このコンテナイメージは、cronの機能とrcloneによるオフサイト転送機能を内包しており、環境変数をいくつか設定するだけで、バックアップからオフサイト転送までの一連のプロセスを、完全に独立したコンテナとして自動実行してくれます。

具体的には、既存のcompose.yamlに、このバックアップコンテナをサービスとして追記するだけで使えます。
ホストOSはDockerを動かすための最小限の環境に保たれ、アプリケーションに関する全ての責務をコンテナの世界に閉じ込めることができ、より気持ちのいい状態を実現できます。

使いたければ使ってください。

ダウン時にメンテナンスページを表示する

サーバーの運用において、アプリケーションの更新や予期せぬ障害によるサービスの停止は避けられません。
その際、利用者にCloudflareの無機質なエラーページを見せてしまうのは、管理者としてあまりにも不親切です。

ここでは、Misskey本体の前にリバースプロキシとしてCaddyを配置し、Misskeyがダウンしている場合には、あらかじめ別の場所に用意した静的なメンテナンスページへリダイレクトさせる構成を構築します。

本来、リバースプロキシはアプリケーションサーバーとは物理的に別のサーバーに設置するのが、可用性やセキュリティの観点から望ましいアーキテクチャです。
しかし、今回は個人や小規模なコミュニティでの運用を想定しているため、構成の簡潔さを優先し、同一のサーバー上に同居させる構成を採用します。

メンテナンスページ自体を外部サービスに置くことで、仮にサーバー全体が応答不能になった場合でも、ユーザーへの案内が可能になる、より堅牢な構成です。

メンテナンスページの作成

まず、メンテナンス中に表示するための簡単なページを作成します。
何も考えたくない場合はv0.dev🔗あたりを使うといいでしょう。

サイトが完成したら、VercelやCloudflare Pagesといった、任意の静的ホスティングサービスにデプロイし、そのURLを控えておきます。
このとき、Misskeyをホストしているドメインのサブドメイン(例: maintenance.mi-test.mq1.dev)に紐づけておくと、後々の管理が少し楽になります。

Caddyfileの作成

次に、リバースプロキシの動作を定義するCaddyの設定ファイルを作成します。./misskey/config/ ディレクトリ内に Caddyfile という名前で保存してください。

./config/Caddyfile
:3000 {
	encode gzip
	header /assets Cache-Control "public, max-age=31536000, immutable" 
	file_server

	reverse_proxy mi:3000 {
		# health check
		health_uri /api/server-info
		health_interval 10s
		health_timeout 2s
		health_status 200
	}

	handle_errors {
		@backend_down `{err.status_code} in [500, 501, 502, 503, 504, 522]`
		redir @backend_down メンテナンスページのURL
	}
}

メンテナンスページのURLの部分を、https://maintenance.mi-test.mq1.dev/ のような形式で先ほど用意したメンテナンスページのURLに書き換えてください。

compose.yamlの更新

これまでのcompose.yamlを、CaddyをMisskeyの前に置く構成に変更します。
Tunnel側の設定変更を避けるため、サービス名をapp(Caddy)とmi(Misskey)に変更し、依存関係を整理します。

./compose.yaml
services:
  app:
    image: caddy:2
    profiles:
      - proxy
    volumes:
      - ./config/Caddyfile:/etc/caddy/Caddyfile
      - ./data/caddy/data:/data
      - ./data/caddy/config:/config
    networks:
      - internal_network

  mi:
    image: misskey/misskey:latest
    restart: always
    links:
      - db
      - redis
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal_network
    volumes:
      - ./config/default.yml:/misskey/.config/default.yml:ro

  redis:
    restart: always
    image: eqalpha/keydb:alpine
    networks:
      - internal_network
    volumes:
      - ./data/keydb:/data
    healthcheck:
      test: "redis-cli ping"
      interval: 5s
      retries: 20

  db:
    restart: always
    image: groonga/pgroonga:latest-debian-16
    ports:
      - '5430:5432'
    networks:
      internal_network:
    env_file:
      - ./config/.env
    volumes:
      - ./data/db:/var/lib/postgresql/data
    healthcheck:
      test: "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"
      interval: 5s
      retries: 20

  tunnel:
    restart: always
    image: cloudflare/cloudflared
    command: tunnel --no-autoupdate run
    profiles:
      - tunnel
    env_file:
      - ./config/.env
    networks:
      - internal_network

networks:
  internal_network:

反映と動作確認

設定をすべて保存したら、以下のコマンドでコンテナを再起動します。
--remove-orphans オプションは、古い定義のコンテナを削除するために付与しています。

sudo docker compose --profile proxy --profile tunnel up -d --remove-orphans

正常にサイトが表示されることを確認後、sudo docker compose stop mi コマンドで意図的にMisskeyのコンテナを停止させてみてください。
ブラウザをリロードし、無機質なエラー画面の代わりに、先ほど作成したメンテナンスページへリダイレクトされれば設定は成功です。

確認後は sudo docker compose start mi で忘れずにコンテナを起動し直しておきましょう。

最低限のセキュリティ

サーバーを公開するということは、あなたの城を、悪意に満ちたインターネットの荒野に晒すということです。
城壁を築き、見張りを立てなければ、その城は一夜にして蹂躙されるでしょう。
セキュリティはオプションではなく、サーバー管理者の最も基本的な責務です。

ホストの対策

攻撃者が最初に狙うのは、あなたのサーバーそのものです。
OSレベルでの防御は、まず全ての入口を閉ざし、信頼できる経路のみを許可することから始まります。

従来、SSHポートを公開し、公開鍵認証で保護するのが一般的でした。
しかし、SSHポートをインターネットに晒すこと自体がリスクであり、鍵の管理も煩雑です。
そこで、当エントリではTailscale SSH🔗の利用を推奨します。
https://tailscale.com/kb/1193/tailscale-ssh🔗

Tailscale🔗は、あなたの所有するデバイス間だけで構成されるプライベートなネットワークを構築するサービスです。
これをサーバーに導入することで、SSHポートをインターネットに一切公開することなく、あなたのTailscaleネットワーク内部からのみ、安全にサーバーへアクセスできるようになります。
認証はTailscaleのアカウントで行われ、定義したACLが適用されるため、鍵管理の手間からも解放されます。

この構成を、ufwによって強制します。

# デフォルトですべてのインバウンドを拒否
sudo ufw default deny incoming
# デフォルトですべてのアウトバウンドを許可
sudo ufw default allow outgoing
# Tailscaleのネットワーク(100.64.0.0/10)からのみSSH(ポート22)への着信を許可
sudo ufw allow from 100.64.0.0/10 to any port 22
# ufwを有効化
sudo ufw enable

この設定により、あなたのサーバーのSSHポートは、もはや広大なインターネットからは完全に閉ざされたものとなります。

許可されたサブネット外からのインバウンド通信はすべて破棄されるため、ブルートフォース攻撃の脅威に晒されることはありません。
そのため、fail2banのような侵入防止ツールも基本的には不要になります。

Misskeyの管理者アカウントを保護

管理者アカウントの二要素認証を有効化してください。

これは、交渉の余地なく、必須の作業です。
あなたの管理者アカウントが乗っ取られることは、城の鍵を敵に渡すことと同義です。

Misskeyにログイン後、設定 > セキュリティ > 二要素認証より、今すぐ設定を行ってください。

WAF(おまけ)

そもそも、Cloudflare Tunnelを利用している時点で、あなたのサーバーのIPアドレスは秘匿されており、直接的な攻撃を受けるリスクは大幅に軽減されています。
さらに金銭的な余裕がある場合には、Cloudflareが提供する有償のセキュリティ機能を活用します。

Web Application Firewall (WAF)を有効化してください。
CloudflareのダッシュボードからSecurity > WAFを開き、Managed rulesタブからCloudflare Free Managed Rulesetを有効にします。
これは、SQLインジェクションやクロスサイトスクリプティングといった、ウェブアプリケーションに対する典型的な攻撃パターンを検知し、自動的にブロックしてくれる盾の役割を果たします。

サーバーの監視

インスタンスを建て、動かし始めたらそれで終わり、ではありません。
むしろ、そこからが本当の始まりです。
あなたのインスタンスが健全な状態を保っているか、あるいは何らかの脅威に晒されていないかを常に把握し、問題が発生した際には迅速に対応する。
そのための目となるのが監視です。

監視を設定するということは、問題が起きてからユーザーに指摘される受動的な対応から、問題が深刻化する前、あるいは利用者が気づく前に問題を検知する能動的な管理へと移行することを意味します。
そして、あなたの平穏な夜が、いつアラートによって叩き起こされるかもしれないという、新たなスリルを受け入れるということです。

ようこそ、眠れないサーバー管理者の世界へ。

外形監視

最も基本的で、そして最も重要なのが、外部からあなたのサーバーが正常に応答しているかを確認する「外形監視」です。

これには、高機能な監視SaaSであるBetter Stackの利用を推奨します。
無料プランでも十分な数の監視項目と、ステータスベージを作成できます。

ステータスページの例

Better Stackにサインアップし、最低限以下の項目を監視し、異常を検知した際にはあなたに通知が飛ぶように設定してください。

  • HTTP(s)監視

    MisskeyインスタンスのURL を定期的に監視します。

  • Ping監視
    サーバーへの基本的な疎通性を確認します。
    FWでICMPを拒否しないようにしておきましょう。

これにより、「急にインスタンスへのアクセスができなくなってしまった!!なにもしてないのに壊れました!!」という最も致命的な事態を即座に把握できます。

内部監視

外から見えていても、サーバー内部が悲鳴を上げていることは頻繁にあります。
CPU使用率、メモリ使用量、ディスクI/Oといった内部状況を把握するのが内部監視です。

ここではNew Relicの導入を推奨します。
無料プランの範囲が非常に広く、個人利用であればほとんどの機能を無償で利用できます。
他のものでも要件が満たせれば正直何でも構いません。

New Relicにサインアップし、表示される手順に従って、あなたのVPSに監視エージェントをインストールしてください。
Dockerコンテナの監視も自動的に認識され、美しいダッシュボードで各コンテナのパフォーマンスを詳細に可視化できます。

特に注意深く見るべきは、CPU使用率、メモリ使用量、そしてディスクの空き容量です。

ログ監視

メトリクスはサーバーのバイタルサインですが、ログはサーバーの声です。
アプリケーションで何が起きているのか、エラーの原因は何なのか、その全てはログに記録されています。

docker logs {cid}コマンドでログを確認することもできますが、それは一時的な確認手段に過ぎません。
恒久的なログの収集と分析のために、New Relicのログ転送機能を設定することを強く推奨します。

設定は、New Relicのドキュメントに従い、Docker用のログ転送設定をサーバーに追加するだけです。
これにより、Misskey本体やデータベースなど、全てのコンテナから出力されるログが自動的にNew Relicへ集約され、「特定のエラーログが一定数以上記録された場合に、アラートを発報させるー」みたいなことができます。

これらの監視を設定し、サーバーの状態を定常的に把握することが管理者としての最低限の責務です。

Misskeyをアップデートする

Misskey本体の更新頻度はかなり高く、月に1-3回のアップデートが行われています。
マイグレーション等、手動実行が必要なものがあればこのように🔗リリースノートに書いてあるので、そちらを参照してください。

一般的には以下のコマンドでアップデートが可能です。

sudo docker pull misskey/misskey:latest && sudo docker compose up -d --build

この時、profileを指定してしまうとメンテナンスページが表示されないので、必ず引数に--profileを含めないようにしてください。

人間を招く(Optional)

ここまでの手順は、いわば決定論的な世界の話でした。
正しく設定すれば、システムは期待通りに動きます。
しかし、人間を招き入れるという行為は、全く異なる、そして遥かに複雑で非合理な問題領域へと足を踏み入れることを意味します。

サーバーを建てる人間が抱きがちな、最も甘美な幻想の一つは、「まともな人間が集まるだろう」というものです。
しかし、現実は異なります。
多くの人間は、あなたが期待するような理知的で配慮のある振る舞いは決してしません。
むしろ、あなたの善意を食い潰し、絶え間ないストレスの源泉となりかねない存在です。
その事実を理解した上で、改めて選択肢を見てみましょう。

コントロールパネル > 設定 > モデレーションより、新規登録の受け入れ設定が可能です。

この点について、わたしは語りたいことが山ほどあるのですが、あまりにも長すぎるので興味があれば、わたしの盛大な失敗談を読んで笑ってやってください。
Misskey鯖缶後悔記🔗

あなたがこれから作るのは、あなたの時間と精神を注ぎ込む、あなたの城です。
その門の鍵を誰に渡すのかは、あなたが決めることです。

自身の選択を後々後悔することがないよう祈っています。

ユーザーが増えてしまった!どうしよう!?

おめでとうございます。そして、ご愁傷様です。
あなたの手で生まれた小さな世界に人々が定住し始め、サーバーは成長という新たな、そして厄介な段階を迎えました。
これは喜ばしいことであると同時に、あなたの平穏な日々との別れを意味します。

技術的な対処

サーバーのユーザー数が増加し始めると、運用は新たなフェーズに移行します。これはサーバーの成功を示す喜ばしい兆候であると同時に、これまでとは質の異なる、予測可能な技術的課題の始まりでもあります。
ここからは、たスケーリングのロードマップを、一般的?な指標🔗と共に解説します。

トータル300人規模の壁 (同時接続100人〜)

最初に直面するのは、多くの場合、単純なリソース不足です。
LTLが活発化するにつれて、サーバーの性能が追いつかなくなってきます。
この段階の最も標準的な対策は、スケールアップです。
最低でも2CPU / 8GBメモリ程度のスペックが推奨されます。

トータル700人規模の壁 (同時接続200人〜)

この規模になると、問題はより専門的になります。
単純なスケールアップだけでは解決できない、データベースのボトルネックが顕在化し始めます。

  • RDBの接続詰まり

    Misskeyの各ワーカーからの大量の接続要求に、PostgreSQLのデフォルト設定が耐えきれなくなります。
    この問題を解決するには、pgBouncerのようなコネクションプーラを導入する必要がありますが、その設定は非自明であり、かなりめんどくさいものです。

  • ジョブキューの詰まり

    サーバー内外への投稿を処理するInbox queueDeliver queueが詰まり、投稿が遅延するようになります。
    .config/default.ymlのワーカー設定の調整が有効です。
    clusterLimitの値を増やすことでワーカープロセスを増やし、処理能力を向上させることができますが、メモリ消費量もその倍数で増加します。
    サーバーが実際に処理できる能力と設定値を乖離させすぎると、スワップが発生してしまいます。

トータル1000人規模の壁 (同時接続300人〜)

データベースへの参照が遅くなり、APIの応答が全体的に緩慢になるという問題が発生します。
この段階では、データベースにリードレプリカを設置し、読み取り処理をそちらに分散させることで、プライマリデータベースの負荷を軽減するアーキテクチャが一般的です。
同時接続が400人を超えてくると、プライマリDB自体のスペックアップや、リードレプリカの追加増設も視野に入ってきます。

マネージドサービスへの移行

もしあなたが幸運にもインスタンスのマネタイズに成功したのであれば、これを機にGCP等のマネージドサービスへ完全に移行するのが、賢明な選択でしょう。

自前でコネクションプーラを管理し、リードレプリカを運用する煩雑さは、計り知れません。
その複雑な責務を、料金と引き換えに任意のベンダに丸投げすることで、あなたは本来集中すべきコミュニティの運営にリソースを割くことができます。
規模が大きくなったサーバーの管理は、わりと苦行でしかなさそうです。
しあわせになりましょう。

人間的な対処

技術的なスケーリングと並行して、あるいはそれ以上に重要となるのが、人間的な問題への対処、すなわちコミュニティのスケールアップです。

ユーザー数の増加は、必然的に利用者間の衝突、ルール違反、そして管理者への様々な要求の増大を招きます。
これらすべてに自分一人の時間と精神力で対応し続けることは、現実的に厳しいかと思います。

現に、多くの管理者が役目を終えるのは、単純な資金難等が理由ではなく終わりのない人間的な対応からくる精神的なストレスが原因である、という事実に留意しておくべきでしょう。

そうなる前に対策を講じる必要があります。
まずは信頼できる人間にモデレーター権限を委譲し、負荷を分散もとい押し付けましょう。
あなたのサーバーの文化をよく理解しているユーザーに声をかけ、権限を付与してください。

サーバー設立時に定めたはずのルールは、公平なモデレーションを行うための揺るぎない基準となります。
そのため、不備があればこの際まとめて改定しておくといいでしょう。

もし、なおコミュニティの成長速度があなたの管理能力を上回っていると感じたなら、迷わず新規登録を一時的に停止してしまいましょう。
登録を招待制に切り替えることで、問題の流入を止め、コミュニティを安定させるための時間を確保できます。

サーバーの成長は、あなたが単なる技術的な管理者から、コミュニティの秩序を設計し、維持する運営者へと役割を変えることを要求します。
残酷ですね。

トラブルシューティングの基本

サーバーが期待通りに動き続けると考えるのは、楽観的すぎる幻想です。

問題は必ず、そして多くの場合、最も都合の悪いときに発生します。
その際に重要なのは、パニックに陥らず、論理的に原因を切り分ける、体系的な考え方です。

まず、あなたのインスタンスにアクセスできなくなった時、外から内へと問題を切り分けていくのが基本です。

最初に確認すべきは、あなたのサーバーの外側、すなわちCloudflareです。
あなたが設定した外形監視が、おそらく最初の異常を知らせてくれているはずですが、Cloudflare自体のステータスページを確認し、サービスに障害が発生していないかを見ます。
次に、Zero Trustダッシュボードで、あなたのTunnelが正常に稼働しているものとして認識されているかを確認してください。

ここまでが正常であれば、問題はあなたのVPS内部にあると判断できます。

サーバーにSSHで接続したら、最初に行うべきはログを読むことではありません。
まずは、sudo docker ps コマンドを実行し、各コンテナが意図した通りに稼働しているか、その全体像を把握します。
今回の構成の場合、app, mi, db, redis、いずれかのコンテナがExitedRestartingといった異常な状態にないかを確認してください。

例えば、dbコンテナがUnhealthyであれば、問題の根源はデータベースにあると、この時点で大きく絞り込むことができます。

問題のあるコンテナを特定できて初めて、そのコンテナのログの確認に移ります。
sudo docker compose logs --tail 500 mi | grep -E 'ERROR|FATAL' のように、問題が疑われるサービスのログを追跡し、ERRORFATALといったキーワードを頼りに、何が起きているのかを読み解きます。
New Relicのようなログ集約基盤を導入していれば、過去のログを横断的に検索し、「このエラーはいつから発生しているのか」といった時間軸での分析もかなり楽になるでしょう。

もし、全てのコンテナが正常に稼働しているにも関わらずサイトが極端に遅い、といった場合は、ホストOS自体の健康状態を疑います。
df -hでディスクの空き容量を確認し(ストレージの逼迫はデータベースを停止させる致命的な原因です)、htopでCPUやメモリを異常に消費しているプロセスがないかを確認します。
あるいはdmesgで、カーネルがOOM Killerを発動させていないかといった、より低レイヤーの問題を探ります。

もちろん、あなたが設定したであろう内部監視は、これらの兆候をグラフとして明確に示しているはずです。

外形監視 → コンテナの状態 → ログ → ホストOSの健康状態。
この外から内へという一貫した流れで原因を絞り込んでいく思考こそが、トラブルシューティングの要諦です。

そして、問題の原因は、多くの場合、あなたが最後に行った変更の中にあります。
冷静に、一つずつ確認していくことが結局のところ一番早い解決方法でしょう。

サーバーを畳む

すべてのものには終わりがあります。
Misskeyインスタンスの運用も例外ではありません。

燃え尽き、時間的制約、あるいは金銭的な理由。
いずれ訪れるかもしれないその日のために、ここでは責任ある管理者として、サーバーの役割を正式に終わらせる爆破手順について解説します。

無言でサービスを停止するような夜逃げに等しい行いは、最も無責任で、これまであなたのサーバーを利用してくれたユーザーを裏切る行為です。
最後まで、管理者としての責務を全うしましょう。

告知

ユーザーが心の準備をし、自身のデータを退避させるための時間を十分に確保することが、倫理的なインスタンス爆破の大前提です。
最低でも1ヶ月、できれば2ヶ月前には、コントロールパネル > 管理 > お知らせから、全ユーザーに閉鎖の意向を告知してください。

新規登録の停止

告知と同時に、コントロールパネル > 設定 > モデレーションより、招待制に設定し、これ以上ユーザーが増えないようにします。

410 Gone

ActivityPubで連合しているインスタンスを閉鎖する際には、単にコンテナを停止するだけでは不十分です。
それは、連合していた他のインスタンスに対して、無駄な通信を永遠に試みさせる厄介な行為に他なりません。

あなたがサーバーを停止すると、あなたをフォローしていた、あるいは過去にあなたの投稿をRNしたサーバーは、あなたのサーバーが応答しないことを検知します。
しかし、それが一時的なメンテナンスなのか、恒久的な閉鎖なのかを判断できないため、健気にも何度も再接続を試み続けます。
この無駄な再試行が、他のサーバーのリソースを僅かながら、しかし確実に消費させ続けるのです。

この問題を解決し、各連合先に対して立つ鳥跡を濁さずの礼儀を尽くすのが、HTTPステータスコード 410 Gone です。
これは、「この場所は完全に、そして永久に消滅した」という明確な意思表示であり、これを受け取った他のサーバーは、あなたのサーバーへの通信を諦め、キューから削除してくれます。

設定は簡単です。
Caddyの設定を410を返すだけのシンプルなものに書き換えて、Caddyコンテナを再起動します。

./config/Caddyfile
:3000 {
	respond "This server is permanently gone." 410
}
sudo docker compose restart app

この状態で、最低でも数週間、可能であれば1ヶ月ほどサーバーを稼働させ続け、連合先のサーバーに閉鎖を周知させます。

注意点として、一度410を返したドメインに対して、他のサーバーは通信を恒久的に停止します。
もし将来、同じドメイン名でサーバーを再建する可能性が少しでもあるならば、この手順は慎重に検討しましょう。

サービスの完全停止

周知期間が十分に経過したら、Misskeyを構成する全てのリソースを完全に停止・削除します。
最後に、VPS等の契約している各種サービスを整理し、すべての工程が終了します。

お疲れ様でした。
これにて、あなたのインスタンスの一生は幕を閉じます。

サーバーはデジタルな塵と消え失せましたが、そこでの経験や繋がった人々との記憶は、あなたの内に残ります。

さいごに

ここまでお疲れ様でした。
サーバーの契約から始まり、ドメインの取得、Dockerによる構築、各種設定、そして運用と、いつか訪れるかもしれない爆破に至るまで、このマニュアルはMisskeyサーバー管理の一生を駆け足で巡るものでした。

このエントリを通じてわたしが一貫して伝えたかったのは、技術的な手順そのものよりも、その先にある「サーバーを運用し続ける」という行為の重みです。

技術的な問題は、時間をかければ、あるいはお金で殴ってしまえば、多くの場合解決できます。
しかし、あなたが本当に向き合うことになるのは、それ以上に複雑で、そして終わりなき人間という存在です。

あなたは今、自分だけの城を手にしました。
その城壁の中で何を守り、何を育み、そして何を拒絶するのか。
その全ての判断と責任は、あなた一人の肩にかかっています。

当エントリが少しでも道標となれば幸いです。

なお、このマニュアルはわたしの知見に基づくものであり、全ての情報を網羅できているわけではありません。
もし内容に不足している点や、「こういう内容も書き足してほしい」といった要望があれば、何らかの方法でご連絡ください。
今後の改訂の参考にさせていただきます。

それでは。

ブログの更新をお知らせ

RSSで購読すると新しい記事の投稿を知ることができます。