このブログをセルフホストする構成――Directus、Astro SSR、k3s、Traefikを一枚につなぐ
2026-07-15
このブログをセルフホストする構成――Directus、Astro SSR、k3s、Traefikを一枚につなぐのカバー画像

ブログをセルフホストしたい人向けの記事ですが、実例はよそのサイトではありません。いまあなたが読んでいるこのページが、この記事で説明する構成のうえで配信されています。

このブログ(notes.midnightstops.com)は、ヘッドレスCMSのDirectusに記事を書き、AstroのSSRサーバが配信し、個人のk3sクラスタでTraefik越しに公開しています。アクセス解析もセルフホストのMatomoです。

WordPressを1コンテナ立てる話でも、GitHub Pagesに置く話でもありません。その分だけ運用は増えますが、CMS、配信、画像、問い合わせ、アクセス解析までを自分の管理下に置けます。

この記事では、現在の構成に加え、静的ビルド配信の仕組みを2年弱使ってからSSRへ作り直した理由と、移行時に実際に踏んだ罠を紹介します。公開に不要な内部名や認証情報は省略しています。

先に結論

現在の構成は、次のようになっています。

[読者のブラウザ]
      │ HTTPS
      ▼
[Traefik] ── TLS終端
      │
      ▼
[Astro SSR(Node.js)] ── リクエスト時にHTMLを生成
      │ クラスタ内Service
      ▼
[Directus] ── 記事・画像・問い合わせデータを管理
      ├─ PostgreSQL(本文・メタデータ)
      └─ Redis(キャッシュ)

画像:       ブラウザ → Traefik → Directusの公開アセット
アクセス解析: ブラウザ → Traefik → Matomo
問い合わせ:  Astroのサーバ側API → Capで検証 → Directusへ保存

ポイントは4つです。

  • 記事を書く=公開。 現行実装では、Directusで保存した内容が次のリクエストから反映されるため、記事更新のたびに再ビルドする必要がない
  • コードを変える=タグを付けてpush。 新しいイメージが作られると、Argo CD Image UpdaterとArgo CDがGitの変更履歴を介して本番へ反映する
  • 最初からこの形だったわけではない。「code-serverで編集→クラスタ内でビルド→nginxで静的配信」という構成を2年弱使い、2026年7月にSSRへ作り直した
  • SSRでは、静的サイト向けの検索、環境分岐、URL規約がそのままでは動かないことがある。後半では、実際に遭遇した3つの問題を紹介する

何に困っていたか――「書く場所」と「配信する場所」の距離

セルフホストでブログを運営する方法は、大きく3つに分けられます。

方式 書く→公開の距離
一体型CMS WordPress、Ghost 保存すれば公開できるが、CMSが配信も担う
静的サイトジェネレータ Hugo、Astroの静的出力 軽量に配信できるが、更新のたびにビルドが要る
ヘッドレスCMS+フロント Directus/Strapi+Astro/Next.js CMSと表示を分離できるが、接続部分は自分で設計する

私は当初から、DirectusとAstroを組み合わせていました。

記事を特定のブログエンジンだけで扱える形式に閉じ込めたくなかったことと、本文やメタデータをPostgreSQL上のデータとして管理したかったことが理由です。別のCMSへ移る場合にも移行作業は必要ですが、記事の正本を独自のバイナリ形式ではなく、自分のデータベースとして保持できます。

問題は、Astroをどのようにビルドし、配信するかでした。

旧構成――クラスタ内でkubectl execビルドしていた

2024年10月から2026年7月まで使っていた旧構成は、いま振り返るとかなり歪でした。

[code-server Pod] ← ブラウザからAstroプロジェクトを編集
      │ 共有ストレージ
[ビルドJob] ── code-server Pod内でpnpm buildを実行
      │ ビルド成果物
[nginx Pod] ── 静的ファイルを配信

Astroの編集環境として、ブラウザ版VS Codeであるcode-serverをクラスタ内に常駐させていました。コードや記事を変更した後はKubernetesのJobを実行し、そのJobからcode-serverのPod内でpnpm buildを動かします。生成された静的ファイルを共有ストレージへ置き、nginxから配信していました。

実際に長期間動いていましたが、問題も明確でした。

  • ビルド環境がPod内の状態に依存する。 node_modulesや設定が編集用Podの状態に依存し、リポジトリだけでは再現しにくい
  • 編集環境と配信環境が密結合になる。 編集用Pod、共有ストレージ、配信用nginxが一つの運用経路につながっていた
  • 記事の反映にもビルドが必要になる。 Directusで保存した後、公開サイトへ反映するためにJobを実行する必要があった

2026年7月11日にSSR版へ切り替え、翌日に旧構成を停止しました。旧データはすぐには削除せず、復旧に不要と判断できるまで保全しています。

新構成――DirectusとAstro SSRの分業

新構成では、AstroをSSRモードでビルドし、Node.jsサーバとしてコンテナイメージにしています。UIの一部にはSvelteを使っています。

役割分担は次のとおりです。

  • 記事・画像・問い合わせデータはDirectusで管理する。 AstroのSSRサーバは、クラスタ内のServiceを通じてDirectusから記事を取得する
  • 画像データはAstroが中継しない。 記事内の画像URLにはDirectusの公開アセットURLを使い、ブラウザがTraefik経由で直接取得する
  • HTMLの構造やデザインはAstroのコードで管理する。 コードはイメージに含まれるため、変更時にはビルドとデプロイが必要になる

その結果、「書く」と「コードを変える」が、別の経路になりました。

経路1: 記事を書く

Directusの管理画面で記事を書き、保存します。

SSRサーバはリクエスト時にDirectusからデータを取得するため、現行実装では次のアクセスから新しい内容が表示されます。旧構成で必要だった記事更新用のビルドJobはなくなりました。

経路2: コードを変える

  1. ローカルでAstroプロジェクトを変更し、semver形式のタグを付けてpushする
  2. BuildKitでコンテナイメージを作り、自前のレジストリへpushする
  3. Argo CD Image Updaterが新しいタグを検知し、デプロイ用Gitリポジトリのイメージタグを更新する
  4. Argo CDがGitの変更を本番へ同期する

Gitの正本には自前のForgejoを使い、外部のGitサービスにもミラーしています。人間が担当するのは、基本的にタグを付けてpushするところまでです。

デプロイ内容はGitのcommitとして残るため、ロールバックもイメージタグを以前の値へ戻す変更として扱えます。通常の更新でkubectlを直接操作する必要はありません。

地味だが効いている設定

manifest側では、次の点を重視しています。

  • ヘルスチェック専用のAPIを用意する。 SSRサーバでは、プロセスが動いていてもCMSへ接続できない状態が起こり得るため、startup、readiness、liveness probeから確認できるエンドポイントを設けている
  • コンテナを非root・読み取り専用で動かす。 Linux capabilityも不要なものは付与せず、一時ファイルだけを書き込み可能な領域へ置く
  • 更新時に旧Podを先に停止しない。 新しいPodの準備が完了してから切り替えることで、デプロイに伴う停止時間を避ける
  • DirectusのトークンやCAPTCHAの鍵をGitへ置かない。 OpenBaoとExternal Secretsを使い、実行時に注入する

認証情報の管理については、それだけで長くなるため別の記事に分けます。

SSRにして踏んだ3つの罠

SSRにすれば記事更新時の再ビルドは不要になります。しかし、静的サイト向けに作った機能が、そのまま動くとは限りません。

以下は、移行時に実際に遭遇した問題です。

罠1: 静的HTMLを前提とする検索では、SSRの記事を索引できない

旧構成のサイト内検索にはPagefindを使っていました。Pagefindは、ビルド後のHTMLを読み取り、静的な検索インデックスを生成する仕組みです。

SSR化後は、記事ページのHTMLがビルド成果物として並びません。そのため、検索UI自体は表示されても、記事がインデックスに含まれず、結果が空になります。

そこで、検索語をAstroのサーバ側APIへ送り、Directusを検索する方式へ変更しました。

sitemapやRSSのように「全記事を列挙する」処理も同様です。SSRでは、リクエスト時にCMSから記事一覧を取得して生成する実装が必要になります。

罠2: ビルド時のNODE_ENVで、本番用コードが消えた

Dockerfileのビルド段でNODE_ENV=developmentが設定されたままpnpm buildを実行していました。

その結果、クライアント側コードのimport.meta.env.PRODfalseとして静的に置き換えられ、次のような本番用分岐がビルド成果物から除外されました。

if (import.meta.env.PROD) {
  // 本番用の処理
}

症状は、検索機能が本番でも開発用のダミー応答を返し続けることでした。

ソースコードだけを見ても原因が分からなかったため、実際に配信されているJavaScriptを確認しました。ビルドキャッシュを無効化しても成果物が変わらないことから、ソースではなくビルド時の環境設定が原因だと切り分けられました。

罠3: 末尾スラッシュの規約が、自作APIにも影響した

このサイトでは、AstroをtrailingSlash: "always"で構成しています。

そのため、クライアントから末尾スラッシュなしで自作APIへアクセスすると404になり、末尾スラッシュを付けると正常に動くケースがありました。問い合わせフォームが本番環境だけで動かなかった原因は、このURLの不一致でした。

ページURLだけでなく、クライアントコードから呼び出すAPIのURLも、サイト全体の規約に合わせる必要があります。

共通する教訓

SSR化は、単に出力方式を変更する作業ではありません。

ビルド時に決まるものと、リクエスト時に決まるものの境界を引き直す作業です。

検索、一覧、RSS、sitemap、環境分岐、APIのURLなど、その境界に関わる機能は一つずつ確認する必要がありました。

アクセス解析もセルフホスト――Matomo

アクセス解析には、Matomoを同じクラスタで動かして使っています。

Google Analyticsを使わないのは、読者の行動データを外部の解析事業者へ送らず、自分の管理下に置くというブログの方針に合わせるためです。

個人ブログで確認したいのは、主に次のような情報です。

  • どの記事が入口になっているか
  • どの記事が継続的に読まれているか
  • 内部リンクから別の記事へ移動しているか

現在の用途では、Matomoで十分です。

JavaScriptを無効にしている読者については、<noscript>内の画像ビーコンでPVを補助的に記録しています。ただし、画像ビーコンではJavaScriptによる計測と同じ情報は取得できません。

また、広告ブロッカーなどで計測リクエスト自体が止められる場合もあります。そのため、表示される数字は実際の閲覧数より少なくなり得るものとして見ています。

運用と壊れ方

このブログは、高可用性を最優先にした構成ではありません。その代わり、どこが止まると何が起きるかを把握し、戻す経路を用意しています。

  • Directusが停止すると、新しいページを生成できない。 現在はリクエストごとに記事を取得しており、静的なフォールバックを持たないため、CMSの障害がブログ表示にも影響する。これはSSR化の最も大きな代償
  • AstroのPodは作り直せる。 記事データはPostgreSQL、イメージはコンテナレジストリ、manifestはGitにあるため、Pod自体は使い捨てにできる
  • デプロイの失敗はGitから戻す。 問題のあるイメージを配信した場合は、Git上のイメージタグを以前の値へ戻して再同期する
  • バックアップの中心はPostgreSQLと画像アセット。 manifestやイメージだけでは記事本文と画像は戻らないため、データとファイルを別々に保護する必要がある

SSRへ移行したことで更新は楽になりましたが、CMSの可用性がそのまま配信の可用性へ影響するようになりました。ここは、静的配信との明確なトレードオフです。

向いている人・向かない人

この構成が向いている人

  • すでにk3sやKubernetesを個人で運用しており、既存基盤へブログを追加したい人
  • 記事データを自分のPostgreSQL上で管理したい人
  • 記事を書く頻度が高く、CMSでの保存をすぐ公開へ反映したい人
  • GitOpsやコンテナの運用自体を楽しめる人

Traefik、Argo CD、データベース、監視などを他のアプリと共用できる環境なら、ブログを追加するための負担は比較的小さくなります。

この構成が向かない人

  • ブログのためだけにKubernetesを構築しようとしている人
  • CMSやビルドパイプラインの保守に時間を使いたくない人
  • 高い可用性が必要な人
  • インフラよりも、記事を書くことへ時間を集中したい人

ブログだけが目的なら、WordPressやGhostを単体で動かすか、静的サイトジェネレータとマネージドな配信先を組み合わせるほうが、運用は大幅に簡単です。

まとめ

このブログの更新経路は、最終的に次の2本へ整理されました。

  • Directusに記事を書けば、次のリクエストから公開内容へ反映される
  • コードへタグを付けてpushすれば、Gitの履歴を介してデプロイされる

ただし、ここへ至るまでには、クラスタ内の編集用Podでkubectl execビルドを行う構成を2年弱使い、SSR移行後には検索、環境変数、末尾スラッシュの問題を踏みました。

セルフホストブログでは、構成図の美しさだけでなく、書いてから公開されるまでの距離と、壊れた後に戻せるかが重要です。

現在の構成は、その2点を自分なりに両立させたものです。そして、いまあなたがこのページを読めていること自体が、現行構成で配信できている一つの実例になっています。

次に読む記事

このブログをセルフホストする構成――Directus、Astro SSR、k3s、Traefikを一枚につなぐ
http://notes.midnightstops.com/posts/36/
作者
kairo
公開日
2026-07-15