Active Storage
Active Storage (introduced in Rails 5.2) can be used to attach files (for example avatar images, CSV uploads, or invoice PDFs) to models and store those files on the local disk, on a network share, or in a cloud service like S3, GCS or Azure Blob Storage.
Active Storage can not just store files but also resize, convert, preview and analyse them. Here I just show you how to attach a file to give you the basic idea.
Avatar Example
First of all I’m sorry for not coming up with a more original example. Everybody uses avatars to describe how to attach something. I do it too because it is such a common use case.
We create a new phone book application which stores basic user
information in the User model:
$ rails new phone_book
[...]
$ cd phone_book
$ bin/rails generate scaffold User first_name last_name email_address
$ bin/rails db:migrate
To work with images we need access to ImageMagick or libvips. Install one with your package manager:
# macOS with Homebrew
$ brew install vips # (preferred)
# or
$ brew install imagemagick
Rails 8 configures image_processing with libvips by default
(faster and more memory-efficient than ImageMagick). The
Gemfile already contains:
# Use Active Storage variants
gem "image_processing", "~> 1.2"
Older editions of this book activated mini_magick
manually. Rails 7+ defaults to image_processing, which
wraps either libvips or ImageMagick — pick whichever you
already have installed. image_processing is in the
Gemfile of a fresh Rails 8 app, so you don’t need to add
anything.
|
To use Active Storage we run bin/rails active_storage:install:
$ bin/rails active_storage:install
$ bin/rails db:migrate
== 20260419100000 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs)
-> 0.0020s
-- create_table(:active_storage_attachments)
-> 0.0018s
-- create_table(:active_storage_variant_records)
-> 0.0011s
== 20260419100000 CreateActiveStorageTables: migrated (0.0049s) ===============
These tables take care of all the storage metadata. We don’t
have to change the users table at all to add an avatar. We do
that in the model:
class User < ApplicationRecord
has_one_attached :avatar
end
Now you have access to the avatar method on the User model.
Let’s create a new user in the console and attach an image from
the local file system:
$ bin/rails console
Loading development environment (Rails 8.1.3)
irb(main):001> user = User.create(first_name: "Stefan", last_name: "Wintermeyer")
TRANSACTION (0.1ms) BEGIN
User Create (1.4ms) INSERT INTO "users" (...) VALUES (?, ?, ?, ?) RETURNING "id"
TRANSACTION (1.5ms) COMMIT
=> #<User id: 1, first_name: "Stefan", last_name: "Wintermeyer", email_address: nil, ...>
irb(main):002> user.avatar.attach(io: File.open("/Users/xyz/Desktop/stefan-wintermeyer.jpg"), filename: "stefan-wintermeyer.jpg", content_type: "image/jpeg")
Disk Storage (3.1ms) Uploaded file to key: C8uKHdsuSemKP1iJXDcB5Kcf (checksum: ...)
ActiveStorage::Blob Create (1.0ms) ...
ActiveStorage::Attachment Create (0.9ms) ...
Enqueued ActiveStorage::AnalyzeJob ...
=> #<ActiveStorage::Attachment id: 1, name: "avatar", ...>
You can use avatar.attached? to check if a given user has an
avatar:
irb(main):003> user.avatar.attached?
=> true
To see the avatar we update the show view:
<% if @user.avatar.attached? %>
<p>
<%= image_tag @user.avatar.variant(resize_to_limit: [300, 300]) %>
</p>
<% end %>
<div>
<strong>First name:</strong>
<%= @user.first_name %>
</div>
<div>
<strong>Last name:</strong>
<%= @user.last_name %>
</div>
<div>
<strong>Email address:</strong>
<%= @user.email_address %>
</div>
<div>
<%= link_to "Edit this user", edit_user_path(@user) %> |
<%= link_to "Back to users", users_path %>
<%= button_to "Destroy this user", @user, method: :delete %>
</div>
image_tag @user.avatar.variant(resize_to_limit: [300, 300])
resizes the image to a maximum of 300x300 pixels on the fly
(letting libvips do the work) and caches the result. The first
time the page loads it takes a moment; subsequent loads are
instant.
Uploading from the console is nice but usually we want a form upload. Let’s update the form partial so it accepts a file:
<%= form_with(model: user) do |form| %>
<% if user.errors.any? %>
<div style="color: red">
<h2><%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% user.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div>
<%= form.label :first_name, style: "display: block" %>
<%= form.text_field :first_name %>
</div>
<div>
<%= form.label :last_name, style: "display: block" %>
<%= form.text_field :last_name %>
</div>
<div>
<%= form.label :email_address, style: "display: block" %>
<%= form.text_field :email_address %>
</div>
<div>
<%= form.label :avatar, style: "display: block" %>
<%= form.file_field :avatar %>
</div>
<div>
<%= form.submit %>
</div>
<% end %>
form_with(model: user) is already a multipart/form-data
form as soon as you include a file_field inside. In old
Rails you sometimes had to add multipart: true by hand;
not anymore.
|
And in the controller we permit :avatar in the strong
parameters — Rails 8 uses params.expect(…) which is similar
to .require.permit but also rejects malformed arrays/hashes
with a 400 Bad Request:
def user_params
params.expect(user: [ :first_name, :last_name, :email_address, :avatar ])
end
That’s all. Rails sees the avatar key, recognises it as an
uploaded file, and has_one_attached :avatar takes over. You
don’t need to call @user.avatar.attach(…) manually in the
create or update actions.
Beyond the Basics
Active Storage can do a lot more:
-
Store files on S3, GCS, Azure Blob Storage or any S3-compatible service. Configure the service in
config/storage.ymland setconfig.active_storage.service = :amazon(or similar) in the environment config. -
Generate preview images for PDFs and videos.
-
Serve files via signed, time-limited URLs (the default in production).
-
Support direct uploads from the browser straight to cloud storage, bypassing your Rails server entirely.
Please have a look at https://guides.rubyonrails.org/active_storage_overview.html for a complete overview and documentation.