anvilsign in

collin/anvil

RenderedSource

Deploying anvil on hagrid

anvil runs as a single Docker container at anvil.richardscollin.com, fronted by hagrid's Caddy reverse proxy.

  • Web — anvil listens on :3000 inside the container. Caddy (on the hagrid Docker network) reverse-proxies to it and provides HTTPS via Let's Encrypt. The web port is not published to the host.
  • SSH — Caddy only fronts HTTP(S), so anvil's SSH server is published directly to the host on :2222. Git-over-SSH connects to anvil.richardscollin.com:2222.
  • Runtime — fully self-contained: SQLite and the SSH crypto are compiled in, and gix is pure-Rust. No git, OpenSSH, or system sqlite in the image.

1. DNS

Add an A/AAAA (or CNAME to hagrid) record:

anvil.richardscollin.com  ->  <hagrid's public IP>

This one record covers both the web (443, via Caddy) and SSH (2222, direct to the host).

2. Caddy + index (in the hagrid repo)

In ~/Code/hagrid/Caddyfile, add:

anvil.richardscollin.com {
	reverse_proxy anvil:3000
}

In ~/Code/hagrid/sites.yaml, add an entry:

- name: anvil
  host: anvil.richardscollin.com

Then reload Caddy (./hagrid.sh reload, or ./hagrid.sh deploy to push to the host). Caddy resolves anvil:3000 by container name over the hagrid network, so the anvil container must join that network (the run script does this).

3. Build & run the container

On the hagrid host, from a checkout of this repo:

./deploy/run.sh

That builds anvil:latest and runs:

docker run -d --name anvil --network hagrid --restart unless-stopped \
    -p 2222:2222 \
    -v anvil-data:/data \
    anvil:latest
  • --network hagrid — so Caddy can reach anvil:3000.
  • -p 2222:2222 — publishes SSH to the host.
  • -v anvil-data:/data — a named volume holding the SQLite DB, the bare repos, and the persistent SSH host key. Use a named volume (not a host bind mount) so it's owned by the in-container anvil user.

The baked config lives at /etc/anvil/anvil.toml (see deploy/anvil.toml). Override it by bind-mounting your own file over that path.

4. First run: create your account, key, and the repo

# admin user
docker exec anvil anvild -c /etc/anvil/anvil.toml \
    user create collin --email you@example.com --password '<password>' --admin

# your SSH public key (so you can push over SSH)
docker exec -i anvil anvild -c /etc/anvil/anvil.toml \
    user add-key collin --title laptop --key "$(cat ~/.ssh/id_ed25519.pub)"

# the anvil repo itself
docker exec anvil anvild -c /etc/anvil/anvil.toml repo create collin/anvil \
    --description "a minimal git forge in Rust"

(You can also create the user, add keys, and create repos from the web UI once signed in — the CLI is just convenient for the first admin.)

5. Self-host anvil on anvil

From your local anvil checkout:

git remote add origin ssh://git@anvil.richardscollin.com:2222/collin/anvil.git
git push -u origin main

Then browse it at https://anvil.richardscollin.com/collin/anvil. HTTPS clone also works: git clone https://anvil.richardscollin.com/collin/anvil.git (pushes over HTTPS require your account password as the git password).

Operations

  • Update: re-run ./deploy/run.sh (rebuilds the image, recreates the container; the anvil-data volume persists). NOTE: adding new DB tables in a future version won't auto-apply to an existing database yet (Toasty migration support is pending) — the git repos on disk are unaffected, but repo metadata in SQLite may need recreating until migrations land.
  • Backup: snapshot the anvil-data volume, e.g. docker run --rm -v anvil-data:/data -v "$PWD":/out debian:bookworm-slim \ tar czf /out/anvil-data.tgz -C /data .
  • Logs: docker logs -f anvil.
  • System routes live under /-/ (e.g. sign in at https://anvil.richardscollin.com/-/login); /{username} is the user/repo namespace.