Host your Tangled repos on your own knot
Your git repos on your own VM, with tangled.org on top.
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/stdinTangled 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.shif you signed up there with an email, or your Bluesky handle; it becomes the knot's ownerKNOT_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/stdinWhen 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.serviceWhat 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 letsknot-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'smy-knot.exe.xyz(not the spindle's), and thatcurl https://my-knot.exe.xyz/xrpc/_healthworks from your computer (if it asks you to log in, the VM isn't public yet: runssh exe.dev share set-public my-knot). - Creating a repository is refused. Only the knot's owner and
members may: the first DID in
adminsin/var/lib/knot/config.tomlmust be the account you're signed in to tangled.org with (curl https://my-knot.exe.xyz/.well-known/did.jsonshows 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
crawlersin the config, thensudo 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-gitshows which key you offer.- The push hangs or
sshreaches the VM's shell. Your URL isn't rewritten: checkgit remote -vandgit config --global --get-regexp '^url\.'. - The knot won't start.
journalctl -u knot.service -n 50names 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