Back to posts

Selfhosting Snac2 on Tailscale Funnel

This is a blogpost describing my journey trying to selfhost snac2[1] via Tailscale funnel[2] on a spare computer. I started the process in April and finally got the server working yesterday (11th October 2024) because of stupid mistakes made early on.

[1]: snac2 software

[2]: Tailscale Funnel

Quick Background

I have a couple of accounts on the Fediverse: my main Mastodon[3] on Mastodon.social[4] and a second account on another Mastodon server[5]. I have been on the Fediverse since 2018 and had 2 accounts for that time, however, the second account has changed servers a few times because of server closure. For a long time it was on a Pleroma server, then briefly on an Epicyon server. As such, I've not become fixed to specifically Mastodon software and I've looked to experiment with other software. Snac2 is nice simple Fediverse software that does (mostly) what I want (and is written in a language I understand), so I decided to try it (I'll go into "mostly" later). Self-hosting, though, can be expensive with a VPS and I'm behind a NAT at home. Fortunately, when Tailscale originally launched their "Funnel" feature, Mastodon was on the rise in the tech press so they mentioned[6] it could be used "to host your personal blog or a small Mastodon server on your own computer". This got it noticed and promoted on the Fediverse, so I thought I'd try it.

[3]: Mastodon software

[4]: My account on Mastodon.social

[5]: My other Mastodon account

[6]: Blogpost introducing Tailscale Funnel

Starting off...

I decided to use a small HP Mini-PC running Ubuntu so I could set it up as a home server without it taking much space, yet not having to leave behind the amd64 architecture and struggle with Arm, like I would with a Raspberry Pi. I set up a local SSH and installed Tailscale software (I don't remember much of how I did either of these as it was months ago, so I'll gloss over it). I then downloaded the snac2 software with a `git clone`, *but* I had no intention of running it directly...

Forking Snac2

Remember that "mostly" from earlier? There are two ways to interpret the ActivityPub "Like" activity[7] and different Fediverse software use different interpretations. I call them "Like" and "Favourite", because Twitter has used both over time and that's what it referred to them as. Essentially, a "Like" is a "Favourite" that's also shared with your followers like a "boost"/"repost"/"repeat"/"reblog" (this other action has lots of names across the Fediverse, so I'm listing the most common). I prefer Favourites as, if I want to share a post as well, I'll boost at the same time. It just makes sense to me to keep the actions separate. Snac2, however, uses the "Like" interpretation, so I decided to soft-fork snac2 and make some changes[8].

First change was obviously to make the "Like" action (grouped with boost and called "Admire" in the sourcecode) not share the post. This turned out to be remarkably simple[9] and just required checking in the "Admire" function that the type was an "Announce" (what the sourcecode calls boosts) before I'd let it be boosted, so needed one line of code. However, while I was there, I made some other changes. The Commandline interface to snac has the ability to boost but not like/favourite a post, so I added[10] that (along with the reverse of unliking/unfavouriting it). I then set the site icon and default user display picture to be my own, albeit converted to monochrome PNG8 with the command:

convert Av.png -dither FloydSteinberg -fx '(r+g+b)/3' -colors 2  -colorspace Gray PNG8:monoAv.png

I added the Base64 value to the code[11] in place of the default, though I didn't bother removing the special code for changing the icon on certain days (I may do in the future: I notice that today it's different, so I may have to soon). I also changed the version number to indicate it was a fork[12].

[7]: ActivityPub Like activity primer

[8]: snac2-dhfork - my soft-fork of snac2

[9]: The commit changing the Like action

[10]: The commit adding the commandline Like/Favourite action.

[11]: The commit changing the display-picture and site-icon

[12]: Commit changing version number. Not really important.

Continuing...

Having done all that, I compiled and installed it and got to work setting up the server. I created a "snac" user and created the snac2 datafolder in its "$HOME" directory (with it having the only permissions over it). I linked the snac2 certificate settings to the Tailscale ones (vitally important). I set up NginX[13] to be an interface between the snac2 server and the funnel, so I could get all the HTTP headers etc working. I based my configs on the example for a snac2 systemd daemon[14] and NginX configuration[15]. These configurations turned out to be a mistake. Here is a redacted version of my Systemd configuration:

[Unit]
Description=A simple, minimalistic ActivityPub instance
Documentation=https://codeberg.org/grunfink/snac2/src/branch/master/doc
After=network.target
Wants=network-online.target

[Service]
DynamicUser=yes
User=snacuser
Group=snacuser
StateDirectory=/home/snacuser/snacdata
ExecStart=/usr/local/bin/snac httpd $STATE_DIRECTORY
Restart=on-failure

[Install]
WantedBy=multi-user.target

# This is a systemd global service example. Edit and run:
# 
# cp snac-global.service /etc/systemd/system/snac.service
# sudo snac init /var/lib/snac
# sudo snac adduser /var/lib/snac USER
# systemctl enable snac
# systemctl start snac

DO NOT USE THIS VERSION. It didn't work at all. People who know Systemd will be screaming at me at this point, but I was bodging a script from limited knowledge, so I carried on, changing:

ExecStart=/usr/local/bin/snac httpd $STATE_DIRECTORY

to

ExecStart=/usr/local/bin/snac httpd /home/snacuser/snacdata 2>&1 >>/var/log/snaclog.txt

This *SEEMED* to work, but had issues: I couldn't seem to control it from the web interface. The commandline version worked fine though, so I did some tests.

[13]: NginX

[14]: Example Systemd Daemon

[15]: Example NginX configuration

More bugs

I managed to do lots of stuff from the commandline, including posting, following, liking/favouriting and boosting, but couldn't set anything up from the web end. I ended up giving up for a while, revisiting over the coming months and getting more and more frustrated. I tried many NginX configurations but none helped. Eventually, I found documentation to get Tailscale Funnel working directly[16], though it didn't work at first because the format of the commands had since changed. The main documentation[17] set me back on track though.

One thing I noticed was that the logs were empty, so I bumped the debug level up to 3 and tried again. No logs in `/var/log`, but systemd had some, so I muddled on. After a while, I noticed the systemd warning that `StateDirectory` was an absolute path, so changed it to `~snacuser/snacdata` which seemed to fix it (it didn't though). Eventually, I tried changing `ExecStart` to:

ExecStart=/usr/local/bin/snac httpd /home/snacuser/snacdata 2>&1 |tee /var/log/snaclog.txt

This didn't work, because Systemd interpretted the `|tee` as an argument for snac! Frustrated, I wrapped the thing in brackets for a subshell, only to discover that Systemd doesn't do shell-expansion stuff. "Aha!", I thought. "This must be why I can't get logs in `/var/log`!", so I made a bash script to do the piping to `tee` separately and set `ExecStart` to that.

This gave a different error, but an intriguing one: `tee` couldn't access `/var/log/`, saying "access denied" for reasons of a "read-only filesystem". People who know systemd will be screaming at me again, but I interpreted this as needing a directory in `/var/log/` chowned to snacuser. I still got the same error though with logs in this directory, so I put it in `$HOME` instead. Same error. Huh. Some frantic websearching led me (after multiple false turns and worries about my filesystem) to a stackoverflow post[18]. I checked all the mentioned directives and found nothing, but the comment "check man systemd.exec for what your version supports"[19] later in the page, twigged that I should look up these directives. As usual, I should RTFM...

Near the mentioned directives, I spotted something about the `User` and `Group` directives being affected by the `DynamicUser` directive, so I skimread that and it twigged that, despite setting up the snacuser user, the systemd configuration was creating a dynamic user on the fly with the same name. I commented out the `DynamicUser` directive and, after some odd messages about changing from a Dynamic user to a static one, I finally figured out what was going on:

As the Systemd users knew from the start, the `DynamicUser` directive creates a temporary user with access only to the directory mentioned in the `StateDirectory` directive and passed to the Exec with the `$STATE_DIRECTORY` variable. Everything else is read-only at that point. Including the snac2 data directory! That's why I couldn't access it from the web, as well as why the logs didn't work! I rewrote the systemd configuration to something along the lines of the following and it worked!

[Unit]
Description=A simple, minimalistic ActivityPub instance
Documentation=https://codeberg.org/grunfink/snac2/src/branch/master/doc
After=network.target
Wants=network-online.target

[Service]
User=snacuser
Group=snacuser
ExecStart=/usr/local/bin/snac httpd /home/snacuser/snacdata
Restart=on-failure

[Install]
WantedBy=multi-user.target

# This is a systemd global service example. Edit and run:
# 
# cp snac-global.service /etc/systemd/system/snac.service
# sudo snac init /var/lib/snac
# sudo snac adduser /var/lib/snac USER
# systemctl enable snac
# systemctl start snac

[16]: Tailscale funnel with NginX forum query

[17]: Tailscale funnel commandline documentation

[18]: StackOverflow post entitled "Is there a way to control filesystem access with systemd?"

[19]: The comment

Finishing up...

I set up a custom stylesheet, based on the "lowkey cyber dark" theme[20] one and made funnel host that too. My tunnel config is now basically:

sudo tailscale funnel --bg=true --set-path /.well-known/webfinger localhost:8001/
sudo tailscale funnel --bg=true --set-path /.well-known/host-meta localhost:8001/
sudo tailscale funnel --bg=true --set-path /.well-known/nodeinfo localhost:8001/
sudo tailscale funnel --bg=true --set-path /api/v1/ localhost:8001/
sudo tailscale funnel --bg=true --set-path /api/v2/ localhost:8001/
sudo tailscale funnel --bg=true --set-path /oauth/ localhost:8001/
sudo tailscale funnel --bg=true --set-path / localhost:8001/
sudo tailscale funnel --bg=true --set-path /lowtest.css /home/snacuser/css/lowkey-test.css

I haven't tested this with Apps, but it hopefully will work.

One thing I have found is that I'm getting errors regarding webfinger:

 output message: sent to inbox https://retro.social/inbox 401 [{"error":"Webfinger error when resolving dheadshot@sna...]

with lots of requeue errors later, so I haven't completely ironed out the gremlins. I suspect that this is something to do with being behind Tailscale funnel though, so might not be fixable.

The server isn't currently permanently running, so not always accessible. This could cause errors with ActivityPub and federating, but things worked fine yesterday and seem to today (and seem faster today, even!).

I'll keep the server's address not too public at the moment as I dodn't know how it'll handle load, but it's up as I type this!

[20]: "lowkey cyber dark" theme CSS

Conclusion

Not much left for this post except to thank grunfink for building snac2 in the first place, so:

 ********** **                         **                              
/////**/// /**                        /**      **   **                 
    /**    /**       ******   ******* /**  ** //** **   ******  **   **
    /**    /******  //////** //**///**/** **   //***   **////**/**  /**
    /**    /**///**  *******  /**  /**/****     /**   /**   /**/**  /**
    /**    /**  /** **////**  /**  /**/**/**    **    /**   /**/**  /**
    /**    /**  /**//******** ***  /**/**//**  **     //****** //******
    //     //   //  //////// ///   // //  //  //       //////   ////// 




                                   **** **          **    
  *****                           /**/ //          /**    
 **///** ****** **   ** *******  ****** ** ******* /**  **
/**  /**//**//*/**  /**//**///**///**/ /**//**///**/** ** 
//****** /** / /**  /** /**  /**  /**  /** /**  /**/****  
 /////** /**   /**  /** /**  /**  /**  /** /**  /**/**/** 
  ***** /***   //****** ***  /**  /**  /** ***  /**/**//**
 /////  ///     ////// ///   //   //   // ///   // //  //