TREKひとつのつもりが、20個のスタックに

TREKひとつのつもりが、20個のスタックに

GitHub Trendingで旅行計画ツールを見つけ、自分でも動かしたくなりました。どうせ新しいVPSを借りるなら、ついでにもう少し、いや、だいぶ色々入れてしまおうと。

先日GitHub TrendingでTREKというプロジェクトを見つけました。みんなで同じ地図上にルートを組み、宿を決め、予算を立てて、そのままほかの人に渡して編集を続けてもらえるツールです。デモサイトを触ってみると、とてもよくできていました。

彼女との旅行計画はずっとObsidianで継ぎはぎしていました。いくつものdaily noteをリンクで行き来しながら書くうちにどんどん散らかり、出発当日でも「午前はここ、午後はあそこ」程度の大ざっぱな予定のままです。作者のデモをそのまま使うこともできますが、旅程を他人のマシンに置くのは少し落ち着きません。いずれは自分でホストすることになります。

自分でホストするなら、いっそ新しいVPSを借りることにしました。

Netcup ARM

VPS選びには少し悩みました。DigitalOceanとLinodeは同等構成で倍以上するので除外。Hetznerはコスパがいいものの、ARMの拠点を柔軟に選べません。中国国内の小規模業者は安いですが、ICP届出が厄介ですし、帯域も悲しくなるほど細いです。一周して、結局Netcupにしました。

選んだのはVPS 2000 ARM G11。10 vCore、メモリ16G、512G NVMe、2.5 Gbpsで、税込月額€13.41です。同じ構成をx86で組むと少なくとも倍はかかります。転送量はflatrateで、24時間の移動平均が2 TBを超えたときにだけ200 Mbpsへ制限されます。私の小さなサイトでは到底届きません。

注文前にGoogleで探すと50%オフのクーポンが見つかり、初回は€6.7ちょっとになりました。更新時は通常価格ですが、これはお得です。

拠点はニュルンベルク、ウィーン、アムステルダム、米国東海岸のマナサスの4つ。中国国内からの一般的な経路では欧州3拠点よりpingが低かったので、マナサスにしました。メール用IPの評判については、送信をすべて後述のResend HTTPSブリッジ経由にしていて、ローカルIPから配信しません。そのために欧州へこだわる必要はありませんでした。

ゼロからTREKまで

注文からログイン情報が届くまで約5分でした。最初にやったのはインストールではなくSSHの制限です。rootログイン禁止、パスワード認証禁止、ポート変更、ufwの受信デフォルト拒否。fail2banを2日ほど動かしてログを見ると、22番ポートを変える前は1日5,000回以上の総当たり攻撃がありました。

ここ2年ほどでARM64イメージの環境はずいぶんよくなりました。あとから入れた20個あまりのスタックのうち、Caddy、Postgres、Stalwart、SnappyMail、Open WebUI、Vaultwarden、Dagu、Glance、Karakeep、Paperlessにはすべて公式のマルチアーキテクチャイメージがあり、docker pullでそのまま動きます。少し手間だったのはForgejo runnerだけでした。ベースイメージはARM64対応ですが、runner内でdocker composeをデプロイツールとして使いたかったので、docker-cliとdocker-compose-pluginを自分で追加しました。最後はbuildxでARM64イメージを自分のForgejo registryへpushし、runner起動時にpullするようにしました。

ようやくTREK本体です。mauriceboe/TREKには公式のdocker-compose.ymlがあるので、コピーして数行変えました。

  • ポートはwebというDockerネットワーク内だけから見えるようにする。
  • ENCRYPTION_KEYはopenssl rand -base64 32で新規生成。
  • DATABASE_URLはSQLite。自分用なら十分です。
  • external networkのwebを追加してCaddyとつなぐ。

Caddyfileはreverse_proxy trek:3000の1行で完了。docker compose up -dからtrek.shinya.clickのログイン画面が出るまで、15分もかかりませんでした。

最初のアカウントを登録して空のダッシュボードを眺めたとき、反射的に考えたのは旅程をどこから作るかではなく、このマシンにほかに何を詰め込めるかでした。

20個あまりのスタック

深夜の作業は、たぶんこの思いつきから始まりました。~~手が勝手に。~~どうせ性能には余裕があるし、遊ばせておくのも何なので、前からセルフホストしたかったものを全部載せることにしました。

翌朝起きて、その流れでコードホスティング用にForgejoを入れ、ブログも作り直しました。これはあとで詳しく書きます。メールはStalwart、SnappyMail、自作Resend bridgeの3点セットです。AI系はCLIProxyAPIでOpenAI、Claude、Geminiのサブスクリプションをまとめ、画面はOpen WebUIにしました。その後はパスワード管理、オンラインストレージ、画像ホスティング、ノート、あとで読む、RSS、PDFツール集と、セルフホストできるものはほぼ一通り試しました。SSOはAuthelia。バックアップはDaguのDAGで毎日暗号化スナップショットをR2に送ります。

少しずつ入れ終えると、全体はこんな構成になりました。

描いた時点ではブログを入れていませんでした。今ならさらに詰まっています

入口はCloudflare SaaSのスマートDNS振り分けでオリジンへ接続します。Caddyだけを外向きのTLS終端にし、Hostヘッダーに応じて各コンテナへリバースプロキシします。ログインが必要なサービスはすべてAuthelia forward_auth配下です。送信メールはResendのHTTPS APIブリッジを通し、Netcupの25番ポート制限を回避しています。

Dockgeの画面では20個あまりのスタックが全部緑になっています。もちろん、かなりの部分はデプロイが楽しくなって歯止めが利かなくなった結果です。最初はTREKが欲しかっただけなのに、最後には図の小さな1マスに収まっていました。

ブログの移行

以前のブログはAstro + retypesetで、MarkdownをpushするとCIがビルド・公開する構成でした。1年以上大きな問題はありませんでしたが、気になる点はありました。誤字を1つ直すだけでもcommit、push、build待ち。リンク集や独立ページを追加するにもファイルツリーをいじり直す必要があります。旅行記の写真が増えると、ビルドもとんでもなく遅くなりました。

せっかくなので、この機会に作り直しました。

新しい構成はSvelteKit 2 + adapter-node SSR + UnoCSSです。見た目はretypesetを引き継ぎ、スタイルをUnoCSSで書き直しました。コンテンツのバックエンドはPocketBaseへ変更し、7つのcollectionで記事、タグ、旅行記、旅行記の日程、旅行記の写真、独立ページ、リンク集をSQLiteへ収めました。i18nはzh / en / jaの3言語併存です。Astroから書き出したMarkdownは移行スクリプトでまとめてPBへ流し込み、123記事、4旅行記、148件の旅行写真レコードに加えてpagesとfriendsもすべてDBに入りました。

デプロイはブルーグリーン方式にしました。blog-blueとblog-greenの2コンテナを常駐させ、稼働側を/opt/app/blog/activeに記録します。Caddyの転送先は/opt/app/caddy-blog/upstream.caddyからimportします。CIが新イメージをpushしたらswitch.shを実行し、切り替え先の色を起動、healthcheck待ち、importファイル変更、caddy reloadで通信切り替え。失敗時は自動ロールバックです。これで公開のたびに3分のビルドを待つ必要がなくなり、PBの管理画面で編集して保存すれば即反映されます。

管理画面はSvelteKit内のSPAで、/adminに置き、Authelia forward_authを通しています。管理画面からPBへの書き込みはCaddyの同一オリジンのリバースプロキシ/api/pb/*を経由し、トークンはCaddyが挿入します。ブラウザーにもGitリポジトリにもキーを渡しません。PBへの書き込みでJSフックが動き、ブログコンテナ内部のエンドポイントへPOSTしてサーバーキャッシュを無効化し、さらにCloudflare APIでエッジキャッシュをpurgeします。公開ページは引き続き、大半がCDNのキャッシュから配信されます。

移行全体は2晩で終わりました。

あとがき

何日もいじったのに、TREKではまだ1つも旅程を作っていません = =

中国本土向けMacでApple Intelligenceを使う:地域変更からChatGPTの取り出しまで
Microsoft Flight Simulator 2024をあれこれ試す