# Knot Docker
> **IMPORTANT**
> This is a community maintained repository, support is not guaranteed.
Docker container and compose setup to run a [Tangled](https://tangled.org) knot
and host your own repository data.
## Pre-built Images
There is a [repository](https://atcr.io/r/tangled.org/knot) 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](#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.
```sh
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.
```sh
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.
```yaml
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](https://docs.tangled.org/knot-self-hosting-guide.html#migrating-to-knot-2) 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.db` from the guide
- `./repositories`: repo scan path
- `./keys/ssh_host_ed25519_key`: the host key that the container's `sshd` generated
- `./knot2`: knot 2's data directory, `/var/lib/knot` in the guide
### Dress rehearsal
Generate the master key as the guide describes,
then rehearse with the Go knot still up:
```sh
mkdir knot2
export KNOT_MASTER_KEY=
export KNOT_SERVER_HOSTNAME=
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:
```sh
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:
```sh
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:
```sh
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:
```yaml
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
```sh
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:
```sh
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:
```sh
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.