- Shell 84%
- Ruby 16%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| backup | ||
| caddy | ||
| diun | ||
| fedifetcher | ||
| mastodon | ||
| node-exporter | ||
| .gitignore | ||
| docker-tailscale-routing.service | ||
| README.md | ||
| update-mastodon.sh | ||
mastodon-compose
Docker Compose files for my Mastodon instance.
Contents
- backup: Backup script that backs up everything to a B2 bucket, scheduled via cron. See backup.sh for details. Requires restic and jq to be installed.
- caddy: The reverse proxy; terminates TLS. Uses a lean custom Caddy image bundling the Cloudflare DNS module for DNS-01 ACME challenges.
- fedifetcher: My FediFetcher install.
- mastodon: The Docker Compose stack for Mastodon. Currently running
glitch-soc. - update-mastodon.sh: Script to automate Mastodon updates.
- docker-tailscale-routing.service: See below.
- mastodon/rack_attack_safelist.rb: Rate-limit exemptions and diagnostics. See below.
Rate limiting and client IPs
See Exempting an IP from Mastodon’s 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_attacknotification subscriber logging any throttle or blocklist match with the IP. Quiet unless something matches, so it stays enabled. - A probe behind
RACK_ATTACK_PROBE=truethat 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