## The variables

| Variable | Notes |
|---|---|
| `S3_BUCKET` | The bucket name. |
| `S3_REGION` | `auto` works for several S3-compatible providers. |
| `S3_ENDPOINT` | Only for non-AWS. Cloudflare R2, Backblaze B2, MinIO all use this. |
| `AWS_ACCESS_KEY_ID` | Named for the protocol, not the vendor. |
| `AWS_SECRET_ACCESS_KEY` | Same. |

```bash
bin/kamal app exec 'bin/rails runner "puts ActiveStorage::Blob.service.name"'
```

That prints which service is live, which is the only check worth trusting. Setting the variables and
assuming is how people discover in a month that everything still went to disk.

## Why this matters more on a container deploy

The container filesystem is replaced on every deploy. Uploads stored on local disk survive only
because they sit on a mounted volume declared in `config/deploy.yml`. That works, but it ties your
files to one server, it puts them in the same backup problem as the database, and it makes moving
hosts a copy operation.

Object storage removes all three. It is also the thing that has to be true before a second app server
is possible at all.

## Migrating what is already there

New uploads go to the new service. Old ones do not move, and Active Storage will keep looking for
them where they were. Rails ships a mirror service for exactly this transition: write to both, let it
run until the old files are copied, then switch reads.

Do not skip straight to switching. A deploy that changes the service with files still on disk turns
every existing attachment into a broken link.

## Keep the bucket private

Active Storage serves files through your app by default, with signed URLs. A public bucket
short-circuits that and makes every file world-readable by URL, which is rarely what you want for
customer uploads. Leave public access blocked and let the app do the authorizing.

## What stays local

Nothing else. The SQLite databases live on the volume regardless, and they should: they need local
disk latency and they are not what object storage is for.
