Skip to main content
Active Storage saves uploaded files on the local disk by default, and Sevalla resets an application’s filesystem on every deployment. With Active Storage’s S3 service, Rails saves them in a Sevalla object storage bucket instead, through its S3-compatible API, so every instance and every deployment sees the same files. Browsers can upload straight to the bucket, and Rails resizes images with libvips, which both build strategies already include.

At a glance

What this guide adds to the application from Deploy a Rails application:

Environment variables

The application’s other variables are in Deploy a Rails application.

Before you begin

Create a bucket

In the sidebar, click Object storage, then click Create object storage. Enter storefront-media as the Name, choose the Location closest to the application, and click Create object storage. Keep the bucket private: Rails links to each file with a URL that expires.

Add the bucket details to the application

The values come from the bucket’s Settings tab. See Connect with the S3 API for each one.
1

Open the environment variables

In the sidebar, click Applications, then click the application. Click the Environment variables tab.
2

Add the variables

Click Add environment variables. Add the four variables and click Add environment variables. They apply with the next deployment.

Allow uploads from the browser

Direct uploads send each file from the browser to the bucket, so large files don’t pass through Puma. The browser needs the bucket’s permission for that: a CORS rule.
1

Open the bucket settings

In the sidebar, click Object storage, click the bucket, then click the Settings tab.
2

Create the rule

In CORS policy, click Create rule. Under Allowed methods, select only PUT. Under Allowed origins, enter the application’s address, for example https://storefront-x7k2m.sevalla.app; click Add allowed origin to add a custom domain too. Under Allowed headers, enter Content-Type, Content-MD5 and Content-Disposition, clicking Add allowed header for each. Click Create CORS rule.
The Create CORS rule sheet with the allowed methods, origins and headersThe Create CORS rule sheet with the allowed methods, origins and headers
These are the headers Active Storage sends with each direct upload. See CORS policy.

Configure Active Storage

Add the AWS SDK for S3, which Active Storage’s S3 service uses, and create Active Storage’s tables:
Add a service for the bucket at the end of config/storage.yml:
config/storage.yml
  • region: auto is the region of every Sevalla bucket.
  • The two checksum options make the AWS SDK add its own checksum only to requests that need one. Active Storage already sends an MD5 checksum with each upload, and without the options the SDK adds a CRC32 one too, which the bucket rejects with You can only specify one non-default checksum at a time.
In config/environments/production.rb, use the service:
config/environments/production.rb
Development keeps storing files on the local disk with the local service.

Attach photos to products

Give each product a photo:
app/models/product.rb
In app/controllers/products_controller.rb, allow the new field in product_params:
app/controllers/products_controller.rb
In app/views/products/_form.html.erb, add a file field above the submit button. direct_upload: true makes the browser upload the file to the bucket before it sends the form:
app/views/products/_form.html.erb
In app/views/products/_product.html.erb, show the photo resized to fit 400 by 400 pixels, after the description:
app/views/products/_product.html.erb
Direct uploads need Active Storage’s JavaScript. Pin it in config/importmap.rb and start it in app/javascript/application.js:
config/importmap.rb
app/javascript/application.js
Commit and push the changes, Gemfile.lock and db/schema.rb included. If Auto-deploy is off, click Deploy now on the application’s Overview tab. The Migrate job creates Active Storage’s tables before the new version starts.

Verify

  1. Open the application’s domain, click New product, enter a name, choose an image in Photo, and click Create Product. The product’s page shows the photo, at most 400 pixels wide.
  2. In the sidebar, click Object storage, click the bucket, and look at the Objects tab: it lists two objects with random names, the upload and its resized variant. See Manage objects.
  3. Deploy the application again, then reload the product’s page: the photo is still there, because it’s in the bucket and not on the instance’s disk.
The image’s address is a Rails URL under /rails/active_storage/representations/redirect/, which redirects to a link on the bucket that expires after 5 minutes. Rails creates each variant the first time a page asks for it, and stores it in the bucket next to the original. Active Storage also queues ActiveStorage::AnalyzeJob for each upload, to record an image’s width and height. Run it with Solid Queue as in Run Rails background jobs; variants work without it.

Check from a terminal

These checks need no dashboard. API, H, APP_ID, WEB_ID, APP_HOST and BUCKET_ID are set as in Deploy with the API.
  1. Upload a file through Active Storage and get its expiring URL, with Run a command in a process (execProcessCommandV3) in the web process, the API’s counterpart of the web terminal. The command runs without a shell, and the key needs write access to the application. The output also holds Active Storage’s log line, so grep keeps the URL. Then open the URL:
  2. The file in the bucket under a random key, from List objects (listObjectStorageObjectsV3):
  3. The CORS rule, from List CORS policies (listObjectStorageCorsPoliciesV3), with your domain in origins:
CLI: sevalla object-storage objects list --bucket-id $BUCKET_ID and sevalla object-storage cors-policies list --bucket-id $BUCKET_ID. MCP: sevalla_api_update with execProcessCommandV3, and sevalla_api_read with listObjectStorageObjectsV3.

Deploy with a Dockerfile

The Dockerfile from Deploy with a Dockerfile needs no change: it already installs libvips, which the image_processing gem in Rails’ Gemfile uses to create variants, and aws-sdk-s3 is plain Ruby, which bundle install installs from Gemfile.lock. With Ingot, the build plan’s Native libraries row shows libvips42t64 (ruby-vips): Ingot installs it for the same gem.

Troubleshoot

Deploy with the API

The same steps as Create a bucket, Add the bucket details to the application and Allow uploads from the browser, as calls to the Sevalla API. APP_ID and APP_HOST are the application and its domain from Deploy with the API in the deploy guide, with API and H set as there. The API key also needs the capabilities to create, read and update object storage. Push the Active Storage changes first: the deployment builds the branch as it is.
1

Create the bucket

Create object storage bucket (createObjectStorageV3) returns the bucket’s name, endpoint and keys. location is the region hint closest to the application, here weur (Western Europe) for Frankfurt:
The response holds the secret key, so the command prints only the other fields. CLI: sevalla object-storage create --name storefront-media --location weur. MCP: sevalla_api_create with createObjectStorageV3 and the same body.
2

Add the environment variables

Create environment variable (createEnvironmentVariableV3), once per variable, from the bucket’s details, for runtime only:
CLI: sevalla apps env-vars create $APP_ID --key S3_BUCKET --value <bucket name>, once per variable, for build and runtime. MCP: set_environment_variables with runtime: true and buildtime: false.
3

Create the CORS rule

Create CORS policy (createObjectStorageCorsPolicyV3) adds the rule for direct uploads:
CLI: sevalla object-storage cors-policies create $BUCKET_ID --origins https://$APP_HOST --methods PUT --headers Content-Type,Content-MD5,Content-Disposition. MCP: sevalla_api_update with createObjectStorageCorsPolicyV3 and the same body.
4

Deploy

Trigger deployment (triggerDeploymentV3):
CLI: sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.
5

Wait for the deployment

Poll Get deployment (getDeploymentV3) until the status is final: success, failed, cancelled or skipped. The loop keeps polling through a failed request and stops after 30 minutes:
CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.
Then run the checks in Check from a terminal.