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:v1.20.0
Note that these are not official images, you use them at your own risk.
:latest now refers to our rust knot from the v1.20.0 images onward!
The golang knot is now deprecated full-stop, thus its last image is v1.16.1-alpha,
and we're only keeping it around to make it easier to migrate off.
If migrating from the golang knot to the rust one, please read Migrating to the rust knot and follow the upgrade steps/info.
Building The Image #
By default the Dockerfile will build the rust knot from the latest tagged release,
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'll also create a knot user with user and group ID 1000:1000,
owning /data, 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're bind-mounting the data dir on the host, such as in the compose files here in codebase.
Migrating to the rust knot #
:latest follows the rust knot from the v1.20.0 images on,
and docker-compose.golang.yml pins the golang knot's final v1.16.1-alpha
just in case you need that for something when upgrading.
The Dockerfile will build from master, and takes TAG override arg if you need it.
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: the rust knot's data directory,/var/lib/knotin the guide./knot2/lfs: the LFS/blob store, which the migration now creates beside your repos; v1.20.0 onwards requires this set!
Dress rehearsal #
Generate the master key as the guide describes, then rehearse with the golang 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:latest \
--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 -f docker-compose.golang.yml down
at the point at which it says systemctl disable --now.
down will remove the container,
so machine-restart won't take port 5555 back from the rust knot.
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 the old compose's 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 docker-compose.golang.yml,
2222:2222 under docker-compose.yml, and the rust knot's own SSH listener behind that.
Ie. nobody has to change a git remote.
Then start the knot, with KNOT_MASTER_KEY and KNOT_CONTACT_EMAIL
in your environment or in a .env file next to the compose file.
The email is mandatory as of v1.20.0; the knot serves it on describeKnot.
docker compose 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 the knot from the environment and an env var wins out.
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.
The knot 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 down
docker compose -f docker-compose.golang.yml 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 the rust knot between the switchover and the rollback shall be gone.
Starting fresh on the rust knot #
The knot will check at startup that its scan path + blob store exist and are writable, and won't create the following for you:
mkdir -p knot2/repos knot2/lfs
sudo chown -R 1000:1000 knot2
export KNOT_SERVER_HOSTNAME=example.com
export KNOT_SERVER_OWNER=did:plc:yourdidgoeshere
export KNOT_CONTACT_EMAIL=you@example.com
export KNOT_MASTER_KEY=$(openssl rand -base64 32)
docker compose --profile caddy up -d
knot2/lfs is the blob store, new and required as of v1.20.0;
keep it under ./knot2, on the /data mount with everything else.
The caddy profile sets you up with a reverse proxy too;
drop the flag when something of your own already fronts port 5555.
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.
- Docker 91%
- Shell 9%