連載「Ruby on Rails 8で作る見積・請求管理システム」も最終回です。前回(第8回)までに、見積書の作成から請求書への変換、入金管理までの機能がそろいました。ただし、ここまでは手元のパソコンで動くだけです。社内の人が使うには、サーバーに置いて、https で開ける状態にする必要があります。
「システムはできたと聞いたけど、実際にどこでどう動かすのか、誰が責任を持つのかが見えない…」
この回では、Rails 8 に標準で付いてくる配備ツール Kamal(カマル) を使って、1台のサーバーに見積・請求システムを公開する手順を解説します。結論から言うと、Kamal は「Dockerイメージを作り、サーバーに送り、新しい版に切り替える」までを1つのコマンド(kamal deploy)にまとめたツールです。設定ファイルは config/deploy.yml の1枚で、何をどのサーバーに置くかが読める形で残ります。発注者にとっては、運用が特定の担当者の頭の中に閉じない点が大きな利点です。
この回の内容は次のとおりです。
- Kamal が何をしてくれるのか(仕組み)
- Dockerfile と
config/deploy.ymlの中身 - 秘密情報(パスワードなど)を安全に扱う方法
- 配備のコマンドと、つまずきやすい点
- 発注者が開発会社に確認したいこと
Kamalとは?Dockerのコンテナを1台のサーバーへ送り込む道具
まず用語を整理します。
- Docker(ドッカー):アプリを動かすのに必要なもの一式(Ruby本体、部品、設定)を「イメージ」という1つの箱にまとめる仕組み。箱の中身は、どこで動かしても同じになります。
- コンテナ:イメージを実際に動かしている状態。
- Kamal:イメージをサーバーに送って起動し、古い版から新しい版へ切り替える道具。Rails 8 の標準ツールとして紹介されています。
📰 出典:Kamal 公式サイト
Kamal は、手元のパソコン(またはCI)からサーバーへSSH接続して作業します。サーバー側にはDockerだけがあればよく、専用の管理画面や常駐ソフトは要りません。流れは次のとおりです。
| 順番 | 何が起きるか | 担当する場所 |
|---|---|---|
| 1 | Dockerfile からイメージを作る | 手元のパソコン/CI |
| 2 | イメージをレジストリ(イメージの置き場)に預ける | Docker Hub などの外部サービス |
| 3 | サーバーがイメージを受け取り、新しいコンテナを起動する | 配備先サーバー |
| 4 | 起動の確認(ヘルスチェック)が通ったら、通信を新しい版へ切り替える | サーバー内のプロキシ |
| 5 | 古いコンテナを止める | 配備先サーバー |
手順4のおかげで、切り替えの瞬間にも画面が止まりにくいのが特徴です。通信の入口にはKamalが入れるプロキシ(kamal-proxy)が立ち、https の証明書(Let’s Encrypt)の取得と更新も自動で行います。
これまで連載した他の回では、配備先に Amazon ECS や Google Cloud Run のような「マネージドなサービス」を使いました。それに対して Kamal は、自分で借りたサーバー(VPSやEC2など)に直接置く考え方です。月額の固定費を抑えやすい反面、サーバーの更新や監視を自分たちで受け持つ必要があります。この違いは、後ほど発注者向けメモで整理します。
実装1:Kamalをプロジェクトに入れる
Kamal は Gemfile に追加します。require: false は「アプリの起動時には読み込まない(配備のときだけ使う)」という意味です。
Gemfile
# Deploy this application anywhere as a Docker container [https://kamal-deploy.org]
gem "kamal", require: false
bundle install のあと、bin/kamal から実行できるようにしてあります(bin/kamal version で確認できます)。
bin/kamal
#!/usr/bin/env ruby
# frozen_string_literal: true
require "rubygems"
require "bundler/setup"
load Gem.bin_path("kamal", "kamal")
実装2:本番用のDockerfile
Rails 8 の標準的な Dockerfile を、この連載の構成(PostgreSQL・Prawn)に合わせて作ります。ポイントは3つです。
- ビルド用と実行用でステージを分ける:コンパイラなどは最終イメージに残さず、サイズと攻撃されうる面を小さくします。
- root ではなく専用ユーザーで動かす:万一アプリに問題があっても、サーバー全体に影響が及びにくくします。
- 本物の鍵をイメージに入れない:アセットのビルド時だけダミーの鍵(
SECRET_KEY_BASE_DUMMY=1)を使います。
Dockerfile(抜粋)
ARG RUBY_VERSION=3.3.6
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
WORKDIR /rails
# libpq5 は PostgreSQL に接続するために必要
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq5 && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development:test"
# --- ビルド専用ステージ ---
FROM base AS build
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential git libyaml-dev pkg-config libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile -j 1 --gemfile
COPY . .
RUN bundle exec bootsnap precompile -j 1 app/ lib/
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# --- 最終イメージ ---
FROM base
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails log storage tmp
USER 1000:1000
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
CMD の bin/thrust は、Gemfile に最初から入っている Thruster(スラスター) です。80番ポートで通信を受け、静的ファイルの配信や圧縮を担当して、Rails本体(Puma)に残りを渡します。Kamal のプロキシは、この80番に通信を転送します。
起動時に毎回データベースを整えるため、bin/docker-entrypoint で db:prepare(データベースが無ければ作成し、未適用の変更があれば適用する)を実行します。
bin/docker-entrypoint
#!/bin/bash -e
if [ -z "${LD_PRELOAD+x}" ]; then
LD_PRELOAD=$(find /usr/lib -name libjemalloc.so.2 -print -quit)
export LD_PRELOAD
fi
# サーバー起動のときだけ、DBの作成とマイグレーションを済ませる
if [ "${@: -2:1}" == "./bin/rails" ] && [ "${@: -1:1}" == "server" ]; then
./bin/rails db:prepare
fi
exec "${@}"
また、秘密情報や不要なファイルがイメージに紛れ込まないよう、.dockerignore で config/*.key・.env*・log/・.git/ などを除外しています。
実装3:config/deploy.yml に「何をどこに置くか」を書く
Kamal の設定はこの1ファイルです。サンプルでは、Webアプリ用コンテナとPostgreSQL用コンテナを、同じ1台のサーバーに置く最小構成にしています。
config/deploy.yml
service: mitsumori
image: your-registry-user/mitsumori
servers:
web:
- 192.0.2.10 # 配備先サーバーのIPアドレス(例示用)
proxy:
ssl: true # Let's Encrypt の証明書を自動で取得・更新
host: mitsumori.example.com
registry:
username: your-registry-user
password:
- KAMAL_REGISTRY_PASSWORD
env:
secret:
- RAILS_MASTER_KEY
- MITSUMORI_DATABASE_PASSWORD
clear:
DB_HOST: mitsumori-db
SOLID_QUEUE_IN_PUMA: true
volumes:
- "mitsumori_storage:/rails/storage"
accessories:
db:
image: postgres:16
host: 192.0.2.10
port: "127.0.0.1:5432:5432"
env:
clear:
POSTGRES_USER: mitsumori
POSTGRES_DB: mitsumori_production
secret:
- POSTGRES_PASSWORD
directories:
- data:/var/lib/postgresql/data
読み方のポイントを表にします。
| 設定 | 意味 | なぜこうしたか |
|---|---|---|
servers | 配備先のサーバー | 1台構成。台数を増やすときはここに行を足す |
proxy.ssl / host | https化とドメイン | 見積書・請求書は取引先情報を含むため、https は必須 |
env.secret | 秘密情報の名前だけを書く | 値は次の .kamal/secrets から渡す。ファイルに値を書かない |
DB_HOST: mitsumori-db | DBの接続先 | accessories の名前 db が「サービス名-db」というホスト名になる |
SOLID_QUEUE_IN_PUMA | ジョブ処理をWebと同じプロセスで動かす | 第6回で作ったメール送信の非同期処理を、小規模なら別サーバーなしで動かせる |
volumes / directories | サーバー側に残すデータ | コンテナを入れ替えてもDBのデータとアップロードファイルが消えない |
port: "127.0.0.1:5432:5432" | DBへはサーバー自身からだけ接続可 | DBをインターネットに公開しない |
第3回〜第8回で作った機能は、第0回から設定してある「Solid Queue/Solid Cache/Solid Cable」がすべてPostgreSQLに保存される構成のため、Redis などの別サービスを足さずに動きます。config/database.yml の本番設定では、画面用・キャッシュ用・ジョブ用・通信用の4つのデータベースを使います。db:prepare が存在しないものを作成するため、PostgreSQL側で手作業のデータベース作成は不要です。
実装4:秘密情報の渡し方(.kamal/secrets)
秘密情報には、DBのパスワード、Rails の暗号鍵(RAILS_MASTER_KEY)、レジストリのパスワードがあります。.kamal/secrets には値ではなく、取り出し方だけを書きます。
.kamal/secrets
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
RAILS_MASTER_KEY=$(cat config/master.key)
MITSUMORI_DATABASE_PASSWORD=$MITSUMORI_DATABASE_PASSWORD
POSTGRES_PASSWORD=$MITSUMORI_DATABASE_PASSWORD
$変数名は、配備する人のパソコンの環境変数から読み取ります。config/master.keyは.gitignore済みで、git にもDockerイメージにも入りません。- 1Password などのパスワード管理ツールから取り出す書き方(
kamal secrets fetch)もあります。
ここに本物のパスワードを書かないことが最大の注意点です。 このファイルはgitに入る前提のため、値を書くと履歴に残り続けます。
配備のコマンドと動作確認
サーバー(Ubuntuなど)を用意し、SSHで入れる状態にしたら、手元で次を実行します。
# 初回だけ:サーバーにDockerを入れ、DBを起動し、アプリを初回配備する
bin/kamal setup
# 2回目以降:新しい版に切り替える
bin/kamal deploy
# 状態確認・ログ・Railsコンソール
bin/kamal app details
bin/kamal app logs
bin/kamal app exec --interactive "bin/rails console"
何か問題が起きたときの切り戻しは、前の版を指定して bin/kamal rollback <版> で行えます(版は bin/kamal app containers で確認します)。
この回で確認できたことと、できていないこと
筆者の実行環境では、次の範囲まで確認しました。
bin/kamal configがconfig/deploy.ymlと.kamal/secretsを読み込めること(ダミー値で確認)bin/rubocop(指摘なし)・bin/brakeman(警告なし)・bin/bundler-audit(脆弱性なし)
一方、次は実行環境にDockerデーモンと配備先サーバーが無く、確認できていません。
docker buildの成功と、イメージの起動- 実サーバーへの
kamal setup/kamal deploy、Let’s Encrypt の証明書取得 - 配備後の画面表示
実際に使う前に、ステージング用のサーバーで1度通しで試してください。ドメインのDNS設定、サーバーの80/443番ポート開放、レジストリの認証など、環境ごとに必要な準備があります。
つまずきやすい点・運用の注意
- 証明書が取れない:ドメインのDNSがサーバーを向いていない、80番が閉じている、が定番の原因です。
- DBのバックアップは別途必要:Kamal は配備の道具であり、バックアップは取ってくれません。DBのデータはサーバー内の
dataディレクトリにあるため、定期的に別の場所へ保存し、復元できることも確認する必要があります。 - サーバー自体の更新:OSのセキュリティ更新、Dockerの更新、ディスク容量の監視は、Kamal の対象外です。
- 1台構成の限界:サーバーが止まればシステムも止まります。止められない業務なら、台数を増やす設計や、マネージドなDBの利用を検討します。
- 本番運用に必要だが連載では省略したこと:監視・アラート、ログの保管、メール配信サービスの本番設定(
config/environments/production.rbの送信元ホストはexample.comのままです)、複数台構成。
発注者向けメモ:「配備」と「運用」を誰が持つかを決める
見積・請求のような業務システムは、完成したあとの運用が長く続きます。配備まわりで、開発会社に次の点を確認しておくと安心です。
発注者がやること チェックリスト
- ☐ 本番環境のサーバーやクラウドの契約者が自社名義になっているか確認する
- ☐ 配備の手順(コマンド・設定ファイル)が自社でも見られる場所に保管されるか確認する
- ☐ パスワードや鍵の保管場所と、誰が持つのかを決める
- ☐ データベースのバックアップの頻度・保存先・復元テストを誰がいつ行うかを決める
- ☐ OSやDockerの更新、監視、障害時の連絡の担当を、契約(保守範囲)に書く
- ☐ 業務を止められない度合いに応じて、1台構成かどうかを決める
開発会社への質問例
- 「本番サーバーはどこに置き、契約名義はどちらになりますか。サーバーの更新や監視は保守の範囲に含まれますか」
- 「配備の設定ファイルや手順書は納品物に含まれますか。担当者が変わっても、別の会社が引き継げますか」
- 「データのバックアップはどのくらいの頻度で、どこに保存されますか。復元の訓練は行いますか」
- 「更新のとき、システムが止まる時間はどのくらいですか。問題が出たとき、前の版にどう戻しますか」
- 「マネージドなサービス(ECSやCloud Runなど)と、自分たちで借りたサーバーに置く方法で、費用と運用の手間はどう違いますか」
まとめ:Kamalは「配備を設定ファイルに残す」ための道具
この連載で作った見積・請求管理システムは、Rails 8 の標準機能(Solid Queue、認証、Hotwire)と PostgreSQL を中心に、外部サービスを最小限にした構成です。Kamal を使うことで、その配備も1枚の設定ファイルと1つのコマンドで再現できるようになります。
一方で、サーバーの更新・監視・バックアップは、誰かが引き受けなければなりません。「作る」と同じくらい「動かし続ける」の担当と費用を、発注の段階で決めておくことが、長く使えるシステムにするコツです。
連載を通して作ったコードは、サンプルプロジェクトにまとまっています。ご自身の業務に合わせて、税率・帳票の記載事項・権限などを調整して使ってください(税や法令の扱いは一般論にとどめているため、実際の運用では税理士などの専門家に確認してください)。最後までお読みいただきありがとうございました。
この連載の記事一覧
この記事は連載「Railsで作る見積・請求管理システム」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【Railsで作る見積・請求管理システム 第0回】Rails 8の雛形を動かして、Excel見積からの脱却を始める
- 【Railsで作る見積・請求管理システム 第1回】取引先と品目のマスタを作る(scaffoldと入力チェック)
- 【Railsで作る見積・請求管理システム 第2回】ログインを付ける(Rails 8標準の認証と権限の分け方)
- 【Railsで作る見積・請求管理システム 第3回】見積書と明細を作る(Hotwireで明細行をその場で追加)
- 【Railsで作る見積・請求管理システム 第4回】金額・消費税・端数処理を正しく計算する(税率ごとに1回だけ丸める)
- 【Railsで作る見積・請求管理システム 第5回】見積書をPDFで出力する(Prawnで日本語フォントを扱う)
- 【Railsで作る見積・請求管理システム 第6回】見積書をメールで送る(Solid Queueで送信を非同期にする)
- 【Railsで作る見積・請求管理システム 第7回】受注から請求書へ変換する(状態遷移と請求番号、インボイスの記載事項)
- 【Railsで作る見積・請求管理システム 第8回】入金管理と督促の一覧を作る(一部入金・期限超過の抽出)
- 【Railsで作る見積・請求管理システム 第9回】Kamalでサーバーに配備する(Dockerイメージ・SSL・秘密情報・運用の注意)(この記事)










コメント
コメント一覧 (2件)
[…] 【Railsで作る見積・請求管理システム 第9回】Kamalでサーバーに配備する(D… […]
[…] 【Railsで作る見積・請求管理システム 第9回】Kamalでサーバーに配備する(D… […]