ActivityPub
    Single Binary

    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

    ProjectGoToSocial v0.21.x
    LicenseAGPL-3.0
    Recommended Plan2 vCPU / 3 GB Premium NVMe Cloud VPS
    Personal/SQLite1 vCPU / 1 GB Lite KVM works fine
    OSUbuntu 24.04 LTS / Debian 12 / AlmaLinux 9
    Estimated Setup Time45–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.

    1

    Initial Server Prep

    Create non-root admin user
    adduser admin
    usermod -aG sudo admin
    rsync -a /root/.ssh /home/admin/
    chown -R admin:admin /home/admin/.ssh

    Lock root login + password auth in /etc/ssh/sshd_config:

    /etc/ssh/sshd_config
    PermitRootLogin no
    PasswordAuthentication no
    Patch + base packages
    sudo 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 restic
    UFW
    sudo 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 enable
    2

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

    Create DB + user
    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;
    SQL
    /etc/postgresql/16/main/pg_hba.conf — local only
    host    gotosocial    gotosocial    127.0.0.1/32    scram-sha-256
    host    gotosocial    gotosocial    ::1/128         scram-sha-256
    Restart + verify
    sudo systemctl restart postgresql
    PGPASSWORD='your-password' psql -h 127.0.0.1 -U gotosocial -d gotosocial -c '\conninfo'
    3

    Install GoToSocial

    Layout + system user
    sudo mkdir -p /gotosocial/storage/certs
    sudo useradd -r -s /usr/sbin/nologin -d /gotosocial gotosocial
    Pull the latest stable release
    cd /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 /gotosocial

    For ARM64, substitute GTS_TARGET=linux_arm64. 32-bit hosts are explicitly experimental — don't.

    4

    Write config.yaml

    Create a minimal config that only contains overrides — makes upgrades dramatically less painful than copying the 600-line example.

    /gotosocial/config.yaml
    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: 2
    • bind-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.
    Lock down perms (contains DB + SMTP creds)
    sudo chown gotosocial:gotosocial /gotosocial/config.yaml
    sudo chmod 600 /gotosocial/config.yaml
    5

    systemd Unit

    Install upstream unit
    sudo cp /gotosocial/example/gotosocial.service /etc/systemd/system/
    sudo systemctl daemon-reload

    In /etc/systemd/system/gotosocial.service verify:

    • User=gotosocial + Group=gotosocial
    • WorkingDirectory=/gotosocial
    • ExecStart=/gotosocial/gotosocial --config-path /gotosocial/config.yaml server start
    • AmbientCapabilities=CAP_NET_BIND_SERVICE is commented out (nginx binds 80/443, GoToSocial doesn't need it)
    • Leave the upstream sandboxing (ProtectSystem=strict, ProtectHome=true, PrivateTmp=true, etc.) in place
    Enable + start
    sudo systemctl enable --now gotosocial.service
    sudo systemctl status gotosocial.service
    sudo journalctl -u gotosocial.service -n 100 --no-pager
    6

    nginx Reverse Proxy

    /etc/nginx/sites-available/social.example.com.conf
    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 $host is 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 40M matches GoToSocial's video upload ceiling — nginx defaults to 1 MB.
    Enable + reload
    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 nginx
    7

    TLS with Let's Encrypt

    certbot with nginx plugin
    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-run

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

    8

    Create Your Admin Account

    CLI account creation
    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 admin

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

    9

    Encrypted Offsite Backups With Restic

    Initialize repo (once)
    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
    /usr/local/sbin/gotosocial-backup.sh
    #!/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"
    Schedule
    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.

    10

    Hardening, Maintenance & Upgrades

    Hardening beyond defaults:

    • fail2ban for nginx catches brute-force logins and 401-storms — enable nginx-http-auth and nginx-limit-req jails.
    • nginx rate limitinglimit_req_zone with 10 r/s burst on /api/ and /.well-known/webfinger absorbs federation discovery storms.
    • Domain blocks in /settings/admin/domain-permissions are the heart of running a sane instance.
    Routine: monthly Postgres maintenance
    sudo -u postgres psql -d gotosocial -c "VACUUM ANALYZE;"

    Upgrade procedure:

    1. Take a fresh database dump and confirm it restores cleanly.
    2. Read the release notes for every version between current and target.
    3. Diff your config.yaml against the new example.
    4. sudo systemctl stop gotosocial
    5. sudo mv /gotosocial/gotosocial /gotosocial/gotosocial.bak
    6. Extract the new tarball over /gotosocial, replacing the binary and web directory.
    7. sudo chown -R gotosocial:gotosocial /gotosocial
    8. sudo systemctl start gotosocial and watch journalctl -u gotosocial -f as 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.