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 deployto 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:
- the Guix manual, especially the section on Guix Deploy,
- the GoToSocial manual.
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:
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:
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:
- MFA: Set up Multi-Factor Authentication (MFA) on my (test) account.
- Backup: Figure out a backup and disaster recovery strategy.
- GoToSocial upgrade: Figure out the GoToSocial upgrade mechanism.
- Observability and metrics.
- Migration: Migration from and to other Fediverse instances, including importing posts from previous instances.
- OSA considerations: As a prospective single-user instance admin, I don't think the UK Online Safety Act is going to be a blocker, but I'll certainly have to prepare some paperwork around this.
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 "...")))))