active-storage
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:
# Check for Active Storage tables
bin/rails runner "puts ActiveStorage::Blob.table_exists?"
# Check for storage.yml
ls config/storage.yml
# Check for Active Storage migrations
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 blobsactive_storage_variant_records— tracks generated variants
Verify image processing is available:
# Check for libvips (preferred)
vips --version
# Or ImageMagick (fallback)
convert --version
Add to Gemfile if needed:
gem "image_processing", "~> 1.2" # Required for variants
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/environments/development.rb
config.active_storage.service = :local
# config/environments/test.rb
config.active_storage.service = :test
# config/environments/production.rb
config.active_storage.service = :amazon # or :google, :azure
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:
# has_one_attached
def user_params
params.expect(user: [:name, :email, :avatar])
end
# has_many_attached
def message_params
params.expect(message: [:title, :content, images: []])
end
Attaching files programmatically:
# From an upload
user.avatar.attach(params[:avatar])
# From an IO object
user.avatar.attach(
io: File.open("/path/to/file.jpg"),
filename: "avatar.jpg",
content_type: "image/jpeg"
)
# From a URL (download first)
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.
# WRONG — N+1 query for each user's avatar
@users = User.all
# In view: user.avatar triggers a query per user
# CORRECT — eager load attachments
@users = User.with_attached_avatar
# For has_many_attached
@messages = Message.with_attached_images
When rendering variants, also load variant records:
# In view, after eager loading attachments:
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
# Synchronous — deletes blob + file from storage immediately
user.avatar.purge
# Async — deletes via background job (preferred in controllers)
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 %>
# In controller or model
after_save :purge_avatar, if: -> { saved_change_to_attribute?(:remove_avatar) }
# Or handle in controller:
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:
# test/test_helper.rb or application_system_test_case.rb
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
# WRONG — MiniMagick syntax with Vips processor
variant(resize: "100x100")
# CORRECT — Vips syntax (Rails 7+ default)
variant(resize_to_limit: [100, 100])
4. Not handling missing attachments
# WILL RAISE if no avatar attached
user.avatar.variant(:thumb)
# SAFE
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
# Attachment status
record.avatar.attached? # => true/false
record.avatar.blank? # => true/false (opposite of attached?)
# File metadata
record.avatar.filename # => "avatar.png"
record.avatar.content_type # => "image/png"
record.avatar.byte_size # => 12345
record.avatar.blob.metadata # => {"identified"=>true, "width"=>100, "height"=>100, "analyzed"=>true}
# URLs
url_for(record.avatar) # Redirect URL
rails_blob_path(record.avatar) # Path helper
rails_storage_proxy_path(record.avatar) # Proxy URL
# Downloading
record.avatar.download # => binary string
record.avatar.open { |f| ... } # => yields Tempfile
# Purging
record.avatar.purge # Sync delete
record.avatar.purge_later # Async delete
Configuration Options
# config/application.rb or environment files
config.active_storage.variant_processor = :vips # :vips (default) or :mini_magick
config.active_storage.resolve_model_to_route = :rails_storage_proxy # Enable proxy mode
config.active_storage.draw_routes = false # Disable default routes (for custom auth)
config.active_storage.track_variants = true # Track variant records (default)
config.active_storage.service = :local # Set storage service
More from thinkoodle/rails-skills
minitest
Expert guidance for writing fast, maintainable Minitest tests in Rails applications. Use when writing tests, converting from RSpec, debugging test failures, improving test performance, or following testing best practices. Covers model tests, policy tests, request tests, system tests, fixtures, and TDD workflows.
32caching
Expert guidance for Rails caching — fragment caching, Russian doll caching, cache keys/versioning, low-level caching (Rails.cache), conditional GET (stale?/fresh_when), and cache stores (Solid Cache, Redis, Memcached). Use when implementing cache, caching, fragment cache, Russian doll, Rails.cache, Solid Cache, cache key, HTTP caching, stale?, fresh_when, cache store, or optimizing performance.
4uuid-primary-keys
Expert guidance for implementing UUID primary keys in Rails applications. Use when setting up UUIDs as primary keys, choosing between UUIDv4 and UUIDv7, configuring generators for UUID defaults, writing migrations with id colon uuid, adding UUID foreign keys, implementing base36 encoding for URL-friendly IDs, configuring PostgreSQL pgcrypto or gen_random_uuid, implementing SQLite binary UUID storage, choosing a primary key type, using non-sequential IDs, secure IDs, random IDs, or any ID generation strategy beyond auto-increment integers.
4security
Expert guidance for writing secure Rails applications. Use when dealing with security, CSRF protection, XSS prevention, SQL injection, authentication, authorization, sanitize, html_safe, credentials, secrets, content security policy, session security, mass assignment, strong parameters, secure headers, file uploads, open redirects, or vulnerability remediation. Covers every major attack vector and the Rails-idiomatic defenses.
4stimulus
Expert guidance for building Stimulus controllers in Rails applications. Use when creating JavaScript behaviors, writing data-controller/data-action/data-target attributes, building interactive UI components, or working with Hotwire Stimulus. Covers controller creation, targets, values, actions, classes, outlets, lifecycle callbacks, progressive enhancement, and common patterns like clipboard, flash, modal, toggle, and form validation.
4testing
Expert guidance for Rails testing infrastructure, test types, and what to test. Use when writing tests, setting up a test suite, choosing between test types, configuring system tests (Capybara), request tests, integration tests, helper tests, mailer tests, job tests, Action Cable tests, parallel testing, CI setup, test database management, or improving test coverage. Covers the test runner, fixtures vs factories, parallel testing, system tests (drivers, screenshots), request tests, controller tests (legacy), helper tests, mailer tests, job tests, Action Cable tests, test coverage, CI patterns, and test database strategies. Trigger on "test", "testing", "test suite", "system test", "request test", "integration test", "test runner", "parallel testing", "capybara", "test database", "CI testing", "test coverage".
4