Deploy GoToSocial on a RamNode VPS
A production-ready Fediverse instance — Go-based ActivityPub server, PostgreSQL, nginx + Let's Encrypt, and an offsite backup workflow you can actually trust. ~250–350 MiB RAM idle.
At a Glance
| Project | GoToSocial v0.21.x |
| License | AGPL-3.0 |
| Recommended Plan | 2 vCPU / 3 GB Premium NVMe Cloud VPS |
| Personal/SQLite | 1 vCPU / 1 GB Lite KVM works fine |
| OS | Ubuntu 24.04 LTS / Debian 12 / AlmaLinux 9 |
| Estimated Setup Time | 45–60 minutes |
Pick Your Domain First — It's Permanent
The host value in config.yaml is permanent. Once federation begins and remote servers cache your actor's public key under a hostname, you cannot change it without breaking every existing relationship. social.example.com is fine. gts-test-1.example.com will haunt you for years.
Initial Server Prep
adduser admin
usermod -aG sudo admin
rsync -a /root/.ssh /home/admin/
chown -R admin:admin /home/admin/.sshLock root login + password auth in /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication nosudo systemctl reload ssh
sudo apt update && sudo apt full-upgrade -y
sudo apt install -y ufw curl wget gnupg ca-certificates \
nginx postgresql postgresql-contrib certbot \
python3-certbot-nginx resticsudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enablePostgreSQL Setup
SQLite works for tiny instances but migrating to Postgres later is manual and not officially supported. If there is any chance your instance grows, start on Postgres.
sudo -u postgres psql <<'SQL'
CREATE USER gotosocial WITH PASSWORD 'replace-with-a-long-random-password';
CREATE DATABASE gotosocial OWNER gotosocial;
\c gotosocial
GRANT ALL PRIVILEGES ON SCHEMA public TO gotosocial;
SQLhost gotosocial gotosocial 127.0.0.1/32 scram-sha-256
host gotosocial gotosocial ::1/128 scram-sha-256sudo systemctl restart postgresql
PGPASSWORD='your-password' psql -h 127.0.0.1 -U gotosocial -d gotosocial -c '\conninfo'Install GoToSocial
sudo mkdir -p /gotosocial/storage/certs
sudo useradd -r -s /usr/sbin/nologin -d /gotosocial gotosocialcd /tmp
GTS_VERSION=0.21.2
GTS_TARGET=linux_amd64
wget "https://codeberg.org/superseriousbusiness/gotosocial/releases/download/v${GTS_VERSION}/gotosocial_${GTS_VERSION}_${GTS_TARGET}.tar.gz"
sudo tar -xzf "gotosocial_${GTS_VERSION}_${GTS_TARGET}.tar.gz" -C /gotosocial
sudo chown -R gotosocial:gotosocial /gotosocialFor ARM64, substitute GTS_TARGET=linux_arm64. 32-bit hosts are explicitly experimental — don't.
Write config.yaml
Create a minimal config that only contains overrides — makes upgrades dramatically less painful than copying the 600-line example.
host: "social.example.com"
account-domain: ""
protocol: "https"
bind-address: "127.0.0.1"
port: 8080
trusted-proxies:
- "127.0.0.1/32"
- "::1/128"
db-type: "postgres"
db-address: "127.0.0.1"
db-port: 5432
db-user: "gotosocial"
db-password: "replace-with-the-password-you-set-above"
db-database: "gotosocial"
db-tls-mode: "disable"
instance-languages: ["en"]
instance-expose-public-timeline: false
instance-expose-peers: false
instance-expose-suspended: false
accounts-registration-open: false
accounts-approval-required: true
accounts-reason-required: true
media-image-size-hint: 8388608 # 8 MiB
media-video-size-hint: 41943040 # 40 MiB
media-remote-cache-days: 7
media-cleanup-from: "00:00"
media-cleanup-every: "24h"
storage-backend: "local"
storage-local-base-path: "/gotosocial/storage"
letsencrypt-enabled: false
smtp-host: "smtp.mailgun.org"
smtp-port: 587
smtp-username: "postmaster@mg.example.com"
smtp-password: "your-smtp-password"
smtp-from: "GoToSocial <noreply@example.com>"
smtp-disclose-recipients: false
advanced-rate-limit-requests: 300
advanced-throttling-multiplier: 8
advanced-sender-multiplier: 2bind-address: 127.0.0.1— without this anyone scanning your IP can hit GoToSocial directly on 8080, bypassing nginx.trusted-proxies— without it, every rate-limit decision is made against nginx's IP, so one misbehaving remote actor throttles the whole instance.accounts-registration-open: false— flip later through the admin panel once you have a moderation policy.
sudo chown gotosocial:gotosocial /gotosocial/config.yaml
sudo chmod 600 /gotosocial/config.yamlsystemd Unit
sudo cp /gotosocial/example/gotosocial.service /etc/systemd/system/
sudo systemctl daemon-reloadIn /etc/systemd/system/gotosocial.service verify:
User=gotosocial+Group=gotosocialWorkingDirectory=/gotosocialExecStart=/gotosocial/gotosocial --config-path /gotosocial/config.yaml server startAmbientCapabilities=CAP_NET_BIND_SERVICEis commented out (nginx binds 80/443, GoToSocial doesn't need it)- Leave the upstream sandboxing (
ProtectSystem=strict,ProtectHome=true,PrivateTmp=true, etc.) in place
sudo systemctl enable --now gotosocial.service
sudo systemctl status gotosocial.service
sudo journalctl -u gotosocial.service -n 100 --no-pagernginx Reverse Proxy
server {
listen 80;
listen [::]:80;
server_name social.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
client_max_body_size 40M;
}Host $hostis not optional — GoToSocial uses it for HTTP signatures on outbound federation. Get this wrong and every federation attempt returns 401.- Upgrade/Connection headers are required for the streaming API used by mobile clients.
client_max_body_size 40Mmatches GoToSocial's video upload ceiling — nginx defaults to 1 MB.
sudo ln -s /etc/nginx/sites-available/social.example.com.conf /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxTLS with Let's Encrypt
sudo certbot --nginx -d social.example.com \
--redirect --hsts --staple-ocsp \
--email you@example.com --agree-tos --no-eff-email
sudo systemctl reload nginx
sudo certbot renew --dry-runcertbot edits the nginx config in place, adding the SSL listen directives, certificate paths, and a redirect block from 80 → 443. The certbot.timer systemd unit handles renewal automatically.
Create Your Admin Account
sudo -u gotosocial /gotosocial/gotosocial --config-path /gotosocial/config.yaml \
admin account create \
--username admin \
--email you@example.com \
--password 'replace-with-a-strong-password'
sudo -u gotosocial /gotosocial/gotosocial --config-path /gotosocial/config.yaml \
admin account confirm --username admin
sudo -u gotosocial /gotosocial/gotosocial --config-path /gotosocial/config.yaml \
admin account promote --username adminLog in at https://social.example.com and reach the admin panel at /settings. Tusky (Android), Feditext (iOS), and Semaphore (web) all work cleanly. The official Mastodon apps work too with minor UI quirks.
Encrypted Offsite Backups With Restic
export RESTIC_REPOSITORY="s3:s3.example.com/gotosocial-backups"
export RESTIC_PASSWORD="long-random-restic-password"
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
restic init#!/bin/bash
set -euo pipefail
export RESTIC_REPOSITORY="s3:s3.example.com/gotosocial-backups"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
export AWS_ACCESS_KEY_ID_FILE="/etc/restic/aws-key"
export AWS_SECRET_ACCESS_KEY_FILE="/etc/restic/aws-secret"
DUMP_DIR="/var/backups/gotosocial"
mkdir -p "$DUMP_DIR"
chmod 700 "$DUMP_DIR"
sudo -u postgres pg_dump -Fc gotosocial > "$DUMP_DIR/gotosocial.dump"
restic backup \
"$DUMP_DIR/gotosocial.dump" \
/gotosocial/config.yaml \
/gotosocial/storage \
--tag gotosocial \
--exclude='*.tmp'
restic forget --tag gotosocial \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
rm -f "$DUMP_DIR/gotosocial.dump"chmod +x /usr/local/sbin/gotosocial-backup.sh
echo '15 3 * * * /usr/local/sbin/gotosocial-backup.sh >> /var/log/gotosocial-backup.log 2>&1' \
| sudo crontab -A backup that has never been restored is a hope, not a backup. Test with restic restore latest --target /tmp/restore-test.
Hardening, Maintenance & Upgrades
Hardening beyond defaults:
- fail2ban for nginx catches brute-force logins and 401-storms — enable
nginx-http-authandnginx-limit-reqjails. - nginx rate limiting —
limit_req_zonewith 10 r/s burst on/api/and/.well-known/webfingerabsorbs federation discovery storms. - Domain blocks in
/settings/admin/domain-permissionsare the heart of running a sane instance.
sudo -u postgres psql -d gotosocial -c "VACUUM ANALYZE;"Upgrade procedure:
- Take a fresh database dump and confirm it restores cleanly.
- Read the release notes for every version between current and target.
- Diff your
config.yamlagainst the new example. sudo systemctl stop gotosocialsudo mv /gotosocial/gotosocial /gotosocial/gotosocial.bak- Extract the new tarball over
/gotosocial, replacing the binary andwebdirectory. sudo chown -R gotosocial:gotosocial /gotosocialsudo systemctl start gotosocialand watchjournalctl -u gotosocial -fas migrations run.
v0.21.0 in particular shipped non-trivial migrations that took several minutes — never interrupt a migration mid-flight.
What You Have Now
A GoToSocial v0.21.x instance running as a sandboxed systemd service, fronted by nginx with auto-renewing Let's Encrypt certificates, backed by PostgreSQL with regular encrypted offsite backups, and reachable from any Mastodon-compatible client. Total resource footprint on a 2 vCPU / 3 GB RamNode VPS sits comfortably under 1 GB of RAM in steady state.
