Skip to content

Host your Tangled repos on your own knot

Your git repos on your own VM, with tangled.org on top.

Deploy on exe.dev

Shelley handles the setup. About 30 minutes, most of it compiling.

Prefer a terminal?
curl -fsSL https://exe.dev/deploy/tangled/prompt.txt | ssh exe.dev new --prompt=/dev/stdin

Tangled is a place to host and work on code, like GitHub, built on the AT Protocol, the network behind Bluesky. You sign in with an atproto account; stars, issues and pull requests are records in it. The git repositories live on knots, small servers that anyone can run, and tangled.org shows them all in one place.

This page runs your own knot on an exe.dev VM. exe.dev's HTTPS proxy gives it a stable address with TLS, https://my-knot.exe.xyz/, which becomes its identity on the network. You push over SSH through exe.dev; people and tangled.org read it over HTTPS. The VM is always on and keeps your repositories on its disk.

You need a Tangled account. Sign up with an email to get a handle like alice.tngl.sh, or sign in with your Bluesky handle: either is an atproto account. Use that handle below; the knot's owner must be the account you use on tangled.org.

Deploy with Shelley#

Click Deploy and Shelley will set up Tangled knot on a new VM, then check that it works. No config-file wrangling required.

Have these ready; Shelley asks for them:

  • TANGLED_HANDLE: the handle you sign in to tangled.org with: alice.tngl.sh if you signed up there with an email, or your Bluesky handle; it becomes the knot's owner
  • KNOT_CONTACT_EMAIL: an email address the knot publishes for abuse reports and questions
Prefer a terminal?
curl -fsSL \
  https://exe.dev/deploy/tangled/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin

When it's done, run this on your computer (only you can):

ssh exe.dev share set-public my-knot
ssh my-knot.exe.xyz sudo systemctl restart knot.service
What Shelley is told
TANGLED_HANDLE=<fill-this-in>
KNOT_CONTACT_EMAIL=<fill-this-in>

Set up Tangled knot on this VM by following the exe.dev recipe at https://exe.dev/deploy/tangled.md from top to bottom.

The values at the top of this message are mine:
- TANGLED_HANDLE: the handle you sign in to tangled.org with: `alice.tngl.sh` if you signed up there with an email, or your Bluesky handle; it becomes the knot's owner
- KNOT_CONTACT_EMAIL: an email address the knot publishes for abuse reports and questions
Ask me for any that still say <fill-this-in> before you need them.

The recipe's example VM name is my-knot. This VM's name is the output of `hostname`: use it wherever the recipe writes my-knot. Skip the step that creates the VM; you are on it.

You are done when all of these are true. Check each one yourself:
- Tangled's knot 1.20 at `https://my-knot.exe.xyz/`, owned by your atproto DID, restarted on failure and after a reboot.
- `/xrpc/_health` and `/.well-known/did.json` answering on the knot's address, ready to add on tangled.org.
- `knot-member add <handle>` on the VM, to let other people create repositories on the knot.
- The knot's SSH server on port 2222, for pushes through exe.dev's SSH.
- Repositories, keys and settings in `/var/lib/knot` and `/etc/knot`, backed up nightly to `/var/backups/knot`, with a restore checked.

Read the whole recipe, closed details included, and follow "Set up by
hand" in order, skipping VM creation; the walkthrough above it is for
the owner. Resolve TANGLED_HANDLE to a DID as written and stop if it
doesn't resolve. The build takes 15-25 minutes: run it in the
background. On an existing VM, ask before replacing anything on port
8000. Don't make the VM public yourself: the owner runs `ssh exe.dev
share set-public` on their computer. Show the owner the master key's
line from /etc/knot/knot.env once, and tell them to store it off the
VM. Finish with the `set-public` command, then "Add the knot on
tangled.org" and "Push over SSH" for the owner to follow.

Finish with the URLs I should open and these commands, which only I can run, from my own computer (with this VM's name in them):
  ssh exe.dev share set-public my-knot
  ssh my-knot.exe.xyz sudo systemctl restart knot.service

Also at /deploy/tangled/prompt.txt. The recipe it follows is /deploy/tangled.md.

Make the knot public#

On your computer:

ssh exe.dev share set-public my-knot
ssh my-knot.exe.xyz sudo systemctl restart knot.service

tangled.org has to reach the knot without an exe.dev login, and so does anyone cloning. This opens port 8000, the knot, to the internet. SSH to the VM stays behind exe.dev. The restart makes the knot announce itself to the atproto relays again, now that they can reach it. Check it from anywhere:

curl https://my-knot.exe.xyz/xrpc/_health

Expect {"version":"knot 1.20.0"}.

Add the knot on tangled.org#

On tangled.org, open Settings → Knots (tangled.org/settings/knots), press Add a knot, enter my-knot.exe.xyz as the Knot address and press Add. That puts it in the list you pick from when you create a repository. The knot itself decides who may create repositories on it: you, as its owner, and the members you add.

Then open Settings → Keys (tangled.org/settings/keys) and add the SSH public key you use for exe.dev (cat ~/.ssh/id_ed25519.pub). The knot lets you push with the keys you list there.

Create a repository#

On tangled.org, press New → New repository, name it, and pick my-knot.exe.xyz under Select a knot. The repository's Code menu shows https://tangled.org/alice.tngl.sh/hello, which sends git on to your knot, and git@my-knot.exe.xyz:alice.tngl.sh/hello for SSH.

Anyone can clone over HTTPS, with no login:

git clone https://tangled.org/alice.tngl.sh/hello

Push over SSH#

The SSH URL that tangled.org shows uses port 22, which on my-knot.exe.xyz is exe.dev's SSH. The knot's SSH listens on the VM's port 2222, and exe.dev's SSH can carry you there. On your computer, add an alias:

cat >> ~/.ssh/config <<'EOF'

Host my-knot-git
  HostName localhost
  Port 2222
  User git
  ProxyJump my-knot.exe.xyz
  HostKeyAlias my-knot-git
EOF

Then let git rewrite the knot's SSH URLs to it, so you can copy them from tangled.org as they are:

git config --global url."my-knot-git:".insteadOf "git@my-knot.exe.xyz:"

Clone with the SSH URL and push something:

git clone git@my-knot.exe.xyz:alice.tngl.sh/hello
cd hello
echo "# hello" > README.md
git add README.md && git commit -m "First commit"
git push origin main

The commit shows on the repository's page on tangled.org. In a clone made over HTTPS, push over SSH with git remote set-url --push origin git@my-knot.exe.xyz:alice.tngl.sh/hello. To move an existing repository, create an empty one on the knot, then git remote add tangled git@my-knot.exe.xyz:alice.tngl.sh/hello and git push tangled --all.

Note

The alias needs SSH access to the VM, so it's for you (and your team). Collaborators without it push over HTTPS, below.

Push over HTTPS#

Knot 2 accepts pushes over HTTPS too, authenticated with a short-lived token from your atproto account. The community helper git-credential-tangled fetches one, using an app password for your atproto account (in the Bluesky app: Settings → Privacy and security → App passwords). With Go installed:

go install tangled.org/pds.dad/git-credential-tangled@latest
git config --global credential.https://my-knot.exe.xyz.helper ""
git config --global --add credential.https://my-knot.exe.xyz.helper "tangled --handle alice.tngl.sh"

Push to the knot's own address, not tangled.org's:

git remote set-url --push origin https://my-knot.exe.xyz/alice.tngl.sh/hello
git push

It asks for the app password on the first push.

Let other people in#

On a new knot, only you can create repositories. To let someone else create them, make them a member, on the VM:

sudo knot-member add friend.tngl.sh

(sudo knot-member remove friend.tngl.sh undoes it.) They add my-knot.exe.xyz under Settings → Knots too. To let someone push to one repository, add them under Collaborators in the repository's Settings → Access. Who can open issues and pull requests is each repository's contribution policy; new ones start with anyone.

Add CI#

Tangled's CI runners are called spindles, and workflows live in .tangled/workflows/ in the repository. The quickest start is Tangled's own: pick spindle.tangled.network under Select a spindle when you create the repository, or later under Spindle in its Settings → Pipelines. To run CI on your own machine, set up a spindle on a second VM, in Run your own spindle below.

The nuts and bolts#

Happy with what Shelley set up? Stop here. Below are the steps it followed, and maintenance for later.

Set up by hand

Create the VM#

On your computer (pick another name if my-knot is taken; the name ends up in every clone URL):

ssh exe.dev new --name=my-knot
ssh my-knot.exe.xyz

Everything else runs on the VM.

Build the knot#

Tangled publishes the knot as source and as a container image that needs a registry login, so build it. Install Rust and the build tools:

sudo apt-get update
sudo apt-get install -y build-essential cmake perl pkg-config git
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain none
. ~/.cargo/env

Build the v1.20.0 release. The repository pins its Rust version, which cargo downloads first; the build takes 15-25 minutes on a 2-CPU VM. CARGO_NET_GIT_FETCH_WITH_CLI lets git fetch one dependency that cargo's built-in fetcher stalls on:

git clone --depth 1 --branch v1.20.0 https://tangled.org/tangled.org/core ~/tangled-core
cd ~/tangled-core
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --locked -p knot-server
sudo install -m 755 target/release/knot-server /usr/local/bin/knot-server
knot-server --help | head -1

Configure it#

The knot runs as a git system user and keeps everything in /var/lib/knot:

sudo useradd --system --home-dir /var/lib/knot --shell /usr/sbin/nologin git
sudo install -d -o git -g git -m 700 /var/lib/knot /var/lib/knot/repos /var/lib/knot/lfs

The owner is your DID. Look it up from your handle, and set the contact address the knot publishes:

HANDLE=alice.tngl.sh        # TANGLED_HANDLE
EMAIL=you@example.com           # KNOT_CONTACT_EMAIL
OWNER_DID=$(curl -fsS "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$HANDLE" | python3 -c 'import json,sys; print(json.load(sys.stdin)["did"])')
echo "$OWNER_DID"

Expect did:plc:… (or did:web:…). The master key seals the knot's signing keys. Generate it into an env file only root can read:

sudo install -d -m 755 /etc/knot
sudo tee /etc/knot/knot.env >/dev/null <<EOF
KNOT_MASTER_KEY=$(openssl rand -base64 32)
KNOT_ADMIN_SECRET=$(openssl rand -hex 24)
EOF
sudo chmod 600 /etc/knot/knot.env

Note

Copy that key off the VM now (sudo cat /etc/knot/knot.env) and keep it with your passwords. Without it, a backup of the knot can't prove who owns its repositories. The admin secret lets knot-member, below, add members.

Write the config. The hostname is the knot's identity (did:web:my-knot.exe.xyz), so keep it. The knot serves plain HTTP on port 8000, where exe.dev's proxy, which handles TLS, sends HTTPS traffic. The proxy reaches it from 127.0.0.1, so the knot trusts X-Forwarded-For from there to rate-limit each client by its own address. crawlers adds Tangled's indexer, which 1.20.0 leaves out of its default list, to the services the knot announces itself to; without it, new repositories don't show up on tangled.org:

sudo -u git tee /var/lib/knot/config.toml >/dev/null <<EOF
[server]
hostname = "$(hostname).exe.xyz"
admins = ["$OWNER_DID"]
listen_addr = "[::]:8000"
ssh_listen_addr = "[::]:2222"
ssh_host_key_file = "/var/lib/knot/ssh/host_key"
appview_endpoint = "https://tangled.org"

[repo]
scan_path = "/var/lib/knot/repos"

[git]
object_format = "sha1"

[secrets]
sealed_key_file = "/var/lib/knot/secrets/sealed.bin"
master_key_env = "KNOT_MASTER_KEY"

[atproto]
plc_directory = "https://plc.directory"
crawlers = ["https://index.tangled.network", "https://relay.fire.hose.cam", "https://relay3.fr.hose.cam", "https://bsky.network", "https://northamerica.firehose.network", "https://europe.firehose.network", "https://asia.firehose.network", "https://atproto.africa", "https://relay.upcloud.world"]

[acl]
legacy_admin_secret_env = "KNOT_ADMIN_SECRET"

[xrpc]
trusted_proxy_header = "x-forwarded-for"
trusted_proxies = ["127.0.0.1", "::1"]

[tls]
http3 = false

[lfs]
store_path = "/var/lib/knot/lfs"

[contact]
email = "$EMAIL"
EOF

object_format = "sha1" keeps new repositories compatible with GitHub and existing clones; see the FAQ. Every other setting is in the knot configuration reference. Check it:

sudo -u git env $(sudo cat /etc/knot/knot.env) knot-server --config /var/lib/knot/config.toml validate

Run it#

Use Tangled's systemd unit, which locks the knot into /var/lib/knot:

curl -fsSL https://tangled.org/tangled.org/core/raw/v1.20.0/systemd/knot.service | sudo tee /etc/systemd/system/knot.service >/dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now knot.service

On its first start the knot creates its SSH host key and sealed key store.

Check it#

systemctl is-active knot.service
curl -fsS localhost:8000/xrpc/_health; echo
curl -fsS localhost:8000/.well-known/did.json; echo
ssh-keyscan -p 2222 localhost 2>/dev/null | head -1

Expect active, the knot's version, a DID document for did:web:my-knot.exe.xyz, and an ssh-ed25519 host key. The VM can't reach its own https://my-knot.exe.xyz/; once the owner has made it public, check that from any other computer.

Add members#

Members are the accounts that may create repositories on the knot. Tangled's knot-server admin member add sends its request to the knot's public HTTPS address, which the VM can't reach, so this small script asks the knot directly on port 8000, with the admin secret:

sudo tee /usr/local/bin/knot-member >/dev/null <<'EOF'
#!/bin/bash
# knot-member add|remove HANDLE: change who can create repositories on this knot.
set -euo pipefail
case "${1:-}" in
  add) method=createRecord; record=',"record":{"$type":"org.tangled.knot.member"}' ;;
  remove) method=deleteRecord; record="" ;;
  *) echo "usage: knot-member add|remove HANDLE" >&2; exit 2 ;;
esac
did=$2
if [[ $did != did:* ]]; then
  did=$(curl -fsS "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$did" |
    python3 -c 'import json,sys; print(json.load(sys.stdin)["did"])')
fi
. /etc/knot/knot.env
knot=did:web:$(sed -n 's/^hostname = "\(.*\)"/\1/p' /var/lib/knot/config.toml)
curl -fsS -o /dev/null -u "admin:$KNOT_ADMIN_SECRET" -H 'content-type: application/json' \
  -d "{\"repo\":\"$knot\",\"collection\":\"org.tangled.knot.member\",\"rkey\":\"$did\"$record}" \
  "http://localhost:8000/xrpc/com.atproto.repo.$method"
echo "$1: $did"
EOF
sudo chmod 755 /usr/local/bin/knot-member

Check it with any handle, then take it out again:

sudo knot-member add tangled.org
curl -fsS "localhost:8000/xrpc/com.atproto.repo.listRecords?repo=did:web:$(hostname).exe.xyz&collection=org.tangled.knot.member"; echo
sudo knot-member remove tangled.org

Expect add: did:plc:…, a record named after that DID, and remove: did:plc:….

Back up nightly#

A backup is the whole of /var/lib/knot (repositories, LFS objects, sealed keys, SSH host key, config) and /etc/knot. This script stops the knot for the few seconds a copy takes, so no push lands half-way, and keeps the 14 newest:

sudo tee /usr/local/bin/knot-backup >/dev/null <<'EOF'
#!/bin/bash
# Back up the knot to /var/backups/knot/knot-<time>.tar.gz. Keeps the 14 newest.
set -euo pipefail
umask 077
mkdir -p /var/backups/knot
out=/var/backups/knot/knot-$(date -u +%Y%m%d-%H%M%S).tar.gz
systemctl stop knot.service
trap 'systemctl start knot.service' EXIT
tar -czf "$out.part" -C / var/lib/knot etc/knot
systemctl start knot.service
trap - EXIT
mv "$out.part" "$out"
ls -1t /var/backups/knot/knot-*.tar.gz | tail -n +15 | xargs -r rm --
echo "$out"
EOF
sudo chmod 755 /usr/local/bin/knot-backup

Run it nightly, around 03:30 UTC:

sudo tee /etc/systemd/system/knot-backup.service >/dev/null <<'EOF'
[Unit]
Description=Back up the Tangled knot

[Service]
Type=oneshot
ExecStart=/usr/local/bin/knot-backup
EOF
sudo tee /etc/systemd/system/knot-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Back up the Tangled knot nightly

[Timer]
OnCalendar=*-*-* 03:30:00 UTC
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now knot-backup.timer

Make one now and check that it unpacks:

backup=$(sudo /usr/local/bin/knot-backup)
sudo tar -tzf "$backup" | grep -E 'config.toml|host_key|sealed|knot.env'
systemctl is-active knot.service

Expect the four files and active. The backup holds the master key, so keep copies as private as the VM. Copy /var/backups/knot off the VM now and then; otherwise the VM's disk holds the only copy.

Back up and restore

To restore on a fresh VM with the same name (or on this one), build the knot and create the git user as in "Set up by hand", copy a backup to the VM, then:

sudo systemctl stop knot.service 2>/dev/null || true
sudo rm -rf /var/lib/knot /etc/knot
sudo tar -xzf knot-YYYYMMDD-HHMMSS.tar.gz -C /
curl -fsSL https://tangled.org/tangled.org/core/raw/v1.20.0/systemd/knot.service | sudo tee /etc/systemd/system/knot.service >/dev/null
sudo systemctl daemon-reload
sudo systemctl enable --now knot.service
curl -fsS localhost:8000/xrpc/_health; echo

The knot keeps its hostname, keys and repositories; tangled.org doesn't need to know. A knot on a different hostname is a different knot: its repositories would have to be recreated there.

Update

Check the release tags and the upgrade notes, then build the new tag (here v1.21.0) and swap the binary:

. ~/.cargo/env
cd ~/tangled-core
git fetch --depth 1 origin tag v1.21.0 && git checkout v1.21.0
CARGO_NET_GIT_FETCH_WITH_CLI=true cargo build --release --locked -p knot-server
sudo /usr/local/bin/knot-backup
sudo install -m 755 target/release/knot-server /usr/local/bin/knot-server
sudo systemctl restart knot.service
curl -fsS localhost:8000/xrpc/_health; echo

The knot finishes running pushes (up to 40 seconds) before it stops.

Run your own spindle

A spindle runs your repositories' CI. It needs its own public HTTPS address, so it gets its own VM, which also keeps CI jobs away from your repositories. It runs each step in a Docker container (Tangled's nixery engine), so workflows on it say engine: nixery.

On your computer:

ssh exe.dev new --name=my-spindle
ssh my-spindle.exe.xyz

On the VM, start Docker and install Nix, which is how Tangled builds the spindle (there's no container image). The build takes about 20 minutes. The spindle needs a newer git than Ubuntu's, so the same Nix profile holds git:

sudo systemctl enable --now docker.service
sh <(curl --proto '=https' --tlsv1.2 -sSfL https://nixos.org/nix/install) --daemon --yes
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
sudo env "PATH=$PATH" nix --extra-experimental-features 'nix-command flakes' \
  profile add --profile /nix/var/nix/profiles/spindle \
  'git+https://tangled.org/tangled.org/core?ref=refs/tags/v1.20.0#spindle' nixpkgs#git
sudo ln -sf /nix/var/nix/profiles/spindle/bin/spindle /usr/local/bin/spindle

Make its signing key (back it up: sudo cat /var/lib/spindle/key.pem) and its settings. It serves on port 8000, lets tangled.org's pages read logs from it, and runs one job at a time on this 2-CPU VM:

HANDLE=alice.tngl.sh        # TANGLED_HANDLE
EMAIL=you@example.com           # KNOT_CONTACT_EMAIL
OWNER_DID=$(curl -fsS "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=$HANDLE" | python3 -c 'import json,sys; print(json.load(sys.stdin)["did"])')
sudo install -d -m 700 /var/lib/spindle
sudo openssl ecparam -name prime256v1 -genkey -noout -out /var/lib/spindle/key.pem
sudo chmod 600 /var/lib/spindle/key.pem
sudo install -d -m 755 /etc/spindle
sudo tee /etc/spindle/spindle.env >/dev/null <<EOF
SPINDLE_SERVER_HOSTNAME=$(hostname).exe.xyz
SPINDLE_SERVER_OWNER=$OWNER_DID
SPINDLE_CONTACT_EMAIL=$EMAIL
SPINDLE_SERVER_KEY_PATH=/var/lib/spindle/key.pem
SPINDLE_SERVER_LISTEN_ADDR=0.0.0.0:8000
SPINDLE_SERVER_DB_PATH=/var/lib/spindle/spindle.db
SPINDLE_SERVER_REPO_DIR=/var/lib/spindle/repos
SPINDLE_SERVER_HYDRANT_DIR=/var/lib/spindle/hydrant
SPINDLE_ARTIFACT_STORES_DISK_DIR=/var/lib/spindle/artifacts
SPINDLE_SERVER_CORS_ORIGINS=https://tangled.org
SPINDLE_SERVER_MAX_JOB_COUNT=1
SPINDLE_NIXERY_PIPELINES_MAX_JOB_MEMORY_MB=4096
SPINDLE_NIXERY_PIPELINES_MAX_CONCURRENT_WORKFLOWS=2
EOF
sudo chmod 600 /etc/spindle/spindle.env

Run it as a service. It runs as root, because it drives Docker:

sudo tee /etc/systemd/system/spindle.service >/dev/null <<'EOF'
[Unit]
Description=Tangled spindle
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service

[Service]
EnvironmentFile=/etc/spindle/spindle.env
Environment=PATH=/nix/var/nix/profiles/spindle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
ExecStart=/nix/var/nix/profiles/spindle/bin/spindle run
WorkingDirectory=/var/lib/spindle
StateDirectory=spindle
StateDirectoryMode=0700
LogsDirectory=spindle
Restart=always
RestartSec=5
TimeoutStopSec=600

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now spindle.service
sleep 5
curl -fsS localhost:8000/_health; echo

Expect {"status":"ok"}. On your computer, make it public and check:

ssh exe.dev share set-public my-spindle
curl https://my-spindle.exe.xyz/_health

On tangled.org, open Settings → Spindles (tangled.org/settings/spindles), press Add a spindle, enter my-spindle.exe.xyz and press Add. Pick it under Select a spindle when you create a repository, or under Spindle in an existing one's Settings → Pipelines. Add a workflow, .tangled/workflows/test.yml:

when:
  - event: ["push", "manual"]
    branch: main

engine: nixery

dependencies:
  nixpkgs:
    - go

steps:
  - name: Test
    command: go test ./...

Push it; the run shows under the repository's Pipelines tab. At first the spindle runs CI only for you; to let someone else use it:

sudo spindle allowlist add --db /var/lib/spindle/spindle.db friend.tngl.sh

It doesn't need a restart. The workflow guide has the rest.

To update, swap the tag (here v1.21.0) and restart:

. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
sudo env "PATH=$PATH" nix --extra-experimental-features 'nix-command flakes' profile remove --profile /nix/var/nix/profiles/spindle spindle
sudo env "PATH=$PATH" nix --extra-experimental-features 'nix-command flakes' profile add --profile /nix/var/nix/profiles/spindle 'git+https://tangled.org/tangled.org/core?ref=refs/tags/v1.21.0#spindle'
sudo systemctl restart spindle.service
Something not working?
  • Creating a repository fails with Request failed with status code 404. The address under Select a knot isn't a knot: check it's my-knot.exe.xyz (not the spindle's), and that curl https://my-knot.exe.xyz/xrpc/_health works from your computer (if it asks you to log in, the VM isn't public yet: run ssh exe.dev share set-public my-knot).
  • Creating a repository is refused. Only the knot's owner and members may: the first DID in admins in /var/lib/knot/config.toml must be the account you're signed in to tangled.org with (curl https://my-knot.exe.xyz/.well-known/did.json shows the knot's side). An account made on tangled.org (alice.tngl.sh) is a different DID from your Bluesky handle.
  • A new repository doesn't show on tangled.org. Tangled's indexer hasn't crawled the knot: check crawlers in the config, then sudo systemctl restart knot.service, which announces the knot again.
  • Permission denied (publickey) on push. The knot checks your key against the SSH keys in Settings → Keys on tangled.org and caches them for about an hour. Add the key, wait, and try again. ssh -v my-knot-git shows which key you offer.
  • The push hangs or ssh reaches the VM's shell. Your URL isn't rewritten: check git remote -v and git config --global --get-regexp '^url\.'.
  • The knot won't start. journalctl -u knot.service -n 50 names the setting it doesn't like; knot-server --config /var/lib/knot/config.toml validate (with the env file, as above) checks the config.

Frequently asked questions#

What is Tangled?

A code collaboration site, like GitHub, built on the AT Protocol (atproto), the network behind Bluesky. You sign in with an account made on tangled.org, or your Bluesky or other atproto account; stars, issues, pull requests and comments are records in your own atproto repository. The git data lives on knots, small servers anyone can run. tangled.org (the appview) shows them all in one place.

Do I need my own knot to use Tangled?

No. Tangled runs knots anyone can create repositories on. Run your own to keep your code on a server you control, to set your own limits, or to host a team's or community's repositories.

Why does the VM have to be public?

tangled.org, other knots, spindles and people cloning your repositories all talk to the knot over HTTPS, without an exe.dev login. Making the VM public opens port 8000, the knot, to the internet; SSH stays behind exe.dev. The knot decides who may push and create repositories, by atproto identity.

Why not push to git@my-knot.exe.xyz like tangled.org says?

Port 22 on my-knot.exe.xyz is exe.dev's SSH, which logs you in to the VM, not the knot. The my-knot-git alias jumps through exe.dev's SSH to the knot's own SSH server on port 2222, and git's insteadOf rewrites tangled.org's URLs to it, so you can copy them unchanged. People without SSH access to the VM push over HTTPS instead.

Can I use my own domain?

Yes, but decide before the first start: its hostname is its identity (did:web:<hostname>). Point a CNAME at my-knot.exe.xyz, run ssh exe.dev domain add my-knot git.example.com (see custom domains), then set hostname in /var/lib/knot/config.toml before the first start.

Why SHA-1 repositories?

Knot 2 creates SHA-256 repositories by default, which can't push to or fetch from SHA-1 ones, and GitHub and most tools are SHA-1. This page sets object_format = "sha1" so you can push existing repositories and mirror to GitHub. Change it before creating repositories if you want SHA-256; it only affects new ones.

What about CI?

Tangled's CI runners are called spindles. Pick Tangled's spindle when you create a repository, or run your own spindle on a second VM (below): one VM serves one public port, and a spindle is a separate service with its own hostname.

What does it cost?

Tangled and the knot are free (MIT). The knot uses little memory when idle; you pay for your VM plan, and disk for your repositories.

Updated Nov 4, 2026 · tested end to end Nov 4, 2026 · Markdown · manifest.json · Something wrong? Tell us