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 the image on your Mac, ship it to hagrid

Do not build on the VPS — a release build needs ~2–4 GB peak and OOMs a cheap, swap-less droplet. Instead, cross-compile a static binary on your Mac (native speed, no QEMU) and copy it into a thin image.

One-time toolchain setup:

brew install zig
cargo install cargo-zigbuild
rustup target add x86_64-unknown-linux-musl

Then, from a checkout of this repo on your Mac:

./deploy/build.sh

That:

  1. cargo zigbuild --release --target x86_64-unknown-linux-musl — cross-compiles a fully static x86_64-musl binary natively (~2 min, no emulation),
  2. stages it at deploy/anvild and builds a thin image that just COPYs it in (the Dockerfile does no compilation — fast),
  3. ships it: docker save | gzip | ssh hagrid 'docker load'.

The VPS never compiles anything.

4. Run the container on hagrid

On the hagrid host (only runs docker — no build):

./deploy/run.sh

which does:

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.

If you must build on the VPS anyway (not recommended): give it swap and cap parallelism, or it will OOM —

sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile \
  && sudo mkswap /swapfile && sudo swapon /swapfile   # persist in /etc/fstab
# then build with CARGO_BUILD_JOBS=1 (slow, but survives 1 GB RAM)

5. 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.)

6. 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.