Rails Active Storage S3: Direct Uploads, Varianten en Productieconfiguratie
De eerste keer dat ik een Rails-app zag bezwijken door bestandsuploads was het geen piekbelasting. Het was een verkoopmedewerker die een PowerPoint van 40MB uploadde via een adminpanel. Één request. Veertig megabyte die een Puma-worker opslokte. Elk ander request stond te wachten. De thread bleef veertien seconden geblockeerd terwijl de browser het bestand langzaam doorstuurde. Veertien seconden upload, veertien seconden verloren Puma-worker.
Dat was lang geleden. Rails Active Storage, uitgebracht met Rails 5.2, geeft een beter antwoord. Voeg S3 toe als storage-backend, schakel direct uploads in, en de bestandsdata raakt je appserver nooit meer aan. Je Puma-workers doen wat ze horen te doen — logica afhandelen, geen bytes doorsluizen. Hier is hoe je het correct instelt: de varianten- en achtergrondverwerkingspatronen die echt werken, en de productiedetails die in geen enkele README staan.
Rails Active Storage Instellen met S3
Active Storage zit standaard in Rails. Je schakelt het in:
bin/rails active_storage:install
bin/rails db:migrate
Dit maakt de tabellen active_storage_blobs, active_storage_attachments en active_storage_variant_records aan. Rails beheert deze — je bevraagt ze zelden direct.
Voeg de AWS SDK toe:
# Gemfile
gem "aws-sdk-s3", "~> 1.177", require: false
gem "image_processing", "~> 1.2" # voor varianten
Configureer de S3-service in config/storage.yml:
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.aws.access_key_id %>
secret_access_key: <%= Rails.application.credentials.aws.secret_access_key %>
region: eu-west-1
bucket: <%= Rails.application.credentials.aws.s3_bucket %>
upload:
server_side_encryption: "AES256"
Stel de service per omgeving in:
# config/environments/production.rb
config.active_storage.service = :amazon
# config/environments/development.rb
config.active_storage.service = :local # schijfopslag, geen S3 nodig lokaal
Koppel bestanden aan je model:
class Document < ApplicationRecord
has_one_attached :file
has_many_attached :images
end
Dat zijn de basis. De meeste tutorials stoppen hier. Het interessante begint nu.
Rails Active Storage Direct Uploads: Je Appserver Overslaan
De standaard Active Storage-flow stuurt bestanden via je Rails-server naar S3. Je Puma-worker ontvangt de upload, buffert deze, en stuurt hem door naar S3. Voor een profielfoto van 100KB is dit prima. Voor een video van 50MB gebruik je een Puma-worker als een duur doorgeefluik.
Direct uploads werken anders. De browser vraagt jouw Rails-app om een presigned S3-URL, de app geeft die terug, en de browser uploadt direct naar S3. Je server verwerkt twee kleine JSON-requests — URL verstrekken, blob bevestigen — in plaats van vijftig megabyte te proxyen.
Activeer het met de JavaScript-bibliotheek die meegeleverd wordt met Rails:
// app/javascript/application.js
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()
Voeg direct_upload: true toe aan je formulier:
<%= form_with model: @document do |form| %>
<%= form.file_field :file, direct_upload: true %>
<%= form.submit "Uploaden" %>
<% end %>
Dat is de volledige clientzijde-aanpassing. De @rails/activestorage-bibliotheek onderschept de formulierinzending, ruilt het bestand in voor een presigned URL via een POST naar /rails/active_storage/direct_uploads, uploadt naar S3, en vult vervolgens het signed blob-ID terug in het formulier voor indiening.
Voor grotere bestanden of betere UX, luister naar de voortgangsgebeurtenissen:
import { DirectUpload } from "@rails/activestorage"
const input = document.querySelector('input[type=file]')
const url = input.dataset.directUploadUrl
input.addEventListener('change', (event) => {
const file = event.target.files[0]
const upload = new DirectUpload(file, url, {
directUploadWillStoreFileWithXHR(xhr) {
xhr.upload.addEventListener("progress", (event) => {
const percent = (event.loaded / event.total * 100).toFixed(0)
console.log(`Uploadvoortgang: ${percent}%`)
// werk hier een voortgangsbalk bij
})
}
})
upload.create((error, blob) => {
if (error) {
console.error("Upload mislukt:", error)
} else {
const hiddenField = document.createElement("input")
hiddenField.type = "hidden"
hiddenField.name = input.name
hiddenField.value = blob.signed_id
input.form.appendChild(hiddenField)
}
})
})
CORS-configuratie voor S3 Direct Uploads
Direct uploads vereisen dat je S3-bucket verzoeken van je domein accepteert. Zonder CORS blokkeert de browser de upload voordat hij begint. Configureer het bucketbeleid in AWS:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT", "POST"],
"AllowedOrigins": ["https://jouwapp.nl"],
"ExposeHeaders": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
"MaxAgeSeconds": 3600
}
]
Vergrendel AllowedOrigins op je echte domein. * werkt lokaal, maar laat je bucket in productie PUT-verzoeken accepteren van overal — inclusief iemand die verzoeken fabriceert om data op jouw kosten te uploaden.
Afbeeldingsvarianten: Transformaties op Aanvraag
Active Storage-varianten laten je afbeeldingen verkleinen, bijsnijden en converteren zonder van tevoren meerdere kopieën op te slaan. De variant wordt gegenereerd bij eerste toegang en automatisch gecached in S3.
class User < ApplicationRecord
has_one_attached :avatar
def avatar_thumbnail
avatar.variant(resize_to_fill: [200, 200], format: :webp)
end
end
In een view:
<%= image_tag current_user.avatar_thumbnail if current_user.avatar.attached? %>
De image_processing-gem gebruikt libvips (snel) of ImageMagick (universeel). Voor productie verwerkt libvips afbeeldingen tien tot twintig keer sneller dan ImageMagick en gebruikt aanzienlijk minder geheugen. Zorg dat het in je Dockerfile staat:
RUN apt-get install -y libvips
Benoemde Varianten voor Consistentie
Voor consistente afmetingen door de hele app, definieer benoemde varianten direct op het model:
class Product < ApplicationRecord
has_one_attached :photo do |attachable|
attachable.variant :thumb, resize_to_fill: [120, 120], format: :webp
attachable.variant :medium, resize_to_limit: [600, 600], format: :webp
attachable.variant :large, resize_to_limit: [1200, nil], format: :webp
end
end
Gebruik ze met:
<%= image_tag product.photo.variant(:thumb) %>
Benoemde varianten worden gevalideerd bij declaratie — als je een variantnaam verkeerd typt, krijg je een fout bij het opstarten van de app in plaats van stilzwijgend een kapotte afbeelding te serveren in productie.
Varianten op de Achtergrond Verwerken
Standaard worden varianten synchroon verwerkt bij het eerste request. Onder belasting wacht de eerste gebruiker die een nieuw geüploade afbeelding opent op libvips. Verplaats de verwerking naar de achtergrond:
# config/application.rb
config.active_storage.variant_processor = :vips
# app/jobs/process_image_variants_job.rb
class ProcessImageVariantsJob < ApplicationJob
queue_as :default
def perform(attachment_id)
attachment = ActiveStorage::Attachment.find(attachment_id)
blob = attachment.blob
return unless blob.image?
attachment.record.class.reflect_on_all_attachments.each do |reflection|
next unless reflection.name == attachment.name.to_sym
reflection.options.fetch(:variants, {}).each_key do |variant_name|
blob.variant(variant_name).processed
end
end
end
end
Activeer de job na upload:
class Document < ApplicationRecord
has_one_attached :cover_image
after_commit :process_variants, on: [:create, :update]
private
def process_variants
ProcessImageVariantsJob.perform_later(cover_image.attachment.id) if cover_image.attached?
end
end
Voor meer achtergrondtaakpatronen, zie de gids over Solid Queue en Sidekiq.
Presigned URL’s en CDN: Bestanden Serveren Zonder Je App te Belasten
Standaard loopt Active Storage-serving via je Rails-app: elke url_for(user.avatar) genereert een redirect naar een kortdurende Rails-URL, die op zijn beurt doorverwijst naar een kortdurende S3-URL. Twee redirects per afbeelding. Je Rails-server verwerkt elk beeldverzoek ook al komen de bytes uit S3.
Voor een volledig publieke bucket geserveerd via CloudFront, zet public: true in storage.yml:
# config/storage.yml
amazon_public:
service: S3
access_key_id: <%= Rails.application.credentials.aws.access_key_id %>
secret_access_key: <%= Rails.application.credentials.aws.secret_access_key %>
region: eu-west-1
bucket: <%= Rails.application.credentials.aws.s3_bucket %>
public: true
Met public: true geeft url_for(user.avatar) een directe S3-URL terug zonder vervaldatum. Koppel er een CloudFront-distributie voor de bucket aan en je krijgt mondiale CDN-caching zonder één byte via Puma.
Voor privébestanden — contracten, facturen, gebruikersspecifieke documenten — gebruik de standaard Active Storage URL-strategie of genereer presigned URL’s direct:
# app/helpers/application_helper.rb
def presigned_url(blob, expires_in: 15.minutes)
blob.service.url(
blob.key,
expires_in: expires_in,
filename: blob.filename,
disposition: :inline,
content_type: blob.content_type
)
end
Stel de globale vervaltijd in je omgevingsbestand in:
# config/environments/production.rb
config.active_storage.service_urls_expire_in = 1.hour
Rails Active Storage Productieconfiguratie: De Details die je Bijten
Na negentien jaar Rails-apps naar productie brengen, dit zijn de Active Storage-valkuilen die ik het vaakst zie:
Content-type-verificatie. Rails valideert het inhoudstype van geüploade bestanden op basis van de magische bytes van het bestand, niet het door de browser opgegeven Content-Type-header. Als een gebruiker een .exe hernoemt naar .jpg, pakt Rails dat. Houd je toegestane inline-typen beperkt:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.content_types_allowed_inline = %w[
image/png image/jpeg image/gif image/webp image/svg+xml
application/pdf
]
Bestandsgroottelimieten. Stel ze in op controllerniveau voordat de upload Active Storage bereikt, niet in een modelvalidatie die pas wordt uitgevoerd nadat de bytes al in het geheugen staan:
class DocumentsController < ApplicationController
MAX_FILE_SIZE = 50.megabytes
before_action :check_file_size, only: [:create]
private
def check_file_size
return unless params.dig(:document, :file)&.size.to_i > MAX_FILE_SIZE
render json: { error: "Bestand overschrijdt de limiet van 50MB" }, status: :unprocessable_entity
end
end
Verweesd geraakte blobs opruimen. Direct uploads maken een blob-record aan voordat het formulier wordt ingediend. Als de gebruiker het tabblad sluit halverwege een upload, heb je een verweesde blob in S3 die voor eeuwig opslag kost. Rails levert een opruimtaak:
bin/rails active_storage:purge_unattached
Voer dit uit in een geplande job. Wekelijks is gewoonlijk genoeg:
# app/jobs/purge_orphaned_blobs_job.rb
class PurgeOrphanedBlobsJob < ApplicationJob
def perform
ActiveStorage::Blob.unattached.where(created_at: ..2.days.ago).find_each(&:purge_later)
end
end
De buffer van 2.days.ago voorkomt dat blobs van lopende meerstapsformulieren worden verwijderd waarbij iemand een tussenstap heeft opgeslagen voor het uploaden.
Testen zonder S3 te raken. Gebruik de :test-service in tests — deze slaat bestanden in het geheugen op en heeft geen AWS-credentials nodig:
# config/environments/test.rb
config.active_storage.service = :test
# spec/models/document_spec.rb
RSpec.describe Document, type: :model do
it "koppelt een bestand" do
document = Document.new(title: "Spec")
document.file.attach(
io: File.open(Rails.root.join("spec/fixtures/files/sample.pdf")),
filename: "sample.pdf",
content_type: "application/pdf"
)
expect(document.file).to be_attached
end
end
Gebruik nooit de S3-service in tests. Zelfs met een dedicated testbucket loop je tegen rate limits aan, voeg je netwerklatentie toe, en creëer je neveneffecten over parallelle CI-runs heen.
Active Storage met S3 direct uploads is in een kwartier te configureren en behoedt je voor een klasse productieproblemen die niets te maken hebben met je applicatielogica — geblokkeerde Puma-workers, geheugenpieken door grote uploads, timeout-cascades onder belasting. Stel het eenmalig op deze manier in, definieer varianten als benoemde declaraties op het model, verwerk ze op de achtergrond, en je hebt een bestandsverwerkingsstack die schaalt zonder onderhoud.
Voor deploymentpatronen die je Rails-app stabiel houden tijdens infrastructuurwijzigingen, behandelt de gids over zero-downtime database-migraties dezelfde discipline van achterwaartse compatibiliteit — nuttig als je ooit bijlagenamen wijzigt of variantconfiguraties aanpast op een live database.
Heb je last van een bestandsuploadstack die productieproblemen veroorzaakt? TTB Software bouwt Rails-applicaties al negentien jaar. We hebben elke S3-configuratie en variantverwerkingsfout gezien. We lossen het op.
Veelgestelde Vragen
Hoe werkt Rails Active Storage direct upload met S3?
Wanneer een gebruiker een bestand selecteert, stuurt de @rails/activestorage JavaScript-bibliotheek een POST naar /rails/active_storage/direct_uploads met de bestandsmetadata (naam, grootte, inhoudstype, checksum). Rails maakt een ActiveStorage::Blob-record aan en geeft een presigned S3 PUT-URL terug. De browser uploadt de bestandsbytes direct naar die URL. Als de XHR-upload voltooid is, wordt het signed blob-ID teruggeplaatst in het oorspronkelijke formulierveld en dient het formulier normaal in. De bestandsbytes raken je Rails-server nooit aan.
Wat is het verschil tussen has_one_attached en has_many_attached in Rails?
has_one_attached koppelt één bestand aan een record — de avatar van een gebruiker, de omslagfoto van een product. attach aanroepen op een has_one_attached-associatie vervangt het bestaande bestand. has_many_attached koppelt meerdere bestanden — een fotogalerij bij een post, bijlagen bij een rapport. attach aanroepen voegt toe in plaats van te vervangen. Beide slaan bestandsmetadata op in active_storage_blobs en gebruiken active_storage_attachments als polymorfische koppeltabel.
Hoe serveer ik privé S3-bestanden in Rails zonder de bucket direct bloot te stellen?
Gebruik de standaard Active Storage URL-strategie, die via je Rails-app loopt en kortdurende redirects naar presigned S3-URL’s verstrekt. Stel config.active_storage.service_urls_expire_in in op een duur die past bij jouw gebruik — 15 minuten voor downloadlinks, 1 uur voor afbeeldingen in HTML-responses. Voor downloadlinks die direct na levering moeten verlopen, genereer de presigned URL direct met blob.service.url(blob.key, expires_in: 5.minutes, ...) en stuur die direct mee in de response.
Hoe configureer ik Rails Active Storage S3 in CI zonder echte AWS-credentials?
Gebruik config.active_storage.service = :test in config/environments/test.rb. De testservice slaat bestandsdata op in het geheugen voor de duur van de testrun en heeft geen AWS-configuratie nodig. Voor integratietests die echt S3-gedrag willen verifiëren — presigned URL-generatie, content-type-detectie — gebruik een dedicated testbucket met een niet-productie IAM-gebruiker met uitsluitend schrijftoegang tot die bucket. Deel nooit test- en productiebuckets.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Rails Webhook Processing: Handtekeningverificatie, Idempotentie en Achtergrondverwerking
April 14, 2026
Ruby on Rails Feature Flags: Complete Guide met Flipper, Rollout en Custom Redis Implementatie
April 13, 2026
Rails Concerns: Wanneer Ze Code Opschonen en Wanneer Ze Complexiteit Verbergen
March 13, 2026
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation