GoToSocial Adventures: run on OpenBSD
2285 words, 11 minutes
I used to run GoToSocial on OpenBSD but, one day, the port was marked BROKEN and I switched to using Mastodon on Linux. Still, I kept testing running GoToSocial on NetBSD, Illumos, FreeBSD and Linux. Not so long ago, GoToSocial started to compile and work pretty nice again on OpenBSD.
After using it for about a year on a SearXNG side project , I decided that I would also use it as my primary account on the Fediverse . This post is about installing and running GoToSocial on OpenBSD .
Install OpenBSD
I’m running OpenBSD 7.7 on an amd64 virtual machine using the bhyve hypervisor on OmniOS. The side-project instance is running on a 2 vCPU, 2GB of RAM and 16GB of storage, for a single user. Everything works great.
As I plan to have from 3 to 5 users on the new instance, I’ll go with 2 vCPU and 4GB of RAM; time will tell if this is more than needed.
My current single-user Mastodon instance uses between 30GB and 64GB of storage; depending on how well my automated cleaning scripts actually run or not… To avoid storage issues with 3-5 users, I’ll go with 128GB of storage.
The GoToSocial instance will have several disks attached. One for the
system, one for /root
, one for /var/www
. This will allow data
isolation and make storage management easier in the future.
On OmniOS, using zadm
, an OpenBSD VPS is created this way:
# zadm create -b bhyve gts-tumfatig
{
"autoboot" : "true",
"bootdisk" : {
"blocksize" : "8k",
"path" : "tank/zones/gts-tumfatig/root",
"size" : "10G",
"sparse" : "false"
},
"brand" : "bhyve",
"cdrom" : [
"/zones/iso/install77.iso"
],
"disk" : [
{
"blocksize" : "8k",
"path" : "tank/zones/gts-tumfatig/home-root",
"size" : "5G",
"sparse" : "false"
},
{
"blocksize" : "8k",
"path" : "tank/zones/gts-tumfatig/var-www",
"size" : "128G",
"sparse" : "false"
}
],
"net" : [
{
"global-nic" : "igb0",
"physical" : "gtstmf0"
}
],
"ram" : "4G",
"rng" : "on",
"vcpus" : "2",
"vnc" : "off",
"zonename" : "gts-tumfatig",
"zonepath" : "/zones/gts-tumfatig"
}
# zadm start -c gts-tumfatig
boot> set tty com0
boot> boot
During installation, every disks is configured independently.
Available disks are: sd0 sd1 sd2.
Which disk is the root disk? ('?' for details) [sd0] ?
sd0: VirtIO, Block Device (10.0G)
sd1: VirtIO, Block Device (5.0G)
sd2: VirtIO, Block Device (128.0G)
On the “system” disk, the EFI/GPT configuration must be selected in order to boot properly. On some other hypervisor, MBR may be required instead of UEFI.
Use (W)hole disk MBR, whole disk (G)PT or (E)dit? [whole] G
An EFI/GPT disk may not boot. Proceed? [no] yes
Setting OpenBSD GPT partition to whole sd0...done.
The auto-allocated layout for sd0 is:
# size offset fstype [fsize bsize cpg]
a: 1177.2M 532544 4.2BSD 2048 16384 1 # /
b: 256.0M 2943424 swap
c: 10240.0M 0 unused
d: 3072.0M 3467712 4.2BSD 2048 16384 1 # /usr
e: 2048.0M 9759168 4.2BSD 2048 16384 1 # /home
i: 260.0M 64 MSDOS
Given that I’m using extra disks, I configure the “system” in a custom way.
Use (A)uto layout, (E)dit auto layout, or create (C)ustom layout? [a] E
(...)
sd0*> p g
OpenBSD area: 532544-20971487; size: 9.7G; free: 0.0G
# size offset fstype [fsize bsize cpg]
a: 0.5G 1590464 4.2BSD 2048 16384 1 # /
b: 0.5G 532544 swap
c: 10.0G 0 unused
d: 2.0G 2650720 4.2BSD 2048 16384 1 # /tmp
e: 2.0G 6859744 4.2BSD 2048 16384 1 # /var
f: 3.0G 11068768 4.2BSD 2048 16384 1 # /usr
g: 1.7G 17366240 4.2BSD 2048 16384 1 # /usr/local
i: 0.3G 64 MSDOS
Once the “system” disk is configured, the two others can be configured.
Available disks are: sd1 sd2.
Which disk do you wish to initialize? (or 'done') [done] sd1
Use (W)hole disk MBR, whole disk (G)PT or (E)dit? [whole] W
Setting OpenBSD MBR partition to whole sd1...done.
Label editor (enter '?' for help at any prompt)
sd1> a a
offset: [64]
size: [10485696] *
FS type: [4.2BSD]
mount point: [none] /root
sd1*> w
sd1> q
Available disks are: sd2.
Which disk do you wish to initialize? (or 'done') [done] sd2
Use (W)hole disk MBR, whole disk (G)PT or (E)dit? [whole] W
Setting OpenBSD MBR partition to whole sd2...done.
Label editor (enter '?' for help at any prompt)
sd2> a a
offset: [64]
size: [268435392] *
FS type: [4.2BSD]
mount point: [none] /var/www
sd2*> w
sd2> q
From there, installation proceeds as usual. Once the installation is finished, halt the system, remove the ISO reference and reboot on the new OpenBSD instance.
Install GoToSocial
There is no maintained port anymore and no official binary available. So
GoToSocial sources will be fetched and updated using git
and compiled
using go
. Fortunately, OpenBSD provides updated binary ports for
those.
# pkg_add go git
(...)
go-1.24.1: ok
git-2.49.0: ok
WebAssembly is not available on OpenBSD, at the time of writing. So GoToSocial has to be compiled without it; and ffmpeg has to be installed from packages. See building without Wazero / WASM for more information.
# pkg_add ffmpeg
(...)
ffmpeg-6.1.2p1v1: ok
GoToSocial compilation is quite straightforward.
# export GOTMPDIR=~/tmp
# [ ! -d "$GOTMPDIR" ] && mkdir -p "$GOTMPDIR"
# mkdir -p ~/go/src/code.superseriousbusiness.org
# git clone https://codeberg.org/superseriousbusiness/gotosocial.git \
~/go/src/code.superseriousbusiness.org/gotosocial
# cd ~/go/src/code.superseriousbusiness.org/gotosocial
# git checkout v0.19.1
# GO_BUILDTAGS=nowasm ./scripts/build.sh
(...)
# ./gotosocial --version
!! you are using an unsupported build configuration of gotosocial with WebAssembly disabled !!
!! please do not file bug reports regarding media processing with this configuration !!
!! it is also less secure; this does not enforce version checks on ffmpeg / ffprobe versions !!
gotosocial version v0.19.1+git-6574dc8
The static assets can either be grabbed from a GtS release tarball or
generated from OpenBSD.
For the “fun”, let’s do it using the yarn
binary package.
# pkg_add yarn
(...)
yarn-1.22.22p0:node-22.15.1v0: ok
yarn-1.22.22p0: ok
# cd ~/go/src/code.superseriousbusiness.org/gotosocial
# yarn --cwd ./web/source install
# yarn --cwd ./web/source ts-patch install
# yarn --cwd ./web/source build
GoToSocial will run as a dedicated unprivileged user.
# useradd -m -c "GoToSocial daemon" -d /var/www/gotosocial \
-g =uid -r 16000..16999 -s /sbin/nologin _gotosocial
To ease the upgrade / rollback processes, I like to have a dedicated
directory per GtS version and a symlink that points to the current
version I want to run.
Everything is installed in /var/www
where httpd(8)
can access
the assets.
# export VERSION="0.19.1"
# [ ! -d ~_gotosocial/$VERSION ] && mkdir -p ~_gotosocial/$VERSION
# cd ~/go/src/code.superseriousbusiness.org/gotosocial
# install -p -m 0755 -o root -g _gotosocial gotosocial \
~_gotosocial/$VERSION/gotosocial
# tar cpf - example/config.yaml web/{assets,template} | \
tar xpf - -C ~_gotosocial/$VERSION/
# cd ~_gotosocial/$VERSION
# find ./example ./web -type d \
-exec chown root:_gotosocial {} \; \
-exec chmod 0755 {} \;
# find ./example ./web -type f \
-exec chown root:_gotosocial {} \; \
-exec chmod 0644 {} \;
# cd ~_gotosocial
# ln -s $VERSION current
Because I only host a few users and PostgreSQL is nearly unknown to me, I use SQlite. The database and storage directories are created in the GtS user’s home; with the appropriate rights.
# cd ~_gotosocial
# doas -u _gotosocial mkdir db storage
Configure GoToSocial
As a first installation, the configuration is copied from the release directory. During upgrade, it has to be compared to the new release configuration file and modified, if needed.
# cd ~_gotosocial
# cp current/example/config.yaml .
# vi config.yaml
The instance can now be run from the console to check basic things. To access GtS, use a Web browser and point it to the listener; with or without SSH forwarded port.
# cd ~_gotosocial
# doas -u _gotosocial ~_gotosocial/current/gotosocial \
--config-path ~_gotosocial/config.yaml server start
rc.d management script
Once the installation looks good, it is time to have it started and ran properly using a home-made rc.d script.
# cat > /etc/rc.d/gotosocial
#!/bin/ksh
daemon="~_gotosocial/current/gotosocial"
daemon_execdir="~_gotosocial"
daemon_flags=""
daemon_logger="daemon.info"
daemon_user="_gotosocial"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_cmd $1
^D
# chmod 0755 /etc/rc.d/gotosocial
# rcctl enable gotosocial
# rcctl set gotosocial flags \
"--config-path ~_gotosocial/config.yaml server start"
# rcctl start gotosocial
For easier management, I like to use a dedicated log files for
gotosocial; using syslogd(8)
features.
# touch /var/log/gotosocial
# vi /etc/syslog.conf
(...)
!!gotosocial
*.* /var/log/gotosocial
!*
(...)
# rcctl reload syslogd
# vi /etc/newsyslog.conf
(...)
/var/log/gotosocial 640 7 * $D0 ZB
Create the GoToSocial user(s)
Users can be created using the command line. Simply run command(s) as described in the Creating Users section of the GoToSocial Documentation .
# doas -u _gotosocial ~_gotosocial/current/gotosocial \
--config-path ~_gotosocial/config.yaml \
admin account create \
--username bob --email noreply@example.com \
--password '<change me>'
# doas -u _gotosocial ~_gotosocial/current/gotosocial \
--config-path ~_gotosocial/config.yaml \
admin account promote --username bob
Expose GoToSocial
GoToSocial will be exposed on the Wild Wild Web through relayd(8)
and
httpd(8)
. The first will be facing Internet and do a bunch of Web
filtering stuff. The second will deal with TLS certificate renewal and
static assets publication.
Let’s Encrypt certificate
Once the server’s public IP has a registered DNS entry, configure
httpd(8)
to accept Let’s Encrypt requests.
# vi /etc/httpd.conf
server "default" {
listen on * port 80
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location * {
block return 302 "https://$HTTP_HOST$REQUEST_URI"
}
}
# rcctl enable httpd
# rcctl start httpd
Then configure the acme-client(1)
so that it gets and maintains the
required TLS certificate. Note that the chain certificate has a specific
name that will please relayd(8)
.
# vi /etc/acme-client.conf
(...)
domain gts.example.com {
domain key "/etc/ssl/private/gts.example.com.key"
domain full chain certificate "/etc/ssl/gts.example.com.crt"
#sign with letsencrypt-staging
sign with letsencrypt
# acme-client gts.example.com
The staging infrastructure can be used to test acme-client(1)
and
httpd(8)
.
Distribute static files
The GoToSocial binary can serve assets and media. But to avoid hitting
GtS for such files, I’m serving those using httpd(8)
. Read the Caching assets and
media
for more details. This is why GtS files are installed inside /var/www
.
# vi /etc/httpd.conf
(...)
server "gts.example.com" {
listen on 127.0.0.1 port 8443
log { style forwarded }
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/assets/*" {
gzip-static
root "/gotosocial/current/web/assets"
request strip 1
}
location "/fileserver/*" {
root "/gotosocial/storage"
request strip 1
}
}
types {
include "/usr/share/misc/mime.types"
}
# find /var/www/gotosocial/current/web/assets -type f \
\( -name *css -o -name *js -o -name *txt -o -name *svg \) \
-exec gzip -kf {} \;
# rcctl restart httpd
The gzip
trick allows to save some bandwidth.
Publish the instance
relayd(8)
is configured to publish GoToSocial. It does a few checks at
the HTTP level, adds a bit of security caching and forwards HTTP
requests to the various configured endpoints depending on the request.
# vi /etc/relayd.conf
(...)
http protocol wwwtls {
tls keypair gts.example.com
tcp { backlog 128, nodelay, sack, socket buffer 65536 }
http websockets
block
# --------------------------------------------------------------
block request quick path "/metrics*"
match request header "Host" value "gts.example.com" tag "gts"
# logging ------------------------------------------------------
match url log
match header log "Host"
match header log "X-Forwarded-For"
match header log "User-Agent"
match header log "Referer"
match header log "Scheme"
match header log "Connection"
match header log "Upgrade"
# reverse-proxy headers ----------------------------------------
match request header set "Keep-Alive" value "$TIMEOUT"
match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match request header set "X-Forwarded-Proto" value "https"
# security headers ---------------------------------------------
match response header set "Strict-Transport-Security" \
value "max-age=63072000; includeSubDomains"
# cache headers ------------------------------------------------
match request tagged "gts" path "/assets/*.css" tag "gts-cached"
match request tagged "gts" path "/assets/*.js" tag "gts-cached"
match request tagged "gts" path "/assets/*.ico" tag "gts-cached"
match request tagged "gts" path "/assets/*.jpg" tag "gts-cached"
match request tagged "gts" path "/assets/*.png" tag "gts-cached"
match request tagged "gts" path "/assets/*.svg" tag "gts-cached"
match request tagged "gts" path "/assets/*.webp" tag "gts-cached"
match request tagged "gts" path "/assets/*.woff2" tag "gts-cached"
match response tagged "gts-cached" header set "Cache-Control" \
value "private, max-age=604800, must-revalidate"
# select backend based on HTTP request -------------------------
pass request quick tagged "gts" path "/.well-known/acme-challenge/*" forward to <acme>
pass request quick tagged "gts" path "/assets/*" forward to <static>
pass request quick tagged "gts" path "/fileserver/*" forward to <static>
pass request quick tagged "gts-cached" path "/assets/*" forward to <static>
pass request quick tagged "gts-cached" path "/fileserver/*" forward to <static>
pass request tagged "gts" forward to <gts>
}
relay www4tls {
listen on $ipv4_addr port 443 tls
protocol wwwtls
forward to <acme> port 80 check http "/" code 302
forward to <static> port 8443 check http "/assets/logo.png" code 200
forward to <gts> port 8080 check http "/readyz" code 418
}
(...)
# rcctl enable relayd
# rcctl start relayd
Split-domain
Following the Split-domain deployment model
, I
configured my instance so that my accounts are @bob@example.com
but
the GoToSocial server is gts.example.com
.
In my case, the first domain already hosts this blog. So I modified the
blog’s httpd(8)
configuration to allow replying to ActivityPub
requests.
# vi /etc/httpd.conf
(...)
server "example.com" {
(...)
location "/.well-known/webfinger" {
block return 301 "https://gts.example.com$REQUEST_URI"
}
location "/.well-known/host-meta" {
block return 301 "https://gts.example.com$REQUEST_URI"
}
location "/.well-known/nodeinfo" {
block return 301 "https://gts.example.com$REQUEST_URI"
}
(...)
# rcctl reload httpd
Final thoughts
From there, the GoToSocial is ready to be used. It’s been working great so far.
The collectd(1)
monitoring system indicates quite low resource
usage - especially compared to Mastodon. The prometheus
endpoints
allows monitoring what happens at the software level. Those metrics
include migrations that occured from my Mastodon and Pixelfed accounts;
CPU and SQlite ops are mostly those.
There have been moments when the default OpenBSD resources limitation hit the services. I had to add more listening processes and allow more open files. As of today, those values seem to be enough, for me:
process | number | openfiles-max |
---|---|---|
gotosocial | 1 | 1024 |
httpd | 5 | 1024 |
relayd | 5 | 2048 |
And that’s all for now! More about the accounts migration later on.
See you on the Fediverse ;-)