Docker config for self-hosting a Tangled knot on fly.io knot.h14h.com
README.md

Self-hosted Tangled knot on Fly.io #

This repo contains the configuration I used to self-host a Tangled knot on Fly.io.

It is not a polished installer or reusable tool. It is a working example of one Fly-based knot deployment: a Docker image that builds the Tangled knot binary from a pinned upstream tag, runs the knot HTTP server behind Fly HTTPS, exposes Git-over-SSH, and keeps knot state on a Fly volume.

If you want to copy this setup, expect to read through the files, replace the deployment-specific values, and adapt it to your own domain, Fly app, region, and Tangled account.

How this setup works #

  • Fly serves HTTPS traffic to the knot HTTP/XRPC server on internal port 5555.
  • Fly exposes public SSH port 22 and forwards it to sshd inside the container on port 2222.
  • Git-over-SSH runs as the git user.
  • The knot database, repositories, generated SSH host keys, authorized_keys, and MOTD are stored on a Fly volume mounted at /home/git.
  • sshd asks the knot for authorized keys with: AuthorizedKeysCommand /usr/local/bin/knot keys -o authorized-keys.

The important files are:

  • Dockerfile — builds Tangled core at the explicit KNOT_VERSION tag and prepares the runtime image.
  • docker/entrypoint.sh — initializes persisted SSH/git state and starts the knot server plus sshd.
  • docker/sshd_config — SSH configuration for Git-over-SSH.
  • docker/keys-wrapper.sh — wrapper used by AuthorizedKeysCommand.
  • docker/guard-wrapper.sh — wrapper used as the git user's forced shell.
  • fly.toml — Fly app, services, ports, environment, and volume mount.

Values you would need to change #

This repository's fly.toml contains concrete values from my deployment. If you copy this setup, replace them with your own:

  • Fly app name: app
  • Fly region: primary_region
  • Knot hostname: KNOT_SERVER_HOSTNAME
  • Fly volume name: [[mounts]].source
  • Tangled owner DID: set as the Fly secret KNOT_SERVER_OWNER, not committed to the repo
  • Knot version: KNOT_VERSION in Dockerfile and fly.toml [build.args]

For example, your local planning variables might look like:

APP_NAME="my-knot"
REGION="iad"
VOLUME_NAME="knot_data"
HOSTNAME="knot.example.com"
OWNER_DID="did:plc:REPLACE_ME"
KNOT_VERSION="v1.14.1-alpha"

Rough setup flow #

This is the shape of the setup I used. Treat it as notes to adapt, not a copy/paste installer.

Create the Fly app and persistent volume:

fly apps create "$APP_NAME"
fly volumes create "$VOLUME_NAME" --region "$REGION" --size 10 --app "$APP_NAME"

Set the Tangled account DID that owns the knot:

fly secrets set KNOT_SERVER_OWNER="$OWNER_DID" --app "$APP_NAME"

Deploy the app:

fly deploy --app "$APP_NAME" --build-arg KNOT_VERSION="$KNOT_VERSION"

Ask Fly to provision TLS for your knot hostname and inspect the IPs you need for DNS:

fly certs add "$HOSTNAME" --app "$APP_NAME"
fly ips list --app "$APP_NAME"

Then create the requested DNS records with your DNS provider.

For SSH on public port 22, I found that a dedicated Fly IPv4 address was necessary. Fly's shared IPv4 proxying can be unsuitable for OpenSSH on port 22 in some setups.

If there's a way to serve knot SSH traffic over IPv6 I didn't find it, if one exists that may be a workaround to paying $2/mo to fly.io. However, it's worth noting that Fly's current policy is not to bill for monthly invoices under $5, so if this is your only Fly project, it will likely be free.

Registering with Tangled #

Once the service is deployed and DNS resolves, register or verify the knot hostname in Tangled settings. After that, create a repository that uses the knot and push to the SSH remote Tangled gives you.

A remote will generally look like:

git remote add origin git@knot.example.com:owner.example/repo-name
git push -u origin main

One local gotcha: if your shell sets GIT_SSH_COMMAND for another host, make sure it does not force Tangled pushes through that host. For a one-off push, you can clear it:

env -u GIT_SSH_COMMAND git push -u origin main

Updating the knot version #

The upstream Tangled core version is explicit rather than floating. At the moment this setup builds KNOT_VERSION=v1.14.1-alpha.

There are two places to check when changing it:

  • Dockerfile has the default build argument: ARG KNOT_VERSION=...
  • fly.toml passes the same value under [build.args]

To update, choose the upstream tag you want, update both values, and deploy:

git ls-remote --tags https://tangled.org/@tangled.org/core 'v*'
fly deploy --app "$APP_NAME"

You can also override the configured value for a one-off build:

fly deploy --app "$APP_NAME" --build-arg KNOT_VERSION="v1.14.1-alpha"

After deploy, the container writes the configured tag and checked-out commit to /home/git/.knot.env as KNOT_VERSION and KNOT_REVISION, which is useful when checking what is actually running.

Operational notes #

  • /home/git must be persistent. Losing it means losing repositories, knot database state, generated SSH host keys, and git user SSH state.
  • The knot SQLite database lives at /home/git/knotserver.db.
  • Generated SSH host keys live under /home/git/.ssh/host_keys so the SSH host identity survives deploys and restarts.
  • Password login and root SSH login are disabled.
  • Fly volumes are tied to regions and machines. For a small personal knot, a single persistent volume is the simplest setup.

Relationship to Tangled docs #

The Tangled self-hosting guide documents NixOS, community Docker, and manual setup options. This repository follows the manual setup shape inside a container: build the knot binary from the configured KNOT_VERSION tag in tangled.org/core, run knot server, configure sshd with Tangled's authorized-keys command, and persist /home/git.