GoToSocial self-hosting tutorial

Fabio Natali, 17 May 2026

Intro

Last year I went through a self-hosting spree during which I deployed some services (e.g. Nextcloud, Mattermost, and Prosody) to a bunch of VPSes, while documenting the process on this blog. This post is a follow-up on that series, detailing the deployment of GoToSocial, a lightweight ActivityPub (Fediverse) server written in Go.

We'll be using Guix and the powerful guix deploy command to provision and install a Hetzner VPS.

Some prep work

Make sure you have these things ready if you want to follow along and set up your instance while you read this post.

Hetzner account

You'll need a Hetzner account. Create an API token from the Hetzner control panel. We will need the token later, when we run guix deploy.

Warning: Provisioning a machine on Hetzner will cost you money; familiarise yourself with Hetzner's pricing model and how guix deploy works before you start!

Domain name

Your GoToSocial instance will require a domain name, e.g. social.example.com. Once you've registered the domain, you'll need to configure it to point to the Hetzner VPS. This is likely to be a three-step process:

  • launch guix deploy to provision the VPS and get its IP address,
  • update the domain name's DNS to point to the VPS IP address,
  • SSH into the machine and restart the GoToSocial service.

A Guix machine

You'll need a Guix machine to launch guix deploy from. You don't necessarily need the Guix operating system, any other Linux flavour will work as long as you can install Guix on it as a package manager (called installation on a foreign distro).

Docs!

You may want to keep the following resources at your fingertips:

Four definitions

Four main elements are needed: a GoToSocial Guix package, a service, a system definition, and a Guix Deploy machine definition.

A package

At the time of writing, GoToSocial is not in Guix - let's package it then. For simplicity, let's take a shortcut and use the pre-built binary provided by the GoToSocial maintainers, instead of compiling it from source as you typically do with Guix packages. The latest GoToSocial version is 0.21.2.

(define gotosocial
  (package
    (name "gotosocial")
    (version "0.21.2")
    (source
     (origin
       (method url-fetch)
       (uri (string-append
             "https://codeberg.org/superseriousbusiness/gotosocial"
             "/releases/download/v" version
             "/gotosocial_" version "_linux_amd64.tar.gz"))
       (sha256
        (base32
         "00s422gfrx9k1zla8pb2p73n37d72cdlv3046h8rr3l3cyxhb9wh"))))
    (build-system copy-build-system)
    (arguments
     (list
      #:substitutable? #f
      #:install-plan
      #~'(("gotosocial" "bin/gotosocial")
          ("web/" "share/gotosocial/web/"))
      #:phases
      #~(modify-phases %standard-phases
          (add-after 'unpack 'chdir-to-source
            (lambda _
              (chdir "..")))
          (add-after 'install 'chmod-binary
            (lambda* (#:key outputs #:allow-other-keys)
              (let ((bin (string-append (assoc-ref outputs "out")
                                        "/bin/gotosocial")))
                (chmod bin #o755)))))))
    (home-page "https://gotosocial.org/")
    (synopsis "ActivityPub social network server")
    (description
     "GoToSocial is a lightweight, customisable, and safety-focused
ActivityPub social network server, written in Go.")
    (license license:agpl3+)
    (supported-systems '("x86_64-linux"))))

A service

Next, define gotosocial-service-type, a mechanism to conveniently start and stop the GoToSocial service using the Shepherd, the init system used by Guix.

(define (gotosocial-shepherd-services _)
  "Return a Shepherd service that runs GoToSocial as the 'gotosocial' user.
All configuration is passed via environment variables so no config file
is needed."
  (list
   (shepherd-service
    (documentation "Run the GoToSocial ActivityPub server.")
    (provision '(gotosocial))
    (requirement '(networking user-processes))
    (start
     #~(make-forkexec-constructor
        (list #$(file-append gotosocial "/bin/gotosocial")
              "server" "start")
        #:user "gotosocial"
        #:group "gotosocial"
        #:log-file "/var/log/gotosocial.log"
        #:environment-variables
        (list
         "GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db"
         "GTS_DB_TYPE=sqlite"
         "GTS_HOST=social.example.com"
         "GTS_LETSENCRYPT_CERT_DIR=/gotosocial/storage/certs"
         "GTS_LETSENCRYPT_EMAIL_ADDRESS=user@example.com"
         "GTS_LETSENCRYPT_ENABLED=true"
         "GTS_LETSENCRYPT_PORT=80"
         "GTS_PORT=443"
         "GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/storage"
         (string-append "GTS_WEB_TEMPLATE_BASE_DIR="
                        #$gotosocial
                        "/share/gotosocial/web/template/")
         (string-append "GTS_WEB_ASSET_BASE_DIR="
                        #$gotosocial
                        "/share/gotosocial/web/assets/"))))
    (stop #~(make-kill-destructor)))))

(define gotosocial-service-type
  (service-type
   (name 'gotosocial)
   (description "Run GoToSocial as a native Shepherd service.")
   (extensions
    (list
     (service-extension shepherd-root-service-type
                        gotosocial-shepherd-services)
     (service-extension account-service-type
                        (const (list (user-group
                                      (name "gotosocial")
                                      (system? #t))
                                     (user-account
                                      (name "gotosocial")
                                      (group "gotosocial")
                                      (system? #t)
                                      (comment "GoToSocial server user")
                                      (home-directory "/gotosocial")
                                      (shell (file-append bash "/bin/bash"))))))
     (service-extension activation-service-type
                        (const #~(begin
                                   (use-modules (guix build utils))
                                   (let ((user (getpw "gotosocial")))
                                     (for-each
                                      (lambda (dir)
                                        (mkdir-p dir)
                                        (chown dir
                                               (passwd:uid user)
                                               (passwd:gid user)))
                                      '("/gotosocial/storage"
                                        "/gotosocial/storage/certs"))))))))
   (default-value #f)))

Some of the configuration variables, e.g. GTS_HOST, are just placeholders and need to be replaced with appropriate values.

A system definition

The third element is the Guix system definition. We define a minimalist Guix system that works nicely on Hetzner and that includes the GoToSocial service.

(define %nftables-conf
  (plain-file
   "nftables.conf"
   "flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;
        iif lo accept
        icmpv6 type {nd-neighbor-solicit, nd-router-advert,
                     nd-neighbor-advert} accept
        ct state vmap {established: accept, related: accept, invalid: drop}
        ct state new limit rate over 1/second burst 10 packets drop

        tcp dport ssh accept
        tcp dport http accept
        tcp dport https accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }
}"))

(define gotosocial-system
  (operating-system
    (host-name "gotosocial")
    (timezone "Etc/UTC")
    (bootloader
     (bootloader-configuration
      (bootloader grub-bootloader)
      (targets (list "/dev/sda"))
      (terminal-outputs '(console))))
    (initrd-modules
     (cons "virtio_scsi" %base-initrd-modules))
    (file-systems (cons (file-system
                          (mount-point "/")
                          (device "/dev/sda1")
                          (type "ext4"))
                        %base-file-systems))

    (users (list (user-account
                  (name "user")
                  (group "users")
                  (supplementary-groups '("wheel")))))
    (sudoers-file (plain-file
                   "sudoers"
                   (string-append
                    (plain-file-content %sudoers-specification)
                    "%wheel ALL = NOPASSWD: ALL")))

    (packages
     (cons* gotosocial %base-packages))

    (services
     (cons*
      ;; Networking
      (service dhcpcd-service-type)
      (service nftables-service-type
               (nftables-configuration (ruleset %nftables-conf)))
      (service ntp-service-type)
      ;; SSH
      (service openssh-service-type
               (openssh-configuration
                (authorized-keys
                 `(("user"
                    ,(plain-file "ssh.pub" "ssh-rsa ..."))))
                (permit-root-login 'prohibit-password)))
      ;; GoToSocial
      (service gotosocial-service-type)

      ;; Base services, with sysctl modified to allow unprivileged port binding
      ;; (needed for GTS to bind :443 and :80 as a non-root user).
      (modify-services %base-services
        (sysctl-service-type
         config =>
         (sysctl-configuration
          (settings
           (append
            '(("net.ipv4.ip_unprivileged_port_start" . "80"))
            %default-sysctl-settings)))))))))

A machine definition

Finally, we need a machine definition, i.e. a Scheme record that includes relevant information about the server that we want to provision with Guix Deploy.

(list
 (machine
  (operating-system gotosocial-system)
  (environment hetzner-environment-type)
  (configuration (hetzner-configuration
                  (server-type "cx23")
                  (ssh-key "...")))))

server-type indicates the type of Hetzner instance we want to provision, e.g. CX23 (2 VCPUs, 4GB RAM, 40GB SSD). Full list here.

Deployment

As mentioned at the beginning, the deployment happens in three steps. The reason for this is that GoToSocial requires a domain name pointing to it - so you typically need to:

  • provision the VPS and obtain its IP address,
  • configure the DNS to point to the address,
  • restart the GoToSocial server.

Let's look at this step by step.

Provision the VPS

Once you have set the GUIX_HETZNER_API_TOKEN environment variable with the Hetzner API token, provisioning the VPS is as simple as:

export GUIX_HETZNER_API_TOKEN=...
guix deploy --load-path=. gotosocial.scm

Under the hood, this involves quite a few steps and it may take a while to complete. If all goes well, the process returns the IPv4 and IPv6 addresses of the provisioned machine.

$ guix deploy --load-path=. gotosocial.scm
...
building /gnu/store/ijy2b1c06r0gz8bmpgdnp8l6j62r0f57-grub.cfg.drv...
building /gnu/store/rmmpmw2x6xjyyrndndi0wlz7sxsikzp6-install-bootloader.scm.drv...
building /gnu/store/mdqqjilac1xv5q7dkyvl1r3xh3k7xqzb-remote-exp.scm.drv...
guix deploy: sending 10 store items (52 MiB) to '192.0.2.1'...
guix deploy: successfully deployed gotosocial

Double-check that you can now SSH into the machine.

Configure the DNS

We have specified a domain name for our GoToSocial service already:

"GTS_HOST=social.example.com"

We need to configure the domain's DNS to point to the IP address returned by Guix Deploy. Once that's done and the DNS has propagated successfully, we can restart the GoToSocial service.

Restart GoToSocial

SSH into the machine and run:

sudo herd enable gotosocial
sudo herd restart gotosocial

Alternatively, you can run the commands from your machine, via Guix Deploy:

export GUIX_HETZNER_API_TOKEN=...
guix deploy --load-path=. gotosocial.scm --execute -- herd enable gotosocial
guix deploy --load-path=. gotosocial.scm --execute -- herd restart gotosocial

Now that the DNS is configured correctly, GoToSocial will be able to get a TLS certificate from LetsEncrypt and start correctly.

Check whether the installation was successful - open a browser and visit your GoToSocial domain. Hopefully, you should see something like this:

gotosocial-screenshot-0.jpg

Figure 1: A screenshot of the GoToSocial web interface, immediately after installation.

Some troubleshooting ideas

Here are some troubleshooting ideas if your GoToSocial domain doesn't show up as expected.

SSH into the VPS and:

  • Check that LetsEncrypt certificates have been created, see /gotosocial/storage/certs.
  • Check the GoToSocial logs, see /var/log/gotosocial.log.
  • Check other system logs, see /var/log/messages.
  • Restart the service with herd restart gotosocial, possibly while monitoring the log files.
  • Triple-check the firewall configuration with nft list ruleset.

GoToSocial setup

Now it's time to create a GoToSocial user:

sudo -u gotosocial \
    GTS_HOST=social.example.com \
    GTS_DB_TYPE=sqlite \
    GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db \
    gotosocial admin account create \
        --username user \
        --email user@example.com \
        --password 'YOUR-PASSWORD-GOES-HERE'

Try to log in via the web interface. You should see a page like this:

gotosocial-screenshot-1.jpg

Figure 2: A user profile page on the GoToSocial web interface.

If this user is supposed to be an instance admin, then:

sudo -u gotosocial \
    GTS_HOST=social.example.com \
    GTS_DB_TYPE=sqlite \
    GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db \
    gotosocial admin account promote \
        --username user

You can get a list of instance users with:

sudo -u gotosocial \
    GTS_HOST=social.example.com \
    GTS_DB_TYPE=sqlite \
    GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db \
    gotosocial admin account list

I'm completely new to GoToSocial, so I redirect you to the project documentation for anything beyond this.

Did I say I'm new to this?

I've just installed this under a test domain and I don't have any previous experience using a GoToSocial instance (as an admin or otherwise) - I'm very much still figuring things out myself. These are some of the next steps in my to-do list:

Get in touch if you notice any issues with the post or if there's anything that I missed, especially on the GoToSocial side.

Putting it all together

Thank you for reading this far! If it's useful, here is the entirety of the code presented in this post in one single block. Save it to gotosocial.scm and you're good to go:

(define-module (gotosocial)
  #:use-module (gnu)
  #:use-module (gnu machine hetzner)
  #:use-module (gnu machine)
  #:use-module (gnu packages bash)
  #:use-module (gnu services networking)
  #:use-module (gnu services shepherd)
  #:use-module (gnu services ssh)
  #:use-module (gnu services sysctl)
  #:use-module (guix build-system copy)
  #:use-module (guix download)
  #:use-module (guix packages)
  #:use-module ((guix licenses) #:prefix license:))

(define gotosocial
  (package
    (name "gotosocial")
    (version "0.21.2")
    (source
     (origin
       (method url-fetch)
       (uri (string-append
             "https://codeberg.org/superseriousbusiness/gotosocial"
             "/releases/download/v" version
             "/gotosocial_" version "_linux_amd64.tar.gz"))
       (sha256
        (base32
         "00s422gfrx9k1zla8pb2p73n37d72cdlv3046h8rr3l3cyxhb9wh"))))
    (build-system copy-build-system)
    (arguments
     (list
      #:substitutable? #f
      #:install-plan
      #~'(("gotosocial" "bin/gotosocial")
          ("web/" "share/gotosocial/web/"))
      #:phases
      #~(modify-phases %standard-phases
          (add-after 'unpack 'chdir-to-source
            (lambda _
              (chdir "..")))
          (add-after 'install 'chmod-binary
            (lambda* (#:key outputs #:allow-other-keys)
              (let ((bin (string-append (assoc-ref outputs "out")
                                        "/bin/gotosocial")))
                (chmod bin #o755)))))))
    (home-page "https://gotosocial.org/")
    (synopsis "ActivityPub social network server")
    (description
     "GoToSocial is a lightweight, customisable, and safety-focused
ActivityPub social network server, written in Go.")
    (license license:agpl3+)
    (supported-systems '("x86_64-linux"))))

(define (gotosocial-shepherd-services _)
  "Return a Shepherd service that runs GoToSocial as the 'gotosocial' user.
All configuration is passed via environment variables so no config file
is needed."
  (list
   (shepherd-service
    (documentation "Run the GoToSocial ActivityPub server.")
    (provision '(gotosocial))
    (requirement '(networking user-processes))
    (start
     #~(make-forkexec-constructor
        (list #$(file-append gotosocial "/bin/gotosocial")
              "server" "start")
        #:user "gotosocial"
        #:group "gotosocial"
        #:log-file "/var/log/gotosocial.log"
        #:environment-variables
        (list
         "GTS_DB_ADDRESS=/gotosocial/storage/sqlite.db"
         "GTS_DB_TYPE=sqlite"
         "GTS_HOST=social.example.com"
         "GTS_LETSENCRYPT_CERT_DIR=/gotosocial/storage/certs"
         "GTS_LETSENCRYPT_EMAIL_ADDRESS=user@example.com"
         "GTS_LETSENCRYPT_ENABLED=true"
         "GTS_LETSENCRYPT_PORT=80"
         "GTS_PORT=443"
         "GTS_STORAGE_LOCAL_BASE_PATH=/gotosocial/storage"
         (string-append "GTS_WEB_TEMPLATE_BASE_DIR="
                        #$gotosocial
                        "/share/gotosocial/web/template/")
         (string-append "GTS_WEB_ASSET_BASE_DIR="
                        #$gotosocial
                        "/share/gotosocial/web/assets/"))))
    (stop #~(make-kill-destructor)))))

(define gotosocial-service-type
  (service-type
   (name 'gotosocial)
   (description "Run GoToSocial as a native Shepherd service.")
   (extensions
    (list
     (service-extension shepherd-root-service-type
                        gotosocial-shepherd-services)
     (service-extension account-service-type
                        (const (list (user-group
                                      (name "gotosocial")
                                      (system? #t))
                                     (user-account
                                      (name "gotosocial")
                                      (group "gotosocial")
                                      (system? #t)
                                      (comment "GoToSocial server user")
                                      (home-directory "/gotosocial")
                                      (shell (file-append bash "/bin/bash"))))))
     (service-extension activation-service-type
                        (const #~(begin
                                   (use-modules (guix build utils))
                                   (let ((user (getpw "gotosocial")))
                                     (for-each
                                      (lambda (dir)
                                        (mkdir-p dir)
                                        (chown dir
                                               (passwd:uid user)
                                               (passwd:gid user)))
                                      '("/gotosocial/storage"
                                        "/gotosocial/storage/certs"))))))))
   (default-value #f)))

(define %nftables-conf
  (plain-file
   "nftables.conf"
   "flush ruleset

table inet firewall {
    chain inbound {
        type filter hook input priority 0; policy drop;
        iif lo accept
        icmpv6 type {nd-neighbor-solicit, nd-router-advert,
                     nd-neighbor-advert} accept
        ct state vmap {established: accept, related: accept, invalid: drop}
        ct state new limit rate over 1/second burst 10 packets drop

        tcp dport ssh accept
        tcp dport http accept
        tcp dport https accept
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
    }
}"))

(define gotosocial-system
  (operating-system
    (host-name "gotosocial")
    (timezone "Etc/UTC")
    (bootloader
     (bootloader-configuration
      (bootloader grub-bootloader)
      (targets (list "/dev/sda"))
      (terminal-outputs '(console))))
    (initrd-modules
     (cons "virtio_scsi" %base-initrd-modules))
    (file-systems (cons (file-system
                          (mount-point "/")
                          (device "/dev/sda1")
                          (type "ext4"))
                        %base-file-systems))

    (users (list (user-account
                  (name "user")
                  (group "users")
                  (supplementary-groups '("wheel")))))
    (sudoers-file (plain-file
                   "sudoers"
                   (string-append
                    (plain-file-content %sudoers-specification)
                    "%wheel ALL = NOPASSWD: ALL")))

    (packages
     (cons* gotosocial %base-packages))

    (services
     (cons*
      ;; Networking
      (service dhcpcd-service-type)
      (service nftables-service-type
               (nftables-configuration (ruleset %nftables-conf)))
      (service ntp-service-type)
      ;; SSH
      (service openssh-service-type
               (openssh-configuration
                (authorized-keys
                 `(("user"
                    ,(plain-file "ssh.pub" "ssh-rsa ..."))))
                (permit-root-login 'prohibit-password)))
      ;; GoToSocial
      (service gotosocial-service-type)

      ;; Base services, with sysctl modified to allow unprivileged port binding
      ;; (needed for GTS to bind :443 and :80 as a non-root user).
      (modify-services %base-services
        (sysctl-service-type
         config =>
         (sysctl-configuration
          (settings
           (append
            '(("net.ipv4.ip_unprivileged_port_start" . "80"))
            %default-sysctl-settings)))))))))

(list
 (machine
  (operating-system gotosocial-system)
  (environment hetzner-environment-type)
  (configuration (hetzner-configuration
                  (server-type "cx23")
                  (ssh-key "...")))))

Revision 77e4901.