MENU

問い合わせ


    【Railsで作る見積・請求管理システム 第9回】Kamalでサーバーに配備する(Dockerイメージ・SSL・秘密情報・運用の注意)

    連載「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だけがあればよく、専用の管理画面や常駐ソフトは要りません。流れは次のとおりです。

    順番何が起きるか担当する場所
    1Dockerfile からイメージを作る手元のパソコン/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つです。

    1. ビルド用と実行用でステージを分ける:コンパイラなどは最終イメージに残さず、サイズと攻撃されうる面を小さくします。
    2. root ではなく専用ユーザーで動かす:万一アプリに問題があっても、サーバー全体に影響が及びにくくします。
    3. 本物の鍵をイメージに入れない:アセットのビルド時だけダミーの鍵(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 / hosthttps化とドメイン見積書・請求書は取引先情報を含むため、https は必須
    env.secret秘密情報の名前だけを書く値は次の .kamal/secrets から渡す。ファイルに値を書かない
    DB_HOST: mitsumori-dbDBの接続先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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      コメント

      コメント一覧 (2件)

      目次