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:

processnumberopenfiles-max
gotosocial11024
httpd51024
relayd52048

And that’s all for now! More about the accounts migration later on.
See you on the Fediverse ;-)