| title | GotoSocial Setup on Synology with Custom Domain (Revised) | |
|---|---|---|
| tags |
|
GotoSocial is a lightweight ActivityPub server that provides Fediverse functionality (like Mastodon) with minimal resource requirements. This guide covers setting it up on a Synology NAS with a custom domain, using DSM's reverse proxy and Let's Encrypt.
- Synology NAS running DSM 7+ with Container Manager
- Portainer installed (optional)
- Custom domain with DNS control (e.g., Cloudflare)
- A Cloudflare API token scoped to edit DNS for your zone (for DNS-01 certs — no port 80 needed)
Your fediverse handle is determined the first time GoToSocial runs and cannot be changed afterward without breaking every federation relationship. Two choices:
- Simple: handle is
@you@social.yourdomain.org. Only setGTS_HOST. - Split-domain (nicer): handle is
@you@yourdomain.orgwhile the server lives atsocial.yourdomain.org. Set bothGTS_HOSTandGTS_ACCOUNT_DOMAIN, and serve a webfinger redirect from the apex (see Step 9).
Pick now. You can't migrate later.
In Cloudflare:
- Add CNAME:
social→social.yoursynology.synology.me - Set to DNS-only (gray cloud), NOT proxied
Cloudflare's proxy is gray-clouded not because ActivityPub fundamentally can't traverse a CDN, but because in practice CF tends to break federation in subtle ways: header rewriting can invalidate HTTP signatures on /inbox, default WAF rules sometimes drop federation POSTs, and you lose access to any non-standard port. Gray cloud avoids all of it.
If you're using split-domain, also ensure your apex (yourdomain.org) resolves somewhere you can serve a small redirect from (see Step 9).
Create docker-compose.yml. Note: pin the image to a specific X.Y.Z tag — upstream docs explicitly warn against :latest, since a surprise schema migration is not something you want during dinner.
services:
gotosocial:
image: docker.io/superseriousbusiness/gotosocial:0.20.0 # pin; bump deliberately
container_name: gotosocial
user: "1026:1026" # your Synology UID:GID
environment:
TZ: "America/Los_Angeles"
GTS_HOST: social.yourdomain.org
# GTS_ACCOUNT_DOMAIN: yourdomain.org # uncomment ONLY for split-domain setup
GTS_DB_TYPE: sqlite
GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
GTS_LETSENCRYPT_ENABLED: "false" # DSM handles TLS at the reverse proxy
GTS_WAZERO_COMPILATION_CACHE: /gotosocial/.cache
GTS_MEDIA_REMOTE_CACHE_DAYS: "30" # caps remote media disk growth
GTS_TRUSTED_PROXIES: "127.0.0.1/32,172.16.0.0/12" # see note below
volumes:
- /volume1/docker/gotosocial:/gotosocial/storage
ports:
- "127.0.0.1:8321:8080" # bind to host loopback only; reverse proxy reaches it
restart: unless-stoppedAbout GTS_TRUSTED_PROXIES: this is critical. Without it, every request appears to come from the reverse proxy IP, so GoToSocial's per-IP rate limiter hits its cap almost immediately and starts 429-ing legitimate traffic. Verify your actual Docker bridge subnet with docker network inspect bridge and adjust the second range if needed.
mkdir -p /volume1/docker/gotosocial
sudo chown -R 1026:1026 /volume1/docker/gotosocial # match the UID:GID in composeDSM 7 supports Let's Encrypt via DNS challenge — you never have to expose port 80.
- Control Panel → Security → Certificate → Add → "Get a certificate from Let's Encrypt"
- Toggle "Use DNS provider"
- Provider: Cloudflare. Paste a scoped API token with
Zone → DNS → Editon your zone - Domain:
social.yourdomain.org(or both, if split-domain)
Cert renews automatically. Port 80 stays closed forever.
Control Panel → Application Portal → Reverse Proxy → Create:
- Source: HTTPS,
social.yourdomain.org, port 443 - Destination: HTTP,
localhost, port8321
Then — and this is the part everyone forgets — open the rule and configure these two tabs:
Custom Header tab → Create → WebSocket. This adds the Upgrade / Connection headers needed for Mastodon clients to receive streaming timelines. Without it, push updates silently don't work.
Advanced Settings tab → set a generous request body size (e.g. 40m). DSM's nginx defaults to ~1MB; without bumping this, any image or video upload over a megabyte fails with 413 and the install appears "broken" until someone tries to post a photo.
If your DSM version doesn't expose body size in the UI, SSH in and add a snippet to /usr/syno/share/nginx/server.mustache or use the supported Location overrides — but the GUI option exists on current DSM.
Control Panel → Security → Certificate → Settings → find social.yourdomain.org in the services list → assign the Let's Encrypt cert → OK.
Note: -i (not -it) — no TTY needed, and it lets you script these.
sudo docker exec -i gotosocial ./gotosocial admin account create \
--username yourusername \
--email your@email.com \
--password 'yourpassword'
sudo docker exec -i gotosocial ./gotosocial admin account promote --username yourusername
sudo docker exec -i gotosocial ./gotosocial admin account confirm --username yourusernameAny Mastodon-compatible app (Ivory, Tusky, Ice Cubes, Phanpy, etc.):
- Add account
- Server:
https://social.yourdomain.org(orhttps://yourdomain.orgfor split-domain) - Sign in with username + password
You can also use the web settings panel at https://social.yourdomain.org/settings to manage your profile, follows, blocks, and admin functions. GoToSocial doesn't have a built-in web posting UI — that's what the Mastodon clients are for.
For handles to resolve as @you@yourdomain.org, the apex domain must redirect three well-known paths to the GoToSocial host:
/.well-known/webfinger/.well-known/nodeinfo/.well-known/host-meta
Easiest options:
- Cloudflare Worker / Page Rule on the apex: 301 those three paths to
https://social.yourdomain.org/...preserving the query string - Existing web server on the apex: add
Redirect 301rules
Verify with:
curl -sSI 'https://yourdomain.org/.well-known/webfinger?resource=acct:yourusername@yourdomain.org'Should show a 301 to the social subdomain.
SQLite + a media directory — both need to be backed up.
# Nightly DB snapshot (safe to run while container is up — uses SQLite's online backup)
sudo docker exec -i gotosocial \
sqlite3 /gotosocial/storage/sqlite.db \
".backup '/gotosocial/storage/backup-$(date +%F).db'"Schedule it via DSM Task Scheduler. Then add /volume1/docker/gotosocial/ to Hyper Backup so both the DB snapshots and the media directory go offsite.
Prevents your media volume from quietly growing forever:
sudo docker exec -i gotosocial ./gotosocial admin media prune-orphanedSchedule weekly via Task Scheduler. The GTS_MEDIA_REMOTE_CACHE_DAYS: "30" setting in the compose file already handles routine remote-media expiry; this command cleans up orphans the cache TTL doesn't catch.
# Public instance endpoint
curl https://social.yourdomain.org/api/v1/instance | jq .
# Webfinger from the account domain (split-domain only)
curl 'https://yourdomain.org/.well-known/webfinger?resource=acct:yourusername@yourdomain.org'
# Container logs
docker logs gotosocial --tail 50
# Env sanity check
docker exec gotosocial env | grep ^GTS_Almost always TLS/cert chain. Some servers (Mastodon in particular) are strict about intermediate certs. Test with curl -v https://social.yourdomain.org/ from a non-Synology host and confirm the chain is complete.
You forgot GTS_TRUSTED_PROXIES, so the rate limiter sees every request as coming from the reverse proxy. Add it, restart the container.
The reverse proxy body size limit (Step 5). Bump it.
The reverse proxy WebSocket custom header (Step 5). Add it.
You enabled "Always Use HTTPS" or a page rule that forces proxy. Disable for the social subdomain specifically.
- Confirm DNS-only (gray cloud), not proxied
dig social.yourdomain.org— should return your Synology DDNS target- Local cache:
sudo dscacheutil -flushcacheon macOS
Wipes your DB and media. Do not run unless you mean it.
sudo docker stop gotosocial
sudo rm -rf /volume1/docker/gotosocial/*
sudo docker start gotosocial
# Recreate admin account from Step 7# Edit docker-compose.yml, bump the image tag to the new X.Y.Z
sudo docker compose pull
sudo docker compose up -d
# Watch logs through the migration
docker logs -f gotosocialAlways read the GoToSocial release notes for the version you're moving to — schema migrations are one-way.