https://blog.naskya.net/

[[ 🗃 ^wzWnj blog ]] :: [📥 Inbox] [📤 Outbox] [💥 Errbox] [🐤 Followers] [🤝 Collaborators] [🏗 Projects] [🛠 Commits]

Clone

HTTPS: git clone https://code.naskya.net/repos/wzWnj

SSH: git clone USERNAME@code.naskya.net:wzWnj

Branches

Tags

main :: content / post /

6kic0tebueju.md

{{< toc >}}

これはなに

これは Firefish Advent Calendar 2023 の 12/11 の記事です。

この記事では、私なりの Firefish サーバーの立て方を紹介します。必ず以下の注意点を念頭に置いて読んでください。

とはいえ、サーバーを立てている皆様やこれからサーバーを立てようと思っている方にとって少しは参考になるかもしれません。ゆっくりしていってね!

環境

立てるサーバーの環境はこんな感じです。

項目 内容
自分の手元のマシンの OS {{< note これはあまり重要ではないですが、環境によって VPS に接続するときに用いるコマンドなどが変わるかもしれません >}} Arch Linux
VPS ConoHa VPS
オブジェクトストレージ MinIO を同じサーバーに立てて使う
サーバーの OS Arch Linux
JavaScript ランタイム Node.js v21
データベース PostgreSQL v16
インメモリデータベース Redis v7 {{< note 他にも DragonflyDB, KeyDB などを使えるらしい。使ったことはない。 >}}{{< note Redis v4 でも動かせるらしい。どうやるのかは知らない。https://sudo.mkdir.uk/notes/9mgqy1z3viqs07y4 >}}
Web サーバー Caddy
サーバーへのアクセスのフィルター{{< note この手のソフトウェアをなんて呼ぶべきなのか分からない >}} nftables
ファイヤーウォール fail2ban
rustc に使わせるリンカー mold
インストールする Firefish のフレーバー https://code.naskya.net/naskya/firefish
全文検索エンジン PGroonga
バックアップの暗号化 GnuPG
バックアップの管理 rclone, systemd タイマー
パスワード管理ツール{{< note 多用するので何かしら使ってください >}} Bitwarden

Astrophysics の The First Sound of The Future Past{{< link https://astrophysicsbrazil.bandcamp.com/album/the-first-sound-of-the-future-past >}}{{< link https://open.spotify.com/album/7sNxOLcGgeSCWlTDnoeblU >}} を聴きながら作業します。

VPS の契約

VPS インスタンスの追加

日本の会社であることと、500 JPY/mo 程度でそこそこのスペックのインスタンスを使えることから ConoHa VPS を選びました。それ以外に理由は特にありません。もっと良いサービスを知っていたら教えてください!

ConoHa VPS にアクセスし、アカウントの登録などを済ませてから、ダッシュボードの左側にある「セキュリティ」フォルダー内にある「セキュリティグループ」を開きます{{< note この表示が無い場合には下にある「バージョン切替」から v3.0 のコントロールパネルに切り替えてください >}}。 セキュリティグループのボタン

するとこのような画面が出てくるので、 セキュリティグループの一覧の画面

右上のセキュリティグループ追加のボタンを押して Firefish 用のセキュリティグループを追加します。 セキュリティグループを追加する画面

すると「Firefish」というタブが一番上に出てくるので、それを開いて左下のプラスのマークのボタンから

を一つずつ追加します。通信方向は In, プロトコルは TCP とします。

セキュリティルールを追加する画面
セキュリティルールを追加する画面

最後の「好きな整数」は ssh 接続に使うポート番号で、1024 以上 49151 以下の整数を指定します。好きな整数といっても 5432 は PostgreSQL に、6379 は Redis に、9000 と 9001 は MinIO に使ってもらうことにするのでそれと被らないようにします。大きめの値にしておくとよいと思います。今回は 25252{{< link https://dic.pixiv.net/a/%E3%81%AB%E3%81%A3%E3%81%93%E3%81%AB%E3%81%A3%E3%81%93%E3%81%AB%E3%83%BC >}} としました。以下、25252 という値が出てきたら自分で設定した値に読み替えて解釈してください。

セキュリティルールを追加し終えた画面
セキュリティルールを追加し終えた画面

これにより、これら以外のポートへのアクセスは弾かれます。念のため後にフィルターを設定して同様に不正なアクセスを遮断しますが、ここでも設定をしているのは ConoHa VPS がもっと早い段階で(もっと低いレイヤーで)通信を弾いてくれるのではないかと期待してのことです{{< note そんな効果があるのかは知りません >}}。

80 番は http の通信に{{< link https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=http#table-service-names-port-numbers >}}、443 番は https の通信に{{< link https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=https#table-service-names-port-numbers >}}、25252 番は ssh の通信に使用します。ssh はこの後 IPv6 で接続するように設定するので{{< note 私自身は VPN を常用していて IPv6 の通信がよくブロックされるため IPv4 で接続しています >}}わざわざ IPv4 のぶんまで開ける必要がありません。

ssh のポート番号を変更するまではデフォルトの 22 番{{< link https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=ssh#table-service-names-port-numbers >}}で接続する必要があるため、そのための設定も入れています(後で 22 番の設定は削除します)。

セキュリティーグループを追加したら左上の「サーバー追加」のボタンから VPS インスタンスを契約します。

サーバーを追加するボタン
サーバーを追加するボタン

サービスは VPS を、イメージタイプは OS から Arch Linux を選びます。

サーバーを追加する画面(OS の選択)
サーバーを追加する画面(OS の選択)

メモリは頑張れば 1 GB でも動かせますが、 2 GB にしておくと心に余裕ができそうです。2 GB の契約にすればコア数も増えますし、12 か月契約にするとそこまで大きな価格差は無い{{< note 頑張って 12 か月続けようね >}}ので私は 3 vCPUs, 2 GB RAM, 100 GB SSD の 12 か月契約をしています。

root パスワードには英数字と記号からなる長くて複雑なものを設定し、パスワード管理ツールに記録させましょう。ネームタグにはお好きな名前をどうぞ。ここでは Firefish としました。

サーバーを追加する画面(スペックや root パスワードなどの設定)
サーバーを追加する画面(スペックや root パスワードなどの設定)

ssh 鍵の作成

ここで一旦手元のマシンのターミナルに戻り、ssh-keygen コマンドを用いて VPS に接続するための ssh 鍵を作ります。

ssh-keygen -t ed25519 -f ~/.ssh/conoha_vps

パスフレーズを設定してパスワードマネージャーに記録したり、YubiKeyNitrokey などに鍵を載せたりすると更によいです。既に好きな ssh 鍵の管理方法を確立している方はお好きなようにどうぞ。

ここでは鍵の名前を ~/.ssh/conoha_vps としました(ので、適宜読み替えて解釈してください)。

ssh 鍵を作った画面
ssh 鍵を作った画面

さて、ConoHa VPS のページに戻って作業を再開します。「オプションを見る」というタブを開いてさっき作った Firefish のセキュリティグループを選びます。

作った Firefish というセキュリティグループを選択する
作った Firefish というセキュリティグループを選択する

SSH Key は登録方法を「インポート」にして作った公開鍵を貼り付けて追加します。

cat ~/.ssh/conoha_vps.pub
作った公開鍵をターミナルから確認
作った公開鍵をターミナルから確認
作った公開鍵を ConoHa VPS の設定画面に貼り付け
作った公開鍵を ConoHa VPS の設定画面に貼り付け

それ以外の設定はいじらない(自動バックアップは無効、追加ストレージ・スタートアップスクリプトは使用しない)でよいでしょう。最初の 2 つはお金掛かっちゃうし。

最後に「次へ」を押して支払い情報などの設定をします。学生だとちょっと安くできたりするかもしれません{{< note 私は学生なのに学割を適用し損ねた😭 >}}。12 か月分を一気に払うのでいいお値段になってしまいます。

VPS を追加するボタン
VPS を追加するボタン

それからしばらく(数分は掛かるかも!)待つと、VPS にアクセスできるようになります。

サーバーリストの画面に Firefish という項目ができている
サーバーリストの画面に Firefish という項目ができている

ネームタグをクリックして、VPS 設定のフォルダーを開いて削除ロックを有効にします。

削除ロックを ON に設定する画面
削除ロックを ON に設定する画面

これで VPS の準備は完了です🎉

VPS の基本設定

VPS への ssh 接続

「ネットワーク情報」のタブを開いて一番上の IPv6 アドレスをコピーします。

立てた VPS インスタンスに割り当てられた IPv6 アドレスをクリップボードにコピーしている
立てた VPS インスタンスに割り当てられた IPv6 アドレスをクリップボードにコピーしている

ここではコピーした IPv6 アドレスを仮に 2400:8500:1001:2002:3003:4004:5005:6006 とするので、以降はあなたの得たアドレスに読み替えて解釈してください。

まずは、さっき作った鍵を用いて root アカウントで VPS に接続します。

ssh -i ~/.ssh/conoha_vps root@2400:8500:1001:2002:3003:4004:5005:6006 
The authenticity of host '2400:8500:1001:2002:3003:4004:5005:6006 (2400:8500:1001:2002:3003:4004:5005:6006)' can't be established.
ED25519 key fingerprint is SHA256:1oMiR9+uvPAsC/iTsb+YGEMXxBOWnYNu6WHW5OFWaNM.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

のように訊かれるので yes と答え、設定したパスフレーズを入力するなりして VPS に接続します。

適当に whoami などのコマンドを入力すると root と返ってきます。わーい!

立てた VPS インスタンスに ssh 接続できた
立てた VPS インスタンスに ssh 接続できた

tmpfs の無効化

さて、df を実行して記憶域の使用状況を確認してみると、どうやら /tmp というフォルダには tmpfs というファイルシステムが使用されているらしいことが分かります。

VPS でディスク使用量を確認するコマンドを実行した
VPS でディスク使用量を確認するコマンドを実行した

tmpfsはUnix系OSにおける一時ファイルのためのファイルシステム名。tmpfsはファイルシステムとしてマウントされることを意図しており、これによりHDDをはじめとする永続性をもつ記憶装置の代わりに揮発性メモリに保存されるようにできる。RAMディスク(仮想的なディスクドライブ)とは異なり内部に別のファイルシステムを作成せずに使用することができる。

tmpfs - Wikipedia (https://ja.wikipedia.org/wiki/Tmpfs), 2023/12/04 版

そう、/tmp ディレクトリに書き込むとなけなしの計 2 GB のメモリの一部が使われてしまうのです!そこで、おまじない程度の省メモリ効果を期待して tmpfs は使わせないようにします。

ちなみに、Arch Linux 以外の多くのディストリビューション(例えば Ubuntu とか)はこのような仕様になっていません。systemd の元々の挙動では tmpfs を使用するので、他の systemd ベースの多くのディストリビューションではカスタムが行われているということでしょう{{< link https://www.kofuk.org/blog/20230128-ubuntu-tmp/ >}}。

ArchWiki の記述{{< link https://wiki.archlinux.jp/index.php/Tmpfs#.E8.87.AA.E5.8B.95.E3.83.9E.E3.82.A6.E3.83.B3.E3.83.88.E3.81.AE.E7.84.A1.E5.8A.B9.E5.8C.96 >}}に従い、以下のようにします。

systemctl mask tmp.mount
nano /etc/tmpfiles.d/tmp.conf
reboot now

/etc/tmpfiles.d/tmp.conf には以下の内容を貼り付けます。

# see tmpfiles.d(5)
# always enable /tmp folder cleaning
D! /tmp 1777 root root 0

# remove files in /var/tmp older than 10 days
D /var/tmp 1777 root root 10d

# namespace mountpoints (PrivateTmp=yes) are excluded from removal
x /tmp/systemd-private-*
x /var/tmp/systemd-private-*
X /tmp/systemd-private-*/tmp
X /var/tmp/systemd-private-*/tmp

以下では vim をエディターとして使いますが{{< note nano がお好みなら下記の "vim" を全て "nano" と読み替えてください。 >}}、nano が入っていることは必ず確認しておいてください(nano /etc/tmpfiles.d/tmp.conf ができたのなら大丈夫)。なぜなら ssh 接続がうまくいかずに ConoHa VPS のコントロールパネルのコンソールから VPS を操作することになったとき、vim で使う : などのキーがうまく入力できなくて困るからです{{< note これは私が少々特殊な配列のキーボードを使用しているからなのでしょうか……?でも「テキスト送信」の機能を使ってもコロンがちゃんと入力できないんですよね……どうして…… >}}。

システムの完全アップグレード

再起動には 1 分間も掛からないはずです。さっきと同じコマンドで再度 ssh 接続をしたら、まず /etc/pacman.conf を編集し、その後システムをアップグレードします。

vim /etc/pacman.conf

デフォルトでは

# Misc options
#UseSyslog
#Color
#NoProgressBar
CheckSpace
#VerbosePkgLists
#ParallelDownloads = 5

となっている部分がありますが、ColorVerbosePkgListsParallelDownloads のコメントアウトを外して ParallelDownloads の右辺の値をもっと大きくしておきます。以下、このように行頭の # を消してコメントアウトを解除することを「アンコメントする」と表現します{{< note これを表すのに使える和語または漢語を募集中です >}}。

# Misc options
#UseSyslog
Color
#NoProgressBar
CheckSpace
VerbosePkgLists
ParallelDownloads = 20

以上の操作が終わったらシステムをアップグレードしてまた再起動です。初期の状態では sudo コマンドすらインストールされていないので、ついでに base-devel パッケージもインストールしておきましょう。

pacman -Sy archlinux-keyring
pacman -Syu base-devel
reboot now

ちなみに pacman -Sy は危険な操作なので{{< link https://wiki.archlinux.jp/index.php/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%83%A1%E3%83%B3%E3%83%86%E3%83%8A%E3%83%B3%E3%82%B9#.E9.83.A8.E5.88.86.E7.9A.84.E3.81.AA.E3.82.A2.E3.83.83.E3.83.97.E3.82.B0.E3.83.AC.E3.83.BC.E3.83.89.E3.81.AF.E3.82.B5.E3.83.9D.E3.83.BC.E3.83.88.E3.81.95.E3.82.8C.E3.81.A6.E3.81.84.E3.81.BE.E3.81.9B.E3.82.93 >}}普段は使ってはいけません(今は数少ない pacman -Sy の使いどころです)。

作業用ユーザーの作成

再起動を終えてまた VPS に ssh 接続できたら、sudo コマンドが入ったので作業用のユーザーを作って root ユーザーで作業するのをやめましょう。まず、以下のコマンドで作業用のユーザーを作ります。naskya の部分は好きなユーザー名に変えて{{< note 絶対に変えてください!面倒くさいからというだけの理由で他人に私のハンドルネームを使われたら嫌です(記事を書き終えてからここの部分は別の名前にしておけばよかったと後悔しました……) >}}ください。

以下、naskya というユーザー名が出てきたら自分が設定したユーザー名に読み替えてください{{< note 出てくるコマンドを何も考えずにコピペすることはせず、ちゃんとコマンドを読んで置換すべき場所を探したりそのコマンドが何をするのか理解するように努めてください >}}。

useradd --create-home --groups wheel naskya

次に、作ったユーザーのパスワードを設定します。このパスワードは英数字(記号を含まない)で構成された長い(100 文字以上など)ものにすることをおすすめします。

passwd naskya

こんな感じの表示が出れば🆗です。

[root@localhost ~]# useradd --create-home --groups wheel naskya
[root@localhost ~]# passwd naskya
New password:
Retype new password:
passwd: password updated successfully

記号を含めないのは ConoHa VPS のコントロールパネルのコンソールから操作することになった場合を考えてのことです。このパスワードは最初の数回しか使いません。頭で覚える必要なんかありませんから、長いものを設定してパスワード管理をに記録させてとっとと忘れましょう。

さて、このままでは naskya さんは sudo コマンドでスーパーパワーを行使できないので、/etc/sudoers ファイルを編集しなければなりません。/etc/sudoers はとっても大事なファイルなので visudo コマンドで編集する必要があります{{< link https://wiki.archlinux.jp/index.php/Sudo#visudo_.E3.82.92.E4.BD.BF.E3.81.86 >}}。visudo コマンドはデフォルトで vi というエディターを使うので、頭に EDITOR=vim をつけて vim を使わせるようにしましょう。

EDITOR=vim visudo

ファイルを開いたら、

# %wheel ALL=(ALL:ALL) ALL

と書かれている部分を探してアンコメントしてください:

%wheel ALL=(ALL:ALL) ALL

この下に以下のような記述もありますが、それらはアンコメントしないように気をつけてください。

# %wheel ALL=(ALL:ALL) NOPASSWD: ALL
# %sudo ALL=(ALL:ALL) ALL

でもう一度このファイルを編集する必要があるのでここでそれもしてしまいます。ファイルの末尾などに

Defaults env_keep += "SSH_AUTH_SOCK"

を追記しておいてください。

作業用ユーザーで ssh 接続できるようにする

今は ~/.ssh/conoha_vps という鍵で root アカウントに ssh 接続できますが、naskya アカウントには接続できません。少々雑な方法ですが、root 向けの .ssh フォルダの内容を作業用ユーザー向けにコピーしてしまってこれを解決しましょう。

cp --recursive .ssh /home/naskya/.ssh
chown --recursive naskya: /home/naskya/.ssh

これをしたら Ctrl+D で ssh 接続を終了し、手元のマシンから

ssh -i ~/.ssh/conoha_vps naskya@2400:8500:1001:2002:3003:4004:5005:6006 

naskya アカウントとして接続できることを確認してください。接続したら、sudo id などを実行して sudo コマンドがちゃんと使えるか確かめましょう(さっき設定したパスワードの入力が求められます)。

[naskya@localhost ~]$ sudo id

We trust you have received the usual lecture from the local System
Administrator. It usually boils down to these three things:

    #1) Respect the privacy of others.
    #2) Think before you type.
    #3) With great power comes great responsibility.

For security reasons, the password you type will not be visible.

[sudo] password for naskya:
uid=0(root) gid=0(root) groups=0(root)

作業用ユーザーでしか ssh 接続できないようにする

ssh の設定を変更します。

sudo vim /etc/ssh/sshd_config

ポート番号を変更し、IPv6 のみの接続に変更しておきます。すなわち、

#Port 22
#AddressFamily any

Port 25252
AddressFamily inet6

に変更します。 IPv4 による接続のみを許可する場合には inet6 ではなく inet とします。次に、

PermitRootLogin yes
PasswordAuthentication yes
UsePAM yes

をそれぞれ

PermitRootLogin no
PasswordAuthentication no
UsePAM no

に変更して root アカウントへのアクセスやパスワードでのログイン、PAM の使用{{< link https://askubuntu.com/a/1323703 >}}を無効にします。最後に、

#AllowAgentForwarding yes

をアンコメントして

AllowAgentForwarding yes

として保存します。これができたら ssh のサービスを再起動して

sudo systemctl restart sshd

Ctrl+D で接続を終了し、root アカウントでのログインや 22 番ポートでのログインができなくなっていることを確認してください。

# root アカウントだしポート番号を変えていないので弾かれる
ssh -i ~/.ssh/conoha_vps root@2400:8500:1001:2002:3003:4004:5005:6006

# root アカウントなので弾かれる
ssh -p 25252 -i ~/.ssh/conoha_vps root@2400:8500:1001:2002:3003:4004:5005:6006

# ポート番号を変えていないので弾かれる
ssh -i ~/.ssh/conoha_vps naskya@2400:8500:1001:2002:3003:4004:5005:6006

# パスワード認証は禁止したので鍵を指定しないと弾かれる
ssh -p 25252 naskya@2400:8500:1001:2002:3003:4004:5005:6006

# これは通る
ssh -p 25252 -i ~/.ssh/conoha_vps naskya@2400:8500:1001:2002:3003:4004:5005:6006

そうしたらもう一度 Ctrl+D で接続を切り、手元のマシンの ~/.ssh/config に以下の設定を追記します(ファイルが無ければ新規作成してください)。

Host firefish
    Hostname 2400:8500:1001:2002:3003:4004:5005:6006
    User naskya
    Port 25252
    AddressFamily inet6
    IdentityFile ~/.ssh/conoha_vps
    IdentitiesOnly yes
    ForwardAgent yes
    AddKeysToAgent yes

また、シェルの rcfile(~/.bashrc~/.zshrc など)に以下を追記してください。

eval `ssh-agent` > /dev/null

手元のマシンのシェルを再起動すると、以下のコマンドでサーバーに接続できるようになっています!

ssh firefish

ConoHa VPS のページに戻り、Firefish のセキュリティーグループから 22 番ポートを受け入れる設定を削除してしまいましょう。

ConoHa VPS のページで作成した Firefish のセキュリティーグループから 22 番ポートを受け入れる設定を削除する様子
ConoHa VPS のページで作成した Firefish のセキュリティーグループから 22 番ポートを受け入れる設定を削除する様子

sudo の認証を ssh 鍵で行う

せっかく ssh 鍵で認証してサーバーに接続しているのに、sudo コマンドを使うときにいちいちパスワードの入力を求められては面倒です。これを解決するために、pam_ssh_agent_auth というパッケージを入れましょう。

ただしこれは Arch User Repository にあるパッケージなのでインストールやアップデートに少し手間が掛かります。そこで、先に AUR ヘルパーparu をインストールすることにします{{< note AUR ヘルパーを使いたくない方や他にお好きな AUR ヘルパーがある方は好きなようにしてください >}}。

sudo pacman -S git
git clone https://aur.archlinux.org/paru-bin.git
cd paru-bin
makepkg -si
cd ..
rm --recursive --force paru-bin

これで paru コマンドを用いて AUR のパッケージを入れられますが、その前に /etc/paru.conf を少し編集しておきましょう。以下のような箇所があるので BottomUpNewsOnUpgrade をアンコメントしておきます。

#AurOnly
#BottomUp
#RemoveMake
#SudoLoop
#UseAsk
#SaveChanges
#CombinedUpgrade
#CleanAfter
#UpgradeMenu
#NewsOnUpgrade

これで満を持して pam_ssh_agent_auth のインストールです。

paru -S pam_ssh_agent_auth

途中、画面下部に : が表示されて入力待ちで止まったら q キーを押せば進めます。pam_ssh_agent_auth パッケージが入ったら、/etc/pam.d/sudo の(# から始まるコメントを除いた)最初の行に追記をします{{< link https://wiki.gentoo.org/wiki/Pam_ssh_agent_auth#PAM_sudo_file >}}:

#%PAM-1.0
auth		sufficient	pam_ssh_agent_auth.so file=~/.ssh/authorized_keys
auth		include		system-auth
account		include		system-auth
session		include		system-auth

/etc/sudoers の編集は既に行っているのでおっけーです。一度 ssh 接続をし直して sudo id などのコマンドを実行し、パスワードの入力が要求されなくなったことを確かめてください。

[naskya@localhost ~]$ sudo id
uid=0(root) gid=0(root) groups=0(root)

ネットワークの設定

今はやらなくていいことたち

最近の ConoHa VPS では仕様が変わったのか、/etc/resolv.conf のシンボリックリンクが勝手に設定されるようになっているのでここは触らなくて大丈夫そうです。

[naskya@localhost ~]$ ls -l /etc/resolv.conf
lrwxrwxrwx 1 root root 32 Dec  4 11:23 /etc/resolv.conf -> /run/systemd/resolve/resolv.conf

Cloudflare や Google といった大企業のアドレスが指定されているので、これが気に入らない方は他のサーバー{{< note 例えば https://njalla.social/@njalla/109544720312978601 >}}を利用すると良いでしょう。

sudo unlink /etc/resolv.conf
sudo bash -c 'echo "nameserver 95.215.19.53" > /etc/resolv.conf'
sudo bash -c 'echo "nameserver 2001:67c:2354:2::53" >> /etc/resolv.conf'
sudo systemctl restart systemd-resolved

あと、以前は /etc/systemd/network/10-gmo-vps.network というファイルを作成して IPv6 向けの設定をしなければならなかったのですが、ConoHa VPS のアップデートにより不要となった気がする(最初からこのファイルがある)のでこれも触らなくて大丈夫そうです。

一応、以前やる必要があった設定を備忘録として書いておきます:

[Match]
Name=インターフェース名

[DHCP]
DUIDType=link-layer

[Network]
DHCP=yes

[Network]
Address=2400:8500:1001:2002:3003:4004:5005:6006/プレフィックス長
Gateway=ゲートウェイ
DNS=DNSサーバー1 DNSサーバー2

ただし インターフェース名 には ip link を実行すると出てくる lo ではない方の名前を、プレフィックス長, ゲートウェイ, DNSサーバー1, DNSサーバー2 には VPS のコントロールパネルのネットワーク情報に書かれているものを使います{{< note それから 2400:8500:1001:2002:3003:4004:5005:6006 を自分の IPv6 アドレスに置き換えるのも忘れずに >}}。

例えば ip link の実行結果が

ip link を実行した結果 lo と eth0 という 2 つのインターフェースが出ている
ip link を実行した結果 lo と eth0 という 2 つのインターフェースが出ている

のようになっていて、VPS のコントロールパネルに

ConoHa VPS のネットワーク情報の欄
ConoHa VPS のネットワーク情報の欄

と表示されている場合には

[Match]
Name=eth0

[DHCP]
DUIDType=link-layer

[Network]
DHCP=yes

[Network]
Address=2400:8500:1001:2002:3003:4004:5005:6006/64
Gateway=2400:8500:2002:3171::1
DNS=2400:8500:7703:502:150:95:10:8 2400:8500:7703:502:150:95:10:9

とします(以前やる必要があった設定はここまで)。

フィルタリング

nftables をインストールします。

paru -S nftables

設定方法がよく分からないし調べる気も起きません。nftables wiki にある Simple ruleset for a server という例{{< link https://wiki.nftables.org/wiki-nftables/index.php/Simple_ruleset_for_a_server >}}を真似します。ただし、ssh のために書かれている 22 番ポートを許可する設定は削除して IPv6 の 25252 番を許可するように書き換えます。

sudo rm /etc/nftables.conf
sudo vim /etc/nftables.conf
flush ruleset

table inet firewall {
    chain inbound_ipv4 {
    }

    chain inbound_ipv6 {
        # Accept neighbour discovery otherwise connectivity breaks
        icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept

        # Allow ssh
        tcp dport 25252 accept
    }

    chain inbound {
        # By default, drop all traffic unless it meets a filter
        # criteria specified by the rules that follow below.
        type filter hook input priority 0; policy drop;

        # Allow traffic from established and related packets, drop invalid
        ct state vmap { established : accept, related : accept, invalid : drop }

        # Allow loopback traffic.
        iifname lo accept

        # Jump to chain according to layer 3 protocol using a verdict map
        meta protocol vmap { ip : jump inbound_ipv4, ip6 : jump inbound_ipv6 }

        # Allow HTTP(S) TCP/80 and TCP/443 for IPv4 and IPv6.
        tcp dport {80, 443} accept
    }

    chain forward {
        # Drop everything (assumes this device is not a router)
        type filter hook forward priority 0; policy drop;
    }
}

設定を適用します。

sudo systemctl enable --now nftables

念のため ssh 接続を切ってちゃんと再接続ができるか試します{{< note もしここで再接続ができなくなっていたらコントロールパネルのコンソールから操作する羽目になる😇 >}}。

ssh firefish

Firefish をインストールする準備

使い方にもよるとは思いますが、私は小規模サーバーにオブジェクトストレージは不要だと思っています。私一人のサーバーは半年間続けてオブジェクトストレージを 400 MB も使わなかったし、お金掛かっちゃうし。

しかし、オブジェクトストレージ無しでサーバーを動かすと万が一後でオブジェクトストレージを使いたくなったときにサーバー上にある既存のファイルを移行させられなくて困ります{{< link https://mk.yopo.work/notes/9khu1cht1w >}}。そんなわけで、MinIO というオブジェクトストレージのサービスをセルフホストします。

ここでは仮に

とします。以降、これらのドメイン名が出てきたら自分が使うものに読み替えてください。

DNS の設定

サーバーの IPv4 アドレスをコピーし、使用する 3 つのサブドメインそれぞれに対して同じ IPv4 アドレスを用いて A レコードを設定します。

ConoHa VPS のコントロールパネルからサーバーの IPv4 アドレスをクリップボードにコピーする firefish というサブドメインに A レコードを追加する storage.firefish というサブドメインに A レコードを追加する minio.firefish というサブドメインに A レコードを追加する

次にコントロールパネルから IPv6 アドレスをコピーし、同様に 3 つのサブドメインそれぞれに対して AAAA レコードを設定します。

リバースプロキシを立てる

Caddy の設定

Caddy をインストールします。

paru -S caddy

Caddy の設定ファイルである /etc/caddy/Caddyfile を削除し、同名のファイルを新規作成して以下のように書きます。

firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	@proxy path /storage/*
	redir @proxy https://storage.firefish.example.com/firefish{uri}

	reverse_proxy http://127.0.0.1:3000

	log {
		output file /var/log/caddy/firefish.log
	}
}

storage.firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	reverse_proxy http://127.0.0.1:9000

	log {
		output file /var/log/caddy/storage.log
	}
}

minio.firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	reverse_proxy http://127.0.0.1:9001
  
	log {
		output file /var/log/caddy/minio.log
	}
}

この Caddyfile は 3 つのブロック (firefish.example.com, storage.firefish.example.com, minio.firefish.example.com) に分かれています。これは 1 台のサーバーで 3 つのドメインを扱っているためで、このサーバーへのアクセスはドメイン名によって振り分けられます。

例えば 2 つ目のブロックは storage.firefish.example.com 宛てにきた通信を処理するためのものです。

storage.firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	reverse_proxy http://127.0.0.1:9000

	log {
		output file /var/log/caddy/storage.log
	}
}

2 行目から 5 行目までの記述により、私は robots.txt の内容を無視する Bytespider というクローラー{{< link https://im-in.space/@Yuki/110911419969276600 >}}をブロックしています。やるかどうかはご自由に。

真ん中の reverse_proxy は、この通信をローカルの 9000 番のポートで動くサービス(オブジェクトストレージ)に渡しています。

最後の log はログをファイルに出力する設定です{{< link https://caddyserver.com/docs/caddyfile/directives/log#examples >}}。

3 つ目のブロックも同様で、minio.firefish.example.com 宛ての通信を 9001 番のポートで動くサービス(MinIO の管理画面)に渡しています。

1 つ目のブロック (firefish.example.com) を見ると、ここには少し違う処理が入っています。

@proxy path /storage/*
redir @proxy https://storage.firefish.example.com/firefish{uri}

これは、firefish.example.com 宛てにきた通信のうち、firefish.example.com/storage/** はワイルドカード)に来たものを https://storage.firefish.example.com/firefish に転送するというものです。

これにより、https://storage.firefish.example.com/firefish/hoge.png という場所にあるファイルを https://firefish.example.com/storage/hoge.png という URL で見られるようになります。これがわざわざ MinIO を立てた理由です。

もし今後サーバーの記憶域がいっぱいになったりしてオブジェクトストレージを(例えば Vultr に)お引越ししようと思った場合、引っ越し先のオブジェクトストレージに MinIO にあった全てのファイルを移してから Caddyfile のこの部分を

@proxy path /storage/*
redir @proxy https://bucket.sgp1.vultrobjects.com/firefish{uri}

などに変えれば(bucket はオブジェクトストレージのバケット名)今まで使っていた https://firefish.example.com/storage/hoge.png という URL が今度は https://bucket.sgp1.vultrobjects.com/firefish/hoge.png を参照するようになるので、URL をそのままに(つまりファイルのリンク切れを起こさずに)オブジェクトストレージを引っ越せるようになります。

Caddy を起動します。

sudo systemctl enable --now caddy

オブジェクトストレージを立てる

では、実際にオブジェクトストレージを動かしましょう。

MinIO の設定

MinIO をインストールします。

paru -S minio

/etc/minio.conf を以下のように編集します。ただし RandomUserNameVeryStrongPassword はそれぞれ好きなユーザー名と強いパスワードに置き換えます。このユーザー名とパスワードは MinIO の管理画面 (https://minio.firefish.example.com) へのログインで使うので、パスワード管理ツールに覚えさせておくとよいです。最後の 2 行も使用するドメイン名に合わせてください。

9000 番のポートでオブジェクトストレージのサーバーを、9001 番のポートで管理画面の Web サーバーを動かす設定になっています。

# Local export path.
MINIO_VOLUMES="/srv/minio/data/"
# Server user.
MINIO_ROOT_USER="RandomUserName"
# Server password.
MINIO_ROOT_PASSWORD="VeryStrongPassword"
# Use if you want to run Minio on a custom port.
MINIO_OPTS="--address :9000 --console-address :9001"
MINIO_SERVER_URL="https://storage.firefish.example.com"
MINIO_BROWSER_REDIRECT_URL="https://minio.firefish.example.com"

MinIO を起動します。

sudo systemctl enable --now minio

バケットの作成

これで MinIO の管理画面 (https://minio.firefish.example.com) にアクセスできるようになるので、ここに /etc/minio/minio.conf で指定したユーザー名とパスワードを入れてログインします。

MinIO の管理画面
MinIO の管理画面

firefish という新しいバケットを作成します。

Create a Bucket というリンクにマウスオーバーしている firefish というバケットを作っている

このままではストレージの中身が公開されないので、アクセスポリシーを public に設定します。

バケットを作ったままではアクセスポリシーが設定されていない バケットのアクセスポリシーを public に設定した

また、ストレージに勝手に書き込まれないようにするために Anonymous Access は readonly に設定しておきます。

バケットの Anonymous Access の初期設定は readwrite になっている バケットの Anonymous Access の設定を readonly に変更している

ファイヤーウォールの設定

fail2ban をインストールします。

paru -S fail2ban

/etc/fail2ban/filter.d/caddy-status.conf というファイルを作成し、以下の内容を書き込みます{{< link https://muetsch.io/how-to-integrate-caddy-with-fail2ban.html >}}。

[Definition]
failregex = ^.*"remote_ip":"<HOST>",.*?"status":(?:401|403|500),.*$
ignoreregex =
datepattern = LongEpoch

/etc/fail2ban/jail.conf/etc/fail2ban/jail.local という名前でコピーします{{< link https://wiki.archlinux.jp/index.php/Fail2ban#.E3.83.87.E3.83.95.E3.82.A9.E3.83.AB.E3.83.88_jail >}}。

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo vim /etc/fail2ban/jail.local

[sshd] という項目を探し、ポート番号を変更して enabled = true を追記します:

[sshd]
enabled = true
port    = 25252
logpath = %(sshd_log)s
bachend = %(sshd_backend)s

Caddy 向けの設定を追記します:

[caddy-status]
enabled     = true
port        = http,https
filter      = caddy-status
logpath     = /var/log/caddy/*.log
maxretry    = 10

あと、今回は nftables を使っているのでデフォルトの iptables 向けの設定を nftables 向けに変更します{{< link https://wiki.archlinux.org/title/Fail2ban#Firewall_and_services >}}:

banaction = nftables
banaction_allports = nftables[type=allports]

正直これでいいのかはさっぱりです 取りあえず起動します

sudo systemctl enable --now fail2ban

Firefish のインストール

Firefish のリポジトリの複製

Firefish を実行するためのユーザーを作成します。ホームディレクトリは /opt/firefish にしておきます。

sudo useradd --system --shell /usr/bin/nologin --user-group --home-dir /opt/firefish firefish

一時的にこのユーザーで作業するときと元のユーザーに戻るときはそれぞれ

sudo --user=firefish bash  # firefish ユーザーとして bash を実行する
cd ~  # ホームディレクトリ(この場合 Firefish のリポジトリのディレクトリ)に移る
exit
cd ~

などとします{{< note 可読性のためにこの記事では長いオプション (--user=firefish) を表記していますが、実際に使うときは -u firefish としてもよいです >}}。

/opt/firefish にリポジトリをクローンします。本家の Firefish を使うなら https://git.joinfirefish.org/firefish/firefish.git をクローンします。

sudo git clone https://code.naskya.net/naskya/firefish /opt/firefish
sudo chown --recursive firefish: /opt/firefish

安全性を高めるため Firefish を動かす際にこのディレクトリは読み取り専用にしますが、/files フォルダへは書き込める必要があるため /opt/firefish/files/var/lib/firefish/filesシンボリックリンクとしておきます。

sudo mkdir --parents /var/lib/firefish/files
sudo ln --symbolic /var/lib/firefish/files /opt/firefish/files

firefish ユーザーになって設定ファイル{{< note 名前が .config/default.yml なのはなんで?ってずっと思っています >}}を編集します。

sudo --user=firefish bash
cd ~
cp .config/example.yml .config/default.yml
vim .config/default.yml

url とデータベースの名前・ユーザー名・パスワードを編集します(ExamplePassword と書いたところはランダムなパスワードにしてください)。

url: https://firefish.example.com  <--

...

db:
  host: localhost
  port: 5432
  #ssl: false
  # Database name
  db: firefish_db        <--

  # Auth
  user: firefish         <--
  pass: ExamplePassword  <--

さらに、投稿の最大文字数の設定やリモートのファイルをプロキシする設定{{< note これは「リモートのファイルをキャッシュ」する設定とは違います。あの設定を無効にするならこちらは有効にしておくべきです。 >}}、サーバーのアドレスの指定を追記します。

maxNoteLength: 8000
proxyRemoteFiles: true
bind: 127.0.0.1

一旦作業用のユーザーに戻ります。

exit

データベースの準備

PostgreSQL のインストールとデータベースの作成

PostgreSQL をインストールし、初期化して起動します。en_US.UTF-8ja_JP.UTF-8 といった特定のロケールは指定せずに --no-locale(または --locale=C.UTF-8)を用いることでパフォーマンスが向上するようです{{< link https://www.postgresql.org/docs/current/locale.html#LOCALE-BEHAVIOR >}}{{< link https://qiita.com/fujii_masao/items/2a715fb5a3f718d22ab4 >}}。

paru -S postgresql
sudo --user=postgres \
  initdb --no-locale --encoding='UTF8' --pgdata='/var/lib/postgres/data'
sudo systemctl enable --now postgresql

PostgreSQL に firefish というユーザーと firefish_db というデータベースを作ります。最初のコマンドでパスワードを訊かれたらさっき .config/default.yml に書いたものを答えます。

sudo --user=postgres \
  createuser --no-createdb --no-createrole --no-superuser --encrypted --pwprompt firefish
sudo --user=postgres \
  createdb --encoding='UTF8' --owner=firefish firefish_db

PGroonga のインストールと有効化

Firefish の私のフレーバーには PGroonga が必要なので、インストールして有効化します。インストールには少し時間が掛かります。本家 Firefish を使う場合にこの作業は不要です。

paru -S pgroonga
sudo --user=postgres \
  psql --command='CREATE EXTENSION pgroonga;' --dbname=firefish_db

Redis のインストール

Redis をインストールして起動します。

paru -S redis
sudo systemctl enable --now redis

Rust のインストール

Rust, Clang, mold をインストールします。mold v2.3.3 にはまずいバグがあるようなのですが{{< link https://calc.cune.moe/notes/9mmwfg61hismiyx3 >}}、これを書いている時点で既に v2.4.0 が入るようになっていました。

paru -S rust clang mold

Clang と mold の場所を確認してから /opt/firefish/.cargo/config.toml に mold をリンカとして使わせる設定を追記します{{< link https://stackoverflow.com/a/70378019 >}}。

sudo --user=firefish bash
cd ~
mkdir .cargo
which clang  # /usr/bin/clang
which mold   # /usr/bin/mold
vim .cargo/config.toml
exit
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
rustflags = ["-C", "link-arg=--ld-path=/usr/bin/mold"]

Node.js と npm のインストール

pacman で nodejs パッケージをインストールしても npm はついてこないようなので{{< link https://github.com/misskey-dev/misskey/issues/11425#issuecomment-1657209913 >}}、両方をインストールします。さらに pnpm も使えるようにします。

paru -S nodejs npm
sudo corepack enable

FFmpeg のインストール

動画処理に必要な FFmpeg をインストールします。

paru -S ffmpeg 

アセットファイルの配置

サーバーのアイコンをカスタムしたい場合には custom フォルダに以下の画像を配置します。結構面倒……。

Firefish のビルド

Firefish をビルドします!Rust 製の部分があるので少し時間が掛かります。

# firefish ユーザーで作業する
sudo --user=firefish bash
cd ~

# ビルドする(初回インストール時のみ --install をつける)
./update.sh --install

# 元のユーザーに戻る
exit

( ^-^) < Enjoy your sabakan life~ が出たらビルドはおしまいです。

Firefish のビルドが終わった直後のターミナルの画面
Firefish のビルドが終わった直後のターミナルの画面

readelf コマンドを使えば mold が使われていることが分かります。

readelf コマンドで使用されたリンカーが mold であることを確認した
readelf コマンドで使用されたリンカーが mold であることを確認した

本家版 Firefish を使う場合には

git checkout main  # or beta
corepack prepare pnpm@latest --activate
pnpm install --frozen-lockfile
NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run build
NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run migrate

などとします。

Firefish を起動

以下の内容の /etc/systemd/system/firefish.service を作成します。水無麻那さんの systemd サービス{{< link https://github.com/mizunashi-mana/firefish-dist-pkg/blob/main/debian/firefish/lib/systemd/system/firefish.service >}}と MCtek さんの systemd サービス{{< link https://key.kubiwa.moe/notes/9d7hb1xwbt >}}および DynamicUser の説明記事{{< link https://0pointer.net/blog/dynamic-users-with-systemd.html >}}を参考にしています。

メモリアロケーターを jemalloc にする{{< link https://github.com/misskey-dev/misskey/issues/10984#issuecomment-1672495555 >}}のは効果を感じなかった{{< note サーバーの規模が小さいから? >}}のでしていませんが、jemalloc は Redis のインストール時にくっついてくるので使いたい人は systemd サービスのファイルに 1 行足すだけで使えます{{< note でも使うならちゃんと明示的にインストールしましょう >}}。共有オブジェクトのパスは以下のコマンドなどで調べられます。

paru -Ql jemalloc | grep .so
[Unit]
Description=Firefish daemon
Requires=redis.service minio.service caddy.service postgresql.service
After=redis.service minio.service caddy.service postgresql.service network-online.target

[Service]
Type=simple
User=firefish
Group=firefish
UMask=0027
ExecStart=/usr/bin/pnpm --filter backend run start
WorkingDirectory=/opt/firefish
StateDirectory=firefish/files
Environment="NODE_ENV=production"
Environment="npm_config_cache=/tmp"
# jemalloc を使うならアンコメント
# Environment="LD_PRELOAD=/usr/lib/libjemalloc.so.2"
StandardOutput=journal
StandardError=journal
SyslogIdentifier=firefish
TimeoutSec=60
Restart=always

CapabilityBoundingSet=
DevicePolicy=closed
DynamicUser=true
NoNewPrivileges=true
LockPersonality=true
PrivateDevices=true
PrivateIPC=true
PrivateMounts=true
PrivateUsers=true
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectProc=invisible
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SecureBits=noroot-locked
SystemCallArchitectures=native
SystemCallFilter=~@chown @clock @cpu-emulation @debug @ipc @keyring @memlock @module @mount @obsolete @privileged @raw-io @reboot @resources @setuid @swap
SystemCallFilter=capset pipe pipe2 setpriority

[Install]
WantedBy=multi-user.target

さて、いよいよ Firefish を起動します。起動直後に不審なエラーが出ないか確認するために journalctl で見守ります。

sudo systemctl start firefish
sudo journalctl --catalog --pager-end --follow --unit=firefish

ちなみに、サーバーの ~/.bashrc

alias sudo='sudo '
alias start="bash -c 'systemctl start \$0; journalctl --catalog --pager-end --follow --unit=\$0'"

と書いておくと

sudo start firefish

とするだけでこの 2 つのコマンド(systemctljournalctl)を実行できて便利です{{< link https://fedibird.com/@monaco_koukoku/111448970041094623 >}}{{< link https://fedi.sup39.dev/notes/9mbv5gcrtddik35g >}}{{< link https://syo.bar/objects/b2f243b8-16b8-4002-b1e9-09645a2642cc >}}。

Dec 05 01:49:29 localhost firefish[142300]: DONE *        [core boot]        All workers started
Dec 05 01:49:29 localhost firefish[142300]: DONE *        [core boot]        Now listening on port 3000 on https://firefish.example.com

というようなログが出たら、自鯖 (https://firefish.example.com) にアクセスできるはずです!

Firefish サーバーに初めてアクセスすると現れる、管理者アカウントの設定画面
Firefish サーバーに初めてアクセスすると現れる、管理者アカウントの設定画面

Firefish サーバーの初期設定

もうちょっと我慢

サーバーが立って安心したい気持ちは山々ですが、「サーバー立ったよ!」とインターネットに書くのはまだ早いです。

本家 Firefish を使っている人は、まずはコントロールパネルから新規登録の不許可と

Firefish サーバーの新規登録の受付を停止する設定
Firefish サーバーの新規登録の受付を停止する設定

非公開モードの設定をオンにしましょう。取りあえずこれで勝手に他人が登録してきたりリモートサーバーと通信したりしないのでゆっくり設定できます。今回入れている Firefish のフレーバーでは最初からこの設定になっているのでここはスルーします。

Firefish サーバーの非公開モードの設定
Firefish サーバーの非公開モードの設定

オブジェクトストレージの設定

一度 MinIO の管理画面 (https://minio.firefish.example.com) に戻り、オブジェクトストレージのアクセスキーを作成します。

MinIO の管理画面の Access Keys というページで右上にある Create access key をクリックする
MinIO の管理画面の Access Keys というページで右上にある Create access key をクリックする
出てきたダイアログの右下の Create というボタンをクリックする
出てきたダイアログの右下の Create というボタンをクリックする

そうしたら、Firefish のコントロールパネルに戻ってオブジェクトストレージの設定をします。「中身は storage.firefish.example.com にアップロードしつつ、外部に公開する URL は firefish.example.com/storage の下にしたい」という需要を叶えるために少し特殊な設定が必要です{{< link https://qiita.com/atsu1125/items/8e870ec7c9932210b152 >}}。

Base URL: https://firefish.example.com
  Bucket: firefish
  Prefix: storage
Endpoint: storage.firefish.example.com
  Region: us-east-1

として、さっき取得したアクセスキーとシークレットキーを入力して下部にある 4 つのトグルを全て有効にして右上のチェックマークで設定を保存します。

Firefish のコントロールパネルのオブジェクトストレージの設定
Firefish のコントロールパネルのオブジェクトストレージの設定

画像をアップロードしてみて、動作を確認しましょう。

Firefish に画像をアップロードし、その表示と URL を確認している
Firefish に画像をアップロードし、その表示と URL を確認している
オブジェクトストレージにファイルがアップロードされていることを確認している
オブジェクトストレージにファイルがアップロードされていることを確認している
オブジェクトストレージにアップロードされた画像をプレビューで確認している
オブジェクトストレージにアップロードされた画像をプレビューで確認している

確認ができたらオブジェクトストレージの設定は終了です🎉

その他のサーバー設定

サーバー名・サーバーの説明文・サーバーのテーマやテーマカラーなどを設定しましょう。

私はサーバーメトリクスと identicon{{< note アイコン画像が無いときに表示されるランダムなアイコン >}} の生成を切っています。サーバーメトリクスなんか見ても健康に悪いし、ランダムなアイコンは多くの場合かわいくないし……

Summaly Proxy の設定は要らないかな……って思っています。同じものが内蔵されていてユーザーの IP アドレスはバレないようになっているので{{< link https://forum.misskey.io/d/9-what-is-a-summaly-proxy >}}{{< note お一人様サーバーの場合識別可能性の意味ではサーバーの IP アドレスもユーザーの IP アドレスも大して変わらないかもしれませんが >}}。

それから、プッシュ通知を有効にするために以下のコマンドを実行して{{< link https://hide.ac/articles/Y504SIabp#title-8 >}}生成した鍵のペアを Service Worker の公開鍵と秘密鍵の欄に入力します{{< note ここでは説明のためにスクリーンショット内で秘密鍵を公開していますが、秘密鍵を公開してはいけません >}}。

pnpm dlx web-push generate-vapid-keys
プッシュ通知を有効化し、公開鍵と秘密鍵を入力している
プッシュ通知を有効化し、公開鍵と秘密鍵を入力している

また、リモートのファイルをキャッシュする設定が無効になっていることを確認しましょう。

リモートのファイルをキャッシュする設定が無効になっている
リモートのファイルをキャッシュする設定が無効になっている

まだ念の為非公開モードは外さず、一度スナップショットを取ることにします{{< note もしこの後で何かを壊してしまって最初からやり直しになったら地獄なので >}}。

サーバーの管理

基本事項

エラーの確認

たまに気が向いたときでよいので、失敗している systemd サービスやエラーログがあるかを確認しましょう{{< link https://wiki.archlinux.jp/index.php/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%83%A1%E3%83%B3%E3%83%86%E3%83%8A%E3%83%B3%E3%82%B9#.E3.82.A8.E3.83.A9.E3.83.BC.E3.81.AE.E7.A2.BA.E8.AA.8D >}}。

systemctl --failed
sudo journalctl --catalog --priority=3

ログの削除

サーバーをそのまま動かし続けているとログファイルがどんどん溜まっていくので、古いログは適度に削除させましょう。/etc/systemd/journald.conf[Journal] というセクションに

#SystemMaxUse=

という設定がコメントアウトされているので、これを解除して最大で保存するログの量を 500 MB くらいにしておきましょう。RuntimeMaxUse も同様に設定しておくといいかもです。

SystemMaxUse=500M

設定を反映させます:

sudo systemctl restart systemd-journald

pacman の設定

paccache を用いて pacman のキャッシュを適度に削除するようにしましょう{{< link https://wiki.archlinux.jp/index.php/Pacman#.E3.83.91.E3.83.83.E3.82.B1.E3.83.BC.E3.82.B8.E3.82.AD.E3.83.A3.E3.83.83.E3.82.B7.E3.83.A5.E3.81.AE.E5.89.8A.E9.99.A4 >}}。

paru -S pacman-contrib
sudo systemctl enable --now paccache.timer

また、Reflector を使ってミラーリストを適度に更新するようにするのもよいでしょう{{< link https://zenn.dev/ohno418/articles/13e3472860881d >}}。

paru -S reflector
sudo reflector --country Japan,Australia --age 24 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
paru -Syyu
sudo vim /etc/xdg/reflector/reflector.conf  # 好きなように編集
sudo systemctl enable --now reflector.timer

メンテナンス中に出すページの用意

サーバーのメンテナンス中にはその旨を知らせるページを出すとよいでしょう{{< note 以前、メンテナンス中のページを出さずに 1 時間サーバーを止めていたらアクティビティーの配送を停止されたことがあります >}}。サーバーが落ちているときに自動でそのようなページを出すことも考えられますが、それではサーバーが不具合よって落ちているのかサーバーを意図的に落としているのかの区別がつかないので、ここではちゃんと手動でメンテナンス中にこのページを出すことにします。

HTML や CSS を頑張って凝ったページを作るのも一興ですが、例えば以下のようなテキストファイルを置くだけでも十分でしょう。HTML のページを作った場合にも同様に /srv/firefish.example.com の下に置いておきます。

sudo mkdir /srv/firefish.example.com
sudo vim /srv/firefish.example.com/maintenance.txt
firefish.example.com は現在メンテナンス中です。
メンテナンスは最長でも数時間で終了する予定です。

firefish.example.com is currently under maintenance.
This will only take a few hours at the most.

連絡先/Contacts
     Matrix: @example:matrix.example.com
       XMPP: example@xmpp.example.com
  HAM Radio: JA1XYZ (CW, any frequency within 21150-21450 kHz)
      Email: example@mail.example.com

/etc/caddy/Maintenance.caddyfile というもう一つの Caddy の設定ファイル{{< note caddyfile という拡張子は一般的ではありません >}}を作り、以下のように書きます:

firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	@proxy path /storage/*
	redir @proxy https://storage.firefish.example.com/firefish{uri}

	root * /srv/firefish.example.com
	try_files maintenance.txt
	file_server {
		status 503
	}

	log {
		output file /var/log/caddy/maintenance.log
	}
}

storage.firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	reverse_proxy http://127.0.0.1:9000

	log {
		output file /var/log/caddy/storage.log
	}
}

minio.firefish.example.com {
	@blocked header_regexp User-Agent .*Bytespider.*
	handle @blocked {
		abort
	}

	reverse_proxy http://127.0.0.1:9001

	log {
		output file /var/log/caddy/minio.log
	}
}

Firefish サーバーを落としている間もオブジェクトストレージへのリダイレクトは継続しておくことで投稿の添付画像などがリモートサーバーからずっと見えるようになります{{< note minio のメンテナンスをするときにはこれも停止しなければなりませんが、私は今のところ paru によるアップデート以外に minio をいじったことがありません。なんなら管理画面にもずっとアクセスしていないから管理画面は閉じておいたほうがいいのかも >}}。

以下のコマンドでページの表示を切り替えます。

# メンテナンス中のページを出す
caddy reload --config /etc/caddy/Maintenance.caddyfile

# メンテナンスおしまい
caddy reload --config /etc/caddy/Caddyfile

PostgreSQL の調整

PostgreSQL の初期の設定は Firefish サーバーを動かすのに適していません。

PGTune という Web アプリで、より良い設定を教えてもらえます{{< link https://pgtune.leopard.in.ua/ >}}。今回の例だとこんな感じのサーバー向けの設定を教えてもらうとよいでしょう:

PostgreSQL のバージョン: 16
          OS ファミリー: Linux
      データベースの種類: Data warehouse
      メインメモリの容量: 1500 MB
       (論理)CPU の数: 3
             ストレージ: SSD storage

メインメモリの容量を 2 GB ではなく 1500 MB としているのは、Redis や MinIO やファイヤーウォールなどの他のソフトウェアもメモリを食べることを考えてのことです{{< note 私は以前ここの単位を変えられることに気づかなくて、1 GB のときの結果と 2 GB のときの結果の算術平均を取って使っていました…… >}}。

PGTune で適切な設定を教えてもらった
PGTune で適切な設定を教えてもらった

設定のうちの該当する部分を変更し、PostgreSQL を再起動します:

sudo vim /var/lib/postgres/data/postgresql.conf
sudo systemctl stop firefish
sudo systemctl restart postgresql
sudo systemctl start firefish

また、気が向いた際にデータベースの VACUUM を行うとよいです。少し時間が掛かります。

# サーバーを止めてメンテナンス中のページを出す
sudo systemctl stop firefish
caddy reload --config=/etc/caddy/Maintenance.caddyfile

# データベースの VACUUM をする
sudo --user=postgres \
  psql --dbname=firefish_db --command="VACUUM FULL VERBOSE ANALYZE;"

# メンテナンス中の表示を解除してサーバーを起動する
caddy reload --config=/etc/caddy/Caddyfile
sudo systemctl start firefish

バックアップ

スナップショット

スナップショットを取ると立てた環境をまるまるバックアップできます。アップデートの前とかにやっておくと便利です。

スナップショットを取る前には Firefish の systemd サービスを無効化し(つまりサーバーが起動したときに自動で Firefish が立ち上がらないようにし)、サーバーをシャットダウンしましょう。なぜなら、スナップショットを使うときというのはサーバーがヤバい状態になって心臓がバクバクしているときですから、スナップショットから復元してサーバーを立ち上げたときに Firefish が自動で立ち上がってリモートサーバーから投稿をどんどん受け取るようになっていたら焦るからです{{< note 少なくとも私なら >}}。

sudo systemctl disable firefish
sudo shutdown now

ここまでさんざん systemctl コマンドを使っておいて今いきなりする話ではありませんが、systemctl disable firefish は Firefish を停止しませんし、systemctl enable firefish は Firefish を起動しません。disable/enable はあくまで自動起動の設定を変えるだけです。

Firefish サービスの起動と停止は

sudo systemctl start firefish
sudo systemctl stop firefish

で行います。startenable を両方とも行いたければ

sudo systemctl enable --now firefish

を使えます。

シャットダウンしたら、コントロールパネルの「イメージ保存」のボタンをクリックしてスナップショットを保存します。

ConoHa VPS の管理画面からスナップショットを保存するボタン
ConoHa VPS の管理画面からスナップショットを保存するボタン

スナップショットの名前を設定することを求められますが、ここに日付などを入れても全く意味が無いことに注意してください(スナップショット一覧に日付が表示されるため)。名前にピリオド (.) を含められないのが少し残念。

スナップショットのネームタグを設定する画面
スナップショットのネームタグを設定する画面

左のメニューから「イメージ」のページに飛んで、保存中のスナップショットのステータスが「利用可能」になったらスナップショットの作成は終了し、サーバーをまた起動できます。スナップショットの作成には最大で 10 分間くらい掛かる気がします。

スナップショットが利用可能になった画面
スナップショットが利用可能になった画面

サーバーを立ち上げた後、Firefish が自動起動しない設定になっていることを忘れずに。

sudo systemctl enable --now firefish
スナップショットの肥大化防止

VPS のスナップショットの仕組みはよく知りませんが、スナップショットの大きさが記憶域の実際の使用量より大きくなるという現象が色々な VPS で起きるようです{{< link https://qiita.com/CloudRemix/items/5c6c285af11bea36a270 >}}。

0 で埋められた巨大なファイルで記憶域の空いた部分を埋め、そのファイルを削除するとこの問題を解消できるようなので、それを行うスクリプトを作って{{< link https://code.naskya.net/repos/q32v3/source-by/main/disk_fill_cleanup >}}作業用ユーザーのホームディレクトリなどに置いておきます:

vim disk_fill_cleanup.sh
chmod +x disk_fill_cleanup.sh
#!/bin/sh
set -eu

say() {
  tput setaf 3
  printf '%s\n' "$1"
  tput setaf 7
}

run() {
  tput setaf 5
  printf '[run] $ %s\n' "$1"
  tput setaf 7
  /bin/sh -c "$1"
}

free_space=$(df -m --output=avail "$(pwd)" | tail -1)
say "[info] ${free_space} MiB are available"

tmpfile="$(mktemp --tmpdir=. --dry-run)"

run "dd if=/dev/zero of=${tmpfile} status=progress bs=1M count=$((free_space - 300))"
run "rm ${tmpfile}"
run 'df --human-readable'

free_space=$(df -m --output=avail "$(pwd)" | tail -1)
say "[info] ${free_space} MiB are available"

このスクリプトは途中で失敗すると作ったファイルが削除されずに記憶域がいっぱいになってしまう可能性があるため、自動化せずに必ず手で実行の様子を見守ることにしましょう。

./disk_fill_cleanup.sh
disk_fill_cleanup.sh の実行の様子
disk_fill_cleanup.sh の実行の様子

また、サーバーの動作に不具合が起きた場合に「取りあえず再起動」するのはやめましょう。再起動は様々な問題を解決する可能性のある強力な方法ですが、記憶域がいっぱいであることが不具合の原因であった場合にはサーバーが再び起動してこなくなってしまう可能性があります。何も考えずに再起動をする前に記憶域の使用量を確認しましょう。

データベースのバックアップ

VPS のスナップショットは環境を壊したときにサクっと元の状態に戻せるから便利ですが、

などの理由からあんまり頼りにしてはいません。

ここでは systemd タイマーを使って pg_dump を定期的に行い、外部のストレージ(ここでは例として MEGA を使用します{{< note 私は実際には手元の PC にデータを保存しています。しかしこれは PC が壊れたり物理的にアクセスできなくなったりしたらバックアップを救出できなくなる可能性があるのでおすすめしません。もちろんクラウドストレージにも BAN されるリスクがあるのでなんとも言い難いですが…… >}})にデータベースを暗号化して保存することにします。

MEGA にはファイルを操作するための MEGAcmd という便利なツールがありますが、ここでは外部のストレージの一例として MEGA を用いているだけなので他のストレージを使う場合も考えてより一般的に使える{{< link https://rclone.org/#providers >}}方法を用います。

例えばここではデータベースのバックアップを 1 時間に 1 回行い、 - 1 時間ごとのバックアップ 12 個 - 1 日間ごとのバックアップ 6 個 - 1 週間ごとのバックアップ 3 個 - 1 か月間ごとのバックアップ 3 個

をそれぞれ保持することにします。

この場合、例えばサーバーの状態がマズいことが 4 日後に発覚した場合に最悪 4 日前のバックアップを用いればよいということになります。まずい状態のサーバーを 3 か月間放置してしまったらおしまいです{{< note 急病で長期間入院してしまった場合などにはあり得るかもしれません。その際にはどうにかして VPS かクラウドストレージにアクセスしてバックアップを救出しましょう…… >}}。

手でやってみる

いきなり自動化する前に、まずは手でデータベースをバックアップしてみましょう。バックアップの暗号化に使う長くて複雑なパスフレーズを生成してパスワード管理ツールに記録させ、~/backup_firefish/passphrase という場所に保存しておきます。

cd ~
pwd  # /home/naskya
mkdir backup_firefish
cd backup_firefish
vim passphrase && chmod 400 passphrase

pg_dump を実行してデータベースをダンプします。このとき Firefish サーバーを止める必要はありません{{< link https://www.postgresql.org/docs/current/app-pgdump.html#PG-DUMP-DESCRIPTION >}}。

sudo --user=postgres \
  pg_dump --format=plain --dbname=firefish_db > firefish_db.sql

データベースのダンプはこれでできるのですが、後に自動化する際には sudo コマンドが入ってくると厄介です。

pg_dump --user=firefish --format=plain --dbname=firefish_db > firefish_db.sql

としてデータベースをダンプできるように、~/.pgpass というファイルを作っておきます{{< link https://www.postgresql.org/docs/current/libpq-pgpass.html#LIBPQ-PGPASS >}}。

vim ~/.pgpass && chmod 600 ~/.pgpass

.pgpass ファイルには以下の内容を記述します。最後に書くデータベースのパスワードは /opt/firefish/.config/default.yml に書いてあります。

localhost:5432:firefish_db:firefish:PASSWORD

データベースのダンプが終わるとディレクトリの中身はこのようになります。

[naskya@localhost backup_firefish]$ ls -l --human-readable
total 288K
-rw-r--r-- 1 naskya naskya 283K Dec  7 07:02 firefish_db.sql
-r-------- 1 naskya naskya  129 Dec  7 07:01 passphrase

このバックアップは平文で書かれているので、中身を確認できます:

less firefish_db.sql
--
-- PostgreSQL database dump
--

-- Dumped from database version 16.1
-- Dumped by pg_dump version 16.1

SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;

...

これを gpg2 コマンドで暗号化します:

gpg2 --symmetric --passphrase-file passphrase --pinentry-mode loopback firefish_db.sql

暗号化の前にファイルの圧縮が行われるので{{< link https://security.stackexchange.com/a/84083 >}}、できた firefish_db.sql.gpg は元のファイルよりも小さくなります:

[naskya@localhost backup_firefish]$ ls -l --human-readable
total 336K
-rw-r--r-- 1 naskya naskya 283K Dec  7 07:02 firefish_db.sql
-rw-r--r-- 1 naskya naskya  45K Dec  7 07:04 firefish_db.sql.gpg
-r-------- 1 naskya naskya  129 Dec  7 07:01 passphrase

ここで、passphrase ファイルではなくパスワード管理ツールに保存したパスフレーズを入力して暗号化されたファイルを元に戻せるか確認しておきます。ちゃんと復号できなければバックアップを保存してもタンスの肥やしになるだけです。

gpg2 --decrypt firefish_db.sql.gpg > firefish_db_decrypted.sql
gpg2 コマンドに復号用のパスフレーズを訊かれている画面
gpg2 コマンドに復号用のパスフレーズを訊かれている画面

復号したファイルと元のファイルが完全に一致していれば問題ありません。

diff --report-identical-files firefish_db.sql firefish_db_decrypted.sql
# Files firefish_db.sql and firefish_db_decrypted.sql are identical と出ればよい

そうしたら、暗号化されていない平文のデータは消してしまいます{{< link https://atmarkit.itmedia.co.jp/flinux/rensai/linuxtips/662delfile.html >}}。

shred --remove firefish_db.sql firefish_db_decrypted.sql

これで、残った firefish_db.sql.gpg を外部のストレージに転送しておけばよいというわけです。

自動でやる

rclone をインストールし、rclone のドキュメントを参考に MEGA に接続します{{< link https://rclone.org/mega/ >}}。他のストレージを使う場合にはそのサービスに対応するページを参照してください。最初の設定(ユーザー名やパスワードの入力など)が終わればその後の操作はほとんど同じです。接続先には名前をつける必要がありますが、ここでは remote としています。

paru -S rclone
rclone config
rclone で MEGA 向けの設定を終えた画面
rclone で MEGA 向けの設定を終えた画面

試しに test.txt というファイルを作って転送してみましょう。

echo Hello > test.txt         # Hello という中身の test.txt を作る
rclone copy test.txt remote:  # remote に test.txt をコピーする

クラウドストレージにファイルが転送されます。

MEGA に test.txt が転送された
MEGA に test.txt が転送された

手元のマシンのデータを手元のマシンの別のドライブにバックアップするような用途では rsnapshot というツールがおすすめなのですが、バックアップ先がリモートのストレージの場合に rsnapshot は適していない{{< link https://serverfault.com/a/430774 >}}{{< link https://askubuntu.com/a/35199 >}}ようなので別のツールを探します。

調べてみるとこのようなファイルのバックアップには Kopia, Duplicati, restic, duplicity などのいくつかのツールが使えるようですが、なんだかどれも機能過多な気がしたので自分でスクリプトを書いて使うことにします。

バックアップ用の外部のストレージに以下のようなフォルダの階層を作ります{{< note backup_firefish と firefish_backup というディレクトリがありますが、前者は「Firefish をバックアップするための道具の場所」で後者は「Firefish のバックアップの保存場所」という気持ちによる命名です。紛らわしい場合には名前を揃えてもよいです。 >}}:

.
└─ firefish_backup
   ├─ object_storage
   └─ database
      ├─ hourly 
      ├─ daily
      ├─ weekly
      └─ monthly
rclone mkdir remote:firefish_backup

for name in object_storage database; do
  rclone mkdir "remote:firefish_backup/${name}"
done

for name in hourly daily weekly monthly; do
  rclone mkdir "remote:firefish_backup/database/${name}"
done

作戦はこうです:

本当はもっと工夫の余地があるダサいバックアップ方法ですが、まぁ一人ぼっちサーバーのバックアップなんてこれくらいでいいよね……

使うクラウドストレージのサービスによって仕様が微妙に違う可能性があるので気をつけてください。例えば MEGA では rclone delete でファイルを消してもファイルがゴミ箱に移動するだけで記憶域の使用量が変わらないので、rclone cleanup を用いてゴミ箱の中にあるファイルも消す必要があります。S3 互換のストレージを用いる場合にも追加の設定が必要になることがあるようです{{< link https://post.sup39.dev/notes/9o2qa0iuvamguwax >}}。

./backup_firefish/database.sh hourly
./backup_firefish/database.sh daily
./backup_firefish/database.sh weekly
./backup_firefish/database.sh monthly

のように(hourly などの)引数を与えると上記の内容を実行するようなスクリプトを作ります{{< note 説明のために冗長な書き方をしています >}}。

#!/bin/sh
set -eu

take_snapshot() {
  # ファイル名は 20231224010000.sql のようにする(ソートしやすいため)
  DUMP_FILE=$(printf '%s.sql' "$(date +'%Y%m%d%H%M%S')")

  # さっき手でやったことをここでやる
  pg_dump --format=plain --user=firefish --dbname=firefish_db --file="${DUMP_FILE}"
  gpg2 --symmetric --passphrase-file passphrase --pinentry-mode loopback "${DUMP_FILE}"
  shred --remove "${DUMP_FILE}"

  # 最終的にできるファイル名は 20231224010000.sql.gpg のようになっていることに注意
  printf '%s.gpg' "${DUMP_FILE}"
}

hourly() {
  BACKUP_DIR='firefish_backup/database/hourly'
  MAX_NUM_FILES='12'

  # スナップショットを取り、最後に printf で出力される名前を取得する
  BACKUP_FILE=$(take_snapshot)

  # hourly ディレクトリにコピーする
  rclone copy "${BACKUP_FILE}" "remote:${BACKUP_DIR}"

  # コピーが終わったらローカルにあるバックアップファイルは消してしまう
  shred --remove "${BACKUP_FILE}"

  # ディレクトリの中身はどうなってるかな
  FILES=$(rclone ls "remote:${BACKUP_DIR}" | awk '{ print $2 }' | sort)

  # ファイルはいくつあるかな
  NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)

  # もし保存する最大数を超えていたら古いやつ(ソートして最初にくるやつ)を消そうね
  while [ "${NUM_FILES}" -gt "${MAX_NUM_FILES}" ]; do
    FILE_TO_BE_DELETED=$(printf '%s' "${FILES}" | head -1)
    rclone delete "remote:${BACKUP_DIR}/${FILE_TO_BE_DELETED}"
    # ゴミ箱の中も消す必要がある場合には以下をコメントアウト
    # rclone cleanup remote:

    # 念のためディレクトリをまた確認
    FILES=$(rclone ls "remote:${BACKUP_DIR}" | awk '{ print $2 }' | sort)
    NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  done
}

daily() {
  FROM_DIR='firefish_backup/database/hourly'
  TO_DIR='firefish_backup/database/daily'
  MAX_NUM_FILES='6'

  # hourly ディレクトリにある一番古いファイルは何かな
  TARGET=$(rclone ls "remote:${FROM_DIR}" | awk '{ print $2 }' | sort | head -1)

  # hourly ディレクトリに何もなかったらおしまい
  if [ "${TARGET}" = '' ]; then
    exit 0
  fi

  # hourly ディレクトリにある一番古いファイルを daily ディレクトリに動かそう
  rclone move "remote:${FROM_DIR}/${TARGET}" "remote:${TO_DIR}/"

  # daily ディレクトリの中身はどうなってるかな
  FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)

  # ファイルはいくつあるかな
  NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)

  # もし保存する最大数を超えていたら古いやつ(ソートして最初にくるやつ)を消そうね
  while [ "${NUM_FILES}" -gt "${MAX_NUM_FILES}" ]; do
    FILE_TO_BE_DELETED=$(printf '%s' "${FILES}" | head -1)
    rclone delete "remote:${TO_DIR}/${FILE_TO_BE_DELETED}"
    # ゴミ箱の中も消す必要がある場合には以下をコメントアウト
    # rclone cleanup remote:

    # 念のためディレクトリをまた確認
    FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)
    NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  done
}

weekly() {
  FROM_DIR='firefish_backup/database/daily'
  TO_DIR='firefish_backup/database/weekly'
  MAX_NUM_FILES='3'

  TARGET=$(rclone ls "remote:${FROM_DIR}" | awk '{ print $2 }' | sort | head -1)
  if [ "${TARGET}" = '' ]; then exit 0; fi
  rclone move "remote:${FROM_DIR}/${TARGET}" "remote:${TO_DIR}/"
  FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)
  NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  while [ "${NUM_FILES}" -gt "${MAX_NUM_FILES}" ]; do
    FILE_TO_BE_DELETED=$(printf '%s' "${FILES}" | head -1)
    rclone delete "remote:${TO_DIR}/${FILE_TO_BE_DELETED}"
    # rclone cleanup remote:
    FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)
    NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  done
}

monthly() {
  FROM_DIR='firefish_backup/database/weekly'
  TO_DIR='firefish_backup/database/monthly'
  MAX_NUM_FILES='3'

  TARGET=$(rclone ls "remote:${FROM_DIR}" | awk '{ print $2 }' | sort | head -1)
  if [ "${TARGET}" = '' ]; then exit 0; fi
  rclone move "remote:${FROM_DIR}/${TARGET}" "remote:${TO_DIR}/"
  FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)
  NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  while [ "${NUM_FILES}" -gt "${MAX_NUM_FILES}" ]; do
    FILE_TO_BE_DELETED=$(printf '%s' "${FILES}" | head -1)
    rclone delete "remote:${TO_DIR}/${FILE_TO_BE_DELETED}"
    # rclone cleanup remote:
    FILES=$(rclone ls "remote:${TO_DIR}" | awk '{ print $2 }' | sort)
    NUM_FILES=$(printf '%s' "${FILES}" | wc --lines)
  done
}

if [ "$#" != '1' ]; then exit 1;

elif [ "$1" = 'hourly' ];  then hourly;
elif [ "$1" = 'daily' ];   then daily;
elif [ "$1" = 'weekly' ];  then weekly;
elif [ "$1" = 'monthly' ]; then monthly;

else exit 1; fi

このスクリプトを ~/backup_firefish/database.sh などに保存し、systemd タイマーで決まった時刻にこれを呼ぶ設定をしていきます{{< link https://wiki.archlinux.jp/index.php/Systemd/%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC >}}。

vim ~/backup_firefish/database.sh
chmod +x ~/backup_firefish/database.sh

このスクリプトを実行するための systemd サービスを作ります。

sudo vim /etc/systemd/system/backup_firefish_database@.service
# backup_firefish_database@.service
[Unit]
Description=Backup Firefish database (%I)
Requires=postgresql.service
After=postgresql.service network-online.target

[Service]
Type=oneshot
User=naskya
WorkingDirectory=/home/naskya/backup_firefish
ExecStart=/home/naskya/backup_firefish/database.sh %i
sudo vim /etc/systemd/system/backup_firefish_database@hourly.timer
sudo vim /etc/systemd/system/backup_firefish_database@daily.timer
sudo vim /etc/systemd/system/backup_firefish_database@weekly.timer
sudo vim /etc/systemd/system/backup_firefish_database@monthly.timer
[Unit]
Description=Firefish database hourly backup

[Timer]
OnCalendar=*-*-* *:00:00
Persistent=false

[Install]
WantedBy=timers.target
[Unit]
Description=Firefish database daily backup

[Timer]
OnCalendar=*-*-* 05:30:00
Persistent=false

[Install]
WantedBy=timers.target
[Unit]
Description=Firefish database weekly backup

[Timer]
OnCalendar=Sun 04:30:00
Persistent=false

[Install]
WantedBy=timers.target
[Unit]
Description=Firefish database monthly backup

[Timer]
OnCalendar=Sun *-*-1..7 03:30:00
Persistent=false

[Install]
WantedBy=timers.target

用意した全てのタイマーを有効化すると、データベースが自動的にバックアップされます。

sudo systemctl enable --now \
  backup_firefish_database@hourly.timer  \
  backup_firefish_database@daily.timer   \
  backup_firefish_database@weekly.timer  \
  backup_firefish_database@monthly.timer
しばらくしてから確認するとバックアップファイルがたくさんアップロードされている
しばらくしてから確認するとバックアップファイルがたくさんアップロードされている

オブジェクトストレージのバックアップ

今回はオブジェクトストレージもセルフホストしているので、その中身もバックアップしておくべきです。ただしこちらはドライブのファイルだけなので、常に最新版のバックアップだけ持っていれば大丈夫だと思っています(心配ならこちらもバージョン管理してよいです)。

rclone の接続先にローカルの MinIO を追加する

MinIO は Amazon S3 互換のオブジェクトストレージなので、rclone の接続先にそのまま登録できます。rclone config から local_minio という名前で AWS S3 API 対応のオブジェクトストレージとして登録します。エンドポイントの URL として http://localhost:9000 を用いることに注意します{{< link https://docs.openio.io/latest/source/integrations/cookbook_rclone.html >}}。

rclone の新しい接続先として同じサーバーで動いている MinIO を登録している
rclone の新しい接続先として同じサーバーで動いている MinIO を登録している

アクセスキーとシークレットキーは MinIO の管理画面 (https://minio.firefish.example.com) で生成して使用します。

アクセスキーを入力する画面
アクセスキーを入力する画面

region, locacion_constraint, acl, server_side_encryption など心配になるほど大量な情報の入力を求められますが、全て何も入力せずに Enter キーを押して進んでよいです。

local_minio という接続先の登録が完了した画面
local_minio という接続先の登録が完了した画面
暗号化してバックアップする

登録が完了したら試しに以下のコマンドを実行してみると、firefish バケットの中身が全て手元にコピーされます。

rclone copy local_minio:firefish .
firefish バケットの中身が全てコピーできて、画像ファイルがたくさん手元に保存されている
firefish バケットの中身が全てコピーできて、画像ファイルがたくさん手元に保存されている

この storage ディレクトリを丸ごと暗号化してバックアップ用のストレージに転送すればオブジェクトストレージのバックアップは完了です!

vim ~/backup_firefish/object_storage.sh
chmod +x ~/backup_firefish/object_storage.sh
#!/bin/sh
set -eu

# オブジェクトストレージの中身を手元にコピー
rclone copy local_minio:firefish .

# storage ディレクトリが手元に来るので tar でまとめる
tar --gzip --create --file=storage.tar.gz storage/

# 暗号化
gpg2 --symmetric --passphrase-file passphrase --pinentry-mode loopback storage.tar.gz

# 元々ある storage.tar.gz.gpg を storage.tar.gz.gpg.bak に改名
rclone moveto remote:firefish_backup/object_storage/storage.tar.gz.gpg \
              remote:firefish_backup/object_storage/storage.tar.gz.gpg.bak

# クラウドストレージにアップロード
rclone copy storage.tar.gz.gpg remote:firefish_backup/object_storage

# クラウドの storage.tar.gz.gpg.bak を削除
rclone delete remote:firefish_backup/object_storage/storage.tar.gz.gpg.bak

# ゴミ箱の中のファイルを消す
# rclone cleanup remote:

# 手元のファイルを削除
rm --recursive --force storage/ storage.tar.gz storage.tar.gz.gpg

このスクリプトはバックアップ用のストレージに既に storage.tar.gz.gpg というファイルがあることを仮定して書かれているので、初回実行の前にこの名前のテキストファイルなどを適当に作って置いておく必要があります{{< note 雑でごめん >}}。

storage.tar.gz.gpg という 1 バイトのダミーのファイルが置かれている
storage.tar.gz.gpg という 1 バイトのダミーのファイルが置かれている

これも systemd タイマーで定期的に自動実行します:

sudo vim /etc/systemd/system/backup_firefish_object_storage.service
sudo vim /etc/systemd/system/backup_firefish_object_storage.timer
sudo systemctl enable --now backup_firefish_object_storage.timer
# backup_firefish_object_storage.service
[Unit]
Description=Backup Firefish object storage
Requires=minio.service
After=minio.service network-online.target

[Service]
Type=oneshot
User=naskya
WorkingDirectory=/home/naskya/backup_firefish
ExecStart=/home/naskya/backup_firefish/object_storage.sh
# backup_firefish_object_storage.timer
[Unit]
Description=Firefish object storage hourly backup

[Timer]
OnCalendar=*-*-* *:20:00
Persistent=false

[Install]
WantedBy=timers.target

これでオブジェクトストレージのバックアップは完了です。

Redis のデータは消えてもジョブキューの内容とアンテナに引っ掛かった投稿とレートリミットの情報が飛ぶだけなのでバックアップはしません。

アップデート

Firefish のアップデート

アップデートの前には必ずサーバーを止め、バックアップを取りましょう。ダウンタイム無しのアップデートは難しいらしいです{{< note 私は興味が無いので方法を調べたことがありません。知ってたら教えてください(やらないけど) >}}。

今回インストールした Firefish のフレーバーはローリングリリースを採用しているので、気が向いたときにアップデートスクリプトを回せばよいです。

# サーバーを止めてメンテナンス中のページを出す
sudo systemctl stop firefish
caddy reload --config /etc/caddy/Maintenance.caddyfile

# firefish ユーザーになってアップデートする
sudo --user=firefish bash
cd ~
./update.sh  # 表示される指示に従う
exit

# メンテナンス中のページを出すのをやめてサーバーを起動する
caddy reload --config /etc/caddy/Caddyfile
sudo systemctl start firefish

本家 Firefish をお使いなら、新バージョンがリリースされたらアップデートしましょう。アップデートの部分のコマンドは例えば以下のようにするとよいです:

git pull --ff --no-edit --autostash --strategy-option theirs
corepack prepare pnpm@latest --activate
pnpm install --frozen-lockfile
NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run build
NODE_ENV='production' NODE_OPTIONS='--max_old_space_size=3072' pnpm run migrate

OS やその他のソフトウェアのアップデート

Arch Linux もローリングリリースを採用しているので、気が向いたときにインストールした他のプログラムと一緒にアップデートしましょう。

paru

これはなに (revisited)

なにこれ?

まず目次が長すぎて引きました。

サーバー管理なんて本当にやりたくないです。

でもまぁここまで整備しちゃえばメンテはめちゃ楽だしアップデートなんて何も怖くありません。

おしまい。

[See repo JSON]