Contents

自宅鯖で Hugo の爆速デプロイを実現した

Vercel とか使えばいいじゃんって?うーんまあそうなんだけど……。なんかタイムラグは少ないほうが嬉しいじゃん?

経緯

この話はそもそもお前何回ブログ立てんねんって話から始まります。

古くは blog.kb10uy.org のドメインで October を運用してたりしました。あのころの Laravel は良かった…… その後何やかんやがあって(たしか October 本体の運用が面倒だったとかそんなんだったと思う)、2018 年ぐらいにはてなブログの壁ツェーンに移行しました。ここにはまあ~怪文書がたくさん置いてあります。

しかしはてなブログも完璧なソリューションだったかというとそんなことはなく、微妙につらい部分に目をつぶりつつ使ってきました。 Pro にアップグレードすれば自前のドメインを割り当てたり広告を消すことはできますが、僕がここで問題にしたいのは どちらかというと執筆時の体験です。一番つらかったのは Markdown モードで書いてるせいなのかプレビューにやたら時間がかかっていたところです。これを解決したい。

やはり自前で持つしかないか……。そしてこの記事、このブログに至るわけです。

ソフトウェア選定

というわけで何を使ってブログをやっていくかというのを改めて考えることにします。おおまかな要件としては次のとおり。

  • Rust か Go で書かれていること
    • Docker コンテナに押し込めやすいのでこれはかなり重要
    • 最近書いてるほうの言語ではあるので PR とか立てるのも他の言語よりはやりやすい
  • プレビューが常識的な速度で出力される
    • はてなブログを使い続ける上でかなり悩ましかったポイントなのでここは重視したい
      • 数千文字の分量でも 5 秒ぐらいかかってた
    • ms オーダーで更新できると理想的
  • WYSIWYG ではないこと
    • 個人的な好み
    • Notion スタイルのも今回は除外するものとする

選外: WordPress

PHP なので選外。 1 年以上かけてようやく PHP への依存を(kbs3 以外)引っぺがしたのにまた依存したら意味ないので……。 同様の理由で PHP ベースのものはないものとして扱いました。

脱落: WriteFreely

Go で書かれているかなりシンプルなブログプラットフォーム。 ActivityPub に対応しているので Fediverse に接続して Mastodon などから投稿をフォローすることができるというちょっと面白いものです。

公式サイトを見た感じではなかなか良さそうだったので試験的に立てて使ってみました。執筆体験のところはプレビューは出ませんが、タイトルを含めた記事全体を素の Markdown で書けてすぐに反映できるのでこれは気になりません。 しかし一覧性がよくない、目次が生成できない、カスタム CSS がいまいち当てづらいなどの理由によりやめてしまいました。でも丼に書くには長すぎるコンテンツを流すにはいい設計だと思います。

選外: Plume

Rust で書かれているブログプラットフォーム。 WriteFreely 同様 ActivityPub に対応。

WriteFreely よりかは自分が求めている機能がありそうでしたが、

Currently, Plume developers have less time and Plume is not actively maintained. New features may take time to be implemented. Could you consider similar purpose software: WriteFreely, WordPress with ActivityPub plugin and so on?

という状態らしいので選外となりました。

ここまできて自分が割とアウトラインの豊富さを暗黙的に要求していることに気付きました。

選外: Hexo

HEAVIEST OBJECTS IN THE UNIVERSE

対抗馬: Zola

Rust で書かれている SSG 。まあ 後述する Hugo の Rust 版みたいなものです。

かなり良いんですがテーマの選択肢が少なかったので惜しくも選外となりました。自分で書けばいいだろという話ではあるんですが……

採用: Hugo

大人気 SSG フレームワーク。特に説明は不要でしょう。

サポートしている Markdown の拡張機能の幅が広く、大量に既存テーマの選択肢があるのもいいですね。 というわけで採用です。

デプロイ構成

ところでなぜ Hugo より WriteFreely を先に試したのか気になった方もいるのではないでしょうか?普通 SSG を先にもってくるだろと。

なぜかというと「SSG でやると書かなくなると思ったから」です。僕は過去に何度か SSG を使ってポートフォリオページやブログっぽいものを書いていたことがあったのですが、全部リポジトリが消失するかデプロイが面倒という理由で放棄しています。 後者はともかく前者はありえねえだろと思うかもしれないですが実際消失してしまったもんは仕方ありません。 「デプロイが面倒」というのも今なら Netlify や Vercel のようなホスティングサービスがありますから、それを使えば特段面倒というわけでもありませんが、Netlify の画像配信の遅さにキレた過去があり Vercel に前向きになれないという事情が存在しています。

逆にいえばこれらを解決できれば SSG を使ってもいいじゃんという話です。そこでなんとかしてこれらを解決できないかというのを考えます。

リポジトリの管理

ちゃんとリモートに push しろよ、ということで GitHub に置いておく案がまず上がります。が、静的ホスティングサービスを使わない以上わざわざアメリカまでデータを飛ばす必要があるかというと、ないんですよ。というわけで GitHub には置きません。 代わりにこれまたセルフホストしているGitea インスタンスに置いておくことにしました。デプロイ先のサーバーも自宅鯖なのでリモートではなく消失の危険がややありますがそこには目をつぶることにします。

デプロイの発火

Gitea に push するとなるとどうやってデプロイ発火をかけるか、という問題が発生します。Gitea には GitHub Actions のような機能は実装されていないので、push のタイミングを知る手段としては Webhook ぐらいしかありません。 つまり、自宅鯖自身に Webhook リクエストを投げてデプロイスクリプトを起動してもらう必要があります。どうすればよいでしょうね?

……とお悩みのそこのあなた!adnanh/webhook を使えば Webhook で任意のコマンドを発火できますよ!! トリガー時の検証項目にペイロードのハッシュ値チェックも利用できるからセキュリティーも安心!!

はい、これで解決しましたね。めでたしめでたし。あとはやるだけだ。

手順

というわけでここからは実際の手順を紹介していきます。

1. adnanh/webhook の準備

まず webhook server を動かしておく必要があるので準備します。これも Go で書かれているのでバイナリを落として適当なディレクトリに配置し、systemd unit を書きます。

[Unit]
Description = Webhook daemon
After = network.target

[Service]
Type = simple
WorkingDirectory = /var/www
ExecStart = /var/www/data/webhook/webhook --hooks /var/www/data/webhook/hooks.json --port 8079 --verbose
User = kb10uy
Group = kb10uy

[Install]
WantedBy = multi-user.target
スケベなタイポに御用心

multi-user (マルチユーザー) を mutli-user (ムッツリユーザー) にタイポしないように注意しましょう。

え?そんなタイポするのはお前だけだって?ハッハッハ

無事起動したら、Hook Definition を参考に Webhook 定義を JSON で書いていきます。 Gitea は Webhook ペイロードの HMAC-SHA256 ハッシュをヘッダで送ってきてくれるので、それを見て実際に一致するかをチェックしていきます。みなさんが使う場合は secret を変えるのを忘れずに。

[
    {
        "id": "deploy-blog",
        "command-working-directory": "/var/www/data/blog",
        "execute-command": "/var/www/data/blog/build.sh",
        "trigger-rule": {
            "match": {
                "type": "payload-hmac-sha256",
                "secret": "secretkey",
                "parameter": {
                    "source": "header",
                    "name": "X-Gitea-Signature"
                }
            }
        }
    }
]
execute-command フィールドはフルパスで
正しく shebang や実行権限を設定していてもここをカレントディレクトリからの相対パスで書くと失敗するっぽいです。

2. Webhook の登録

Gitea のリポジトリの Webhook を設定します。

/posts/selfhosted-hugo-pipeline/gitea-webhook.png
Gitea の Webhook 追加画面

Docker コンテナで動かしている都合上 Webhook URL の先は Docker ホストになっている自宅鯖に向ける必要があるため、docker-compose.yml の extra_hosts に書いて対処しています。

Gitea 1.16 での破壊的変更について
Gitea 1.16 で Webhook でアクセス可能なホストのデフォルト設定が変わっています。具体的には 1.15 までは全てのホストにアクセス可能でしたが、 1.16 以降のデフォルトは external です。

テスト配信で 200 OK が帰ってくるのを確認できたら完成です。

3. push する

あとはもう push したら 1 秒ぐらいで更新されるはずです。よかったね。

感想

adnanh/webhook を見つけられたのがデカかった。これがなかったらこの計画も頓挫していたことでしょう。 ありがとう adnanh 。