Skip to content
Hugoのサイトをセルフホストする

Hugoのサイトをセルフホストする

2026年6月13日

はじめに

このブログは元々 Cloudflare Pages で公開していました。
理由は、Forgejo Actions 上で生成した静的ファイルを自宅サーバーに転送する方法がわからなかったからです。

でも、せっかく自宅サーバーがあるんだから、セルフホストで公開したいと思っていました。

そこで、セルフホストの方法について色々調べてみたところ、rsync を使うと簡単にサーバーに転送できることがわかったため、調べながらやってみました。

環境

  • ミニPC上にUbuntu Serverを構築済み (ホームラボ)
  • ホームラボ上に ForgejoForgejo Runner (GitHub Actionsのようなもの) を構築済み
  • Cloudflare Tunnel でホームラボからサービスを公開可能 (Dockflareを利用)
  • Git リポジトリ
    • blog: ブログ記事を管理するリポジトリ
    • blog-infra: ブログ記事を公開するためのリポジトリ (この記事の中で作るもの)

仕組み

ホームラボ上で完結できるような構成にした。

  • ホームラボ上に Nginx のコンテナを建てて Cloudflare Tunnel で公開
    • ホームラボの /var/www/blog/usr/share/nginx/html にバインドマウントしておく。
  • Forgejo Actions の実行
    • main ブランチへの push で発火する。
    • hugo build を実行して静的ファイルを生成する。
    • 生成した静的ファイルを、SSH経由の rsync でサーバー上の /var/www/blog に転送する。
SSH周りについて調べた

SSH経由で rsync を使えるということはわかったけど、そもそもSSHの仕組みについて知らなかったため、まずはSSHについて調べることにした。

SSH接続する際の認証には、ホスト認証とユーザー認証の2ステップある。 そして、それぞれに鍵がある。(ホスト認証用の鍵ペア、ユーザー認証用の鍵ペア)

1. ホスト認証

  • 目的
    • 接続しようとしているサーバーが本物かどうかチェックする
    • クライアント側を守るため
    • サーバー側
      • ホスト秘密鍵
      • /etc/ssh/ssh_host_*_key にある (ls -al /etc/ssh/ で確認可能)
      • sshd が管理しているもの。
    • クライアント (Forgejo Actions)
      • ホスト公開鍵
      • ~/.ssh/known_hosts に、サーバー情報とホスト公開鍵のセットを登録しておく

2. ユーザー認証

  • 目的
    • 接続しようとしているユーザーがログインしていいかを確認する
    • サーバー側がチェックする
    • サーバー側
      • ユーザー公開鍵
      • /home/blog-deploy/.ssh/authorized_keys
        • (blog-deploy がSSHで接続されるユーザーとする。)
        • ユーザー秘密鍵に対応する公開鍵をあらかじめ記載しておく。
    • クライアント (Forgejo Actions)
      • ユーザー秘密鍵

SSH接続の時の流れを書いてみる。

まず、サーバー側とクライアント側で接続前の準備をしておく。

  • サーバー側
    • ユーザー公開鍵を authorized_keys に登録
      • ユーザー公開鍵はクライアント側で生成し、公開鍵をサーバー上に登録する
      • この鍵に対応する秘密鍵はクライアント側が持っている
  • クライアント側
    • ホスト公開鍵を known_hosts に登録 (IPアドレス・ドメインと一緒に登録しておく)
      • サーバーの sshd が管理しているホスト公開鍵を登録する

接続時の流れ

  1. クライアントがサーバーにSSH接続を試みる。
  2. ホスト認証
    1. クライアントの known_hosts を確認し、接続しようとしているサーバーが本物かどうかをチェックする。
      1. サーバーからホスト公開鍵が送られてくる。
      2. known_hosts に接続しようとしているサーバーの情報がある場合、ホスト公開鍵が一致するのであれば、ユーザー認証に進む。
      3. known_hosts にサーバーの情報がない場合、接続していいかを確認し、yes ならユーザー認証に進む。no なら終了。
        • これは対話式の場合の話で、もし CI などでやる場合には、この時点で終了となる。
  3. ユーザー認証
    1. サーバーは、クライアントが持っているユーザー秘密鍵が authorized_keys に登録されているユーザー公開鍵に対応するものかチェックする
    2. チェックがOKであれば、無事接続できる。

また、今回設定するときは以下のようになる。 (後で詳細は説明します)

  • Forgejo Actions側
    • secrets.SSH_KEY: ユーザー秘密鍵
    • env.KNOWN_HOSTS: ホスト公開鍵
  • サーバー側
    • /home/blog-deploy/.ssh/authorized_keys: ユーザー公開鍵
    • /etc/ssh/ssh_host_*_key: ホスト秘密鍵

進め方

  1. デプロイ用ユーザーを作成
  2. 配信用ディレクトリを作成、権限設定
  3. Nginx のサービスを compose で建てる
  4. ユーザー認証用のSSHキーの生成
  5. Forgejo Actions の作成

実施ログ

1. デプロイ用ユーザーを作成

Forgejo Actions の rsync で転送するときに使うユーザーとして blog-deploy を作成する。

sudo adduser --disabled-password blog-deploy

SSHの鍵を使ったアクセスしかしないため、パスワードはなし (--disabled-password) で作成する。

作成できたか確認

$ cat /etc/passwd | grep blog-deploy
blog-deploy:x:1002:1002:,,,:/home/blog-deploy:/bin/bash

2. 配信用ディレクトリを作成、権限設定

rsync の転送先となるディレクトリを作成し、blog-deployが触れるようにしておく。

sudo mkdir -p /var/www/blog

# 所有者、グループを変更
sudo chown blog-deploy:blog-deploy /var/www/blog

# 権限を変更 (所有者だけ書き込めるようにする)
sudo chmod 755 /var/www /var/www/blog

変更されたか確認 (755 になっていればOK)

$ ls -ld /var /var/www /var/www/blog
drwxr-xr-x 14 root        root        4096 Apr 12 18:51 /var
drwxr-xr-x  4 root        root        4096 Jun 12 12:54 /var/www
drwxr-xr-x  2 blog-deploy blog-deploy 4096 Jun 12 12:54 /var/www/blog

3. Nginx のサービスを compose で建てる

Nginx のサービスは「ブログ記事を管理しているリポジトリ」とは別に「ブログのインフラ用のリポジトリ」を新たに作成して管理することにした。
メリットとして、ブログ記事用のリポジトリと分けて管理することで、記事更新とサーバー構成の変更履歴を分けられるようになる。

blog-infra (仮) のリポジトリの中身はこんな感じになる。

$ tree
.
├── compose.prod.yaml
├── compose.yaml
├── example
│   ├── 404.html
│   └── index.html
└── nginx
    └── blog.conf

ローカルで起動の確認ができるように、example/ を作成した。

3-1. 各ファイルの作成

ファイルの中身

compose.yaml

services:
  blog-nginx:
    image: nginx:alpine
    container_name: blog-nginx
    ports:
      - "80:80"
    volumes:
      - ./example:/usr/share/nginx/html:ro
      - ./nginx/blog.conf:/etc/nginx/conf.d/default.conf:ro
    restart: unless-stopped

compose.prod.yaml

services:
  blog-nginx:
    # 外部には公開しない (192.168.3.6ではアクセスできないようにしている)。dockflare から参照できればいい。
    ports: !reset []
    expose:
      - "80"
    # dockflare で公開するためラベルをつけている
    labels:
      - "dockflare.enable=true"
      - "dockflare.hostname=tamago324.com"
      - "dockflare.service=http://blog-nginx:80"
      - "dockflare.access.group=public-default-bypass"
    networks:
      - cloudflare-net
    volumes:
      # /var/www/blog をバインドする
      - /var/www/blog:/usr/share/nginx/html:ro

networks:
  cloudflare-net:
    name: cloudflare-net
    external: true

nginx/blog.conf

root /usr/share/nginx/html;
index index.html;

error_page 404 /404.html;

location / {
    try_files $uri $uri/ =404;
}
  • hugo では layouts/404.html を作成すると public/404.html を作成してくれる。

example/index.html (動作確認用ファイル)

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Hello world</title>
  </head>
  <body>
    <main>
      <h1>Hello world</h1>
    </main>
  </body>
</html>

example/404.html (動作確認用ファイル)

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>404 Not Found</title>
  </head>
  <body>
    <main>
      <h1>404 Not Found</h1>
    </main>
  </body>
</html>

3-2. 試しにローカルで起動してみる

nginx が読めるように権限を変更しておく。

chmod 755 example

起動する。

docker compose up

http://localhost にアクセスして、Hello world が表示されればOK。

3-3. ホームラボ上で起動する

ホームラボ上ではマージして起動する。

docker compose -f compose.yml -f compose.prod.yml up -d --build

自分の場合は、komodo の Stacks で起動するようにした。
とりあえずホームラボ上で起動していればOK。

Dockflare によって cloudflared が設定されて https://tamago324.com に公開される。
Dockflare ステキ!

4. ユーザー認証用SSHキーの作成

Forgejo Actions でSSH接続する際に使用する鍵を作成し、ホームラボ側で接続の準備をする。

4-1. キーの作成

ホームラボ上で以下のコマンドを実行する。

ssh-keygen -t ed25519 -C "forgejo-actions-blog-deploy" -f forgejo_actions_blog_deploy

それぞれ以下のように使う想定。

  • 公開鍵: サーバー側
  • 秘密鍵: Forgejo Actions 側で使用

4-2. 公開鍵をサーバーの blog-deploy ユーザーのホームディレクトリ配下に配置する

これによって、blog-deploy ユーザーでホームラボにSSHで接続できるようになる。

sudo -u blog-deploy mkdir -p /home/blog-deploy/.ssh
sudo chmod 700 /home/blog-deploy/.ssh
sudo tee -a /home/blog-deploy/.ssh/authorized_keys < forgejo_actions_blog_deploy.pub
sudo chmod 600 /home/blog-deploy/.ssh/authorized_keys
sudo chown -R blog-deploy:blog-deploy /home/blog-deploy/.ssh
rm forgejo_actions_blog_deploy.pub
  • .ssh/ ディレクトリ
    • /home/blog-deploy/.ssh を作成し、他のユーザーが読めないようにする
  • .ssh/authorized_keys ファイル
    • forgejo_actions_blog_deploy.pub の鍵に対応する秘密鍵を持っているクライアントしかログインできないようにする。
    • authorized_keys の権限は sshd の推奨している権限の 600 に変更しておく
  • forgejo_actions_blog_deploy.pub
    • 公開鍵を登録できたため、forgejo_actions_blog_deploy.pub は削除しておく。

5. Forgejo Actions の設定

ブログ記事を管理しているリポジトリの Forgejo Actions を作成する。

5-1. Secrets を設定する

SSHの秘密鍵は Forgejo Actions の secrets に設定する。

SSH_KEY   # 4-1 で作成した秘密鍵を設定する

secrets に設定したら、ホームラボ上にある秘密鍵は不要なため、削除しておく。

rm forgejo_actions_blog_deploy

5-2. Forgejo Actions を書く

Forgejo Actions に設定する env の整理

サーバーの設定などはワークフローの env に設定するが、一度整理しておく。

KNOWN_HOSTS   # 接続先のサーバーの host key (ipアドレス、ホスト公開鍵を記載する)
SSH_HOST      # 192.168.3.6
SSH_PORT      # 22
SSH_USER      # blog-deploy
DEPLOY_PATH   # /var/www/blog

KNOWN_HOSTS については、以下のようにして ssh-keyscan を使って確認する。 (ホームラボのIPアドレスが 192.168.3.6)

$ ssh-keyscan -t ed25519 -p 22 192.168.3.6
# 192.168.3.6:22 SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.16
192.168.3.6 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKOMjqGZ6QU+ARfVpCkMRIG/0XBQc5VsNaOaJ6GD1vQT

.forgejo/workflows/deploy.yml を作成する

# .forgejo/workflows/deploy.yml
name: Deploy

# ワークフローごとに最大1つまで
concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  TZ: Asia/Tokyo
  HUGO_VERSION: 0.161.1
  # deploy の設定
  KNOWN_HOSTS: 192.168.3.6 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKOMjqGZ6QU+ARfVpCkMRIG/0XBQc5VsNaOaJ6GD1vQT
  SSH_HOST: 192.168.3.6
  SSH_PORT: "22"
  SSH_USER: blog-deploy
  DEPLOY_PATH: /var/www/blog

jobs:
  deploy:
    name: Hugo Deploy
    runs-on: ubuntu-latest

    # リポジトリ内への変更をできないようにしておく
    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v4

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: ${{ env.HUGO_VERSION }}
          extended: true

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache-dependency-path: go.sum

      - name: Install rsync
        run: |
          apt-get update -y
          apt-get install -y rsync

      - name: Build
        run: hugo --gc --minify --environment production

      - name: Setup SSH
        uses: shimataro/ssh-key-action@v2
        with:
          key: ${{ secrets.SSH_KEY }}
          known_hosts: ${{ env.KNOWN_HOSTS }}
          if_key_exists: replace

      - name: Deploy
        run: |
          rsync -az --delete \
            -e "ssh -p ${SSH_PORT}" \
            ./public/ \
            "${SSH_USER}@${SSH_HOST}:${DEPLOY_PATH}/"
workflowの詳細を見ていく

ワークフローの名前は Deploy

name: Deploy

ワークフローはMAX1つまでの実行

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

また、新しいワークフローの実行が生成された場合は、前の実行はキャンセルする。

イベント

on:
  push:
    branches: [main]
  workflow_dispatch:

以下のいずれかで、実行される。

  • main ブランチにプッシュ
  • 手動実行

環境変数

env:
  TZ: Asia/Tokyo
  HUGO_VERSION: 0.161.1
  # deploy の設定
  KNOWN_HOSTS: 192.168.3.6 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKOMjqGZ6QU+ARfVpCkMRIG/0XBQc5VsNaOaJ6GD1vQT
  SSH_HOST: 192.168.3.6
  SSH_PORT: "22"
  SSH_USER: blog-deploy
  DEPLOY_PATH: /var/www/blog

なぜ、TZ が必要なのか?
→ hugo のビルド時に日付の生成をしたりするときに TZ が関わってくるため。

ジョブ

jobs:
  deploy:
    name: Hugo Deploy
    runs-on: ubuntu-latest

    permissions:
      contents: read
  • ubuntu-latest で実行する
  • permissions.contents に read をつけておくことで、コミットしたり tag を作成したり、リポジトリ内の変更をしたりできないようにできる。

チェックアウト

      - uses: actions/checkout@v4

hugo のセットアップ
https://github.com/peaceiris/actions-hugo

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: ${{ env.HUGO_VERSION }}
          extended: true

go のインストール
https://github.com/actions/setup-go

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod
          cache-dependency-path: go.sum

rsync のインストール

      - name: Install rsync
        run: |
          apt-get update -y
          apt-get install -y rsync

ビルド

      - name: Build
        run: hugo --gc --minify --environment production
  • –gc
    • 不要になったキャッシュなどを削除する
  • –minify
    • 空白や改行を削除して、容量を小さくする最適化をする
  • –environment production
    • production 用としてビルドを実行する。
    • hugo の変数で production かどうかのチェックをしている場合に影響するため一応つけておく

SSHのセットアップ
https://github.com/shimataro/ssh-key-action

      - name: Setup SSH
        uses: shimataro/ssh-key-action@v2
        with:
          key: ${{ secrets.SSH_KEY }}
          known_hosts: ${{ env.KNOWN_HOSTS }}
          if_key_exists: replace

~/.ssh にSSHキーをセットアップするための actionsで、rsync で使う。

  • with
    • key: SSHの秘密鍵 (secrets に設定)
    • known_hosts: 接続する想定のサーバーの情報 (IP、ホスト公開鍵 (ssh-keyscan で取得できるものを設定))
    • if_key_exists: 秘密鍵がすでにあった場合にどうするか。

ファイルの配置

      - name: Deploy
        run: |
          rsync -az --delete \
            -e "ssh -p ${SSH_PORT}" \
            ./public/ \
            "${SSH_USER}@${SSH_HOST}:${DEPLOY_PATH}/"
  • -az
    • -a: アーカイブモード (ファイル構造とかファイルの情報とかを保ったまま転送するモード)
    • -z: データ転送時に圧縮を使う
  • --delete: 存在しないものは削除する
  • ssh -p {port}: 接続するときは ssh を使う
  • ./public/ にあるものを指定のパスに配置する
  • user@host:path: どのサーバーにどのユーザーで接続し、どこのパスに配置するかを指定する

これで完了!

あとは、ブログ記事を書いて main ブランチに push すれば自動で公開される。

まとめ

セルフホストでブログ公開ができてよかった。

SSHの設定さえわかってしまえば、Forgejo Actions 上から rsync でホームラボにファイルを転送して、簡単にデプロイできることがわかった。
また、Forgejo Actions は GitHub Actions と同じものが使えるというのもありがたい。Forgejo 自体をセルフホストしているから、Privateなリポジトリで Actions をどれだけ回しても無料なのも良い。

これからは Hugo の設定とか諸々をやっていきたい。

リンク

Last updated on