惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MyScale Blog
MyScale Blog
U
Unit 42
M
MIT News - Artificial intelligence
小众软件
小众软件
P
Proofpoint News Feed
雷峰网
雷峰网
L
LangChain Blog
S
SegmentFault 最新的问题
腾讯CDC
F
Fortinet All Blogs
A
About on SuperTechFans
WordPress大学
WordPress大学
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
D
Docker
N
Netflix TechBlog - Medium
Apple Machine Learning Research
Apple Machine Learning Research
Recent Announcements
Recent Announcements
D
DataBreaches.Net
Stack Overflow Blog
Stack Overflow Blog

jola.dev

Migrating your Bluesky account the hard way | jola.dev Cluster singleton pattern | jola.dev cove.town, atproto self-hosted self-hosting | jola.dev Speeding up a Phoenix LiveView web app with a CDN | jola.dev Self-hosting an atproto container registry | jola.dev Self-hosting and Tangled | jola.dev Self-hosting your PDS | jola.dev Taking control of your atproto account | jola.dev No cost, no value | jola.dev Latch - an Elixir atproto OAuth library | jola.dev Limited output is a feature | jola.dev Distributed rate limiter with HRW in Elixir | jola.dev A computer can never be held accountable | jola.dev Elixir Cluster 101 | jola.dev How to stop Claude from saying load-bearing | jola.dev Let libraries be libraries | jola.dev CI workflows on Tangled for Elixir | jola.dev Automatically syncing your blog to atproto and standard.site | jola.dev Appreciation for the small web | jola.dev Treating LLMs as programming books Publishing your blog to standard.site in Elixir Generating OG images in Elixir The social contract of writing Highest Random Weight in Elixir bunnyx: a bunny.net Elixir client library Building for the joy of building Running local models on an M4 with 24GB memory How to hit your Claude weekly limit so you can go outside and touch grass Dropping Cloudflare for bunny.net Building a blog with Elixir and Phoenix
Migrating to the new Tangled knot2 | jola.dev
https://jola.dev/about · 2026-08-23 · via jola.dev

Tangled are in the process of migrating to the new knot implementation, knot2. As a quick reminder, the knot is a self-hostable server capable of serving git repos that uses atproto to share information with the ATmosphere, and the Tangled AppView. This means you can self-host your own repo, like gitea or forgejo, while still being able to interact with other people in a single place.

I recently wrote about setting up knot and spindle. I had gotten everything working a week earlier, but it was while I was writing the blog post that I found out about knot2. Oh, well, migrating can’t be too bad, right? And there’s actually a great guide already written up, as well as tooling to help you. This write up provides a more Docker focused version of the migration guide, with my personal experience.

Prep

Okay, I hit the first challenge right off the bat. The Containerfile provided by the Tangled repo pins an amd64 arch runner image that I can’t use, so I stripped that out to clone and build the image locally. The docker image contains the migration script, so that’s why I’m building it locally first.

git clone --depth 1 https://tangled.org/tangled.org/core /tmp/tangled-core

sed -i 's|gcr.io/distroless/cc-debian13:latest@sha256:[a-f0-9]*|gcr.io/distroless/cc-debian13:latest|' /tmp/tangled-core/knot2/Containerfile

docker build -f /tmp/tangled-core/knot2/Containerfile -t knot2:local /tmp/tangled-core

docker image inspect knot2:local --format '{{.Os}}/{{.Architecture}}'

And we have to create the KNOT_MASTER_KEY. Run this and store the value in a password manager.

openssl rand -base64 32

Okay, and the final little bit of setup before we do the dry-run.

printf 'KNOT_SERVER_HOSTNAME=knot.example.com\nKNOT_REPO_SCAN_PATH=/old-repos\n' > knot-migrate.env && chmod 600 knot-migrate.env

You need to have the KNOT_MASTER_KEY in the shell, so export it, and then run.

sudo -E docker run --rm --user 0:0 \

-v "$PWD/server:/old-db" \

-v "$PWD/repositories:/old-repos" \

-v "$PWD/keys:/old-keys:ro" \

-v "$PWD/knot-migrate.env:/old.env:ro" \

-v "$PWD/data:/target" \

-e KNOT_MASTER_KEY \

--entrypoint /usr/local/bin/knot-migrate knot2:local \

--source-db /old-db/knotserver.db \

--env-file /old.env \

--host-key /old-keys/ssh_host_ed25519_key \

--plc-url https://plc.directory \

--target /target \

--dry-run

This is just a dry run. It’ll print out some information. Make sure it says everything looks good! You can run this any number of times. Here’s what my output looked like.

rehearsal: 0.0s

knot owner: did:plc:3pnunm7komgxzfwrrxdtvsvl

members to grant: 2

repos to adopt: 1

collaborator grants: 2

casbin cross-check: the acl and the tables agree

skipped repos: 0

transfer mode: copy

the filesystem checks used /var/lib/knot, since the scan path doesn't exist yet

scan path: writable

room to copy: 93.8GiB free is enough for the 3.7MiB that adoption will copy

host key algorithm: ssh-ed25519

master key: KNOT_MASTER_KEY decodes to a usable key

we left the target alone. Re-run without --dry-run to migrate.

This is the last step before we go into the “scary” part, so make sure you’re happy with the output!

The migration

Okay, game day. It’s time to do this for real. Start by stopping your knot and do a backup.

docker compose stop knot

cp -a server/knotserver.db* /var/tmp/

And here’s the non-dry-run version of the migration command.

sudo -E docker run --rm --user 0:0 \

-v "$PWD/server:/old-db" \

-v "$PWD/repositories:/old-repos" \

-v "$PWD/keys:/old-keys:ro" \

-v "$PWD/knot-migrate.env:/old.env:ro" \

-v "$PWD/data:/target" \

-e KNOT_MASTER_KEY \

--entrypoint /usr/local/bin/knot-migrate knot2:local \

--source-db /old-db/knotserver.db \

--env-file /old.env \

--host-key /old-keys/ssh_host_ed25519_key \

--plc-url https://plc.directory \

--target /target

Here’s my output so you know what to look for.

adoption: 0.0s

cobs: 0.0s

knot owner: did:plc:3pnunm7komgxzfwrrxdtvsvl

members to grant: 2

repos to adopt: 1

collaborator grants: 2

casbin cross-check: the acl and the tables agree

skipped repos: 0

adopted by copy: 1 new, 0 already present, 1 sha1, 0 sha256

member grants: 2 appended, 0 already present

registrations: 1 appended, 0 already present

collaborator grants: 2 appended, 0 already present

knot key identity: did:web:knot.cove.town

host key algorithm: ssh-ed25519

host key fingerprint: SHA256:SKiHHmsnZV+E6fSl3MpvAH2mHQS6UH48XXW7NCeIMi8

config: /target/config.toml

key archive: /target/repo-signing-keys.json

Okay! Unlike knot1, knot2 wants a nice config file with lots of explicit and clean options.

[server]

hostname = "knot.example.com"

admins = ["did:plc:YOUR_DID"]

listen_addr = "[::]:5555"

ssh_listen_addr = "[::]:2222"

ssh_host_key_file = "/data/ssh_host_key"

appview_endpoint = "https://tangled.org"

[repo]

scan_path = "/data/repos"

[secrets]

sealed_key_file = "/data/sealed-keys"

master_key_env = "KNOT_MASTER_KEY"

[atproto]

plc_directory = "https://plc.directory"

[xrpc]

trusted_proxy_header = "x-forwarded-for" # only if the knot sits behind a proxy

[git]

object_format = "sha1" # knot2 defaults to sha256, which can't be pushed to GitHub mirrors

With most of the configuration done, the compose entry is pretty minimal. If you followed my previous guide, you can replace the previous knot service in place.

services:

knot:

image: knot2:local

pull_policy: never

restart: always

ports:

- "2222:2222" # update this to match your actual port, eg 22:2222

environment:

- KNOT_MASTER_KEY=${KNOT_MASTER_KEY}

volumes:

- ./config.toml:/etc/knot/config.toml:ro

- ./data:/data

Now we’re ready to go:

docker compose up -d

here are some commands you can run to verify it works

curl -s https://knot.example.com/xrpc/_health # {"version":"knot 2.0.0"}

curl -s https://knot.example.com/xrpc/sh.tangled.owner # your DID

git ls-remote ssh://git@knot.example.com:2222/did:plc:YOUR_REPO_DID

git ls-remote https://knot.example.com/did:plc:YOUR_REPO_DID

If something did not go as expected, you should be able to bring the old knot back by restoring the compose file and running docker compose up -d. We haven’t actually deleted anything, it’s all still there.

Caveat, I briefly had it start on me before I had properly filled in the config.toml file. If you see logs on startup about generating a new SSH host key, the paths in your config file don’t match your actual migrated file paths. Delete secrets/ and ssh/ folders in the mounted directory, fix the config file, and then start the compose again.

Clean up

Assuming your migration has gone well, you want to delete the migrated signing keys. They were stored in plain text for the migration, so let’s clear those away.

sudo rm data/repo-signing-keys.json

Once you’re comfortable that the migration went well, and you’ve got backups of the new volume, you can delete any other migration leftovers: server/ , repositories/, /var/tmp, and /tmp/tangled-core.

Closing notes

Big thank you to the Tangled team and @oyster.cafe for providing great documentation and a clean migration path. The new knot looks really slick. One of the big new things is that it doesn’t actually delegate to git under the hood, it comes with a full Rust git implementation instead. So, hopefully fewer CVEs 🤞