1. はじめに
Rails8 アプリを Render にデプロイ中、画像処理を多用したところ、メモリ不足でインスタンスが落ちるエラーが発生。
原因と対応方法を忘れないために、まとめました。
開発環境
・Ruby 3.3.6
・Rails 8.0.2.1
・Docker(開発環境)
・DB: PostgreSQL
・画像管理: Cloudinary
・デプロイ: Render / Neon
2. エラーメッセージ
Instance failed: c2g7h
Ran out of memory (used over 512MB) while running your code.
- Render のインスタンスが 512MB 以上のメモリを消費
- メモリ制限を超えたためプロセスが強制終了
つまり サーバー側のメモリ不足 によるクラッシュ
3. 原因
- ActiveStorage 側でバリアント処理(サーバーで画像加工)してた
- 店舗一覧で複数画像を扱う処理でメモリを大量に消費していた
4. 対応方法
サーバーで画像加工せず、バリアント処理を Cloudinary に移行
Cloudinary の公式ドキュメント
4-1. Helperに画像URL生成
app/helpers/spots_helper.rb
module SpotsHelper
# 1枚目の画像URL取得(一覧用)
def display_image_url(record, size: :thumb, image_attribute: :images)
# Cloudinaryでの画像URL生成を試行
cloudinary_url = cloudinary_image_url(record, size: size, image_attribute: image_attribute)
return cloudinary_url if cloudinary_url.present?
# Cloudinary失敗時はActiveStorageでフォールバック
fallback_image_url(record, size, image_attribute)
rescue => e
Rails.logger.error "画像URL生成エラー: #{e.message}"
nil
end
# 全画像のURL配列取得(詳細用)
def display_all_image_urls(record, size: :detail, image_attribute: :images)
return [] unless record.has_images?(image_attribute: image_attribute)
# Cloudinaryで全画像処理を試す
cloudinary_urls = all_cloudinary_image_urls(record, size: size, image_attribute: image_attribute)
return cloudinary_urls if cloudinary_urls.present?
# フォールバック:ActiveStorageで全画像処理
all_fallback_image_urls(record, size, image_attribute)
rescue => e
Rails.logger.error "全画像URL生成エラー: #{e.message}"
[]
end
private
# Cloudinary:1枚目のURL
def cloudinary_image_url(record, size: :thumb, image_attribute: :images)
return nil unless record.has_images?(image_attribute: image_attribute)
image_key = record.image_key(image_attribute: image_attribute)
return nil unless image_key.present?
# 画像サイズ
dimensions = image_dimensions(size)
cl_image_path(image_key,
width: dimensions[:width],
height: dimensions[:height],
crop: :fill,
quality: :auto
)
rescue => e
Rails.logger.error "Cloudinary画像URL生成エラー: #{e.message}"
nil
end
# Cloudinary:全画像のURL配列
def all_cloudinary_image_urls(record, size: :detail, image_attribute: :images)
return [] unless record.has_images?(image_attribute: image_attribute)
image_keys = record.all_image_keys(image_attribute: image_attribute)
return [] unless image_keys.present?
dimensions = image_dimensions(size)
image_keys.map do |key|
cl_image_path(key,
width: dimensions[:width],
height: dimensions[:height],
crop: :fill,
quality: :auto
)
end
rescue => e
Rails.logger.error "Cloudinary全画像処理エラー: #{e.message}"
[]
end
# ActiveStorage:1枚目のURL
def fallback_image_url(record, size, image_attribute)
return nil unless record.has_images?(image_attribute: image_attribute)
first_image = record.first_image(image_attribute: image_attribute)
return nil unless first_image&.blob.present?
rails_representation_url(first_image.variant(size))
rescue => e
Rails.logger.error "ActiveStorage処理エラー: #{e.message}"
nil
end
# ActiveStorage:全画像のURL配列
def all_fallback_image_urls(record, size, image_attribute)
return [] unless record.has_images?(image_attribute: image_attribute)
all_images = record.all_images(image_attribute: image_attribute)
all_images.map do |image|
next unless image&.blob.present?
rails_representation_url(image.variant(size))
end.compact
rescue => e
Rails.logger.error "ActiveStorage全画像処理エラー: #{e.message}"
[]
end
# 画像サイズ設定
def image_dimensions(size)
case size
# 一覧(サムネ)用サイズ
when :thumb
{ width: 200, height: 200 }
# 詳細表示用サイズ
when :detail
{ width: 600, height: 600 }
else
{ width: 200, height: 200 }
end
end
end
なぜHelperに書いたか
画像URL生成は表示用の処理なので、Railsの設計思想では「どう見せるか」に関わる処理はビュー寄りの役割を持つHelperにまとめるのが適切。
Railsでのざっくり分け方
- モデル:データを管理・保存する場所
- ヘルパー:ビューで使うための便利な処理を書く場所
cl_image_tag メソッドを使わなかった理由
# cl_image_tag の場合
= cl_image_tag(image_key, width: 200, height: 200)
# Cloudinary専用の<img>タグが直接生成される
# でも、Cloudinaryでエラーが起きたら画像が表示されない
# cl_image_path の場合
= image_tag cl_image_path(image_key, width: 200, height: 200)
# CloudinaryのURLだけ取得して、通常のimage_tagで表示
# エラーの時はActiveStorageで対応できる
- 保険が作れる: フォールバック機能
- どんな画像でも同じ書き方: 統一インターフェース
4-2.モジュールにモデル共通機能を追加
app/models/concerns/image_processable.rb
# 画像処理
module ImageProcessable
extend ActiveSupport::Concern
# 画像が存在するかの確認
def has_images?(image_attribute: :images)
send(image_attribute).attached?
end
# 最初の画像を取得
def first_image(image_attribute: :images)
images_collection = send(image_attribute)
return nil unless images_collection.attached?
images_collection.first
end
# 全画像を取得
def all_images(image_attribute: :images)
images_collection = send(image_attribute)
return [] unless images_collection.attached?
images_collection.to_a
end
# 画像のキーを取得(Cloudinary用)
def image_key(image_attribute: :images)
first_img = first_image(image_attribute: image_attribute)
first_img&.blob&.key
end
# 全画像のキーを取得(Cloudinary用)
def all_image_keys(image_attribute: :images)
all_images(image_attribute: image_attribute).map do |image|
image&.blob&.key
end.compact
end
# 画像のblobを取得
def image_blob(image_attribute: :images)
first_img = first_image(image_attribute: image_attribute)
first_img&.blob
end
end
モジュールって何?
- モジュールは「共通で使える処理をまとめた箱」みたいなもの
- モデル(UserとかSpotとか)の中に直接書くのではなく、別ファイルにまとめて 必要なモデルに「include」して使う
4-3.views
app/views/maps/search.html.slim
/ 修正前 */
- thumbnail_url = spot.display_image_url(size: :thumb, image_attribute: :images)
/ 修正後 */
- thumbnail_url = display_image_url(spot, size: :thumb, image_attribute: :images)
なぜ修正が必要だったか?
メソッドをモデルから Helper に移動したため、spot.display_image_url ではなく display_image_url(spot) を使用
5. 結果
サーバー側での重い画像処理がなくなったため、Render のメモリ消費が減少
インスタンスが落ちることなく、安定してデプロイ可能になりました。