Skip to content

Instantly share code, notes, and snippets.

@pdxmph
Last active May 12, 2026 05:34
Show Gist options
  • Select an option

  • Save pdxmph/2ff58bbda87fbc90565077cb259496f8 to your computer and use it in GitHub Desktop.

Select an option

Save pdxmph/2ff58bbda87fbc90565077cb259496f8 to your computer and use it in GitHub Desktop.
go2social setup on Synology and Cloudflare with Portainer
title GotoSocial Setup on Synology with Custom Domain (Revised)
tags
gotosocial synology selfhosted fediverse activitypub docker

GotoSocial Setup on Synology with Custom Domain

Overview

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.

Prerequisites

  • 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)

Decide this BEFORE you start: account domain

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 set GTS_HOST.
  • Split-domain (nicer): handle is @you@yourdomain.org while the server lives at social.yourdomain.org. Set both GTS_HOST and GTS_ACCOUNT_DOMAIN, and serve a webfinger redirect from the apex (see Step 9).

Pick now. You can't migrate later.

Step 1: DNS Configuration

In Cloudflare:

  • Add CNAME: socialsocial.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).

Step 2: Docker Setup

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-stopped

About 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.

Step 3: Directory Permissions

mkdir -p /volume1/docker/gotosocial
sudo chown -R 1026:1026 /volume1/docker/gotosocial   # match the UID:GID in compose

Step 4: SSL Certificate via DNS-01 (no port 80)

DSM 7 supports Let's Encrypt via DNS challenge — you never have to expose port 80.

  1. Control Panel → Security → Certificate → Add → "Get a certificate from Let's Encrypt"
  2. Toggle "Use DNS provider"
  3. Provider: Cloudflare. Paste a scoped API token with Zone → DNS → Edit on your zone
  4. Domain: social.yourdomain.org (or both, if split-domain)

Cert renews automatically. Port 80 stays closed forever.

Step 5: Configure Reverse Proxy

Control Panel → Application Portal → Reverse Proxy → Create:

  • Source: HTTPS, social.yourdomain.org, port 443
  • Destination: HTTP, localhost, port 8321

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.

Step 6: Assign Certificate to Reverse Proxy

Control Panel → Security → Certificate → Settings → find social.yourdomain.org in the services list → assign the Let's Encrypt cert → OK.

Step 7: Create Admin Account

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 yourusername

Step 8: Connect with Mastodon Apps

Any Mastodon-compatible app (Ivory, Tusky, Ice Cubes, Phanpy, etc.):

  1. Add account
  2. Server: https://social.yourdomain.org (or https://yourdomain.org for split-domain)
  3. 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.

Step 9: Split-Domain Webfinger Redirect (only if using GTS_ACCOUNT_DOMAIN)

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 301 rules

Verify with:

curl -sSI 'https://yourdomain.org/.well-known/webfinger?resource=acct:yourusername@yourdomain.org'

Should show a 301 to the social subdomain.

Step 10: Backups

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.

Step 11: Media Pruning (recurring)

Prevents your media volume from quietly growing forever:

sudo docker exec -i gotosocial ./gotosocial admin media prune-orphaned

Schedule 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.

Verification

# 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_

Troubleshooting

Federation works inbound but outbound fails

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.

Rate-limited under normal use

You forgot GTS_TRUSTED_PROXIES, so the rate limiter sees every request as coming from the reverse proxy. Add it, restart the container.

Image/video uploads fail with 413 or "network error"

The reverse proxy body size limit (Step 5). Bump it.

Streaming timelines don't update in the client

The reverse proxy WebSocket custom header (Step 5). Add it.

Cloudflare keeps re-proxying the record

You enabled "Always Use HTTPS" or a page rule that forces proxy. Disable for the social subdomain specifically.

DNS doesn't resolve

  • Confirm DNS-only (gray cloud), not proxied
  • dig social.yourdomain.org — should return your Synology DDNS target
  • Local cache: sudo dscacheutil -flushcache on macOS

Complete reset (nuclear option)

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

Upgrades

# 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 gotosocial

Always read the GoToSocial release notes for the version you're moving to — schema migrations are one-way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment