diff --git a/Dockerfile b/Dockerfile --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ workdir /app run apk add git gcc musl-dev -run git clone -b ${TAG} https://tangled.org/@tangled.org/core . +run git clone -b ${TAG} https://tangled.org/did:plc:j5hmlfdrwkvtxm7cjmu7j2is . run go build -o /usr/bin/knot -ldflags '-s -w -extldflags "-static"' ./cmd/knot from alpine:latest @@ -15,7 +15,7 @@ label org.opencontainers.image.title='knot' label org.opencontainers.image.description='data server for tangled' -label org.opencontainers.image.source='https://tangled.org/@tangled.org/knot-docker' +label org.opencontainers.image.source='https://tangled.org/did:plc:f5s5la5wlofsxidb3zemdune' label org.opencontainers.image.url='https://tangled.org' label org.opencontainers.image.vendor='tangled.org' label org.opencontainers.image.licenses='MIT' diff --git a/Dockerfile.knot2 b/Dockerfile.knot2 new file mode 100644 --- /dev/null +++ b/Dockerfile.knot2 @@ -0,0 +1,43 @@ +from rust:1.96-slim-trixie as builder + +arg TAG='master' + +workdir /src +run apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates git build-essential cmake perl pkg-config clang mold \ + && rm -rf /var/lib/apt/lists/* +env RUSTFLAGS="-C linker=clang -C link-arg=-fuse-ld=mold" +run git clone --depth 1 -b ${TAG} https://tangled.org/did:plc:j5hmlfdrwkvtxm7cjmu7j2is . +run cargo build --release --package knot-server --package knot-migrate +run strip target/release/knot-server target/release/knot-migrate + +from debian:trixie-slim +env KNOT_SCAN_PATH=/data/repos +env KNOT_SEALED_KEY_FILE=/data/sealed-keys +env KNOT_SSH_HOST_KEY_FILE=/data/ssh_host_key +env KNOT_MASTER_KEY_ENV=KNOT_MASTER_KEY +expose 5555 2222 + +label org.opencontainers.image.title='knot' +label org.opencontainers.image.description='data server for tangled' +label org.opencontainers.image.source='https://tangled.org/did:plc:f5s5la5wlofsxidb3zemdune' +label org.opencontainers.image.url='https://tangled.org' +label org.opencontainers.image.vendor='tangled.org' +label org.opencontainers.image.licenses='MIT' + +arg UID=1000 +arg GID=1000 + +run apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + && rm -rf /var/lib/apt/lists/* +run groupadd -g $GID -f knot && useradd -u $UID -g $GID -d /data knot +run mkdir -p /data/repos && chown -R $UID:$GID /data +copy --from=builder /src/target/release/knot-server /usr/bin/knot-server +copy --from=builder /src/target/release/knot-migrate /usr/bin/knot-migrate + +healthcheck --interval=60s --timeout=30s --start-period=600s --retries=3 \ + cmd curl -f http://localhost:5555/xrpc/_health || exit 1 + +user knot +entrypoint ["/usr/bin/knot-server"] diff --git a/docker-compose.knot2.yml b/docker-compose.knot2.yml new file mode 100644 --- /dev/null +++ b/docker-compose.knot2.yml @@ -0,0 +1,34 @@ +services: + knot: + image: atcr.io/tangled.org/knot:2 + environment: + KNOT_HOSTNAME: "${KNOT_SERVER_HOSTNAME:?the hostname that this knot serves, matching its did:web}" + KNOT_ADMINS: "${KNOT_SERVER_OWNER:?the did of the account that administers this knot}" + KNOT_MASTER_KEY: "${KNOT_MASTER_KEY:?the base64 master key that seals the knot's own signing identity}" + KNOT_PLC_DIRECTORY: "${KNOT_SERVER_PLC_URL:-https://plc.directory}" + KNOT_GIT_OBJECT_FORMAT: "${KNOT_OBJECT_FORMAT:-sha1}" + KNOT_LISTEN_ADDR: "${KNOT_LISTEN_ADDR:-[::]:5555}" + KNOT_SSH_LISTEN_ADDR: "${KNOT_SSH_LISTEN_ADDR:-[::]:2222}" + volumes: + - ./knot2:/data + ports: + - "5555:5555" + - "2222:2222" + stop_grace_period: 90s + restart: always + frontend: + image: caddy:alpine + command: > + caddy + reverse-proxy + --from ${KNOT_SERVER_HOSTNAME} + --to knot:5555 + depends_on: + - knot + ports: + - ${KNOT_SERVER_PORT:-443}:443 + - ${KNOT_SERVER_PORT:-443}:443/udp + volumes: + - ./caddy_data:/data + restart: always + profiles: ["caddy"] diff --git a/readme.md b/readme.md --- a/readme.md +++ b/readme.md @@ -17,6 +17,10 @@ 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 @@ -57,6 +61,154 @@ 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 @@ -67,7 +219,9 @@ export KNOT_SERVER_HOSTNAME=example.com export KNOT_SERVER_OWNER=did:plc:yourdidgoeshere export KNOT_SERVER_PORT=443 -docker compose up -d +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.