Render にRails アプリをデプロイする

Railsアプリを公開しようとAWS EC2 に無料枠立てられるインスタンスで環境構築を試みた。
しかし、rbenv経由のrubyビルドでディスク容量不足になってしまった。
Fargateなど使う選択肢もあったのだが、なるべく安く済ませたかったので探したうえでRenderを使うことにした。

これを記録的に共有したい。

Renderは無料枠があり、それを使う。
Herokuの課金体系が変わった際には、Renderに乗り換えるという方が多かった様子であった。

参考

導入

ローカル構築

以下のdocker-compose.yml dockerfile を用意する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "127.0.0.1:3000:3000"
volumes:
# :cached を付与して、高速化
- .:/usr/src/app:cached
# vendor以下などホストと連携しなくてもよいものを、上書きして永続化の対象外にする。
# exclude volumes
- /usr/src/app/vendor
- /usr/src/app/tmp
- /usr/src/app/log
- /usr/src/app/.git
tty: true
environment:
- RAILS_ENV=development
- BINDING=0.0.0.0

db:
image: mysql:9.3
networks:
- default
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- "${MYSQL_PORT:-3306}:3306"
volumes:
- ./db/mysql_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-password_root}

volumes:
bundle:
driver: local
1
2
3
4
5
6
7
FROM ruby:3.5-rc

RUN apt-get update && apt-get install -y git build-essential default-mysql-client libreadline-dev

WORKDIR /usr/src/app

EXPOSE 3000

でDockerイメージをビルドして起動する。
以下shellで入って実行。

1
2
3
4
5
6
7
8
$ bundle init
# Gemfile gem "rails" のコメントを外す。

$ bundle install
$ rails new . --database=mysql
# もろもろ選択

$ bundle exec rails db:create

ここまでで初期化完了、適当に1つモデルを追加する。

1
2
$ bundle exec rails generate scaffold Product name:string description:text price:decimal
$ bin/rails db:migrate

で起動すると /products が生えていている。一旦OK。

Render用の構築

Renderでは、データベースはPostgreSQLを使うことなる。
そのため、databaseをPostgreSQLに変更する。

1
2
$ bundle exec rails db:system:change --to=postgresql
$ bundle install

database.yml を以下のように変更する。(本来は環境変数であるところも一度べた書き、実際は環境変数)

database.yml
1
2
3
4
5
6
7
8
default: &default
adapter: postgresql
template: template0
encoding: utf-8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch("POSTGRES_USER") { 'user' } %>
password: <%= ENV.fetch("POSTGRES_PASSWORD") { 'password_root' } %>
host: <%= ENV.fetch("POSTGRES_HOST") { 'pgdb' } %>

一旦docker を停止し、以下をDockercomposeに追加する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  pgdb:
image: postgres
ports:
- 5432:5432
volumes:
- pgdb_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: ${MYSQL_ROOT_PASSWORD:-password_root}
POSTGRES_DB: test

volumes:
pgdb_data:
driver: local

改めてbuildしなおす。

1
2
3
$ docker-compose build
$ docker-compose up -d
$ docker-compose exec app bash
1
2
3
4
$ bundle install
$ bundle exec rails db:create
$ bundle exec rails db:migrate
$ bundle exec rails s

これでpostgresに接続したので起動できることを確認。

また、資料には solid は初期導入の簡素化のために外して個別に入れて欲しい旨の記載がある。

既にアプリは作成済みのため、以下の項目はコメントアウトしておく。

1
2
3
#gem "solid_cache"
#gem "solid_queue"
#gem "solid_cable"

この状態で、bundle install しておく。

Renderにデプロイ

Renderにアカウントを作成し Web Servicesから追加する。

githubから連携できるので連携する。
言語の選択などあるので、ruby を選択したりする。

一旦deployをするがエラーになるので、以下を対応していく。

bin/render-build.sh を作成する。ドキュメントのままコピーする

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env bash

# Exit on error
set -o errexit

bundle install
bin/rails assets:precompile
bin/rails assets:clean

# If you have a paid instance type, we recommend moving
# database migrations like this one from the build command
# to the pre-deploy command:
bin/rails db:migrate

作った後、必ず権限をつけること。
これが無いと実行できなくてコケることになる。

1
$ chmod a+x bin/render-build.sh

bin/rails の権限も必要なので、同様に変更する。

1
$ chmod a+x bin/rails

自身の環境ではこれも効果が無く、git update-index --chmod=+x bin/render-build.sh のように権限を変更した。
どうやらWindows環境である場合には、この対処が必要な模様。

これをcommitしてpushする。
これがまたコケる。
設定画面からBuild Commandを bin/render-build.sh に変更する。
まだ、デプロイできない。

このアプリケーションはRuby 3.5.0 で用意したが、Renderのデフォルトは3.4.4(2025/08/23現在)。
対応バージョンが無いために失敗するので、3.4.4に変更する。

.ruby-version
1
ruby-3.4.4

続けてsecret key baseの設定が不足している指摘が入るので、環境変数に SECRET_KEY_BASE を追加し、bin/rails secret の結果を設定する。
併せて RAILS_ENV=production も設定する。

ここまで設定してデータベースの参照が無いことをへのエラーが入る。
データベースの設定をする。

RenderのPostgreSQLを使う

Render のダッシュボードから Add New -> Postgres を選択し、PostgreSQLのインスタンスを作成する。

名称は任意に設定する。
Dataase の項目は、はじめアプリが参照する名前と一致するため app_production としたが、接尾辞が付与されてしまったので究極なんでもよいと考えられる。
少し待つと作成が完了する。画面を下ると、各種設定の記載があるので、これを環境変数に設定する。

Renderの設定項目 環境変数名
Internal Database URL DATABASE_URL

併せて、database.yml の記載も以下のように直す。

database.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
default: &default
adapter: postgresql
template: template0
encoding: utf-8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: <%= ENV.fetch("POSTGRES_USER") { 'user' } %>
password: <%= ENV.fetch("POSTGRES_PASSWORD") { 'password_root' } %>
host: <%= ENV.fetch("POSTGRES_HOST") { 'pgdb' } %>


development:
<<: *default
database: app_development

test:
<<: *default
database: app_test


production:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
1
NoMethodError: undefined method 'solid_queue' for an instance of Rails::Application::Configuration (NoMethodError)

のようにログが出る。
対応として、Gemfile から solid 関連をコメントアウトしたが利用側が残っているので暫定的にコメントアウトする。

config\environments\production.rb
1
2
3
4
5
6
# Replace the default in-process memory cache store with a durable alternative.
#config.cache_store = :solid_cache_store

# Replace the default in-process and non-durable queuing backend for Active Job.
#config.active_job.queue_adapter = :solid_queue
#config.solid_queue.connects_to = { database: { writing: :queue } }

ここまで実施してデプロイは完了するが、404エラーになる。
原因は、config/environments/production.rbconfig.hosts にホスト名が設定されていないため。これを書き換える。(環境変数で設定の方が望ましいはず。)

config/environments/production.rb
1
config.action_mailer.default_url_options = { host: "hogehoge.onrender.com" }

これで、無事にRailsアプリがRender上で動作した。

データベースを外部に切り替える

RenderのPostgreSQLは、無料枠があるものの、期間が設定されていることからデータべ――スを外部に切り替えてみます。
今回はNEON

これは、NEON から文字列を取得し、Renderの環境変数DATABASE_URL に設定するだけでよい。

cron の実行

定期的に実行したい処理がある。
RailsアプリならWheneverのようなものを使いたいが、Renderではcron自体は使えない。
それは実行とビルドの環境が異なることに起因する。
Renderでは、Cron Jobという機能があるが、無料で使えない。

なので、UpstashQStashを使うことにした。

rubyのgemはサードパーティであればあるのだが、受信側の実装が無い。

github dvmonroe/qstash-ruby

リクエストの真正性の問題があるが、一旦これを使う。

到達する先のコントローラーは次のようにする。

app/controllers/jobs_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class JobsController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:product_add]
require 'rake' require 'rake'

def product_add
fiber = Fiber.new do
Rails.application.load_tasks
Rake::Task['products:create_random'].reenable
Rake::Task['products:create_random'].invoke
end

render plain: "OK"
fiber.resume
end
end

リクエストには、UpstashのスケジュールIDが含まれるので、最低限それを突き合わせる選択肢はあり得る。
(そもそもちゃんと公式のReceiverのような検証すべき。)
「前回実行からxx分経っている」を実行条件にすると仮に連打されても問題は一定程度回避できる。

非常に長いタスクであれば難しい面はあろうが、メッセージを切り替えることくらいでああれば対応できることが見込まれる。


Rails アプリをRender.comにデプロイして、疑似的なCronジョブを実行まで試みました。

ここまで出来れば、かなりサービス運営として十分稼働でできる所感を得られました。

では。