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:

Gemfile
# 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:

app/models/user.rb
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:

app/views/users/show.html.erb
<% 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:

app/views/users/_form.html.erb
<%= 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:

app/controllers/users_controller.rb
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.yml and set config.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.