Rails 8からKamalが標準のデプロイツールになりましたが、いざ本番環境に出そうとすると「AWSのどこに、どうやって置けばいいの?」と手が止まってしまうことはありませんか?
Herokuのようなマネージドサービスと違い、Kamalは「自分で用意したサーバーにDockerコンテナを送り込む」仕組みです。そのぶん自由度もコストメリットも大きいのですが、EC2やRDSの準備・SSH鍵・環境変数まわりでつまずきやすいのも事実です。
この記事では、Rails 8 + Kamal 2で、AWSのEC2(アプリサーバ)+ RDS(PostgreSQL)に、まっさらな状態から本番デプロイするまでの手順をひととおり解説します。最後につまずきやすいポイントの対処もまとめました。
この記事の前提バージョン(2026年時点)
デプロイ系の記事はバージョンで手順が変わりやすいので、先に前提をそろえておきます。
- Rails: 8系
- Kamal: 2系(Rails 8に標準で同梱)
- データベース: PostgreSQL(RDS)
- OS(EC2): Ubuntu Server 24.04 LTS
Kamalのバージョンは bin/kamal version で確認できます。Rails 7でもKamal 2を追加すれば同じ流れでデプロイできますが、本記事はRails 8で生成されるデフォルトの config/deploy.yml を前提にしています。
そもそもKamalとは?なぜAWS + EC2なのか
Kamalは37signals(Railsの開発元)が作ったデプロイツールで、SSHで対象サーバーに接続し、Dockerコンテナとしてアプリを起動・入れ替えるのが基本の仕組みです。Kamal 2からは kamal-proxy というリバースプロキシを内蔵し、ゼロダウンタイムデプロイやLet’s EncryptによるSSL証明書の自動発行までKamalだけでまかなえるようになりました。
「Railsの前段にNginxを立てなくていいの?」と気になる方も多いと思いますが、Kamal 2ではこのkamal-proxyがその役割を担うため、小〜中規模なら別途Nginxを用意する必要はありません。このあたりの考え方はRailsの前段にNginxは必要なのかで詳しく解説しているので、あわせて読んでみてください。
デプロイ先はDockerが動くLinuxサーバーであればどこでもOK(VPSでも可)ですが、この記事ではAWSのEC2を使います。RDSでデータベースを分離しておけば、あとからサーバーを増やしたりインスタンスを作り直したりしてもデータが安全に残るためです。まずは1台構成で「動く状態」を作り、必要になったらスケールさせていく、という進め方がおすすめです。
事前準備
以下のアカウント登録をしておきます。イメージの置き場所は今回Docker Hubを使いますが、GitHub Container Registry(ghcr.io)でも同じ流れで進められます。
- Docker Hub(コンテナイメージの置き場所)
- AWS
また、ローカルにDockerがインストールされ、起動していることも確認しておいてください(Kamalはローカルでイメージをビルドします)。
アプリケーションを新規作成
今回、DBはPostgreSQLを使うので、-d postgresql オプションを付けて作成します。
$ rails new kamal_deploy -d postgresql$ cd kamal_deploy$ rails db:migratebin/devコマンドでRailsアプリケーションを起動します。
$ bin/devhttp://localhost:3000 にアクセスして、Railsが起動していることを確認できたら、いったんコミットします。
$ git add .
$ git commit -m "Initial commit"トップページ作成
デプロイ後に表示を確認できるよう、簡単なトップページを用意します。config/routes.rbを編集して、以下のようにrootを設定します。
root "home#index"HomeControllerを作成します。
class HomeController < ApplicationController
def index
end
endViewファイルを作成します。
<div>
<h1>ホーム</h1>
</div>http://localhost:3000 にアクセスして、ホームと表示されることを確認できたら、いったんコミットします。
$ git add .
$ git commit -m "Add home".envファイル作成
あとでRDSの認証情報など記載するために、.env ファイルを作成します。
$ touch .envこのファイルには認証情報が入るため、絶対にGitにコミットしないでください。Rails 8では、デフォルトの.gitignoreに .env* が最初から記載されているので、通常はそのままで大丈夫です。念のため git status に .env が出てこないことを確認しておくと安心です。
AWS準備
必要な構成
- EC2: アプリケーションサーバ(Dockerコンテナが動く)
- RDS: データベースサーバ (PostgreSQL)
セキュリティグループの設定は、本番運用では必要に応じて適切に制限してください(特にRDSは外部公開せず、EC2からのアクセスのみ許可するのが基本です)。実運用では冗長化や監視の設定も検討しましょう。
EC2設定
- マシンイメージ: Ubuntu Server 24.04 LTS
- インスタンスタイプ: t3.micro(学習用途なら最小構成でOK。メモリが心もとない場合はt3.smallへ)
- セキュリティグループ: SSH(22) と HTTP(80) を許可。独自ドメインでSSL化するならHTTPS(443)も許可
- 固定 IP: Elastic IP(EIP)を割り当てておく(再起動でIPが変わらないように)
Kamalはイメージのビルド・pull・コンテナ起動でそれなりにメモリを使うため、t3.micro(1GB)だとビルド時にメモリ不足になることがあります。その場合は一時的にスワップを設定するか、上位のインスタンスタイプを検討してください。
SSH接続
まずは、EC2作成時にダウンロードした.pemキーでローカルからEC2に接続します。
$ ssh ubuntu@パブリックIP -i ~/.ssh/<pemファイル>接続を確認できたら、いったんローカルに戻ります。毎回.pemを指定するのは面倒なので、自分の公開鍵をサーバーに登録して、pemファイルなしでSSH接続できるようにしておきましょう。
ローカルで公開鍵を表示してコピーします。最近のOpenSSHでは Ed25519 鍵(id_ed25519)がデフォルトなので、まずはそちらを確認します。
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 <your-public-key>
↑ 表示された内容をコピーまだ鍵を作っていない場合は、以下のコマンドで新しく作成できます。
$ ssh-keygen -t ed25519 -C "your_email@example.com"コピーしたら、再度.pemを使ってEC2に接続します。
$ ssh ubuntu@パブリックIP -i ~/.ssh/<pemファイル>ローカルでコピーした公開鍵を、サーバーの ~/.ssh/authorized_keys ファイルに追記します。
$ vi ~/.ssh/authorized_keys
# 末尾に公開鍵を貼り付けて保存するローカルに戻り、pemファイルの指定なしでSSH接続できることを確認します。これができればKamalからの接続もスムーズです。
$ ssh ubuntu@パブリックIPRDS設定
- エンジン: PostgreSQL 17系(2026年時点の推奨。長期安定を優先するなら16系でもOK)
- インスタンスタイプ: db.t3.micro
- セキュリティグループ: インバウンドにEC2のセキュリティグループからのPostgreSQL(5432)を許可
バージョンについて補足すると、以前よく使われていたPostgreSQL 14は2026年11月にコミュニティサポートが終了します。これから新規に作るなら、17系(もしくは安定重視で16系)を選んでおくと、長くアップグレードに追われずに済みます。
作成できたら、RDS のエンドポイントや認証情報を控えておき、後述の .env ファイルに記載します。
デプロイ準備
kamal設定
config/deploy.ymlを編集し、以下のように設定します(ハイライト行が主な編集ポイントです)。
<% require "dotenv"; Dotenv.load(".env") %>
# Name of your application. Used to uniquely configure containers.
service: kamal_deploy
# Name of the container image.
image: <DockerHubのアカウント名>/kamal_deploy
# Deploy to these servers.
servers:
web:
- <EC2のパブリックIP>
# job:
# hosts:
# - 192.168.0.1
# cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# Remove this section when using multiple web servers and ensure you terminate SSL at your load balancer.
#
# Note: If using Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
# proxy:
# ssl: true
# host: app.example.com
# 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のアカウント名>
# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .kamal/secrets).
env:
secret:
- RAILS_MASTER_KEY
- DATABASE_USERNAME
- DATABASE_PASSWORD
- DATABASE_HOST
clear:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true
# Set number of processes dedicated to Solid Queue (default: 1)
# JOB_CONCURRENCY: 3
# Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2
# Match this to any external database server to configure Active Record correctly
# Use kamal_deploy-db for a db accessory server on same machine via local kamal docker network.
# DB_HOST: 192.168.0.2
# Log everything from Rails
# RAILS_LOG_LEVEL: debug
# Aliases are triggered with "bin/kamal <alias>". You can overwrite arguments on invocation:
# "bin/kamal logs -r job" will tail logs from the first server in the job section.
aliases:
console: app exec --interactive --reuse "bin/rails console"
shell: app exec --interactive --reuse "bash"
logs: app logs -f
dbc: app exec --interactive --reuse "bin/rails dbconsole"
# Use a persistent storage volume for sqlite database files and local Active Storage files.
# Recommended to change this to a mounted volume path that is backed up off server.
volumes:
- "kamal_deploy_storage:/rails/storage"
# Bridge fingerprinted assets, like JS and CSS, between versions to avoid
# hitting 404 on in-flight requests. Combines all files from new and old
# version inside the asset_path.
asset_path: /rails/public/assets
# Configure the image builder.
builder:
arch: amd64
# # Build image via remote server (useful for faster amd64 builds on arm64 computers)
# remote: ssh://docker@docker-builder-server
#
# # Pass arguments and secrets to the Docker build process
# args:
# RUBY_VERSION: 3.3.6
# secrets:
# - GITHUB_TOKEN
# - RAILS_MASTER_KEY
# Use a different ssh user than root
ssh:
user: ubuntu
# Use accessory services (secrets come from .kamal/secrets).
# accessories:
# db:
# image: mysql:8.0
# host: 192.168.0.2
# # Change to 3306 to expose port to the world instead of just local network.
# port: "127.0.0.1:3306:3306"
# env:
# clear:
# MYSQL_ROOT_HOST: '%'
# secret:
# - MYSQL_ROOT_PASSWORD
# files:
# - config/mysql/production.cnf:/etc/mysql/my.cnf
# - db/production.sql:/docker-entrypoint-initdb.d/setup.sql
# directories:
# - data:/var/lib/mysql
# redis:
# image: redis:7.0
# host: 192.168.0.2
# port: 6379
# directories:
# - data:/data
Rails 8ではジョブ・キャッシュにRedis不要のSolid Queue / Solid Cacheが標準になり、上のように SOLID_QUEUE_IN_PUMA: true でPumaプロセス内でジョブを処理できます。Redisを立てずに済むのはKamal + 小規模構成と相性が良いポイントです。このあたりの設定はRails8のsolid_queueなどを使う時のdatabase.ymlの書き方もあわせてどうぞ。
.envを編集し、以下のように設定します。
KAMAL_REGISTRY_PASSWORD=<DockerHubのアクセストークン>
DATABASE_USERNAME=<RDSのマスターユーザー>
DATABASE_PASSWORD=<RDSのマスターパスワード>
DATABASE_HOST=<RDSのエンドポイント>.kamal/secretsを編集し、以下のように設定します。
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
# Improve security by using a password manager. Never check config/master.key into git!
RAILS_MASTER_KEY=$(cat config/master.key)
DATABASE_USERNAME=$DATABASE_USERNAME
DATABASE_PASSWORD=$DATABASE_PASSWORD
DATABASE_HOST=$DATABASE_HOSTconfig/database.ymlを編集し、以下のように設定します。productionのprimary部分のみ抜粋しています。
production:
primary: &primary_production
<<: *default
database: kamal_deploy_production
username: <%= ENV["DATABASE_USERNAME"] %>
password: <%= ENV["DATABASE_PASSWORD"] %>
host: <%= ENV["DATABASE_HOST"] %>
ここまでで、いったんコミットします。
$ git add .
$ git commit -m "Setup deploy"デプロイ実行
初回は bin/kamal setup でサーバーのDockerセットアップからデプロイまでまとめて実行します。
$ bin/kamal setupもし permission denied のエラーが出た場合は、EC2にSSHして以下のコマンドを実行し、ubuntuユーザーをdockerグループに追加します(参考)。
$ sudo usermod -aG docker $USER && newgrp dockerデプロイが完了したら、EC2 のパブリック IP にブラウザでアクセスし、「ホーム」が表示されることを確認します。表示されれば成功です。
2回目以降、コードを更新してデプロイする場合は以下のコマンドでOKです。
$ bin/kamal deployデプロイがうまくいかないときは、bin/kamal app logs(エイリアス設定済みなら bin/kamal logs)でコンテナのログを確認すると、原因を切り分けやすくなります。
独自ドメインでSSL化する(任意)
IPアドレスで動作確認できたら、独自ドメイン+HTTPSにしておきたいところです。Kamal 2ではkamal-proxyがLet’s Encryptの証明書を自動取得してくれるので、config/deploy.yml のコメントアウトされている proxy セクションを有効にするだけです。
proxy:
ssl: true
host: app.example.com事前に、使いたいドメイン(例: app.example.com)のAレコードをEC2のElastic IPに向けておきます。DNSが反映され、EC2のセキュリティグループでHTTPS(443)が開いていれば、再デプロイ時にkamal-proxyが証明書を取得し、HTTPSでアクセスできるようになります。
$ bin/kamal deploy注意点:デプロイされるのは「コミット済み」の内容
Kamalはコミットされている内容をデプロイします。そのため、ローカルで一時的に変更した内容をデプロイしたい場合でも、いったんコミットする必要があります。
ちなみに、このときGitHubへのプッシュは不要です(Kamalはローカルのコミットからイメージをビルドするため)。「pushしていないのにデプロイされた/されない」で混乱しやすいポイントなので覚えておきましょう。
まとめ
Rails 8 + Kamal 2で、AWSのEC2+RDSに本番デプロイする流れを最初から解説しました。ポイントを振り返ると次のとおりです。
- Kamalは「SSHでサーバーにDockerコンテナを送り込む」ツール。kamal-proxy内蔵でSSL化までまかなえる
- データはRDSに逃がしておくと、あとからサーバーを作り直しても安全
- SSH鍵はEd25519、PostgreSQLは17系など、バージョンは新しめを選んでおくと寿命が延びる
- デプロイされるのはコミット済みの内容。pushは不要
今回はローカルの手元から bin/kamal deploy を叩きましたが、実運用ではGitHub Actionsなどから自動デプロイできるようにしておくと、チームでも安全に回せます。まずは「1台で動く状態」を作れたことが大きな一歩です。ここから監視・冗長化・CI/CD連携へと少しずつ育てていきましょう。
参考記事
Kamal公式ドキュメント: Environment variables
Kamal公式ドキュメント: Proxy(SSL・独自ドメイン設定)
スポンサーリンク

コメント
Comments are closed.