EmberlyEmberly Docs

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=s3 and the S3_* 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 the emberly-data volume.
  • On a Linux server it defaults to the repository root, in an uploads folder. Set DATA_DIR to 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

STORAGE_PROVIDER="s3"
S3_ENDPOINT="https://s3.example.com"
S3_BUCKET="emberly"
S3_REGION="us-east-1"
S3_ACCESS_KEY_ID="..."
S3_SECRET_ACCESS_KEY="..."

Emberly creates the bucket if it doesn't exist.

VariableNotes
S3_ENDPOINTLeave empty for AWS S3
S3_PUBLIC_ENDPOINTSet this when the address Emberly uses (e.g. http://rustfs:9000 inside Docker) isn't reachable by your users
S3_FORCE_PATH_STYLEDefaults 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:

S3_SECRET_ACCESS_KEY="a-long-random-password"
S3_PUBLIC_ENDPOINT="https://s3.example.com"
docker compose -f compose.yaml -f compose.s3.yaml up -d

RustFS listens on port 9000 (change it with S3_PORT). Point s3.example.com at it through your reverse proxy:

s3.example.com {
    reverse_proxy localhost:9000
}

MinIO

MinIO works the same way as RustFS. Run it however you like, then:

STORAGE_PROVIDER="s3"
S3_ENDPOINT="http://localhost:9000"
S3_PUBLIC_ENDPOINT="https://s3.example.com"
S3_BUCKET="emberly"
S3_ACCESS_KEY_ID="..."
S3_SECRET_ACCESS_KEY="..."

AWS S3

  1. Create a bucket in the S3 console.
  2. Create an IAM user with s3:GetObject, s3:PutObject, s3:DeleteObject and s3:ListBucket on the bucket.
  3. Create an access key for that user.
STORAGE_PROVIDER="s3"
S3_BUCKET="your-bucket"
S3_REGION="eu-west-2"
S3_ACCESS_KEY_ID="AKIA..."
S3_SECRET_ACCESS_KEY="..."

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.

  1. Create a bucket under R2 in the Cloudflare dashboard.
  2. Create an R2 API token with Object Read & Write.
  3. Add a CORS policy allowing PUT from your APP_URL.
STORAGE_PROVIDER="s3"
S3_ENDPOINT="https://<account-id>.r2.cloudflarestorage.com"
S3_BUCKET="emberly"
S3_REGION="auto"
S3_ACCESS_KEY_ID="..."
S3_SECRET_ACCESS_KEY="..."

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.


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 (or S3_ENDPOINT) opens from your own browser.
  • Check that the bucket's CORS rules allow PUT from your APP_URL.

On this page