はじめに
個人開発のRailsアプリを、初めて自分でVPSにデプロイしました。Herokuの有料化以降、「自前でサーバーを立てる経験も積みたい」と思っていたのと、「会社で使っているSSL証明書の手動更新を自動化できないか」という現実的な動機もあり、ConoHa VPS + Let's Encrypt構成にチャレンジしました。
結論から言うと、2日間で本番HTTPS化まで完走できました。ただしその道中、10以上のエラーに詰まりまくりました。この記事は、その詰まりポイントと解決策を、後から見返して自分でも再現できる手順書として残したものです。同じ構成でデプロイする方の参考になれば嬉しいです。
記事で扱う構成
[ブラウザ]
↓ HTTPS
[Nginx] ← Let's Encrypt(certbotで自動更新)
↓ proxy_pass
[Puma] ← systemd で常駐
↓
[Rails 7.2 / Ruby 3.4.2]
↓
[MySQL 8.0]
- サーバー: ConoHa VPS(Ubuntu 22.04, 1GB RAM)
- デプロイ: Capistrano
- ドメイン: お名前.com
- 画像ストレージ: AWS S3(ActiveStorage)
この記事で得られること
- ConoHa VPSの初期ハマりポイント
- Rails 7系 × Puma × systemd の落とし穴
- Capistrano初回デプロイで起きがちなマイグレーション地獄の回避策
-
sudo certbot --nginxの便利さ
全体の流れ
| フェーズ | やったこと |
|---|---|
| 1. VPSセットアップ | セキュリティグループ設定、deployユーザー作成、SSH鍵認証 |
| 2. 実行環境構築 | rbenv + Ruby 3.4.2、MySQL 8.0、Node.js 20 |
| 3. アプリ配置準備 |
/var/www/utuwagatari、Capistrano用のshared/ディレクトリ |
| 4. AWS環境整備 | S3バケット、IAMユーザー(最小権限ポリシー) |
| 5. Capistrano初回デプロイ | ← ここで10個くらいエラー連発 |
| 6. Nginx設定 | リバースプロキシとして設定 |
| 7. Let's EncryptでHTTPS化 |
sudo certbot --nginx 一発 |
詰まりポイント15選
🔥 インフラ・サーバー系
1. ConoHa VPSのセキュリティグループが全閉じ
症状: VPS作ったのにSSHすら繋がらない、pingも通らない。
原因: ConoHa VPSは新規作成時にdefaultセキュリティグループが付与され、これが全ポート閉じた状態になっています。AWSのデフォルトVPCに慣れていると「IPもらえたら繋がる」と思い込みがちですが、ConoHaは違います。
解決:
コントロールパネルからIPv4v6-SSH(22番)とIPv4v6-Web(80/443番)を追加。
学び: クラウドの初期設定はベンダーごとに違う。「IPがあれば繋がる」じゃない。
2. Ubuntuのufwで二重にブロックされていた
症状: ConoHaのセキュリティグループで80/443を開けたのに、ブラウザからERR_CONNECTION_REFUSED。しかもサーバー内からcurl http://127.0.0.1/は成功する。
原因: Ubuntuにはufwというファイアウォールがあり、デフォルトでOpenSSHしか許可されていない状態でした。つまりクラウド側のSGとOS側のufwの両方で許可する必要がある。
解決:
sudo ufw allow 'Nginx Full'
sudo ufw reload
学び: ファイアウォールは多段で存在する。ネットワーク系で詰まったら、「どの層で止まっているか」を上から下に切り分けるのが基本。
3. Rubyビルドに必要な依存ライブラリ不足
症状: rbenv install 3.4.2 が libffi missing で失敗。
解決: Rubyビルドに必要な依存を先にまとめてインストール。
sudo apt install -y \
build-essential libssl-dev libreadline-dev \
zlib1g-dev libffi-dev libyaml-dev \
libmysqlclient-dev
学び: Rubyビルドは依存が多い。公式ドキュメントの推奨パッケージを最初に全部入れておくのが結局一番早い。
4. 1GB RAMでRubyビルドがOOM(Out of Memory)の気配
症状: まだ起きてはいないが、1GB RAMでRubyビルドは怪しいという噂を聞いていた。
解決: 先にスワップを2GB追加しておいた。
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
学び: 低スペックVPSでRuby/Node系をビルドするときは、先にスワップを作っておくのが鉄則。
🔥 Git / SSH 系
5. エージェントフォワーディングでGitHub認証が通らない
症状: ローカルからはGitHubにSSH接続できるのに、サーバーにSSHしてからgit@github.comにアクセスするとPermission denied。
原因: macOSのキーチェーン管理されていない鍵(id_ed25519)だけがSSHエージェントに登録されていて、GitHubに登録している鍵(id_rsa)がエージェントに渡っていなかった。
解決:
ssh-add --apple-use-keychain ~/.ssh/id_rsa
さらに~/.ssh/configを明示的に整備:
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa
AddKeysToAgent yes
UseKeychain yes
Host earth
HostName 133.117.72.20
User deploy
IdentityFile ~/.ssh/id_ed25519
ForwardAgent yes
学び: 複数の鍵がある環境では、どの鍵がどこで使われるか~/.ssh/configで明示するのがベスト。勘で動かすのは事故の元。
6. Capistranoでed25519鍵がサポートされない
症状: cap production deploy が OpenSSL::PKey::PKeyError で落ちる。
原因: net-ssh は ed25519 鍵を使うために追加gemが必要。
解決: Gemfile に追加。
group :development do
gem 'ed25519'
gem 'bcrypt_pbkdf'
end
🔥 Rails / Puma / systemd 系
7. Importmap構成なのにassets:precompileでNode.js必須
症状: デプロイ中にassets:precompileでExecJS::RuntimeUnavailable。
原因: Importmap採用しているのでNode.js不要だと思っていたが、autoprefixer-rails gemがCSSベンダープレフィックス処理のために内部でJSを実行していた。
解決: Node.js 20をインストール。
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
学び: 「Importmap = Node.js完全不要」とは限らない。gemの依存を追うこと。
8. 壊れたマイグレーションが本番で炸裂
症状: 本番でDuplicate column 'active'→直すとDuplicate column 'tag_name'→さらに直すとCannot drop 'store_id'…と3連発でエラー。
原因: 開発途中からClaude Codeに実装を任せる中で、マイグレーションの修正もAIに委ねていたことが原因でした。AIは指示されたタスクは着実にこなしますが、過去のマイグレーション履歴との整合性までは見てくれず、ファイル間の整合性が静かに崩れていました。ローカルではdb:resetで取り繕えていたため気づけず、本番で初回から走らせて初めて発覚しました。
解決: 不要なマイグレーションファイルを整理し、ローカルでもdb:drop db:create db:migrateで全マイグレーションが通ることを確認してから再デプロイ。
学び(一番重要):
マイグレーションのような「履歴に意味がある」変更は、AIに任せる前に人間がレビューする工程を必ず挟む。
「マイグレーションは作ったら消さない・書き換えない」は知識として知っていても、AIに任せる範囲の線引きが曖昧だと結果的に守れなくなる。
知識として持っているだけでは不十分で、AI活用時代には「どこまで任せてどこから人間が見るか」をルール化することが大事だと痛感しました。
9. MySQLパスワードに!を含めて詰む
症状: DATABASE_URL にパスワードを書いてもDB接続エラー。
原因: URLエンコードでは!は%21にしないといけない。さらにBashのヒストリー展開でも問題になり得る。
解決: パスワードを英数字のみ・長めにして再設定。
学び: 設定ファイルに書くパスワードは英数字のみが安全。強度は長さで担保する。
10. capistrano3-pumaの二重拡張子バグ
症状: systemd ユニットファイル名が puma_utuwagatari_production.service.service になり、systemctl で見つからない。
原因: deploy.rb で set :puma_service_unit_name に .service まで含めて書くと、gemが内部で勝手に.serviceを追加して二重になる。
解決: .service を抜く。
# ❌ ダメ
set :puma_service_unit_name, "puma_#{fetch(:application)}_production.service"
# ✅ OK
set :puma_service_unit_name, "puma_#{fetch(:application)}_production"
学び: gemの慣習(規約)はソースかドキュメントを読むしかない。
11. puma.rbでRails定数を直接参照していた
症状: systemd 経由で Puma を起動すると uninitialized constant Puma::DSL::Rails。bin/rails s では動くのに。
原因: config/puma.rb で Rails.env.development? を使っていた。bin/rails s はRailsが先にロードされた状態でPumaを呼ぶので動くが、bundle exec puma(systemd経由)は Pumaが先、Railsはまだロードされていない状態でpuma.rbが評価される。
解決:
# ❌ Railsがロードされていないと落ちる
if Rails.env.development?
# ✅ ENVから読む
if ENV.fetch("RAILS_ENV", "development") == "development"
学び: 起動時に評価されるファイルでは、Rails依存の書き方を避ける。Pumaの起動シーケンスを理解する良いきっかけになった詰まり。
12. Rails 7.1 autoload_lib の generators 除外漏れ
症状: 本番で eager_load 中に uninitialized constant Rails::Generators。
原因: Rails 7.1 から入った config.autoload_lib(ignore: [...]) は lib/ 配下を自動 autoload する。本番では config.eager_load = true で lib/ 以下が全部読まれるが、lib/generators/ には Rails::Generators を継承したクラスがあり、本番には Rails::Generators がロードされていない(generatorsはdevelopmentでしかロードされない)。
解決: config/application.rb
# ❌
config.autoload_lib(ignore: %w[assets tasks])
# ✅
config.autoload_lib(ignore: %w[assets tasks generators])
学び: コメントで例示されてる内容(templates, generators, middleware)は流さずちゃんと読む。Railsのバージョンアップで挙動が変わる箇所は要チェック。
🔥 Nginx / SSL / DNS 系
13. Puma が Unixソケットじゃなくて TCP で動いていた
症状: deploy.rb で puma_bind を Unixソケットに設定していたのに、ls shared/tmp/sockets/ しても puma.sock が存在しない。
原因: config/puma.rb に書いていた port ENV.fetch("PORT", 3000) が効いて、Pumaは実際には3000番ポートでリッスンしていた。deploy.rb の設定と puma.rb の設定が競合していた。
解決: 今回は深追いせず、Nginx側を proxy_pass http://127.0.0.1:3000 に合わせた。本来は puma.rb と deploy.rb の設定を揃えるべき。
学び: 設定と実際の動作が食い違うときは、ss -tlnp で実際に何がどこでリッスンしているかを確認するのが一番早い。
sudo ss -tlnp
14. お名前.comのDNS管理画面が2種類あってワイルドカード制限
症状: レンタルサーバー側のDNS管理画面で*.utsuwagatari.jpと入れるとバリデーションで弾かれる。
原因: お名前.comには「レンタルサーバー管理画面のDNS」と「お名前.com NaviのDNS」の2種類あり、前者はワイルドカード非対応。後者を使うにはネームサーバー変更(反映に時間がかかる)が必要。
解決(今回): test.utsuwagatari.jpを個別Aレコードで追加して当面しのぐ。ワイルドカード対応は後日。
学び: ドメインレジストラのDNS管理は意外とクセがある。Cloudflareに移管するのも選択肢。
15. sudo certbot --nginx が便利すぎた
症状: 症状ではなく感動ポイント。
HTTP版のNginx設定をまず動かした状態で、以下を実行:
sudo certbot --nginx -d utsuwagatari.jp -d test.utsuwagatari.jp
すると certbot が:
- Let's Encryptから証明書を取得
- Nginx設定にHTTPSサーバーブロックを自動追加
- HTTP→HTTPSリダイレクトを自動追加
- systemd timerで自動更新スケジュール設定
これらを一発でやってくれる。ブラウザでhttps://utsuwagatari.jp/にアクセスしたら、すでに🔒マーク付きでログイン画面が表示されました。
学び: 現代のインフラツールは「やってくれる範囲」がとても広い。手書きするより、ツールに任せた方が安全で早いケースが多い。
余談: 前職ではSSL証明書を手動で更新していたので、この仕組みが使えれば自動化できて管理コストがかからないと思いました。
最終的なNginx設定(抜粋)
certbotに書き換えてもらった最終形。
upstream puma {
server 127.0.0.1:3000;
}
server {
server_name utsuwagatari.jp test.utsuwagatari.jp;
root /var/www/utuwagatari/current/public;
location / {
proxy_pass http://puma;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/utsuwagatari.jp/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/utsuwagatari.jp/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = utsuwagatari.jp) {
return 301 https://$host$request_uri;
}
listen 80;
server_name utsuwagatari.jp test.utsuwagatari.jp;
return 404;
}
振り返り
よかったこと
-
エラーが出るたびに諦めず、原因切り分けを続けられた。特にPumaがsystemdで起動しない問題は、
bin/rails sでは動くのにbundle exec pumaでは動かないという挙動の差から、Pumaの起動順序を理解できたのが収穫でした。 - certbotの便利さを実体験できた。
改善したいこと
- ufwの存在を最初から頭に入れておけば、Phase 14の詰まり(ERR_CONNECTION_REFUSED)は防げた。
- Claude Codeを初めて使ってみて、相談しながらかなり早く進められた一方で、任せる範囲と人間が判断すべき範囲の線引きが曖昧だったことが今回のマイグレーション地獄につながった。次回はAIに任せる作業のルールを最初に決めてから進めたい。
次にやる時のチェックリスト
- AIに任せる範囲と人間が必ず確認する範囲を最初に決める(マイグレーション・スキーマ変更は人間が必ず確認)
- ローカルで
db:drop db:create db:migrateが通ることを確認してから本番デプロイ - パスワードは英数字のみで長くする
-
~/.ssh/configを最初に整備 - ファイアウォールは多段(クラウドSG + OS側ufw)、両方確認
-
curl -vでレスポンスを読む。エラーに見えても成功の証のことがある -
--nginxのような自動化オプションは積極的に使う
おわりに
初めての本番デプロイ、想像していた3倍くらい詰まりましたが、その分 「動いている」という状態のありがたさを噛み締めています。
同じ構成で詰まった方の参考になれば嬉しいです。コメント・指摘大歓迎です!