Kamal を使用したステージング環境へのデプロイ
Kamal を使用したステージング環境へのデプロイ
Rails アプリのデプロイに、 Kamal を初めて使ってみました。使うにあたっては、いきなり本番環境へデプロイするのではなく、まずは練習として LAN 内の別の PC にステージング環境を構築し、そこにデプロイをしました。その時のメモを兼ねた事例紹介の記事になります。
この記事は以下に述べます "構成" にあわせた内容であり、汎用的な内容ではありません。ただ、コンテナ・レジストリという外部サービスは必要になりますが、デプロイ先は手元の環境だけで済みますし、また用意しやすいものだと思いますので、これから Kamal を使ってみたい方や、ひとつのステージング環境の例として、参考となることを願っています。
追記@2025/11/5 コンテナ・レジストリについて、 Kamal 2.8 から外部サービスではなくローカルホスト上の Docker 環境に作られるコンテナ・レジストリを指定・利用できるようになりましたので、全て自前の環境で済ませられるようになりました!
見どころとしては、デプロイ先が Windows 上の WSL2 で起動した Linux である点と、 SSL/TLS カスタム証明書の利用といったトピックが特徴的かと思います。
| デプロイ元 | デプロイ先 | |
|---|---|---|
| ホストOS | macOS Monterey | Rocky Linux 9(Windows 11 の WSL2 上) |
| Dockerエンジン | 28.1.1 (darwin/arm64) | 28.3.1 (linux/amd64) |
| デプロイツール | Kamal 2.7.0 | - |
なお Kamal は Rails 専用のツールではありませんが、事例の都合上、 Rails の設定内容が混ざっている箇所があります。できるだけ補足説明をつけていますが、ご了承ください。
0. 構成
これから紹介する例は、次のような構成です:
User
.....................................................................
| | |
| | access |
| v |
| 443 |
| +-----------+ |
| |kamal-proxy| |
| +-----------+ |
| | | access
| |forward |
| v v
| 3001 1080
| +----------------+ +-------------+
| |Thruster + Rails|---["kamal"]--> 1025| MailCatcher |
| +----------------+ Network +-------------+
| ^ ^
| | pull | pull LAN
~~~|~~~~~~~~|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|~~~~~~~~~~~~~~~~~~
v push | | Internet
+------------------------+ +--------------+
| Container Registry | | Docker Hub |
+------------------------+ +--------------+
| 役割 | 説明 |
|---|---|
| User | ステージング環境をテストする開発者。ここからデプロイし、サービスを利用します |
| kamal-proxy | kamal-proxy をエントリーポイント兼 SSL終端 として利用。ポート 443 |
| Thruster + Rails | Puma のラッパー Thruster を利用したアプリケーションサーバ。ポート 3001 。( Rails はポート 3000 ) |
| MailCatcher SMTP | MailCatcher を SMTP サーバとして利用。 Rails からのメールを処理し、通信は Docker 内部ネットワーク "kamal" を利用。これは自動的に作られるネットワークです |
| MailCatcher Web | MailCatcher の Web インターフェース。 Proxy を通さず、そのままポート 1080 で公開します |
プロキシ・サーバ kamal-proxy と、メールサーバの代わりになる MailCatcher は、ユーザは別途構築するなどの手間はありません。いずれも Kamal の設定に含まれており、 Kamal のコマンドによって構築(デプロイ)できます。
また各役割のサーバについて、ステージング環境ではすべて同じホストにデプロイします。上の図では別々に描かれていますが、実際は一つのホスト内にあります。
proxy.ssl.host = servers.web.host = accessories.host
+----------------------------------------------------+
| +-----------+ +----------------+ +-----------+ |
| |kamal-proxy| |Thruster + Rails| |MailCatcher| |
| +-----------+ +----------------+ +-----------+ |
+----------------------------------------------------+
そのデプロイ先のホストは、 LAN 内にすぐに利用できる Windows PC があったので、それを使うことにしました。しかし Windows は使わず、この Windows の WSL2 環境で動く Rocky Linux 9 を使います。その Linux 上のユーザは "rocky" とします。
手元の開発マシンは macOS で、ユーザは "developer" とします。ここからデプロイを実行します。またデプロイ後のアプリのサービスの利用もここから行います。
1. 事前準備
この事例では、あらかじめデプロイ先のホストのセットアップが必要です:
- SSH 公開鍵認証によるログインの設定
- Docker エンジン
- Windows ファイアウォールと、ポート転送の設定
また関連事項として必要とするものもあります:
- コンテナ・レジストリの用意
- SSL/TLS 証明書と hosts ファイル
1-a. SSH 公開鍵認証によるログインの設定
Kamal は、デプロイ先のホストに SSH でログインして、デプロイ先のホストをコントロールします。そのため事前に、指定のユーザが SSH でログインできる環境を整えておく必要があります。
この事例では、デプロイ元の macOS のユーザ developer が、 デプロイ先のホスト上のユーザ rocky として公開鍵認証によって SSH でデプロイ先にログインできるように設定しておきます。
1-b. Docker エンジン
Kamal は Docker エンジンそのもののインストール、セットアップも行う機能を持っていますが、それは今回は使いません。
デプロイ先では、公式のインストールガイドに従って Docker エンジンをインストール、セットアップしました。従ってこの事例では、デプロイ先のホスト上には Docker エンジンがすでに稼働している状態を前提としてます。
Install Docker Engine on CentOS
要約すると、次のようにインストールしました:
$ sudo dnf install -y dnf-plugins-core
$ sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
$ sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
$ sudo systemctl enable --now docker
また今回の事例ではデプロイ先へ SSH 接続するユーザは、一般ユーザ( rocky )を想定しています。そのためにも、( Kamal によるセットアップではなく)ユーザ自身で Docker エンジンをインストール、セットアップする必要があります。
注意点として、一般ユーザが Docker コンテナを作成するには、そのユーザーがグループ docker に属している必要がありますので、その状況を確認してください。
$ id
uid=1000(rocky) gid=1000(rocky) groups=1000(rocky),10(wheel),998(docker)
$
もし属していなければ、次のようにしてグループに追加します(その後、ログインし直します):
$ sudo usermod -aG docker rocky
1-c. Windows のファイアウォール設定、ポート解放
この事例では Windows 11 の WSL2 環境で Rocky Linux 9 を動かしますが、その仮想マシンはあくまでも Windows の内部にあるために、 Windows の外部からのアクセスは Windows のファイアウォールに阻まれます。
従って Windows ファイアウォールで、使用するポートの受け入れルールを追加してください。
| ポート番号 | 用途 |
|---|---|
| 22 | デプロイ元から SSH でアクセスします。 |
| 443 | デプロイしたアプリのサービスの利用は https でアクセスします。 |
| 1080 | 同様に MailCatcher の Web インタフェースはこのポートで動作します。 |
また同時に、それらのポートを WSL2 上の Linux ホストへ転送するようにしておく必要があります。 Windows のことは詳しくないので仔細は省きますが(すみません)、大まかに述べますと管理者権限の PowerShell で次のようなことを行います:
# WSL2 の IPアドレス 確認(複数出ますが、おそらく最初のもの)
PS> wsl hostname -I
172.30.144.223 172.17.0.1 172.18.0.1
PS>
# ポート転送設定
PS> netsh interface portproxy add v4tov4 listenport=22 listenaddress=0.0.0.0 connectport=22 connectaddress=172.30.144.223
PS> netsh interface portproxy add v4tov4 listenport=443 listenaddress=0.0.0.0 connectport=443 connectaddress=172.30.144.223
PS> netsh interface portproxy add v4tov4 listenport=1080 listenaddress=0.0.0.0 connectport=1080 connectaddress=172.30.144.223
PS>
# 一覧表示
PS> netsh interface portproxy show all
ipv4 をリッスンする: ipv4 に接続する:
Address Port Address Port
--------------- ---------- --------------- ----------
0.0.0.0 22 172.30.144.223 22
0.0.0.0 1080 172.30.144.223 1080
0.0.0.0 443 172.30.144.223 443
PS>
なお WSL2 の IP アドレスは Windows を再起動すると変わることがありますので、その際は再設定してください。(余談ですが、私は設定を行うバッチ・スクリプトを ChatGPT に作ってもらいました。)
1-d. コンテナ・レジストリの用意
Kamal はデプロイ時にビルドしたイメージを、コンテナ・レジストリに push します。デプロイ先では、そこからイメージを pull します。
つまりそのために、コンテナ・レジストリにアカウントが必要です。代表的なものには Docker や GitHub といったプロバイダーがあります。
ここでは詳細を省きますが、注意点として、利用するアカウントに適切な権限が設定されていることを確認してください。たとえば GitHub Container Registry を利用するのであれば、 write:packages read:packages delete:packages といった権限が必要です。
追記@2025/11/5 前述の追記のとおり、Kamal 2.8 から外部サービスではなくローカルホスト上の Docker 環境に作られるコンテナ・レジストリを指定・利用できるようになりました。したがって外部サービスのコンテナ・レジストリは必須ではありません。
1-e. SSL/TLS 証明書と hosts ファイル
本番環境のアプリが https で動くので、ステージング環境でも https で動かそうと思います。
SSL/TLS のカスタム証明書については、 Kamal では Let's Encrypt を利用した証明書の作成(更新)を自動で行う機能がありますが、この事例では LAN 内に構築するステージング環境という都合から、 mkcert を利用した自前のルート認証局をあらかじめ作成し、そのルート認証局証明書を、サービスを利用する環境(ユーザの環境: macOS )およびアプリのデプロイ先の環境( Rocky Linux )にインストールしておくことになります。
ルート認証局証明書
$ mkcert -CAROOT
$ ls -l "`mkcert -CAROOT`"
total 16
-r-------- 1 developer staff 2484 7 4 13:38 rootCA-key.pem
-rw-r--r-- 1 developer staff 1761 7 4 13:38 rootCA.pem
$
作成した rootCA.pem を、ステージング環境のアプリにアクセスする各所のマシンのシステムにインストールします。
macOS では "キーチェーンアクセス" に登録します。
Windows の場合は "ユーザー証明書の管理"(プログラム名 "certmgr" で検索)を実行し、「信頼されたルート証明機関」に rootCA.pem を追加してください。
Rocky Linux 9 では、ルート認証局証明書のファイルを /etc/pki/ca-trust/source/anchors/ の中に配置し、コマンド update-ca-trust を実行します(いずれも管理者権限で実行します):
$ sudo cp -ip rootCA.pem /etc/pki/ca-trust/source/anchors/
$ sudo update-ca-trust
$
コマンドの出力は特にありません。 /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem の内容を直接見てみると、冒頭に rootCA.pem の内容が挿入されていることがわかります。
カスタム証明書と hosts ファイル
次に、 mkcert でカスタム証明書を作成する際には、 Common Name となるホスト名が解決できなければいけないため、ステージング環境にアクセスする関係各所の /etc/hosts ファイルにホスト名と、その IPアドレス のペアを登録しておきます。(もし LAN 内のアドレスを解決できる DNS が利用できるのであれば、それを利用すれば十分です)。
この記事では説明のために、ステージング環境のホスト名を "staging.example.com" とします。このホスト名に対するカスタム証明書を作成します:
$ mkcert staging.example.com
$ ls -l *.pem
-rw------- 1 developer staff 1704 7 4 13:40 staging.example.com-key.pem
-rw-r--r-- 1 developer staff 1570 7 4 13:40 staging.example.com.pem
$
これらファイルの内容を Kamal に設定します(後述)。
2. Dockerfile
(ここは Rails の話になりますが、ほかの環境に於いては、 kamal-proxy はデプロイするアプリケーション・サーバのポート番号を知る必要がある、という点に注意してください。)
Dockerfile のうちデプロイに関係するところを示します:
...
...
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
ENV THRUSTER_HTTP_PORT="3001" \
THRUSTER_TARGET_PORT="3000" \
THRUSTER_DEBUG=1
CMD ["bundle", "exec", "thrust", "bin/rails", "server"]
Rails アプリはサーバに Thruster を使うようにしています。コマンド名は thrust です。
Thruster の設定は環境変数で行うことになっていますので、それを設定しています。 Rails アプリが使うポートはデフォルトが 3000 なのでそれを尊重し、 Thruster では 3001 を使用するようにしました。 kamal-proxy はこの 3001 とやりとりすることになります。
3. Kamal の設定
ここまでの内容を踏まえ、 Kamal の設定ファイルを作ります。
3-a. 設定 config/deploy.yml
全体を示します:
require_destination: true
service: myapp
image: myapp-staging
servers:
web:
hosts:
- staging.example.com
proxy:
host: staging.example.com
ssl:
certificate_pem: DEPLOY_CERTIFICATE_PEM
private_key_pem: DEPLOY_PRIVATE_KEY_PEM
app_port: 3001
registry:
server: ghcr.io
username: (アカウント名)
password:
- DEPLOY_REGISTRY_PASSWORD
builder:
arch:
- amd64
- arm64
context: .
dockerfile: Dockerfile
env:
clear:
RAILS_ENV: staging
SMTP_ADDRESS: smtp
SMTP_PORT: 1025
secret:
- SECRET_KEY_BASE
ssh:
port: 22
user: rocky
keys_only: true
keys:
- /Users/developer/.ssh/id_rsa
volumes:
- "/home/rocky/myapp-storage:/rails/storage"
accessories:
mailcatcher:
service: smtp
image: sj26/mailcatcher:v0.10.0
host: staging.example.com
options:
publish:
- "1080:1080"
このうち、特徴的な箇所について説明します。
proxy.host と proxy.ssl
proxy:
host: staging.example.com
ssl:
certificate_pem: DEPLOY_CERTIFICATE_PEM
private_key_pem: DEPLOY_PRIVATE_KEY_PEM
SSL/TLS カスタム証明書の Common Name に相当するホスト名を host に設定する必要があります。(余談:最初、これはデプロイ先のホストかと考えて IP アドレスで記述していたため、うまくいかずにハマりました。)
また proxy.ssl の方ですが、 certificate_pem と private_key_pem の値はシークレットから読み取るように設定します。
registry
registry:
server: ghcr.io
username: (アカウント名)
password:
- DEPLOY_REGISTRY_PASSWORD
コンテナ・レジストリの設定です。 GitHub を利用する場合は server が "ghcr.io" です。 username はアカウント名、 password は Personal Access Token アクセストークンと呼ばれています。これもシークレットから読み取るように設定します。
追記@2025/11/5 コンテナ・レジストリには、 Kamal 2.8 からローカルホストを指定することができるようになりました。
registry.serverにlocalhost:5555(任意のポート番号)を指定するだけです。これによりデプロイ時に、ローカルホスト上の Docker にレジストリとなるコンテナが追加され、そこを介して処理が進むようになります。
builder.arch
builder:
arch:
- amd64
- arm64
今回の事例では、デプロイ元となる開発環境は M1 チップの macOS です。これはアーキテクチャ "arm64" と示されます。一方、デプロイ先は Windows マシンなので、 "amd64" です。つまり Dockerfile のビルドに際しては "amd64" 向けのイメージを作成しなければいけません。
この際なので、両方の環境に対応できるよう、両方ともビルドするようにします。
いずれにしろ、デプロイ元の Docker で buildx が利用できなければいけません。私の macOS の環境では Docker Desktop をインストールしましたが、その中に Docker Buildx は含まれています。次のように確認してみてください:
$ docker buildx version
github.com/docker/buildx v0.23.0-desktop.1 503f948aadbddb6de3ec5581f766e1d27f6975a1
$
なお、ビルダーについては Kamal がデプロイ時に(ビルド時に)、必要に応じてビルダーのコンテナを作るようで、あらかじめ用意する必要はない模様です。
env
env:
clear:
RAILS_ENV: staging
SMTP_ADDRESS: smtp
SMTP_PORT: 1025
secret:
- SECRET_KEY_BASE
これらは Rails アプリ用の動作設定に利用している環境変数の設定であり、 Kamal 自体には関係ありません。
ただし SMTP_ADDRESS として指定している "smtp" 値は、 Docker の内部ネットワーク内で参照できるホスト名を示しています。 "smtp" は accessories.mailcatcher.service の値として設定しているサービス名です。
もし accessories.mailcatcher.service が未設定の場合は、トップレベルの service の値とアクセサリ名を組み合わせた myapp-mailcatcher というホスト名になります。
volumes
volumes:
- "/home/rocky/myapp-storage:/rails/storage"
これもデプロイするアプリの都合のものであり、 Kamal 自体には関係ありません。 Docker のボリュームの設定を、このように記述しますという例です。
このディレクトリは、最初のデプロイの前に作成しておきます。もし作成が後になってしまった場合は、デプロイ後のサーバの起動時に "書き込み権限がない" といった問題に遭遇するかもしれません。
$ mkdir -p /home/rocky/myapp-storage
accessories.mailcatcher.options
accessories:
mailcatcher:
service: smtp
image: sj26/mailcatcher:v0.10.0
host: staging.example.com
options:
publish:
- "1080:1080"
MailCatcher をアクセサリとして追加しますが、 MailCatcher は SMTP と HTTP と、二つのサービスを使用します。
SMTP は Docker の内部ネットワーク "kamal" を使用するので特に指定していませんが、 HTTP はポート 1080 を公開するため、それを accessories.mailcatcher.options.publish にて設定しています。
3-b. シークレットの設定
将来は本番環境のための設定を作ることになるので、あらかじめファイルを secrets.staging と secrets.production とに分けておきました。
余談ですが、デプロイに関する項目は、接頭語として "DEPLOY_" をつけるようにしています( Kamal の制約ではありません)。実際はたくさんの設定項目があるので、グループ分けのためにそうしています。
.kamal/secrets.staging
DEPLOY_REGISTRY_PASSWORD=$DEPLOY_REGISTRY_PASSWORD
デプロイするイメージを push する先となる、コンテナ・レジストリのアカウントのパスワード(トークン)を設定します。
この記述は、「具体的な値は環境変数 $DEPLOY_REGISTRY_PASSWORD からセットする」という記述であって、その値を直接ここに書いてはいけません。
DEPLOY_CERTIFICATE_PEM=$DEPLOY_CERTIFICATE_PEM
DEPLOY_PRIVATE_KEY_PEM=$DEPLOY_PRIVATE_KEY_PEM
ステージング環境では SSL/TLS のための設定としてカスタム証明書とその秘密鍵を使うため、そのためのシークレットを設定します。さきほどと同様に、「具体的な値は環境変数の $DEPLOY_CERTIFICATE_PEM と $DEPLOY_PRIVATE_KEY_PEM からセットする」としています。
SECRET_KEY_BASE=$SECRET_KEY_BASE
SECRET_KEY_BASE は Rails のための設定で、 Kamal は関係ありませんが、アプリの機密情報もシークレットに記述しています、という例示です。
3-c. 環境変数
これまでの説明の中にあるように、設定情報の一部は環境変数を通して設定するようにしています。
これらは秘匿情報を含むので、外部に漏れ出さないように注意が必要です。 dotenv を利用する場合は、少なくとも .gitignore や .dockerignore ファイルなどで .env* が無視リストに含まれているか、確認してください。
.env.staging ファイルを利用すると、次のような記述になります:
DEPLOY_REGISTRY_PASSWORD="abcdefg......"
SECRET_KEY_BASE="0123456789abc......"
DEPLOY_CERTIFICATE_PEM="-----BEGIN CERTIFICATE-----
NIIEWTCCAsGgAwIBAgIQYwiE1ETlNvxeqWT5cDDvnDANBgkqhkiG9w0BAQsFADCB
izEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTAwLgYDVQQLDCdoaXJv
(途中省略)
BGJkgl3TOxkzk6jlYeplHDbyFyRVcaXsV9Xf2D/jgZCKygUxinv8rfhUUZ3Njwo0
KrelGkjiRlvVyYSWlYkHyFIyzmADFxljsU6eXNJnldPsQ7onwhQ5+212mXwTiH0w
wJKsjhJf4F8pZBbQJg==
-----END CERTIFICATE-----
"
DEPLOY_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----
NIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDlWmEM3dIncagc
Z8F+xLr0UNsmwyWCbCTGpkzdRX16zGxlLXi5ChDAhyrBQBtFdjHlfvuPELKa6b+r
(途中省略)
CL+EMm2OYIueHt1OrZ0q/YlNB6PzNpFWDDo9DCZ9gGMqPC4cFYQrFCvOjSyzdVVY
gLvKiU5SfZ0IVnWtSB1u0ZACUWys8ctikQ1mMaWqBq+V3mKASa7p1QXqzhaT19Mu
0JMdXiUTYFhYrTl3AK+AEA==
-----END PRIVATE KEY-----
"
(秘密鍵を露にしてしまっていますが、この鍵はでたらめなものです)
このうち、 DEPLOY_CERTIFICATE_PEM には事前準備で作成したカスタム証明書 staging.example.com.pem の内容を、そして DEPLOY_PRIVATE_KEY_PEM にはカスタム証明書の秘密鍵 staging.example.com-key.pem の内容を、それぞれ貼り付けます。
このように PEM データを環境変数に設定するのは、いささか躊躇われるようなことかもしれません。代わりの手段として、シークレット内で PEM データを環境変数から読み取る代わりに、 PEM ファイルの内容を cat した出力から読み取る、というやり方でもよいと思います。
しかしながら、秘匿情報の入った保護するべきファイルの数を増やしたくないという意図があり、すべての秘匿情報は環境変数(実際は、それを設定した .env ファイル)だけにするという選択をしています。このあたりの事情は運用の方針に従えばよいでしょう。
4. デプロイの実行
環境変数をセットしたら、デプロイを実行します。
最初のデプロイ時には、まず先にアクセサリをデプロイします:
$ dotenv -f .env.staging kamal accessory boot mailcatcher --destination=staging
それから本体のアプリケーションのデプロイを実行します。これにより kamal-proxy もデプロイされます:
$ dotenv -f .env.staging kamal deploy --destination=staging
基本的なデプロイ作業は以上になります。
事前作業がけっこうな手間がかかりましたが、ステージング環境という特殊な事情からのことなので(特に Windows PC を使っていることや、サーバ証明書に関する諸々)、記述したことの多くは Kamal 自体の手間ではありません。
Kamal に限りませんが、公式のドキュメントや、第三者による Web 記事などで得た知識をもとに、いざそれを動かしてみるとつまずくことも多いですが、つまずくことも良い経験になるので、まず手を動かすことは大事なことと思います。しかしいきなり本番環境でやることではありませんから、さしあたっては手近な PC に安全なステージング環境を構築し、そこで練習をしてみるのがよいでしょう。
最後になりましたが、とても参考になった記事とサイトを紹介します。ありがとうございます。
-
Real World Kamal - タケユー・ウェブ株式会社
Kamal の公式ドキュメントだけではイメージを掴みづらいのですが、まさに Real な事例を紹介くださっていることで、理解の助けになりました。 -
Kamal README: 37signalsの多機能コンテナデプロイツール(翻訳) - TechRacho by BPS株式会社
現在のバージョンに対して少し古くなってしまった情報になるのですが、ほかの Web 記事にはないトピックがあったり内容も丁寧なので、 Kamal の世界観を把握しやすくなりました。 -
Kamal discussions
Kamal のリポジトリに付随するディスカッションです。さまざまな視点が見られ、設定ファイルの書き方などで気づくこともよくありました。また Issue よりも気軽に投稿できそうな雰囲気があり、思い悩んだ時に頼りになりそうです。
Discussion