Kamal 2 を使い、インフラに詳しくない人でもNext.jsを296円のVPSにデプロイできるよう、説明してみる

2024/10/02に公開
6

Kamalシリーズ

Kamalについては他にも記事を書いていますので、ご覧ください

はじめに

9月26, 27日に開催され、めちゃくちゃ盛り上がったRails World 2024でKamal 2が発表されました。Kamal 2はRuby on Railsを作った37signals社が、自社の人気サービスをデプロイするのに使用しているツールです。

37signals社はAWS等のクラウドに年間で$3,201,564を使った(2022年: 日本円で4.5億円ほど)らしく、一方でAWSを使ってもインフラ系人員の削減もほとんどできなかったので、全然割に合わないからもうクラウドはやめて自分たちのサーバを使うと宣言しています。Kamal 2はそのために作られた、本格的なデプロイツールです。

(ちなみに自分たちのサーバを使うと言ってもオンプレミスでサーバを運用するというのではなく、さくらの専用サーバに近いイメージです)

  • Dockerベース
    • Docker化されたアプリなら何にでも対応 (Railsだけじゃない!)
    • Dockerさえインストール可能なら、どんなLinuxサーバにもデプロイ可能 (Dockerは自動インストールしてくれる)
  • ゼロダウンタイムデプロイなど、本格的なサービスに対応したデプロイ機能
  • データベース等の付随サービスもKamalが管理し、サーバのDocker上にデプロイされる

Ruby on RailsをKamal 2でデプロイする記事は別途書きましたので、今回はNext.jsをデプロイします。しかも月額たった296円(税込)のVPSサーバ(ConoHa VPS)を使います!

本記事の目標

  1. 商用・個人開発の如何に関わらず、月額定額・全部込みの明朗会計のサービスだけを使い、安心して複数のプロジェクトを公開できること(高額請求の心配がない)
  2. インフラに詳しくないエンジニアやデザイナーが、自分でレンタルVPSをレンタル・セットアップし、Next.jsをデプロイできること
  3. インフラを触って勉強するきっかけになること

前提

以下のものがローカルマシンにすでにインストールされていて、基本的な使っ方ができることが前提になります。Next.jsの開発をしているのならば、3つともインストール済みではないかと思います。

  1. Node.jsおよびNPM
  2. Git
  3. Docker

もし使ったことがなくても非常に基本的な使い方しかしませんので、チュートリアルをやっていただければと思います。

作るもの

基本的なものですが、1時間あたり数千ユーザは捌ける構成です。

左から順番に解説します

  1. インターネットに接続したブラウザからリクエストが来ると、まず最初にVPSプロバイダ(今回はConoha VPS)が用意したファイヤーウォール(Conohaではセキュリティグループと呼びます)に届きます
    1. ここでport 22(SSH), 80(HTTP), 443(HTTPS)向けのトラフィックのみを通します
    2. つまり、外部からアクセスできるのはSSH (外部からターミナル接続できる機能), HTTP (暗号化なしの"http://...")接続機能, HTTPS (暗号化ありの"https://...")接続機能だけに制限します。これはセキュリティ上必須です
  2. ファイヤーウォールを通過したリクエストはKamal Proxyに届きます
    1. Kamal-Proxyに来たリクエストは、URLのホスト部分("app1.example.com"のところ)で振り分けられ、対応するNext.jsアプリのContainerに送られます
    2. Next.jsアプリが複数ある場合は、URLのホスト部分に応じて振り分けられます
    3. Kamal-Proxyのもう1つ大事な機能は、TLS証明書の自動取得・更新です。これがないとHTTPS接続ができず、セキュリティ上の問題になります。一般には有料でかつ複雑なセットアップが必要ですが、Kamalは無料で全自動でやってくれます
  3. Kamal-Proxyによって振り分けられたリクエストは、適切なNext.jsアプリのContainerに届きます
    1. Next.jsアプリのContainerは、皆さんがローカルのマシンでnpm run build, npm run startをやるのとほぼ同じ箇所です。つまりアプリの本体です
    2. コードを書き換えてデプロイするときは、ここだけがスムーズに新旧交代します

デプロイ時の流れ

デプロイは複雑なので完全に理解する必要はありません。以後の設定ファイル編集等に登場する3人の役者、つまりローカルマシン、DockerHub、そしてサーバ(VPS)が存在することだけでも把握していただければと思います。

  1. 「Docker Imageをbuildする」の箇所は、Docker開発をしている人なら馴染んでいるdocker compose buildもしくはdocker build .コマンドに相当します。
    1. Docker Imageは、Next.jsを起動するために必要なOS(Linux)、Node.js、NPM、Next.jsフレームワーク、自分自身が書いたコードをすべてまとめたものです。パソコンのドライブに相当するものです
    2. コードを変更するたびにDocker Imageを作り替えます。それが「Docker Imageをbuildする」ステップです
  2. 「DockerHubにアップロードする」の箇所は、Docker Imageをサーバに配信する準備段階です
    1. 本格運用をすると、サーバは何十、何百台になったりします。Docker Imageはそのすべてのサーバに届ける必要があります。さすがにローカルマシンから直接配信するのは心配です
    2. DockerHubはDocker Imageを保管するRepositoryです。何十、何百のサーバはそれぞれDockerHubにアクセスし、Docker Imageを要求すれば良いのです。そのために「DockerHubにアップロードする」ステップがあります
  3. Kamalは各サーバにDeployの指示を出します
  4. 指示を受けた各サーバはDockerHubに対して、適切なDocker Imageを要求します
  5. 指示されたDocker Imageはサーバにダウンロードされます
  6. 各サーバではDocker Imageを実行します。これはDocker開発をするのであれば、docker compose runもしくはdocker runコマンドに相当します。Docker開発をしていないのであれば、npm run startに相当します。Docker Imageが実行された状態、つまり稼働中のサーバはContainerと呼ばれます
  7. Kamalのデプロイはゼロダウンタイムデプロイと呼ばれるものです。デプロイ時にサービスは一瞬も停止しません
    1. (6)で新しいContainerを立ち上げている間、古いContainerはまだ残しています。そしてインターネットから来たリクエストには、古いContainerが対応します
    2. 新しいContainerが立ち上がったことが確認できたら(healthcheck)、次のリクエストからは新しいContainerに振り分けられます。そして古いContainerは停止されます

VPSのセットアップ

Conoha VPSを使った場合のセットアップ手順については、ビデオで紹介していますので、ご覧ください

https://youtu.be/z2bKG4Ddwzw?si=rJ5hGDXJWNWBVE5W

ここでやっていることをまとめると

  • VPSを契約する
  • VPSにUbuntuをインストールする
  • VPSがSSHとWeb(HTTPとHTTPS)のサービスを提供するようにする
  • ローカルマシンからSSHでログインできるようにする

以降のセットアップのビデオ

VPSのセットアップから実際のデプロイまでの様子(つまり以下全部)をビデオでも紹介しています。ご覧ください

https://youtu.be/x2fOZ0iHbVM?si=RJyrwNCp1Z533m12

ローカルマシンの準備

Node, Git, Dockerのインストール

上述したように、ローカルマシンには下記のものがインストールされている必要があります

  1. Node.js, NPM
  2. Git
  3. Docker

Next.js開発をしてもDockerは使っていない可能性がありますので、必要に応じてインストールしてください。Dockerのウェブサイトからインストールしてください

Kamalの準備

今回はKamalをインストールしないで使います。下記のコマンドだけ、ターミナルから実行してセットアップをしてください[1]

alias kamal='docker run -it --rm -v "${PWD}:/workdir" -v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock" -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/basecamp/kamal:latest'

動作確認のために次のコマンドを実行してください。バージョン番号 (2024年9月30日時点では"2.0.0")が表示されたら成功です。

 kamal version

Create Next AppでNext.jsの新しいアプリを作成

公式ドキュメントの通りに操作します。色々なオプションがありますが、どれでも問題ありません。

Next.jsアプリをDocker化

上記の項目で作成されたNext.jsアプリをDocker化します。公式ドキュメントのSelf-Hostingの項目の"A Node.js server"と"A Docker Container"のところを実施します。

私がやったもののコードがGitHubにありますので、こちらもご覧ください。

package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  }
}

Dockerfile

見本アプリDockerfileをそのままコピーします

見本アプリIn existing projectsに書かれている通りに、next.config.jsを変更します。ESモジュールを使っている場合は下記のようにしてください。

next.config.mjs

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "standalone",
};

export default nextConfig;

publicフォルダ

上記のDockerfileは/publicフォルダの存在を前提としています(無いとbuildが落ちます)。このように/publicフォルダを作り、その中に.keepという空のファイルを作ってください。空のフォルダはGitに無視されるため、.keepのファイルは重要です(何か一つでもファイルがあれば良いので、それがあれば.keepじゃなくても良いです)。

動作確認を実施します。下記のコマンドを実行し、http://localhost:3000でNext.jsのページが表示されることを確認してください

docker build . -t kamal_next
docker run --rm -p 3000:3000 kamal_next

動作確認コマンドの解説)

  • docker build . -t kamal_nextは上記のDockerfileの中身に沿って、Docker Imageを作成するコマンドです。NodeとかNext.jsをインストールし、さらにnpm run buildをしてNext.jsをbuildします。Docker Imageの名前を"kamal_next"にしています(名前は任意)。
  • docker run -p 3000:3000 kamal_nextはDocker Imageを実行し、Docker Containerを実行します。node server.jsを実行して、Next.jsのサーバを立ち上げます。通常の開発時にnpm run startとするのと同じです。

これが成功すれば、Dockerfileまではうまく作れた証明になります!

Next.jsにhealthcheckを導入

ヘルスチェックは、サーバが作業を正常に実行できるかどうかを確認する方法です。KamalではこれからデプロイしようとするDocker Containerが正常に立ち上がったかどうかを確認するのに使用します。

ヘルスチェックを実装するためには、Next.jsアプリに/upのエンドポイントを作成し、これがStatus 200を返すようにします。下記のファイルを作成して実装します。

app/up/route.ts

export async function GET() {
  return new Response("I'm Healthy", {status: 200})
}

Kamalのセットアップ

いよいよKamalのセットアップです。

下記を実行するとデフォルトの設定ファイルが生成されます。

kamal init

DockerHubアクセス用のPersonal Access Tokenの取得

Kamalは先ほどセットアップしたDockerHubのアカウントに接続して、Docker Imageをアップロードしたり、あるいはダウンロードしたりする必要があります。その時のパスワードがわりにpersonal access tokenを作ります(大元のパスワードは漏洩したくないので、必ずpersonal access tokenを使ってください)。

  1. "Account Settings" > "Personal Access Tokens" > "Generate New Token" で新しいpersonal access tokenを作成してください。
  2. "Access Permissions"は"Read & Write"としてください。Docker Imageをアップロードもダウンロードもするためです。
  3. Personal Access Tokenはあとで使いますので、記録しておいてください。

.kamal/secrets-commonの作成

  1. /.kamalフォルダにsecrets-commonというファイルを作成してください。
  2. これは機密情報を含みますので、gitには絶対にコミットしないでください。.gitignoreに/.kamal/secrets-common を登録してください
  3. これは機密情報を含みますので、gitには絶対にコミットしないでください!。.gitignoreに/.kamal/secrets-common を登録してください
  4. これは機密情報を含みますので、gitには絶対にコミットしないでください!!。.gitignoreに/.kamal/secrets-common を登録してください
  5. secrets-commonに、先ほどのPersonal Access Tokenを登録します。書き方は下記の通りになります
  6. .kamal/secretsの方は、KAMAL_REGISTRY_PASSWORDのところをコメントアウトしてください

.kamal/secrets-common

KAMAL_REGISTRY_PASSWORD=[先ほどのDockerHubのPersonal access token]

.kamal/secrets

# Option 1: Read secrets from the environment
# KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD

config/deploy.ymlの編集

これがKamalのメインの設定ファイルです。下記のところを変更してください

# Name of your application. Used to uniquely configure containers.
service: [アプリの名前(任意の名前で良いです):例えば"hotwire-and-next"]

# Name of the container image.
image: [コンテナイメージの名前:<DockerHubのアカウント名>/<イメージ名(任意のもので良いです):例えば"naofumik/hotwire-and-next"]

# Deploy to these servers.
servers:
  web:
    - [VPSのIPアドレス:例えば "123.456.789.99"]xxx.xxx.xxx.xxx
  # job:
  #   hosts:
  #     - 192.168.0.1
  #   cmd: bin/jobs

# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).
# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).
proxy:
  ssl: true
  host: [VPSのホスト名:例えば "hotwire-n-next.castle104.com"]
  app_port: [Next.jsが動くポート番号:"3000"]

# Credentials for your image host.
registry:
  # Specify the registry server, if you're not using Docker Hub
  # server: registry.digitalocean.com / ghcr.io / ...
  username: [DockerHubのアカウント名:例えば"naofumik"]

host:のところは、VPSを登録したときのホスト名でも良いですが、自分でドメイン名を持っていれば、DNSを設定してこのVPSのIPアドレスに向けても良いです。DNSの設定方法については、ここでは紹介しませんが、一般的な内容ですので必要であれば各自でお調べください。

kamal setupコマンドの実行

いよいよデプロイです!

  1. 現在のコードをGitにコミットします(GitHub等にpushする必要はありません。ローカルだけで十分です)
  2. コマンドラインからkamal setupを実行します

そうするとKamalは以下の処理を実行します

  1. 各サーバにDockerがインストールします
  2. デプロイ時の流れに沿ってデプロイを実行します

最後にブラウザからVPSに接続して、Next.jsのページが正しく表示されることを確認します

kamal deployコマンドの実行

2回目以降はkamal deployを実行すれば十分です(setupを実行しても不具合は起こりませんが)

  1. 現在のコードをGitにコミットします(GitHub等にpushする必要はありません。ローカルだけで十分です)
  2. コマンドラインからkamal deployを実行します

そうするとKamalはサーバへのDockerのインストールを省略して、再度、デプロイ時の流れに沿ってデプロイを実行します。

振り返り

流石にVercelにデプロイするときほどは簡単ではありませんが、上記でやったことをまとめました。ぜひお試しいただければと思います。もし引っかかるところやわからないところがありましたら、私もまだ勉強をしたいので、XのDMなどで気軽にご連絡ださい!

  1. VPSをセットアップしました
  2. DockerHubにアカウントを作りました
  3. Next.jsをDocker化しました
  4. Next.jsにhealthcheckのエンドポイントを作成しました
  5. Kamalの設定ファイルを編集しました (7行程度)
  6. kamal setupに実行

Kamalを使って、気軽にたくさんのプロジェクトを作り、たくさん公開しましょう!

脚注
  1. RubyをインストールせずにKamalを実行するためにDockerを使っています。RubyとKamalをインストール済みのDocker Imageをタウンロードして、その中のKamalを実行する仕組みです。Dockerさえインストールされていれば、Rubyをインストールする手間が省けるわけです ↩︎

Discussion

yasuyasu

Kamalの準備のセクションで、kamalのimageのダウンロードについて書かれています。
2024/10/10時点だと、latestタグで最新のimageがダウンロードされました。

Yuta MatsumotoYuta Matsumoto

非常に有用な記事をありがとうございます。
1つのVPS上に、productionとstagingの2つの環境を作成したいと考えています。
stagingのみまたは、productionのみのデプロイを行うことは可能でしょうか?

NaofumiNaofumi

ありがとうございます。
私自身はやったことはないのですが、下記のようにやると良いのではないかと思います。

https://kamal-deploy.org/docs/configuration/overview/#destinations

設定が異なるところはconfig/deploy.staging.ymlに書き込む(差分)とのことです。
そうすると ドメイン名(host)が変えられるはずです。
また環境変数も変えられると思いますので、RailsであればRAILS_ENVも買えると良いと思います。

NaofumiNaofumi

あっ、忘れていましたけど、あとdeploy.staging.ymlではserviceも変えないといけなさそうですね

NaofumiNaofumi

よかったです!

GitHubへのリンク、共有ありがとうございます!