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!