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
:3000inside the container. Caddy (on thehagridDocker 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 toanvil.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:
cargo zigbuild --release --target x86_64-unknown-linux-musl— cross-compiles a fully staticx86_64-musl binary natively (~2 min, no emulation),- stages it at
deploy/anvildand builds a thin image that justCOPYs it in (theDockerfiledoes no compilation — fast), - 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 reachanvil: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-containeranviluser.
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; theanvil-datavolume 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-datavolume, 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 athttps://anvil.richardscollin.com/-/login);/{username}is the user/repo namespace.