Deploying GoToSocial with Traefik and Phanpy
Table of Contents ๐
GoToSocial is a lightweight ActivityPub server for small Fediverse instances. It is especially attractive for single-user or small-group deployments where a full Mastodon stack would be unnecessarily heavy.
This guide focuses on a fresh GoToSocial installation in an existing Traefik v3 and CrowdSec Docker ecosystem. GoToSocial runs as a normal Docker Compose stack on the shared proxy network, Traefik handles public HTTPS, and Phanpy is deployed as a separate static web client.
| โน๏ธ FRESH INSTALL FIRST |
The main path below is a clean installation. If you are replacing an existing Mastodon account, use the optional migration section near the end after GoToSocial itself is already working. |
Changelog
| Date | Change |
|---|---|
| 2026-07-01 | Initial Version: Fresh GoToSocial install with SQLite, Traefik, Phanpy, and optional Mastodon account-move notes. |
1. Prerequisites
You need:
- A working Traefik v3 and CrowdSec stack.
- A subdomain for GoToSocial, e.g.
gotosocial.your-domain.com. - A subdomain for Phanpy, e.g.
phanpy.your-domain.com. - DNS
A/AAAArecords pointing both subdomains at your server. - Docker and Docker Compose.
This guide assumes the Traefik external Docker network is named proxy.
2. GoToSocial Directory
sudo mkdir -p /opt/containers/gotosocial
cd /opt/containers/gotosocial
sudo mkdir -p data cache
sudo chown -R 1000:1000 data cacheThe example below runs the container as UID/GID 1000:1000. If your deployment uses another unprivileged user, adjust ownership and user: accordingly.
3. GoToSocial Docker Compose
Create /opt/containers/gotosocial/docker-compose.yml:
services:
gotosocial:
image: docker.io/superseriousbusiness/gotosocial:0.22.0
container_name: gotosocial
user: 1000:1000
restart: unless-stopped
environment:
GTS_HOST: gotosocial.your-domain.com
GTS_DB_TYPE: sqlite
GTS_DB_ADDRESS: /gotosocial/storage/sqlite.db
GTS_LETSENCRYPT_ENABLED: "false"
GTS_TRUSTED_PROXIES: 172.18.0.0/16
GTS_WAZERO_COMPILATION_CACHE: /gotosocial/.cache
GTS_MEDIA_REMOTE_CACHE_DURATION: "14 days"
GTS_STATUSES_CLEANUP_REMOTE_OLDER_THAN: "30 days"
GIN_MODE: release
TZ: Europe/Berlin
volumes:
- ./data:/gotosocial/storage
- ./cache:/gotosocial/.cache
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.gotosocial.rule=Host(`gotosocial.your-domain.com`)"
- "traefik.http.routers.gotosocial.entrypoints=websecure"
- "traefik.http.routers.gotosocial.tls.certresolver=tls_resolver"
- "traefik.http.routers.gotosocial.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.gotosocial.loadbalancer.server.port=8080"
networks:
proxy:
external: trueConfiguration notes:
GTS_LETSENCRYPT_ENABLED=false: Traefik handles public TLS.GTS_TRUSTED_PROXIES: restrict this to your Docker proxy subnet.GTS_MEDIA_REMOTE_CACHE_DURATION: limits cached remote media growth.GTS_STATUSES_CLEANUP_REMOTE_OLDER_THAN: cleans old remote statuses.- SQLite is appropriate for a small personal instance.
Start the service:
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f gotosocial4. Create the First Account
Create the account from inside the container:
sudo docker exec -it gotosocial \
/gotosocial/gotosocial admin account create \
--username yourusername \
--email you@your-domain.com \
--password 'REPLACE_WITH_A_STRONG_TEMPORARY_PASSWORD'Promote it to admin:
sudo docker exec -it gotosocial \
/gotosocial/gotosocial admin account promote \
--username yourusernameLog in at:
https://gotosocial.your-domain.com/settingsChange the temporary password immediately.
5. Phanpy Web Client
GoToSocial provides settings pages and API endpoints, but you usually want a full client for daily use. Phanpy works well as a static web client.
Create the directory:
sudo mkdir -p /opt/containers/phanpy/html
cd /opt/containers/phanpyDownload and verify the Phanpy release asset you want to run, then extract it into html/.
| ๐ก VERIFY RELEASE ASSETS |
When using prebuilt static client assets, verify the checksum from the release page before serving them publicly. |
Create /opt/containers/phanpy/nginx.conf:
worker_processes 2;
user www-data;
events {
use epoll;
worker_connections 128;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|svg|webp|avif|ico|woff2?)$ {
try_files $uri =404;
access_log off;
expires 30d;
add_header Cache-Control "public, immutable";
}
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache";
}
}
}Create /opt/containers/phanpy/docker-compose.yml:
services:
phanpy:
image: nginx:1.27.1
container_name: phanpy
restart: unless-stopped
volumes:
- ./html:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
labels:
- "traefik.enable=true"
- "traefik.docker.network=proxy"
- "traefik.http.routers.phanpy.rule=Host(`phanpy.your-domain.com`)"
- "traefik.http.routers.phanpy.entrypoints=websecure"
- "traefik.http.routers.phanpy.tls.certresolver=tls_resolver"
- "traefik.http.routers.phanpy.middlewares=security-headers@file,crowdsec-bouncer@docker"
- "traefik.http.services.phanpy.loadbalancer.server.port=80"
networks:
- proxy
networks:
proxy:
external: trueStart it:
sudo docker compose up -dOpen:
https://phanpy.your-domain.com/Choose your GoToSocial instance as the server when logging in.
6. Verification
Check the public profile:
curl -I https://gotosocial.your-domain.com/@yourusernameCheck WebFinger:
curl -s 'https://gotosocial.your-domain.com/.well-known/webfinger?resource=acct:yourusername@gotosocial.your-domain.com'Check logs:
cd /opt/containers/gotosocial
sudo docker compose logs -f gotosocialVerify the Phanpy route:
curl -I https://phanpy.your-domain.com/At this point, the fresh GoToSocial installation is ready. You can use Phanpy or any compatible Mastodon/ActivityPub client to log in.
7. Search, Discovery, and Trending
A fresh GoToSocial instance only knows local accounts and remote objects it has already discovered. Global search is not a search engine over the entire Fediverse.
To discover more content:
- Follow accounts from known instances.
- Open remote profile URLs directly in your client.
- Follow hashtags.
- Interact with posts so your instance learns about those actors and threads.
It is normal for trending posts or broad search results to look empty at first.
8. Backups and Updates
Back up:
/opt/containers/gotosocial/data
/opt/containers/gotosocial/docker-compose.yml
/opt/containers/phanpy/html
/opt/containers/phanpy/docker-compose.yml
/opt/containers/phanpy/nginx.confUpdate GoToSocial by reading the release notes, changing the image tag, pulling, and recreating:
cd /opt/containers/gotosocial
sudo docker compose pull
sudo docker compose up -d --remove-orphansFor Phanpy, replace the static release files in html/ with a verified newer release and recreate the Nginx container if needed.
9. Optional: Migrating from Mastodon
GoToSocial is not a drop-in Mastodon database replacement. If you are replacing an existing Mastodon account, treat it as an ActivityPub identity move after the new GoToSocial instance has been installed and tested.
The migration flow is:
- Keep the old Mastodon account reachable.
- Create the new GoToSocial account.
- Add an alias in GoToSocial pointing to the old Mastodon account.
- Trigger the account move from the old Mastodon account to the new GoToSocial account.
- Import or recreate follows.
- Recreate hashtag follows separately if needed.
| โ ๏ธ KEEP THE OLD ACCOUNT REACHABLE |
ActivityPub account moves depend on the old account being reachable long enough for other servers to observe the move. Do not shut down the old Mastodon hostname immediately after changing DNS. |
9.1. Add the GoToSocial Alias
In GoToSocial settings:
https://gotosocial.your-domain.com/settings/user/migrationAdd the old Mastodon account as an alias, for example:
https://mastodon.your-domain.com/@yourusernameor:
https://mastodon.your-domain.com/users/yourusername9.2. Trigger the Move in Mastodon
Log in to the old Mastodon account and move it to:
@yourusername@gotosocial.your-domain.comMastodon will mark the old profile as moved and notify followersโ servers. Propagation is not instant.
9.3. Following and Hashtags
Follower migration is handled by the ActivityPub move. Following relationships are different: export your Mastodon following list and import it into GoToSocial if the UI supports it, or recreate the follows through a client/API workflow.
Hashtag follows are not guaranteed to migrate with account moves or account CSV imports. Export and recreate them separately.
After the account move, search for the old Mastodon account from another Fediverse instance and verify that it points to the new GoToSocial account.
10. Optional: Legacy Mastodon Proxy
During a migration, it can be useful to keep mastodon.your-domain.com reachable even after the new server handles DNS. A temporary Traefik file-provider route can proxy the old Mastodon host back to the old server.
Remove this after the fallback window has ended. Keeping a legacy proxy forever makes the migration harder to reason about.
Conclusion
GoToSocial fits neatly into a Traefik-based Docker setup: one small application container, SQLite storage for small instances, no public host port, and a separate static web client through the same reverse proxy. If you later replace Mastodon, handle that as an ActivityPub account move, not as a database transplant.





