Hello, Fediworld!

Jan 13, 2026

tl;dr

I spun up a GoToSocial instance hosted on my swarm at gts.spacefish.social!

If you're interested in the technical nitty-gritty of this and not How I Came To The Fediverse, follow this link. (I'm gonna try not to think to hard how that makes me feel like every recipe blog on the planet.)

Hello, Fediworld!

I enjoy reading. I've recently started a project to log the books I read in my own journal, along with a few thoughts about them. Part of this is so I have more detailed notes if the book ever comes up in conversation much later and my notoriously bad memory fails me when I'm discussing with a friend: I can consult my notes (after the conversation) to jog my memory. It's also a useful place to store my To Be Read list. I've also hoped this would help sharpen some of my rhetorical thinking. Historically when I've consumed media I've kinda just ripped through it without necessarily engaging in critical thinking or any sort of literary analysis.

On the one hand, that's... perfectly normal when reading for enjoyment. On another, I also read nearly all of David Weber's bibliography before I decided I had better things to do. (This is very much one of those instances of a negative steam review with like 2k hours on it; No, I wouldn't recommend anyone read Weber. Yes, I have read something like thirty of his books. I digress.)

In parallel I've had an off-and-on idea of tracking interesting articles I read and posting monthly summaries. That's gone... poorly, historically, though one of my partners recommended I try using citation software like Zotero, so we'll see if that improves. But, since the book log is going well, I was like "Oh, I should tidy some of those posts up and post them!"

And then I remembered there's a whole website for that.

The problem with Goodreads

Goodreads, if you don't know, is effectively Facebook, but for books. You can make and share lists, access a gigantic rating corpus, follow particular people whose reviews you may like (or, y'know, authors), form lil reading clubs, post your own reviews or lists or whatever. It would be the perfect place to post my own little commentaries on the things I read, more to fuel conversations with my other ravenous reader friends than because I think I have any sort of novel insight on these things (vs the internet).

So what's the catch?

Amazon bought Goodreads in 2013.

...So?

I don't want to contribute to Amazon's consumption of everything in the world really at all. I order stuff from them but I draw the line at feeding my own interpretations and commentary on media back into their ravenous machine.

Also DRM is bad and dumb, stop buying things on Kindle, but that's a separate rant.

Every social media company is bad, what're you gonna do, not use social media?

Well, funny you should ask...

Enter: the Fediverse

"Fediverse" sounds suspiciously like something J Edgar Hoover dreamed up and it couldn't be further from the truth. The Fediverse is a return to punk rock 90s style hackers building cool things on the internet and "Information just wants to be free, mannnnn" style thinking.

Your Facebook account can't really interoperate with any other services like Twitter or Youtube or whatnot. Your Facebook account is really only good for doing things within Facebook. (Yeah, alright, and Instagram/Threads, but those are all owned by Facebook.) Moreover, if you don't want to support Facebook but you still want to interact with others who still use it, you're stuck. You can't export the data, you can't take it to another service and have it work. You also can't use different interfaces for Facebook or Instagram. You used to be able to with Twitter, before the great un-API-ening of the internet.

Fediverse is like email. You can have accounts wherever you want and they'll talk to each other. They'll talk to other peoples' accounts on different hosts. You can hook whatever client you want up to your email service.

Email works this way because it's a "federated protocol." There's no single company or organization that controls email, there's just a set of established procedures for how you send and receive email. So long as you follow the procedures you can run your own mail server. the "Fediverse" aims to adopt similar principles for "social" style applications. YouTube becomes PeerTube, Twitter becomes Mastodon, Goodreads becomes Bookwyrm. Fediverse applications are designed to be run by both individuals and organizations, tailored to the size of the community they support. The deliberate intent is you could run an instance of whatever application you wanted for your library or book club or makerspace or friend group and have it interact with any other instance in the Fediverse, even from different applications! You can have romance_novel_fan@books.library.town share posts from some_guy@lgbt.tech, or welder_bro@makerspace.city commenting on videos from I_LOVE_DIVING@caves_r_us.water.tv.

There's very powerful outcomes from this. Like with email, censorship becomes significantly harder for a single actor to achieve. Email isn't perfect what with shadow spam filters and such, but it's far, far better than something like Twitter or Facebook. Even more than that, communities can enforce their own standards of what they allow on their instance. You don't have to deal with people who think pineapple doesn't belong on pizza commenting on your post about the best Hawaiian pizza in town if it's a rule of your community that everyone salutes pineapple as the one true pizza topping. Plus, you get some level of interop between different "kinds" of Fediverse services, which is neat.

The "gold standard" (and primary usecase, by traffic) of ActivityPub is Mastodon, which you may have heard of. It's typically presented like Twitter:

a screenshot of Julia Evans's single-user Mastodon instance (it looks like a twitter timeline)
Julia Evans's Mastodon instance

As you might guess, the main function is microblogging. (That's the fancy modern term for "tweeting.")

What does this have to do with Goodreads?

Right, so someone made a Goodreads 'clone' called Bookwyrm that runs off ActivityPub, the protocol behind the Fediverse. It's got all the same stuff Goodreads has, but isn't hosted by a megacorp that wants to slurp your soul out through your wallet.

My original intent was to spin it up on my swarm and then convince all my friends to join, but Bookwyrm is a bit of a niche use of the ActivityPub protocol and isn't necessarily the best for testing interop. Also, they don't yet ship a Docker image that's easy to deploy (though they're working on it).

Enter: GoToSocial

GoToSocial is effectively a lighter weight version of Mastodon. It can't support quite so many users, reportedly, but for my purposes I'd be surprised if I got more than 10, so I think that's fine. Plus, I'm very resource constrained insofar as servers go, so a smaller application footprint is appealing.

Anyway, GoToSocial gives you an Activity-Pub compatible server. It doesn't ship it's own frontend beyond the public view and a configuration page for logged in users.

a screenshot of my profile at gts.spacefish.social/@thefish (it also looks like twitter)
My profile on gts.spacefish.social

GoToSocial expects you to use your own client, either something like phanpy.social for the web or one of the several Mastodon-compatible mobile apps. Beyond that though, it's a fully featured Mastodon!

a screenshot of the Tusky android app open to pinned posts of thefish@gts.spacefish.social
the Tusky app on Android

Having a GoToSocial server running means I can onboard my users to a fully featured Fediverse instance that's largely scoped to our little cohort. As an added bonus, most of those in my cohort are disillusioned with the noisyness of everyday social media and would prefer a quieter environment, which turns the usual mild disappointment of an empty server into a perfect cozy space for friends!

Wait, why would the server be 'empty'?

Julia Evans talks about it in their post on running a single-person Mastodon server, but the short version is that no Fediverse server has innate knowledge of any other. In order to get traffic from another server there has to become interaction between the two: either you replying to or following someone on another server .ActivityPub is a "push" protocol, requiring a particular server to know you exist and that you want updates from it.

A new server, being new, won't get any updates until something in the world knows about it.

Technical Notes

Stack/Compose/NGINX configs
1version: '3.2'
2volumes:
3 # as with my other Swarm services, map volumes onto the shared ceph pool
4 gts_storage:
5 driver: local
6 driver_opts:
7 type: none
8 device: /mnt/swarm-pool/volumes/gts_storage
9 o: bind
10 gts_postgres:
11 driver: local
12 driver_opts:
13 type: none
14 device: /mnt/swarm-pool/volumes/gts_postgres
15 o: bind
16 gts_cache:
17 driver: local
18 driver_opts:
19 type: none
20 device: /mnt/swarm-pool/volumes/gts_cache
21 o: bind
22 tailscale_conf:
23 driver: local
24 driver_opts:
25 type: none
26 device: /mnt/swarm-pool/shared/tailscale/gts_app_conf
27 o: bind
28 tailscale_state:
29 driver: local
30 driver_opts:
31 type: none
32 device: /mnt/swarm-pool/shared/tailscale/gts_app_state
33 o: bind
34services:
35 tailscale:
36 image: tailscale/tailscale:latest
37 hostname: gts-app # Name used within your Tailscale environment
38 environment:
39 - TS_AUTHKEY=${TS_AUTHKEY}
40 - TS_STATE_DIR=/var/lib/tailscale
41 - TS_USERSPACE=false
42 - TS_ENABLE_HEALTH_CHECK=true
43 - TS_LOCAL_ADDR_PORT=127.0.0.1:41234
44 - TS_EXTRA_ARGS=--advertise-tags=tag:container,tag:internet-facing
45 - TS_SERVE_CONFIG=/config/serve.json
46 - TS_EXTRA_ARGS=--accept-dns=true --stateful-filtering=false
47 volumes:
48 - tailscale_conf:/config
49 - tailscale_state:/var/lib/tailscale
50 devices:
51 - /dev/net/tun:/dev/net/tun
52 cap_add:
53 - net_admin
54 - sys_module
55 healthcheck:
56 test: ["CMD", "wget", "--spider", "-q", "http://127.0.0.1:41234/healthz"]
57 interval: 1m # How often to perform the check
58 timeout: 10s # Time to wait for the check to succeed
59 retries: 3 # Number of retries before marking as unhealthy
60 restart: always
61 gotosocial:
62 image: superseriousbusiness/gotosocial:0.20.2
63 container_name: gotosocial
64 restart: unless-stopped
65 environment:
66 # this is the DNS of the internet-facing server
67 # which is how this application appears to the world
68 GTS_HOST: gts.spacefish.social
69 GTS_DB_TYPE: postgres
70 # important: port numbers here can break DNS lookup
71 GTS_DB_ADDRESS: gts-postgres
72 GTS_DB_DATABASE: gotosocial
73 GTS_DB_USER: gotosocial
74 GTS_DB_PASSWORD: ${GTS_DB_PASSWORD}
75 GTS_ACCOUNTS_REGISTRATION_OPEN: "false"
76 GTS_ACCOUNTS_APPROVAL_REQUIRED: "true"
77 GTS_MEDIA_REMOTE_CACHE_DAYS: 7
78 GTS_STORAGE_LOCAL_BASE_PATH: /gotosocial/storage
79 GTS_WAZERO_COMPILATION_CACHE: /gotosocial/.cache
80 # this matches the generated swarm network; YMMV
81 GTS_TRUSTED_PROXIES: "10.0.3.0/24"
82 volumes:
83 - gts_storage:/gotosocial/storage
84 - gts_cache:/gotosocial/.cache
85 depends_on:
86 - postgres
87 - tailscale
88 gts-postgres:
89 image: postgres:15-alpine
90 restart: unless-stopped
91 environment:
92 POSTGRES_DB: gotosocial
93 POSTGRES_USER: gotosocial
94 POSTGRES_PASSWORD: ${GTS_DB_PASSWORD}
95 volumes:
96 - gts_postgres:/var/lib/postgresql/data
1server {
2 server_name gts.spacefish.social;
3
4 location / {
5 # note this matches the hostname of the tailscale sidecar
6 proxy_pass http://gts-app.my_tailnet.ts.net;
7 # setting the Host header broke gts for unknown reasons
8 # proxy_set_header Host $host;
9 proxy_set_header X-Real-Up $remote_addr;
10 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
11 proxy_set_header X-Forwarded-Proto $scheme;
12 client_max_body_size 50m;
13 }
14
15 listen 443 ssl; # managed by Certbot
16 ssl_certificate /etc/letsencrypt/live/gts.spacefish.social/fullchain.pem; # managed by Certbot
17 ssl_certificate_key /etc/letsencrypt/live/gts.spacefish.social/privkey.pem; # managed by Certbot
18 include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
19 ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
20
21}
22server {
23 if ($host = gts.spacefish.social) {
24 return 301 https://$host$request_uri;
25 } # managed by Certbot
26
27 listen 80;
28 server_name gts.spacefish.social;
29}
1{
2 "TCP": {
3 "443": {
4 "HTTPS": true
5 },
6 "80": {
7 // figure it's a bit silly to use HTTPS
8 // over a wireguard link fo ra revproxy
9 "HTTP": true
10 }
11 },
12 "Web": {
13 "${TS_CERT_DOMAIN}:443": {
14 "Handlers": {
15 "/": {
16 // note: gotosocial is the name of the service of the
17 // gts application in the swarm stack
18 "Proxy": "http://gotosocial:8080"
19 }
20 }
21 },
22 "${TS_CERT_DOMAIN}:80": {
23 "Handlers": {
24 "/": {
25 "Proxy": "http://gotosocial:8080"
26 }
27 }
28 }
29 }
30}

Honestly the setup here was pretty straightforward. GoToSocial fields an example compose file that I used as a starting point for my own. Then I added a Postgres database because... I like Postgres. That threw me through a mild loop since somehow including the port in the DB address environment variable broke the DNS lookup within the swarm for the DB container. Then I added a Tailscale sidecar to sidestep handling any ingress mayhem.

To expose this to the internet I pointed gts.spacefish.social to my existing Digital Ocean droplet that I use as a cloud ingress. That server runs nginx as a reverse proxy and directs traffic to services running on the Tailnet, though in this case I have Tailscale serve running on the sidecar to serve as an additional reverse proxy, though this is largely to get around how the swarm handles networking.

De-anonymization

I'm not thrilled about using my existing node since it backs other services I use, which means you can reverse-DNS lookup that same IP and get here, or to other applications I host on other domains. Part of my original hope for running a Fediverse service was to keep it pseudonymous. The consequences of being identifiable on the internet can be high, especially for some groups. This is also why I run most of my private services on my own tailnet and I don't expose them to the internet at all: it reduces attack surface and exposure.

That said, practically speaking if I'm offering gts.spacefish.social and other services to my IRL friends who aren't going to have the same opsec as I am, it won't be that difficult to identify me in the wild for a determined attacker, so in the end I decided this was a threat vector I wasn't going to sink a ton of effort into mitigating.

Normally when I use a sidecar I set the app container (in this case, GoToSocial itself) to network_mode: service:tailscale, which will make the app container use the network of the Tailscale container instead of it's own, which then means from Tailscale's perspective you can route traffic to localhost:whatever, or, from a remote node, address.of.the.sidecar.your_tailnet.ts.net:port. In the swarm's case it's either more difficult to use another service as a network, or just impossible. I think this has something to do with the potential for containers within a stack to be running on different physical swarm nodes. In any case, it's simple enough to set up tailscale serve to point to gotosocial:8080.

So, at the end of the day, a request from the internet will go:

  1. to gts.spacefish.social, a digital ocean droplet running nginx
  2. to the tailscale sidecar, running in the swarm
  3. to the GoToSocial container

Next steps

Various blogs I read through this project recommended services to populate other feeds into my server. I'd like to implement a couple, though I'm wary of bothering some of my friends who were exited at the prospect of a quiet social media.

I also want to actually set up Bookwyrm. I'm hoping the canonical image PR is merged relatively soon, though it looks like there's already test candidates that I could use to get off the ground.

I already have one friend "tooting" (can't believe that's the verb they went with) about Fallen Order and Murderbot, which I consider great success. If I really get my wits about me I hope to stand up a Pixelfed instance as well, though I'm intimidated by the potential storage requirements, and I need to figure out a reasonable backup scheme for these services before I go too much further.

Feel free to say hi!

Other Reading

Julia Evan's post on Mastodon and running a single person instance

Performance metrics between Mastodon and GoToSocial, with links to services to populate an instance with replies to threads and posts pulled from RSS feeds

Post about GoToSocial and phanpy as a UX

https://hnr.spacefish.net/posts/feed.xml