Storage
Store uploads on local disk or in any S3-compatible bucket, such as RustFS, MinIO, AWS S3 or Cloudflare R2.
Emberly stores uploads either on local disk or in an S3-compatible bucket. You can configure storage in two ways:
- Environment variables. Set
STORAGE_PROVIDER=s3and theS3_*variables. This overrides the Admin Panel and suits Docker and PaaS deploys. - Admin → Settings → Storage. Pick a provider and enter its details. Changes apply without a restart.
Local Disk
This is the default. Uploads are written to DATA_DIR:
- In Docker it is
/data, kept in theemberly-datavolume. - On a Linux server it defaults to the repository root, in an
uploadsfolder. SetDATA_DIRto somewhere you back up, such as/var/lib/emberly.
Local disk is the simplest setup, but you can't run more than one app server, and you're responsible for disk space and backups.
On Dokploy or a PaaS, mount a persistent volume at /data. Otherwise uploads are lost on every redeploy.
S3 With Environment Variables
Emberly creates the bucket if it doesn't exist.
| Variable | Notes |
|---|---|
S3_ENDPOINT | Leave empty for AWS S3 |
S3_PUBLIC_ENDPOINT | Set this when the address Emberly uses (e.g. http://rustfs:9000 inside Docker) isn't reachable by your users |
S3_FORCE_PATH_STYLE | Defaults to true when S3_ENDPOINT is set, which suits most self-hosted servers |
Why the public endpoint matters
Some uploads, such as those from Flicker, go straight from the user's device to the bucket using signed URLs. The bucket's address must be reachable from the internet, not just from the Emberly server.
RustFS With Docker Compose
RustFS is a lightweight S3-compatible server. compose.s3.yaml adds it alongside Emberly and sets the storage variables for you.
In .env:
RustFS listens on port 9000 (change it with S3_PORT). Point s3.example.com at it through your reverse proxy:
MinIO
MinIO works the same way as RustFS. Run it however you like, then:
AWS S3
- Create a bucket in the S3 console.
- Create an IAM user with
s3:GetObject,s3:PutObject,s3:DeleteObjectands3:ListBucketon the bucket. - Create an access key for that user.
Leave S3_ENDPOINT empty. Add a CORS rule allowing PUT from your APP_URL, so uploads from the browser work.
Cloudflare R2
R2 has no egress fees.
- Create a bucket under R2 in the Cloudflare dashboard.
- Create an R2 API token with Object Read & Write.
- Add a CORS policy allowing
PUTfrom yourAPP_URL.
Other Providers
Backblaze B2, Wasabi, Vultr, Linode, DigitalOcean Spaces and any other S3-compatible service work the same way: create a bucket and key in their dashboard, then set S3_ENDPOINT to their S3 address.
Link Preview Images
Emberly makes a small WebP copy of each uploaded image for link previews in Discord and other apps. These are kept on local disk in DATA_DIR/cache/previews, whichever storage you use for uploads. They're safe to delete, and are made again when an image is next shared. To make them for images uploaded before 3.0, run bun run previews:backfill.
Checking It Works
Upload a file from the dashboard and open its link. GET /api/health also tests storage by writing and deleting a small file.
If uploads fail:
- Check that the bucket's endpoint is reachable from the Emberly server.
- Check that the access key can read, write and delete.
- Check that
S3_PUBLIC_ENDPOINT(orS3_ENDPOINT) opens from your own browser. - Check that the bucket's CORS rules allow
PUTfrom yourAPP_URL.