このブログで使っているGhostを有料のマネージド版から自分でVPSサーバーを借りてセルフホスト版に移行したので、移行手順等を含めて記録しておきます。

モチベーション

短く言うと、自分に発破をかける意図でマネージドプランをやったけどどうにもうまくいってないので、一旦運営コストを下げるために移行しました。

それまでStarterプラン(年間108ドル)だったのが、APIさえも使えずかなり不便だったため、意を決してCreatorプラン(年間300ドル)に2024/3/25に移行していました。運用コストをかけることでしっかり回収するように自分を誘導したかったのですが、この1年を振り返るとなかなか継続的なアウトプットには繋がらなかったという感じで申し訳ないです。

ありがたいことに支援してくださっている方のおかげで300ドルはペイすることができました。2年目、気を引き締め直して継続するかどうかを悩んだのですが、プランの維持でいっぱいいっぱいよりも別のことに使えた方がベターだと考え、収支バランスを見直すことにしました。

もう1点、マネージドにすることでメンテナンスを省力化することができるというものがあったのですが、昨今はDockerのようなコンテナ技術によってセルフホストも格段に楽になったのも大きな理由の一つです。

VPSサーバー選定

自宅サーバーを色々いじっているので自宅でも良いかと思ったのですが、Stripeなどの決済が絡むので可用性の低い状態で運用するのは望ましくないと考えたのでサーバーを借りることにしました。

AWSやGCPなどのIaaSも検討しましたが、サービス規模的にはVPSで十分です。Ghostの場合はRAM1GBで借りている人が多かった印象なので、TokyoリージョンのあるVPSを中心に検討しましたが、どこも価格的には大きく違いはなく5ドルです。

Linodeはスペック面でも色々と魅力的だったのですが、なんとなくさくらインターネットの田中さんをタイムラインで見る機会が最近多いのでさくらのVPSにしようと思いました。2018年まで使っていたみたいなので7年ぶりです。

移行手順

さくらのVPSにdebian+docker環境でGhostをインストール

自宅サーバーで使っているProxmoxがdebianがデフォ、RasbianもdebianベースなのでVPSもdebianにして統一しました。

OSインストール時にGithubの公開鍵をインポートしてくれる機能があって、細かいところで便利じゃーんって思ったけどログインしたら鍵がなかった。一体どういうことだったんだろう…(いつものようにshellで入れた)

標準的なセットアップなので詳細は割愛。なお、さくらのVPSにはパケットフィルターのような機能が提供されているが、無効にしてOSレベルで設定しておいたほうがよいらしい。Dockerはこれを見ればOK。

Debian
Learn how to install Docker Engine on Debian. These instructions cover the different installation methods, how to uninstall, and next steps.

あとはDocker HubからGhostの公式イメージを入れる。

https://hub.docker.com/_/ghost

services:

  ghost:
    image: ghost:latest
    restart: always
    ports:
      - 8080:2368
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__user: root
      database__connection__password: example
      database__connection__database: ghost
      # this url value is just an example, and is likely wrong for your environment!
      url: http://localhost:8080
      # contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
      #NODE_ENV: development
    volumes:
      - ghost:/var/lib/ghost/content

  db:
    image: mysql:8.0
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - db:/var/lib/mysql

volumes:
  ghost:
  db:

imageをlatestにして、credentialsを適当に変えて docker compose up -dで立ち上がる。楽ちん。

多分手順的にはこの段階でHTTPS化しておいたほうがよい。(後述)

Ghostの環境ができたらmigration作業に入ります。

member以外のマイグレーション

以下のガイドはセルフホストGhostからGhost Pro(マネージド)への移行ですが参考になります。Ghostの管理画面からダウンロードできるものがほとんどですが、images/とmedia/の中の画像ファイルだけはGhostサポート経由じゃないといけないのでghost.ioのサイトアドレスを添えてサポートにメールを出せばOK(全部入りのバックアップzipがもらえます)。

How to migrate data from Ghost to Ghost
Everything you need to know about working with the Ghost professional publishing platform.

リモート先のdockerの中にファイルを送るのよく理解していなかったので調べながらやりました。

scp images.zip {さくらのVPS}:~/ghost/images.zip
scp media.zip {さくらのVPS}:~/ghost/media.zip

vpsにsshして

cd ghost

# 解凍
unzip '*.zip'
docker ps # コンテナ名確認 ここではghost-ghost-1

# Dockerに転送
docker cp images/ ghost-ghost-1:/var/lib/ghost/content/
docker cp media/ ghost-ghost-1:/var/lib/ghost/content/

# パーミッション変更
docker exec ghost-ghost-1 chown -R node:node /var/lib/ghost/content/images
docker exec ghost-ghost-1 chown -R node:node /var/lib/ghost/content/media

ここまでやると画像がちゃんと表示されるはず。

ドメイン切り替え & HTTPS化

memberのマイグレーションにはStripeの再接続が絡んでくるので、この段階でドメインを移行してHTTPS化しておくことにしました。

Cloudflareのドメインを使っているのですが、Ghost ProのときはProxyを使えなかったのが、セルフホストだと使用できるようです。といっても、モードはフル(Cloudflareとオリジンサーバーの間もTLS保護が必要)が必要なので、どちらにせよVPS上で証明書をいい感じにしないといけません。

一昔前ならCertbotを使ってLet's Encryptの自動更新設定などをしていたわけですが、今はOSSのCaddyを使って短いファイルをかけば終わりです。インストールもdocker-compose.ymlにCaddyを追加するだけ。楽ですね。

Caddyfileにはシンプルにwwwありとなしに設定。こう書いてしまうと証明書が2つ発行されてしまうようなのですが、Cloudflareでwwwからネイキッドドメインの方に転送をかけているからか、Caddyでもリダイレクトかけたらなんか挙動が怪しかったのでまぁいいかという感じです。

hanatane.net, www.hanatane.net {
    reverse_proxy localhost:8080
}

memberの移行

さて、memberの移行はStripeと接続しているメンバーがいる場合はちょっと難しいので手順を確認。

How to reinstall Ghost
Find out how to get access to new features by reinstalling Ghost so you can update to the latest major version.

まずはmemberをexportしておく。このままimportしようとしてもStripeと接続されているユーザーはimportできないので注意。

まず、旧サイトの方からStripeの接続を切る必要がある。接続を切るには有料メンバーを削除する必要がある。ドキュメントにあるようにstatusがpaidやcomplimentaryのメンバーを削除するとStripeから切断できるようになる。Stripeと切断してもStripeのサブスクリプションは継続される。

旧サイトのwebhookが残っているので忘れずに切断しておく。

旧サイトとStripeが切断できたので、管理UIの方で新サイトの方でStripeに再接続する。接続先は旧サイトと同じものを使う。

新サイトと接続ができたらmemberをimportする正常に有料メンバーもimportできていることを確認。

Mailgunの設定

ニュースレター機能のためにMailgunを設定します。Mailgunは無料で1日100通までなのでこれ以上の規模の場合は有料を検討します。現在の環境で大量送信するメールを到達させるにはMailgunのようなサービスを使うのが必要です。

Add payment info nowのチェックを外してユーザー登録すると無料で登録できます。

ドメインを1つ接続します。Mailgun用の適当のサブドメインを設定。DKIM keyを自動設定するオプションができていたので利用しました。

DNS設定が色々表示されるのでCloudflareの方に設定しておきます。しばらくするとCheckが押せるようになります。

Ghost側の設定は送信用のアドレスをMailgunのSMTPで作成しているものと一致させるのとdocker-compose.ymlに設定を追加しました。

Dockerで動かすGhostに Mailgunを使って mailの設定をする - Qiita
Ghost では mail の設定が必須です。後回しにしていてとても後悔したので、手順を書いておきます。Ghost での mail 設定について、公式では Maiilgunの使用が推奨されています。…

swapを追加

ここにきて動作がかなり不安定でCPU timeもかなり発生していたのでメモリ不足を疑った。調べてみるとGhost, MySQL, Caddyで1GBをいっぱいいっぱいで使用している。

RAM1GBでGhostを動かしているユーザーはほとんどSwapを使っていて、さくらのVPSでは初期設定でSwapが有効化されていないのでこれを有効化した。これはさくらのVPSのマニュアル通りでOKでした。

swapfileの追加
目次: 対象の標準OS, swapfileの追加- スワップが存在しない事を確認, スワップファイルを作成, スワップファイルの有効化.. 「さくらのVPS」の標準OSで swapfile を利…

Swap領域を作ってからは安定した。

Cloudflareのキャッシュ設定

Cloudflareのドメインを使っているので、Cloudflare周りをうまく設定するとパフォーマンスを無料で上げることができる。Ghost Proの方はこの辺のCDN設定も含まれているので、せっかくなのでできることはやっておこう。

CloudflareのDNS設定でProxyをオン、TLSのEncryptionモードはFull(Strict)にすることで、Cloudflareの恩恵をフルに受けつつGhostを正常に表示できる。

加えてキャッシュルールを以下の通りに従って設定する。

How to Optimize Cloudflare Caching for Blazing Fast Ghost Blog
Updated guide to get the best performance possible for your ghost blog with Cloudflare.

サイト変更時のWebhookでキャシュを削除するCloudflare Workersも設定しておく。

Autopurge CDN cache for Ghost blogs (Cloudflare / Bunny CDN)
In this post, we automate cache purging from CDNs for ghost blogs.

うまくエコシステムを活用できるなぁと思った。

今回採用しなかったもの

Pintura

Ghost Proの方ではPinturaという画像エディタを使っていますが、Ghost用のライフタイムプランが用意されています。ライフタイムといっても1年間のアップデートに限られているので何回か購入し直すような形になるでしょう。

iPhoneから直接アップロードして適当に切り抜いてと便利ではあるのですが、今回は一旦見送りました。

Cloudinary

GhostはAdminからメディアをアップロードしたときに自動で圧縮するわけではなく、テーマからの呼び出しに応じて必要なファイルを作成する方式のようです。よくiPhoneからUniversal Clipboard経由で画像をペーストするので、2MB以上のファイルがかなりあって、少ないSSD容量を圧迫していく恐れがあります。

GhostはCloudinaryのAdapterがあって、内部ストレージを使用せずにCloudinaryにアップロードできるようになります。色々な便利機能を差し置いても、単純にストレージ容量の節約になるのは良さそうだと思いました。

ただGhostの公式Dockerイメージをそのまま使えなくなるので更新周りの作業が重くなりそうだなと思って今回は見送りました。Cloudinaryを使う場合はGhost CLIを使うのが良さそうです。

Official Ghost + Cloudinary Integration
Use Cloudinary in tandem with Ghost either on-the-fly or with a full media library. Automate processing and optimisation of images & video. Find out how 👉
GitHub - eexit/ghost-storage-cloudinary: :rocket: A fully-featured and deeply tested Cloudinary Ghost storage adapter
:rocket: A fully-featured and deeply tested Cloudinary Ghost storage adapter - eexit/ghost-storage-cloudinary

感想

  • Dockerでめちゃくちゃ楽になった
  • Cloudflare以外を使う理由がないくらいに便利
  • サーバーをいじるのはやはり麻薬

支出の改善だけで終わらず、この1年は思ったようなアウトプットができなかったのをしっかり反省して次の1年にもっとうまく運用したいと思います。皆さん支援ありがとうございました。