When I first started building web applications with Ruby on Rails, handling file uploads seemed like a daunting task. I remember spending hours trying to figure out how to securely store user images and documents without slowing down the application. Over time, I discovered several gems that make this process smooth and efficient. In this article, I’ll share my experiences with seven powerful Ruby gems that handle file uploads and storage in Rails applications. I’ll explain each one in simple terms, provide detailed code examples, and offer personal insights to help you choose the right tool for your needs.
File uploads are a common requirement in web apps. Whether it’s profile pictures, document submissions, or media galleries, you need a reliable way to manage files. Rails itself doesn’t include built-in file handling beyond basic features, so developers rely on gems to fill this gap. The right gem can save you time, improve security, and ensure your app scales well. I’ve used all of these in various projects, and each has its strengths depending on what you’re building.
Let’s start with Active Storage, which is now the default solution in Rails. It integrates directly with the framework, making it easy to attach files to ActiveRecord models. I like how it supports multiple cloud services like Amazon S3, Google Cloud Storage, and local disk storage. Setting it up is straightforward. First, you run a migration to add the necessary tables. Then, in your model, you define attachments. For example, in a User model, you can have an avatar and multiple documents. In the controller, you handle the file attachment just like any other attribute. Active Storage also supports direct uploads, where files go straight from the user’s browser to your storage service. This reduces server load and improves performance. I’ve used this in production apps, and it handles large files well without timing out.
Here’s a basic setup for Active Storage. After installing the gem, you generate and run migrations.
rails active_storage:install
rails db:migrate
In your model, you specify the attachments.
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :documents
end
In the controller, you attach files in the update action.
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to @user, notice: 'User updated successfully.'
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar, documents: [])
end
end
In the view, you can enable direct uploads for better performance.
<%= form_with model: @user, local: true do |form| %>
<%= form.label :avatar %>
<%= form.file_field :avatar, direct_upload: true %>
<%= form.submit %>
<% end %>
Active Storage also lets you process images on the fly. For instance, you can create variants for different sizes. I often use this to generate thumbnails without storing multiple copies of the same image. It’s efficient and keeps the code clean.
Next up is CarrierWave, a gem I’ve used in older projects. It’s very flexible and allows you to create custom uploader classes. This means you can define exactly how files are processed, stored, and validated. I appreciate how CarrierWave supports various storage backends, from local files to cloud services. In one project, I used it to handle image uploads with custom processing rules. You create an uploader class that inherits from CarrierWave::Uploader::Base. There, you set storage options, define allowed file types, and add processing steps like resizing images.
Here’s an example of a CarrierWave uploader for avatars.
class AvatarUploader < CarrierWave::Uploader::Base
storage :file # Use local file storage; change to :fog for cloud
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
def extension_allowlist
%w(jpg jpeg gif png)
end
process resize_to_fit: [800, 800]
version :thumb do
process resize_to_fill: [200, 200]
end
end
In the model, you mount the uploader.
class User < ApplicationRecord
mount_uploader :avatar, AvatarUploader
end
In the controller, it’s similar to handling other attributes.
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email, :avatar)
end
end
CarrierWave gives you fine-grained control, but it requires more setup than Active Storage. I found it great for complex scenarios where I needed custom logic, like generating multiple image versions or adding watermarks.
Shrine is another gem I’ve grown to love for its modular design. It separates upload logic from models, which makes the code easier to test and maintain. Shrine uses a plugin system, so you only include the features you need. I’ve used it in apps that require background processing for large files. For example, you can set up validations for file size and type, and then process files in the background using jobs. This prevents the web request from blocking while files are being handled.
Here’s how you might set up a Shrine uploader for images.
require "shrine"
require "shrine/storage/file_system"
Shrine.storages = {
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store")
}
class ImageUploader < Shrine
plugin :validation_helpers
plugin :processing
plugin :versions
Attacher.validate do
validate_max_size 5*1024*1024 # 5 MB
validate_mime_type %w[image/jpeg image/png image/gif]
end
process(:store) do |io, context|
versions = { original: io }
versions[:large] = resize_to_limit(io, 800, 800)
versions[:thumb] = resize_to_limit(io, 200, 200)
versions
end
end
In the model, you include the attachment.
class User < ApplicationRecord
include ImageUploader::Attachment(:avatar)
end
In the controller, you handle the file as part of the params.
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to @user
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, :avatar)
end
end
Shrine’s background processing can be set up with a job. For instance, using Sidekiq.
class ImageUploader < Shrine
plugin :backgrounding
Attacher.promote_block do
PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
end
end
class PromoteJob
include Sidekiq::Worker
def perform(attacher_class, record_class, record_id, name, file_data)
attacher_class = Object.const_get(attacher_class)
record = Object.const_get(record_class).find(record_id)
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
attacher.create_derivatives
attacher.atomic_promote
end
end
I find Shrine excellent for applications that need high performance and custom processing pipelines. It’s a bit more complex to set up, but the flexibility is worth it.
Refile is a gem I’ve used when I wanted something simple and secure. It focuses on ease of use with a minimal DSL. Refile automatically restricts file types and sizes based on your configuration, which helps prevent security issues. I like how it handles direct uploads and includes caching to avoid duplicate uploads. In a recent project, I used Refile for a document management system where users could upload PDFs and images. The setup was quick, and I didn’t need to write much code.
Here’s a basic example with Refile. First, you set up the gem in an initializer.
require "refile/rails"
Refile.secret_key = 'your_secret_key'
In the model, you define the attachment.
class Document < ApplicationRecord
attachment :file, type: :image # Restricts to image types; use :all for any file
end
In the controller, it’s standard Rails stuff.
class DocumentsController < ApplicationController
def create
@document = Document.new(document_params)
if @document.save
redirect_to @document
else
render :new
end
end
private
def document_params
params.require(:document).permit(:file)
end
end
In the view, you use the attachment field.
<%= form_for @document do |f| %>
<%= f.label :file %>
<%= f.attachment_field :file %>
<%= f.submit %>
<% end %>
Refile also supports processing images, but it’s more limited compared to others. I’d recommend it for simpler apps where you don’t need advanced features. It’s secure by default, which I appreciate.
Dragonfly is great for on-the-fly image processing. I’ve used it in apps where I needed to generate different image sizes without pre-processing. Dragonfly stores the original file and creates derivatives when they’re requested via URL. This saves storage space and allows dynamic adjustments. For example, you can resize an image just by changing the URL parameters. I used this in a gallery app where users could view images in various sizes without uploading multiple versions.
Setting up Dragonfly involves configuring the app in an initializer.
Dragonfly.app.configure do
plugin :imagemagick
secret "your_secret"
url_format "/media/:job/:name"
datastore :file, root_path: Rails.root.join('public/uploads').to_s
end
In the model, you use dragonfly_accessor.
class User < ApplicationRecord
dragonfly_accessor :avatar
end
In the view, you can generate URLs with processing parameters.
<%= image_tag @user.avatar.thumb('200x200#').url if @user.avatar %>
Dragonfly also supports cloud storage. I’ve integrated it with S3 for better scalability. The gem handles signing URLs to prevent abuse, which is a nice security feature.
Paperclip is a classic gem that I used in many early Rails projects. While it’s now deprecated in favor of Active Storage, it’s still worth mentioning because many legacy apps use it. Paperclip is stable and well-documented. It creates multiple styles of images during upload and stores metadata in the database. I remember using it for a social media app where we needed profile pics in small, medium, and large sizes. The migration requires adding specific columns for file name, content type, size, and update timestamp.
Here’s a Paperclip example. In the model, you define the attachment with styles.
class User < ApplicationRecord
has_attached_file :avatar,
styles: { medium: "300x300>", thumb: "100x100>" },
default_url: "/images/:style/missing.png"
validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\z/
end
The migration adds the necessary columns.
class AddAvatarToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :avatar_file_name, :string
add_column :users, :avatar_content_type, :string
add_column :users, :avatar_file_size, :integer
add_column :users, :avatar_updated_at, :datetime
end
end
In the controller, you handle the file as part of the params.
class UsersController < ApplicationController
def update
@user = User.find(params[:id])
if @user.update(user_params)
redirect_to @user
else
render :edit
end
end
private
def user_params
params.require(:user).permit(:name, :avatar)
end
end
Paperclip works well, but I’d avoid it for new projects since Active Storage is the modern replacement. If you’re maintaining an old app, though, it’s reliable.
Finally, Cloudinary is a cloud-based service that I’ve used for media-intensive applications. It’s not just a gem; it’s a full solution for storage, transformation, and delivery. The Cloudinary gem integrates easily with Rails and handles everything from uploads to CDN delivery. I’ve used it in e-commerce apps where product images needed various sizes and formats. Cloudinary processes images on their servers, so your app doesn’t handle the load. The gem provides helpers for views and supports direct uploads from the client.
To use Cloudinary, you first sign up for an account and get your cloud name and API keys. Then, in your Rails app, you add the gem and configure it.
In the model, you can set default transformations.
class Product < ApplicationRecord
cloudinary_transformation width: 400, height: 300, crop: :limit
end
In the view, you use cl_image_tag to display images with transformations.
<%= cl_image_tag(@product.image.path, width: 200, height: 200, crop: :fill) %>
For direct uploads, you can use the Cloudinary JavaScript library along with the gem.
<%= cl_image_upload_tag(:image_id, cloud_name: "your_cloud_name") %>
Cloudinary also supports video and other file types. I find it perfect for apps that need heavy media processing without managing infrastructure.
Choosing the right gem depends on your project’s needs. If you’re building a new Rails app, I’d start with Active Storage for its integration and support. For more control, CarrierWave or Shrine are excellent. If simplicity is key, Refile works well. Dragonfly is great for dynamic processing, and Cloudinary offloads everything to the cloud. Paperclip is best for legacy support.
In my experience, I’ve mixed and matched based on requirements. For instance, in one app, I used Active Storage for general files and Shrine for specialized image processing. Always consider factors like scalability, security, and ease of maintenance. Testing file uploads is also important; I write specs to ensure validations and processing work as expected.
I hope this guide helps you navigate the options. File uploads don’t have to be complicated with the right tools. If you have specific questions, feel free to reach out—I’m happy to share more from my journey.