Mastodon

Mastodon on Storm Buckets

Mastodon can keep its media on object storage. Point it at a Storm Bucket and every attachment, avatar, header and cached remote file uploads over S3, while browsers load them from the bucket's public URL. Your server keeps the database and the web app.

The variable names below come from Mastodon's own storage code at v4.7.3.

What you need first

  • A running Mastodon server. Installing Mastodon is Mastodon's own guide.

  • A bucket for the media and nothing else. See Getting Started.

  • An access key for that bucket. See Managing Access Keys.

  • Static hosting enabled on the bucket. See Static hosting. Browsers read media from this URL.

  • A public host you will keep. Every post links its media by this host, so pick it once. A custom domain such as files.example.social lets you move storage later without breaking old posts.

Configure object storage

Mastodon reads its storage settings from .env.production, on Docker and bare metal alike. Type your bucket name and public host, then copy the block into .env.production.

S3_ENABLED=true
S3_BUCKET=your-bucket
AWS_ACCESS_KEY_ID=<your key id>
AWS_SECRET_ACCESS_KEY=<your secret>
S3_REGION=montreal-1
S3_ENDPOINT=https://montreal-1.buckets.stormdevelopments.ca
S3_HOSTNAME=montreal-1.buckets.stormdevelopments.ca
S3_ALIAS_HOST=your-bucket.stormsites.ca

# Garage has no per-object ACLs. Empty disables them.
S3_PERMISSION=
Four values a generic S3 guide gets wrong
  • S3_HOSTNAME. Set it to the Storm endpoint host. Left unset, Mastodon
    builds media URLs against amazonaws.com.

  • Leave S3_OVERRIDE_PATH_STYLE unset. The name reads backwards. Mastodon
    already uses path-style once S3_ENDPOINT is set, and true switches it to
    virtual-host style, which Storm Buckets does not serve.

  • S3_REGION is your endpoint's name, montreal-1. The S3 signature
    includes it, so a wrong value fails every upload.

  • S3_PERMISSION= empty. Garage has no per-object ACLs, and Mastodon's
    default is public-read. Empty skips the ACL calls.

Restart

Docker:

docker compose restart web sidekiq streaming

Bare metal (systemd):

sudo systemctl restart mastodon-web mastodon-sidekiq mastodon-streaming

Post an image, then open the bucket's Files tab in your dashboard. The upload sits under media_attachments/. Open the image in a new browser tab: its address starts with your public host.

Move media you already have

Media uploaded before the switch is still in public/system/ on the server. The object keys in the bucket mirror that folder, so copy it across with rclone:

rclone copy public/system storm:your-bucket --progress
rclone check public/system storm:your-bucket

Run the copy once before you restart with the new settings, then again afterwards with --max-age 3h to catch anything uploaded in between.

Change your media host safely

Mastodon's web app caches its allowed media hosts in a service worker. When you move media to a new host, add it to EXTRA_MEDIA_HOSTS about a week before the switch, so returning visitors' browsers accept it.

EXTRA_MEDIA_HOSTS=files.example.social

Housekeeping

tootctl media remove and tootctl media remove-orphans work against the bucket and free capacity as they go. Skip --fix-permissions: it sets object ACLs, which Garage does not have.

What this does not cover

  • Suspended accounts. With S3_PERMISSION empty, Mastodon cannot flip a suspended user's media to private, so files stay reachable by URL until they are deleted. Mastodon's own docs name this trade for every ACL-less backend.

  • Storage on Storm is single-zone. Keep a second copy of anything you cannot afford to lose, as the Terms of Service say.

Next steps