Skip to content

How to implement file upload with Rails Active Storage | has_one_attached & S3 support

Category: Ruby on Rails

Active Storage, built into Rails 5.2 by default, is a framework for file uploads. It's the official solution replacing CarrierWave and Paperclip, enabling seamless switching between local disk, AWS S3, Google Cloud Storage, and Microsoft Azure storage. This article systematically covers Active Storage setup, has_one_attached definition, validation, S3 configuration (config/storage.yml), and image resizing (Variant).

Controller Model has_one_attached active_storage_blobs (metadata) active_storage_attachments (join table) Service disk / s3 / gcs
Diagram: Active Storage architecture (Model → blobs/attachments → Service)

Active Storage Setup

To start using Active Storage, generate migrations with rails active_storage:install and apply them with rails db:migrate. This creates three tables: active_storage_blobs, active_storage_attachments, and active_storage_variant_records.

# Active Storage のマイグレーションを生成
rails active_storage:install

# マイグレーションを実行
rails db:migrate

# 生成されるテーブル:
# active_storage_blobs          — ファイルのメタデータ(ファイル名、サイズ、MIMEタイプなど)
# active_storage_attachments    — モデルとblobの紐付け(ポリモーフィック関連)
# active_storage_variant_records — 変換済み画像のキャッシュ
# Gemfile
gem 'image_processing', '>= 1.2'  # Variant(画像リサイズ)に必要
# gem 'mini_magick'               # ImageMagick を使う場合
# gem 'ruby-vips'                 # libvips を使う場合(高速)
bundle install

# ImageMagick のインストール(Ubuntu/Debian)
sudo apt-get install imagemagick

# macOS(Homebrew)
brew install imagemagick vips

Defining has_one_attached and has_many_attached

has_one_attached defines one-to-one file attachments, and has_many_attached defines one-to-many attachments. Just add them to the model to attach, retrieve, and delete files.

# app/models/user.rb
class User < ApplicationRecord
  # 1つのアバター画像
  has_one_attached :avatar

  # 複数の添付ファイル
  has_many_attached :documents
end

# app/models/article.rb
class Article < ApplicationRecord
  has_one_attached :cover_image
  has_many_attached :attachments

  # バリデーション(active_storage_validations gem を使用)
  validates :cover_image,
    content_type: { in: %w[image/jpeg image/png image/webp], message: 'はJPEG・PNG・WebPのみ有効です' },
    size: { less_than: 5.megabytes, message: 'は5MB以下にしてください' }

  validates :attachments,
    content_type: %w[application/pdf image/jpeg image/png],
    size: { less_than: 20.megabytes }
end
# コントローラーでの操作例
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = current_user

    if @user.update(user_params)
      redirect_to @user, notice: 'プロフィールを更新しました。'
    else
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:name, :avatar, documents: [])
  end
end
<!-- app/views/users/edit.html.erb -->
<%= form_with model: @user do |f| %>
  <div>
    <%= f.label :avatar, 'プロフィール画像' %>
    <!-- enctype は form_with が自動設定 -->
    <%= f.file_field :avatar, accept: 'image/jpeg,image/png,image/webp' %>

    <%# 現在の画像を表示 %>
    <% if @user.avatar.attached? %>
      <%= image_tag @user.avatar, width: 100 %>
    <% end %>
  </div>

  <div>
    <%= f.label :documents, '添付ファイル(複数可)' %>
    <%= f.file_field :documents, multiple: true %>
  </div>

  <%= f.submit '保存' %>
<% end %>

Validation with active_storage_validations Gem

Since Active Storage's standard model validation is weak, using the active_storage_validations gem allows you to declaratively validate Content-Type, size, width and height in pixels, and more.

# Gemfile
gem 'active_storage_validations'
# app/models/product.rb
class Product < ApplicationRecord
  has_one_attached :main_image
  has_many_attached :gallery_images

  # content_type バリデーション
  validates :main_image,
    attached: true,
    content_type: {
      in: %w[image/jpeg image/png image/webp image/gif],
      message: 'は画像ファイル(JPEG/PNG/WebP/GIF)を選択してください'
    },
    size: {
      between: 1.kilobyte..10.megabytes,
      message: 'は1KB〜10MBの範囲で指定してください'
    },
    dimension: {
      width: { min: 200, max: 4096 },
      height: { min: 200, max: 4096 },
      message: '縦横ともに200〜4096pxの範囲で指定してください'
    }

  validates :gallery_images,
    content_type: %w[image/jpeg image/png image/webp],
    size: { less_than: 5.megabytes },
    limit: { max: 10, message: '画像は最大10枚まで添付できます' }
end

Configuration of config/storage.yml

Active Storage's storage services are defined in config/storage.yml, and you switch which service to use via environment-specific settings in config/environments/.

# config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

amazon:
  service: S3
  access_key_id:     <%= ENV['AWS_ACCESS_KEY_ID'] %>
  secret_access_key: <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
  region:            <%= ENV['AWS_REGION'] %>
  bucket:            <%= ENV['AWS_BUCKET'] %>
  # CloudFront を使う場合
  # upload: { cache_control: 'max-age=3600' }

google:
  service: GCS
  project:     <%= ENV['GOOGLE_CLOUD_PROJECT'] %>
  credentials: <%= ENV['GOOGLE_CLOUD_CREDENTIALS'] %>
  bucket:      <%= ENV['GOOGLE_CLOUD_BUCKET'] %>

azure:
  service: AzureStorage
  storage_account_name: <%= ENV['AZURE_STORAGE_ACCOUNT_NAME'] %>
  storage_access_key:   <%= ENV['AZURE_STORAGE_ACCESS_KEY'] %>
  container:            <%= ENV['AZURE_STORAGE_CONTAINER'] %>
# config/environments/production.rb
Rails.application.configure do
  # S3 を使用
  config.active_storage.service = :amazon
end

# config/environments/development.rb
Rails.application.configure do
  # ローカルディスクを使用
  config.active_storage.service = :local
end
# Gemfile(S3を使う場合)
gem 'aws-sdk-s3', require: false

# Google Cloud Storage を使う場合
gem 'google-cloud-storage', '~> 1.11', require: false

# Azure を使う場合
gem 'azure-storage-blob', require: false

Image resizing with Variant

Using Active Storage's Variant feature, you can dynamically resize and transform saved images for display. Conversion results are cached, so they are delivered quickly from the second time onwards.

# ビューでの Variant 使用例
# app/views/users/show.html.erb

<%# サムネイル表示(100x100にリサイズ)%>
<%= image_tag @user.avatar.variant(resize_to_limit: [100, 100]) %>

<%# アスペクト比を保ちながら幅300pxに収める %>
<%= image_tag @user.avatar.variant(resize_to_limit: [300, nil]) %>

<%# 中央でトリミングして正方形に(400x400)%>
<%= image_tag @user.avatar.variant(resize_to_fill: [400, 400]) %>

<%# WebP に変換(高画質・小容量)%>
<%= image_tag @user.avatar.variant(convert: 'webp', resize_to_limit: [800, 600]) %>

<%# 画質を下げてファイルサイズを削減 %>
<%= image_tag @user.avatar.variant(
      resize_to_limit: [1200, 900],
      saver: { quality: 80 }
    ) %>
# モデルでよく使う Variant を定義する(ヘルパーメソッド)
# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar

  def avatar_thumbnail
    avatar.variant(resize_to_fill: [80, 80], convert: 'webp')
  end

  def avatar_medium
    avatar.variant(resize_to_limit: [400, 400], convert: 'webp', saver: { quality: 85 })
  end
end

# ビューで使用
# <%= image_tag @user.avatar_thumbnail if @user.avatar.attached? %>

❓ Frequently Asked Questions

What is the difference between Active Storage and CarrierWave?
Active Storage has shipped with Rails since 5.2 and handles local disk, S3, GCS and Azure without extra gems. CarrierWave is a long-established third-party gem that integrates with models differently. New projects should start with Active Storage; existing CarrierWave projects should weigh the migration cost before switching.
How do I delete files with Active Storage?
With has_one_attached, user.avatar.purge removes the file and its record synchronously, and purge_later does it in the background. With has_many_attached, user.documents.purge clears everything, while user.documents.find(id).purge removes one. Note that purge deletes the actual file, whereas detach only breaks the association.
How do I access private S3 files via signed URLs in Active Storage?
rails_blob_url(user.avatar, disposition: 'attachment') and url_for(user.avatar) make Active Storage generate a signed URL for you. Set the lifetime in config/environments/production.rb, for example config.active_storage.service_urls_expire_in = 1.hour. Keep the bucket private and serve everything through signed URLs.

Test files for this article (free)