Knot Docker #
IMPORTANT This is a community maintained repository, support is not guaranteed.
Docker container and compose setup to run a Tangled knot and host your own repository data.
Pre-built Images #
There is a repository of pre-built images
for tags starting at v1.13.0-alpha if you prefer.
docker pull atcr.io/tangled.org/knot:latest
Note that these are not official images, you use them at your own risk.
:latest still refers to the Go knot, and knot 2 is :2, built from Dockerfile.knot2.
Swapping the tag alone won't migrate anything,
so read Migrating To Knot 2 before you touch the image line.
Building The Image #
By default the Dockerfile will build the latest tag, but you can change it
with the TAG build argument.
docker build -t knot:latest --build-arg TAG=master .
The command above for example will build the latest commit on the master
branch.
By default it will also create a git user with user and group ID 1000:1000,
but you can change it with the UID and GID build arguments.
docker build -t knot:latest --build-arg UID=$(id -u) GID=$(id -g)
The command above for example will create a user with the host user's UID and GID.
This is useful if you are bind mounting the repositories and app folder on the host,
as in the provided docker-compose.yml file.
When using compose, these can be specified as build arguments which will be passed to the builder.
build:
context: .
args:
TAG: master
UID: 1000
GID: 1000
This will for example tell docker to build it using the master branch like
the command.
Migrating To Knot 2 #
Knot 2 is a second implementation of the knot,
published as atcr.io/tangled.org/knot:2.
Your knot can stay where it is until you edit the image line yourself.
There's no main tangled-proper release tag for knot 2 yet,
thus Dockerfile.knot2 will build from master, and uses the same TAG build argument.
Please read the migration guide first.
What follows is only the difference due to this specific compose file, where your bind mounts stand-in for the guide's host paths:
./server/knotserver.db: the source database,/home/git/knotserver.dbfrom the guide./repositories: repo scan path./keys/ssh_host_ed25519_key: the host key that the container'ssshdgenerated./knot2: knot 2's data directory,/var/lib/knotin the guide
Dress rehearsal #
Generate the master key as the guide describes, then rehearse with the Go knot still up:
mkdir knot2
export KNOT_MASTER_KEY=<the base64 from the guide>
export KNOT_SERVER_HOSTNAME=<the hostname from your .env>
docker run --rm --user 0:0 -e KNOT_MASTER_KEY \
-v "$PWD/server:/app:ro" -v "$PWD/repositories:/home/git/repositories:ro" \
-v "$PWD/keys:/etc/ssh/keys:ro" -v "$PWD/knot2:/data" \
--entrypoint /usr/bin/knot-migrate atcr.io/tangled.org/knot:2 \
--source-db /app/knotserver.db --source-repos /home/git/repositories \
--host-key /etc/ssh/keys/ssh_host_ed25519_key --hostname "$KNOT_SERVER_HOSTNAME" \
--plc-url https://plc.directory --target /data --dry-run
--user 0:0 overrides the image's own unprivileged knot user
because the container's sshd generated your host keys as root at mode 0600.
If you ran Secure Mode, each repo tree has a per-owner uid
and root is the only user that can read them all.
Doing the thing #
Follow the guide's cutover, with docker compose down
at the point at which it says systemctl disable --now.
down will remove the container,
so no machine restart can take port 5555 back from knot 2.
stop would leave it in place for restart: always to bring back later.
Then please run the same command without --dry-run,
and leave every source mount :ro.
knot-migrate opens knotserver.db read-only
and will read through the WAL that docker compose down left behind,
so the repo somebody registered seconds before the cutover comes across
even where its row never made it into the db file.
The migration leaves knotserver.db byte-identical.
Then give the tree to the image's uid:
sudo chown -R 1000:1000 knot2
repo-signing-keys.json will end up in ./knot2 once the migration finishes,
beside your repos and inside whatever your backups already sweep up.
Move it off the server.
Skip the guide's step 4, the sshd port dance:
for us, sshd lives inside the old container and can now go! Bye-bye!
Your users are already on port 2222: 2222:22 under the old compose file,
2222:2222 under docker-compose.knot2.yml, and knot 2's own SSH listener behind that.
Nobody has to change a git remote.
Then start knot 2, keeping KNOT_MASTER_KEY in your environment or in a .env file next to the compose file:
docker compose -f docker-compose.knot2.yml up -d
Add --profile caddy if you were running the bundled proxy,
and leave it off if your own proxy handles port 443.
Check it worked as the guide describes, with -p 2222 on the keyscan:
ssh-keyscan -t ed25519 -p 2222 $KNOT_SERVER_HOSTNAME
The config.toml that the migration wrote is only a record of what it chose,
because compose configures knot 2 from the environment and an environment variable wins.
Add KNOT_OBJECT_FORMAT=sha256 to your .env when you passed --object-format sha256,
where the compose file and the migration both default to sha1.
Behind a reverse proxy #
The bundled caddy profile is a proxy in front of the knot, the same as one of your own.
Knot 2 will rate-limit by the address that a request arrives from,
and every one of your users arrives from the proxy until you set the header it appends and the address it connects from:
KNOT_XRPC_TRUSTED_PROXY_HEADER: x-forwarded-for
KNOT_XRPC_TRUSTED_PROXIES: 172.20.0.0/16
The knot will warn at every start until you set both. With both missing, the warning is that every client shares one ratelimit, because the knot uses the proxy's address to bin. With the header set and the list empty, the warning is that anyone skipping the proxy can pick their own rate limit.
Compose will pick the subnet itself,
so read yours out of docker network inspect $(basename $PWD)_default before you trust the number above.
Stop publishing 5555:5555 at the same time.
Anything that reaches port 5555 directly from an address in the list can write the header itself
and hand the knot whatever address it likes.
The knot will ignore the header from a peer outside KNOT_XRPC_TRUSTED_PROXIES,
rate-limit the peer by the address it connected from,
and warn once with the address in it.
A wrong subnet would show up in that warning,
most often with the proxy connecting over one address family and your list having only the other.
Going back in case of emergency #
docker compose -f docker-compose.knot2.yml down
docker compose up -d
Both lines use the same --profile caddy as the commands above.
Under the default-copy-mode your old data is exactly where it was,
though anything pushed to knot 2 between the switchover and the rollback shall be gone.
Starting fresh on knot 2 #
Knot 2 will check at startup that its scan path exists and is writable, and won't create the following for you:
mkdir -p knot2/repos
sudo chown -R 1000:1000 knot2
export KNOT_MASTER_KEY=$(openssl rand -base64 32)
docker compose -f docker-compose.knot2.yml --profile caddy up -d
Set KNOT_SERVER_HOSTNAME and KNOT_SERVER_OWNER as in the section below.
The knot will generate its own ed25519 host key at knot2/ssh_host_key on that first start,
and ssh-keyscan -t ed25519 -p 2222 $KNOT_SERVER_HOSTNAME reads back the fingerprint your users will see.
Setting Up The Image #
The simplest way to set up your own knot is to use the provided compose file and run the following:
mkdir keys repositories server
export KNOT_SERVER_HOSTNAME=example.com
export KNOT_SERVER_OWNER=did:plc:yourdidgoeshere
export KNOT_SERVER_PORT=443
docker compose --profile caddy up -d
This will setup everything for you including a reverse proxy.
The proxy lives in the caddy profile,
so drop the flag when something of your own already fronts port 5555.