| name | active-storage |
| description | Expert guidance for Rails Active Storage — file uploads, attachments, image variants, direct uploads, and cloud service configuration. Use when working with file uploads, has_one_attached, has_many_attached, image processing, variants, S3/GCS/Azure storage, direct uploads, or file attachment patterns in Rails applications. |
| allowed-tools | Read, Grep, Glob, Write, Edit, Bash(bin/rails active_storage:*), Bash(bin/rails db:migrate), Bash(bin/rails generate), Bash(bundle exec rails active_storage:*), Bash(bundle exec rails db:migrate) |
Rails Active Storage Expert
Implement file uploads, attachments, image processing, and cloud storage in Rails applications using Active Storage.
Philosophy
Core Principles:
- Always run the install generator — Active Storage needs its migrations and config
- Use libvips over ImageMagick — 10x faster, 1/10 memory usage
- Prevent N+1 queries — Always use
with_attached_<name> scopes
- Handle missing attachments — Never assume an attachment exists
- Proxy in production, redirect in development — Proxy mode works with CDNs
- Use named variants — Define variants on the model, not in views
When To Use This Skill
- Adding file uploads to a Rails model
- Configuring cloud storage (S3, GCS, Azure)
- Implementing image resizing/transformation (variants)
- Setting up direct uploads from the browser
- Debugging attachment issues (missing files, broken variants)
- Migrating between storage services
- Testing file uploads
Instructions
Step 1: Verify Setup
ALWAYS check if Active Storage is installed first:
bin/rails runner "puts ActiveStorage::Blob.table_exists?"
ls config/storage.yml
ls db/migrate/*active_storage*
If not installed:
bin/rails active_storage:install
bin/rails db:migrate
This creates three tables:
active_storage_blobs — file metadata (filename, content_type, byte_size, checksum)
active_storage_attachments — polymorphic join table connecting models to blobs
active_storage_variant_records — tracks generated variants
Verify image processing is available:
vips --version
convert --version
Add to Gemfile if needed:
gem "image_processing", "~> 1.2"
Step 2: Configure Storage Service
config/storage.yml — Define available services:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
Set the active service per environment:
config.active_storage.service = :local
config.active_storage.service = :test
config.active_storage.service = :amazon
For cloud services, see reference.md for S3, GCS, and Azure configuration.
Step 3: Declare Attachments on Models
Single file attachment:
class User < ApplicationRecord
has_one_attached :avatar
end
Multiple file attachments:
class Message < ApplicationRecord
has_many_attached :images
end
With named variants (ALWAYS do this):
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100]
attachable.variant :medium, resize_to_limit: [300, 300]
attachable.variant :large, resize_to_limit: [800, 800]
end
end
With a specific storage service:
class User < ApplicationRecord
has_one_attached :avatar, service: :s3
end
Step 4: Handle Attachments in Controllers
Strong parameters — permit attachment params:
def user_params
params.expect(user: [:name, :email, :avatar])
end
def message_params
params.expect(message: [:title, :content, images: []])
end
Attaching files programmatically:
user.avatar.attach(params[:avatar])
user.avatar.attach(
io: File.open("/path/to/file.jpg"),
filename: "avatar.jpg",
content_type: "image/jpeg"
)
user.avatar.attach(
io: URI.open("https://example.com/photo.jpg"),
filename: "photo.jpg"
)
Step 5: Display Attachments in Views
ALWAYS check if attached before rendering:
<%# WRONG — will raise if no avatar %>
<%= image_tag user.avatar %>
<%# CORRECT — guard against missing attachment %>
<%= image_tag user.avatar if user.avatar.attached? %>
<%# BEST — use variant with guard %>
<% if user.avatar.attached? %>
<%= image_tag user.avatar.variant(:thumb) %>
<% end %>
Use named variants (not inline transforms):
<%# WRONG — variant defined in view %>
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
<%# CORRECT — named variant from model %>
<%= image_tag user.avatar.variant(:thumb) %>
Step 6: Prevent N+1 Queries
This is the #1 performance mistake with Active Storage.
@users = User.all
@users = User.with_attached_avatar
@messages = Message.with_attached_images
When rendering variants, also load variant records:
message.images.with_all_variant_records.each do |image|
image_tag image.variant(:thumb).processed.url
end
Step 7: Form Setup
Simple file upload:
<%= form_with model: @user do |form| %>
<%= form.file_field :avatar %>
<%= form.submit %>
<% end %>
Multiple files:
<%= form_with model: @message do |form| %>
<%= form.file_field :images, multiple: true %>
<%= form.submit %>
<% end %>
Preserve existing has_many_attached files on update:
<%# Without this, updating replaces ALL existing attachments %>
<% @message.images.each do |image| %>
<%= form.hidden_field :images, multiple: true, value: image.signed_id %>
<% end %>
<%= form.file_field :images, multiple: true %>
Direct uploads (upload to cloud before form submit):
<%= form.file_field :avatar, direct_upload: true %>
Requires Active Storage JavaScript. See reference.md for setup.
Step 8: Removing Attachments
user.avatar.purge
user.avatar.purge_later
Allow users to remove attachments via a checkbox:
<% if @user.avatar.attached? %>
<%= image_tag @user.avatar.variant(:thumb) %>
<%= form.check_box :remove_avatar, label: "Remove avatar" %>
<% end %>
after_save :purge_avatar, if: -> { saved_change_to_attribute?(:remove_avatar) }
def update
@user.update(user_params)
@user.avatar.purge_later if params[:user][:remove_avatar] == "1"
end
Step 9: Testing
Use file_fixture_upload in integration tests:
class UsersControllerTest < ActionDispatch::IntegrationTest
test "creates user with avatar" do
post users_path, params: {
user: {
name: "Test",
avatar: file_fixture_upload("avatar.png", "image/png")
}
}
user = User.order(:created_at).last
assert user.avatar.attached?
end
end
Place test files in test/fixtures/files/.
Clean up uploaded files after tests:
class ActionDispatch::IntegrationTest
def after_teardown
super
FileUtils.rm_rf(ActiveStorage::Blob.service.root)
end
end
For parallel tests, isolate storage per process:
class ActiveSupport::TestCase
parallelize_setup do |i|
ActiveStorage::Blob.service.root = "#{ActiveStorage::Blob.service.root}-#{i}"
end
end
See reference.md for fixture-based attachment testing.
Common Agent Mistakes
1. Forgetting the install generator
Active Storage won't work without its migrations. Always run bin/rails active_storage:install and bin/rails db:migrate.
2. Missing image processing dependency
Variants require gem "image_processing" AND either libvips or ImageMagick installed on the system. Without these, variant() calls will raise.
3. Wrong variant processor syntax
variant(resize: "100x100")
variant(resize_to_limit: [100, 100])
4. Not handling missing attachments
user.avatar.variant(:thumb)
user.avatar.variant(:thumb) if user.avatar.attached?
5. N+1 queries with attachments
Always use with_attached_<name> scope when loading collections. This is the most common performance issue.
6. Defining variants in views instead of models
Variants defined inline in views are hard to maintain and can't be preprocessed. Define them on the model using the block syntax.
7. Replacing instead of appending with has_many_attached
By default, attaching new files to has_many_attached replaces all existing attachments. Use hidden fields with signed_id to preserve existing ones.
Quick Reference
Variant Transforms (Vips — Rails 7+ default)
| Transform | Syntax | Description |
|---|
| Resize to fit | resize_to_limit: [w, h] | Shrinks to fit within bounds, preserves aspect ratio |
| Resize to fill | resize_to_fill: [w, h] | Fills bounds exactly, crops excess |
| Resize and pad | resize_to_fit: [w, h] | Fits within bounds, may be smaller |
| Resize exact | resize_and_pad: [w, h] | Fits within bounds, pads to exact size |
| Convert format | format: :webp | Convert to another format |
| Quality | saver: { quality: 80 } | Set output quality |
| Rotate | rotate: 90 | Rotate by degrees |
Key Methods
record.avatar.attached?
record.avatar.blank?
record.avatar.filename
record.avatar.content_type
record.avatar.byte_size
record.avatar.blob.metadata
url_for(record.avatar)
rails_blob_path(record.avatar)
rails_storage_proxy_path(record.avatar)
record.avatar.download
record.avatar.open { |f| ... }
record.avatar.purge
record.avatar.purge_later
Configuration Options
config.active_storage.variant_processor = :vips
config.active_storage.resolve_model_to_route = :rails_storage_proxy
config.active_storage.draw_routes = false
config.active_storage.track_variants = true
config.active_storage.service = :local