Hugoのサイトをセルフホストする
はじめに
このブログは元々 Cloudflare Pages で公開していました。
理由は、Forgejo Actions 上で生成した静的ファイルを自宅サーバーに転送する方法がわからなかったからです。
でも、せっかく自宅サーバーがあるんだから、セルフホストで公開したいと思っていました。
そこで、セルフホストの方法について色々調べてみたところ、rsync を使うと簡単にサーバーに転送できることがわかったため、調べながらやってみました。
環境
- ミニPC上にUbuntu Serverを構築済み (ホームラボ)
- ホームラボ上に Forgejo、Forgejo 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 に登録
- ユーザー公開鍵はクライアント側で生成し、公開鍵をサーバー上に登録する
- この鍵に対応する秘密鍵はクライアント側が持っている
- ユーザー公開鍵を authorized_keys に登録
- クライアント側
- ホスト公開鍵を known_hosts に登録 (IPアドレス・ドメインと一緒に登録しておく)
- サーバーの sshd が管理しているホスト公開鍵を登録する
- ホスト公開鍵を known_hosts に登録 (IPアドレス・ドメインと一緒に登録しておく)
接続時の流れ
- クライアントがサーバーにSSH接続を試みる。
- ホスト認証
- クライアントの known_hosts を確認し、接続しようとしているサーバーが本物かどうかをチェックする。
- サーバーからホスト公開鍵が送られてくる。
- known_hosts に接続しようとしているサーバーの情報がある場合、ホスト公開鍵が一致するのであれば、ユーザー認証に進む。
- known_hosts にサーバーの情報がない場合、接続していいかを確認し、yes ならユーザー認証に進む。no なら終了。
- これは対話式の場合の話で、もし CI などでやる場合には、この時点で終了となる。
- クライアントの known_hosts を確認し、接続しようとしているサーバーが本物かどうかをチェックする。
- ユーザー認証
- サーバーは、クライアントが持っているユーザー秘密鍵が authorized_keys に登録されているユーザー公開鍵に対応するものかチェックする
- チェックがOKであれば、無事接続できる。
また、今回設定するときは以下のようになる。 (後で詳細は説明します)
- Forgejo Actions側
secrets.SSH_KEY: ユーザー秘密鍵env.KNOWN_HOSTS: ホスト公開鍵
- サーバー側
/home/blog-deploy/.ssh/authorized_keys: ユーザー公開鍵/etc/ssh/ssh_host_*_key: ホスト秘密鍵
進め方
- デプロイ用ユーザーを作成
- 配信用ディレクトリを作成、権限設定
- Nginx のサービスを compose で建てる
- ユーザー認証用のSSHキーの生成
- Forgejo Actions の作成
実施ログ
1. デプロイ用ユーザーを作成
Forgejo Actions の rsync で転送するときに使うユーザーとして blog-deploy を作成する。
sudo adduser --disabled-password blog-deploySSHの鍵を使ったアクセスしかしないため、パスワードはなし (--disabled-password) で作成する。
作成できたか確認
$ cat /etc/passwd | grep blog-deploy
blog-deploy:x:1002:1002:,,,:/home/blog-deploy:/bin/bash2. 配信用ディレクトリを作成、権限設定
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/blog3. 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-stoppedcompose.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: truenginx/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 uphttp://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_deploy5-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/blogKNOWN_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@v4hugo のセットアップ
https://github.com/peaceiris/actions-hugo
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: ${{ env.HUGO_VERSION }}
extended: truego のインストール
https://github.com/actions/setup-go
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache-dependency-path: go.sumrsync のインストール
- 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: どのサーバーにどのユーザーで接続し、どこのパスに配置するかを指定する- 今回の場合は、
[email protected]:/var/www/blogに配置する
- 今回の場合は、
これで完了!
あとは、ブログ記事を書いて main ブランチに push すれば自動で公開される。
まとめ
セルフホストでブログ公開ができてよかった。
SSHの設定さえわかってしまえば、Forgejo Actions 上から rsync でホームラボにファイルを転送して、簡単にデプロイできることがわかった。
また、Forgejo Actions は GitHub Actions と同じものが使えるというのもありがたい。Forgejo 自体をセルフホストしているから、Privateなリポジトリで Actions をどれだけ回しても無料なのも良い。
これからは Hugo の設定とか諸々をやっていきたい。
リンク
- 静的サイトのセルフホストについて
- Self-hosting static websites :: TheOrangeOne
- このブログ記事を見て、セルフホストをしてみたいと思った。ここで rsync + SSH がおすすめだよって書いてあったからやってみるきっかけになった。
- Self-hosting static websites :: TheOrangeOne
- Forgejo Actions で SSH と rsync を使う
- GitHub ActionsでSSHを使う #GitHubActions - Qiita
- shimataro/ssh-key-action について知れた
- GitHub Actionsとrsyncでデプロイを自動化する|greencider(グリーンサイダー)
- shimataro/ssh-key-action について知れた
- GitHub ActionsでSSHを使う #GitHubActions - Qiita
- SSH について
- 入門OpenSSH / 第3章 OpenSSH のしくみ
- SSHの認証周りについての解説がわかりやすかった。
- OpenSSHのホスト鍵の確認方法(ssh-keygenが使えなくても何とか確認する) #サーバー認証 - Qiita
- 入門OpenSSH / 第3章 OpenSSH のしくみ