1--- 2title: "Embracing ATProto, part 2: Tangled Knots and social coding" 3description: | 4 You thought Github was a social coding platform? Think again, and get ready to tangle! 5 Built on atproto, tangled allows you to use your Bluesky/atproto identity on a (not quite yet) fully feldged git platform! 6date: 2025-09-17 7authors: 8 - name: finxol 9tags: 10 - atproto 11 - self-hosting 12published: true 13bskyCid: 3lyzhrumfu22n 14--- 15 16I recently set up my own atproto PDS, for use with Bluesky and all other atproto apps. 17If you already feel lost, check out last week's post where I quickly explain what atproto is, 18roughly how it works, and the steps I followed to get my PDS running. 19 20::Bookmark{url="/posts/embracing-atproto-pt-1-hosting-pds"} 21Embracing ATProto, part 1: Setting up a PDS 22:: 23 24The PDS setup and migration was an overall very smooth process. 25Bluesky and the AT Protocol are built by a very competent team of well funded engineers who've been working on it for a few years already. 26 27## What's tangled? 28 29[Tangled](https://tangled.sh), on the other hand, was started only about 8 months ago, at the start of 2025 by [two](https://tangled.sh/@oppi.li) [brothers](https://tangled.sh/@icyphox.sh). 30It's a "social-enabled git collaboration platform" built for decentralisation, ownership, and social coding. 31 32The platform has gained a lot of traction since, and the community is very much involved in the development, but for now tangled is still in alpha. 33That doesn't mean it's not usable yet, simply that some things may break. 34 35### What's a Knot? 36 37Just like plain atproto, tangled has some lingo of its own. 38 39In tangled, a knot is essentially an atproto-enabled git server. 40It's sort of like a PDS in the sense that it's where your data—here your code—lives. 41 42That's the main tangled-specific decentralised part, and what makes tangled special. 43You can keep ownership of your code, without cutting it off from a popular git platform—which 44is a side effect of running a private Gitea or Gitlab server. 45 46## Setting up a Knot 47 48Setting up my own knot took a little bit more work than for the PDS. 49 50The [official docs](https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md) give instructions for installation on a NixOS system, 51but I'm not running NixOS on my server. 52Luckily, they also provide a community-maintained [Docker install process](https://tangled.sh/@tangled.sh/knot-docker). 53 54At the top of the README that serves as a documentation page, they talk about a pre-built image to use. 55Perfect! 56That's exactly what I'm looking for. 57Just need to spin up the container and we're done! 58 59Not quite, unfortunately... 60 61I tried that route, added my knot on the tangled UI, but couldn't get it verified. 62I spent way too long trying to debug the parts I control, mainly the Caddy reverse proxy rules. 63It turns out the pre-built image was just out of date, and rebuilding it myself fixed it immediately... 64 65Ultimately, setting up a knot isn't all that hard. 66I just ran into a slightly stupid version mismatch problem a closer inspection could've revealed earlier. 67 68## Spindles & CI 69 70Another piece of lingo from the tangled world is "Spindle". 71 72A Spindle is a very simple CI runner for tangled. 73It spins up one Docker container per run, and gives access to any Nixpkg. 74The syntax is very similar to Github Actions workflow files, with some slight differences. 75 76Since it's brand new, there isn't access to the thousands of pre-made reusable Github Actions, 77but access to the vast nixpkg catalog lets us do basically anything—with a couple extra steps from time to time. 78 79### Self-hosting 80 81As with everything else here, Spindles are self-hostable. 82There is a little gotcha for the moment though. 83 84Since the Spindle runs a Docker container for each workflow run, it needs access to the Docker socket. 85They haven't got Docker-in-Docker working quite yet, so it means the Spindle needs to run natively outside a Docker container. 86 87Although I don't like the idea, it's not really a problem for me. 88I prefer to have everything containerised on my servers to keep things tidy, but it's fine as a temporary solution until they get DinD working. 89 90What's stopping me right now is rather that workflow runs would spin up a Docker container alongside all my other projects I'd rather not break. 91I'm aware it shouln't really be a problem, but it just bothers me. 92It is very much a me problem, so I'll figure out a way around it eventually. 93 94## Migrating this blog 95 96Migrating the repo over is the simplest things ever. 97 98Just create a new repo on tangled—making sure to select your knot, set the remote on your local repo, and push to it! 99If you specified the knot correctly when creating your repo, the repo should now live directly on your Knot. 100 101<img src="/posts/embracing-atproto-pt2/new-repo.png" alt="Select your new knot when creating a repo" width="80%" style="margin: auto;" /> 102 103You can now use git just as you normally do! 104 105### CI for auto-deploy 106 107Migrating CI takes a tiny bit more work to migrate. 108I had a Github Action workflow to automatically deploy this blog to Deno Deploy on push. 109 110<details class="minor-callout"> 111 112<summary>Here's the full file if you're curious.</summary> 113 114```yaml 115name: deno-deploy 116on: 117 push: 118 branches: 119 - main 120jobs: 121 deploy: 122 runs-on: ubuntu-latest 123 permissions: 124 id-token: write # Needed for auth with Deno Deploy 125 contents: read # Needed to clone the repository 126 steps: 127 - uses: actions/checkout@v3 128 129 - uses: pnpm/action-setup@v4 130 name: Install pnpm 131 with: 132 run_install: false 133 134 - uses: actions/setup-node@v3 135 with: 136 node-version: 22 137 cache: pnpm 138 139 - run: pnpm install 140 141 - run: pnpm generate 142 143 - name: Deploy to Deno Deploy 144 uses: denoland/deployctl@v1 145 with: 146 project: finxol-blog 147 entrypoint: https://deno.land/std@0.140.0/http/file_server.ts 148 root: .output/public 149``` 150 151</details> 152 153Let's start off very easy by adapting the triggers. 154Just [follow the docs](https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/pipeline.md), and set the same trigger conditions. 155 156```yaml 157when: 158 - event: ["push"] 159 branch: ["main"] 160``` 161 162I'll have a look at branch deploys later. 163It needs a bit more manual work since the official GH Action doesn't handle it for us. 164 165Since spindles work slightly differently to Github Actions runners, we need to give it a list of dependencies to install. 166It's similar to the setup steps in the GH workflow to install node and pnpm. 167 168```yaml 169dependencies: 170 nixpkgs: 171 - deno 172 - nodejs 173 - pnpm 174 - python3 175 - gnused 176``` 177 178This bit took a bit of trial and error, as you might notice from the `python3` and `gnused` dependencies. 179 180I'd initially set the dependencies to what I set up in the GH workflow, `nodejs` and `pnpm`, plus `deno` to be able to use the `deployctl` cli tool. 181But running that gave a few errors. 182This blog uses Nuxt Content to generate HTML from my Markdown files, and Nuxt Content itself uses `better-sqlite3`, which itself needs python and sed in its post-install script. 183Adding the corresponding nixpkgs in the dependencies array fixes this easily. 184 185Now we can get to the actual steps of the workflow. 186 187Since we don't have access to the existing Github Actions, there's a couple sections that needed adapting or manual work. 188The install and generate steps are exactly the same, but the deploy step changes. 189 190To replace the official Deno Deploy GH Action, we can directly use their `deployctl` cli tool, and give it the appropriate parametres. 191 192I also used this as an excuse to switch to the `jsr:@std/http/file-server` entrypoint instead of the deno.land url style. 193 194```yaml 195steps: 196 - name: Install dependencies 197 command: | 198 pnpm install 199 200 - name: Generate static site 201 command: | 202 pnpm generate 203 204 - name: Install deployctl 205 command: | 206 deno install -gArf jsr:@deno/deployctl 207 208 - name: Deploy to Deno Deploy 209 command: | 210 cd .output/public 211 ~/.deno/bin/deployctl deploy --project finxol-blog --entrypoint jsr:@std/http/file-server --include=. --prod 212``` 213 214Lastly, don't forget to give the workflow permission to deploy by giving it a `DENO_DEPLOY_TOKEN` in the secrets! 215Since Deno Deploy integrates only with Github, the permission won't be given automatically here. 216 217 218 219<details class="minor-callout"> 220 221<summary class="text-stone-500">Here is the full spindle workflow file.</summary> 222 223```yaml 224when: 225 - event: ["push", "pull_request"] 226 branch: ["main"] 227 228dependencies: 229 nixpkgs: 230 - deno 231 - nodejs 232 - pnpm 233 - python3 234 - gnused 235 236engine: "nixery" 237 238steps: 239 - name: Install dependencies 240 command: | 241 pnpm install 242 243 - name: Generate static site 244 command: | 245 pnpm generate 246 247 - name: Install deployctl 248 command: | 249 deno install -gArf jsr:@deno/deployctl 250 251 - name: Deploy to Deno Deploy 252 command: | 253 cd .output/public 254 ~/.deno/bin/deployctl deploy --project finxol-blog --entrypoint jsr:@std/http/file-server --include=. --prod 255``` 256 257</details> 258 259It took me a little bit more time to get things working right. 260I found a little bug in the tangled UI regarding spindle runs. 261 262When pushing to the *official* knot, the workflow got picked up fine by the official spindle, and showed up in the UI. 263When I pushed to *my* knot however, the official spindle ran the workflow, but it didn't show in the UI. 264It took me a while to realise what was going on. 265 266I thought the spindle wasn't picking up the workflow, but the bug was simply with showing the info in the UI. 267[Anirudh](https://tangled.sh/@icyphox.sh) was very quick to find the cause and implement a fix. 268 269And just like that, this blog gets deployed automatically on push, using the tangled spindle!