My Mastodon Server Compose files
  • Shell 84%
  • Ruby 16%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-07 07:56:19 +00:00
backup allow single project backup 2026-08-04 08:26:14 +01:00
caddy Add caddy so we route mastodon directly to the node from internal networks 2026-06-16 07:21:24 +00:00
diun remove pushover notification for diun 2026-07-23 06:53:06 +00:00
fedifetcher try longer backfill 2026-08-07 07:56:19 +00:00
mastodon update to v4.6.5 2026-08-07 07:56:19 +00:00
node-exporter basic node exporter 2026-07-03 08:41:20 +00:00
.gitignore simplify config of backup script 2026-06-25 11:00:11 +00:00
docker-tailscale-routing.service add note regarding routing table 2026-06-18 09:43:55 +00:00
README.md add blog post link 2026-08-06 16:49:59 +01:00
update-mastodon.sh perform backup sooner, so user does not have to wait for pull 2026-06-25 11:55:27 +00:00

mastodon-compose

Docker Compose files for my Mastodon instance.

Contents

Rate limiting and client IPs

See Exempting an IP from Mastodons rate limits for a description of this.

Mastodon exposes no configuration for its rate limits, and config/initializers/rack_attack.rb ships no safelist. rack_attack_safelist.rb is mounted into the web container as config/initializers/zz_rack_attack_safelist.rb (the zz_ prefix makes it load last) and adds three things:

  • A rack_attack notification subscriber logging any throttle or blocklist match with the IP. Quiet unless something matches, so it stays enabled.
  • A probe behind RACK_ATTACK_PROBE=true that logs the client IP of every API request without exempting anything. Noisy — one line per request; only turn it on to discover an IP.
  • The safelist itself, driven by RACK_ATTACK_SAFELIST_IPS (comma-separated IPs or CIDRs). A Rack::Attack safelist short-circuits every throttle, which matters because the API throttles are keyed on the user and token rather than the IP.

FediFetcher is currently the only exemption. Its container IP is pinned to 172.31.240.2 in fedifetcher/docker-compose.yml via an explicit subnet, because a Docker-assigned address drifts across recreates. Both files must be changed together.

Dependency on the edge proxy

Public traffic reaches Mastodon as Cloudflare → Caddy on the Oracle VPS over Tailscale, bypassing caddy here (which only serves host-local and tailnet requests).

That edge Caddyfile is not in this repo, and TRUSTED_PROXY_IP in mastodon/docker-compose.yml depends on it. Its mstdn.thms.uk block must set the client IP explicitly on both handlers:

reverse_proxy <ip-address>:3000 {
    header_up X-Forwarded-For {client_ip}
}

{client_ip} is derived from CF-Connecting-IP under trusted_proxies_strict, so this replaces the whole forwarded chain with one verified entry. Without it, Cloudflare's edge IP lands at the end of X-Forwarded-For, and since Cloudflare's ranges are not in TRUSTED_PROXY_IP, Mastodon treats that as the client: per-IP throttles get bucketed per Cloudflare edge, IpBlock blocks Cloudflare instead of the offender, and the admin UI shows Cloudflare IPs for every account.

Overwriting the header is also what makes safelisting a private address safe — a client cannot forge X-Forwarded-For past the edge. The local Caddy likewise replaces an untrusted client's header with the peer address. If the edge ever stops setting this, revisit the safelist.

To verify after any change to this path, set RACK_ATTACK_PROBE=true and confirm remote_ip is the real client address rather than a Cloudflare range.

Routing service

For a Headscale exit node to work, you need an extra entry in the routing table.

The docker-tailscale-routing.service unit handles this. (See also https://blog.thms.uk/2026/06/docker-tailscale-exit-node.)

To enable it:

sudo ln -s <absolute-directory>/docker-tailscale-routing.service /etc/systemd/system/docker-tailscale-routing.service
sudo systemctl daemon-reload
sudo systemctl enable --now docker-tailscale-routing.service