anvilsign in

collin/anvil

RenderedSource

1# Deploying anvil on hagrid
2
3anvil runs as a single Docker container at `anvil.richardscollin.com`, fronted by
4hagrid's Caddy reverse proxy.
5
6- **Web** — anvil listens on `:3000` *inside* the container. Caddy (on the
7 `hagrid` Docker network) reverse-proxies to it and provides HTTPS via Let's
8 Encrypt. The web port is **not** published to the host.
9- **SSH** — Caddy only fronts HTTP(S), so anvil's SSH server is **published
10 directly to the host** on `:2222`. Git-over-SSH connects to
11 `anvil.richardscollin.com:2222`.
12- **Runtime** — fully self-contained: SQLite and the SSH crypto are compiled in,
13 and gix is pure-Rust. No git, OpenSSH, or system sqlite in the image.
14
15## 1. DNS
16
17Add an `A`/`AAAA` (or `CNAME` to hagrid) record:
18
19```
20anvil.richardscollin.com -> <hagrid's public IP>
21```
22
23This one record covers both the web (443, via Caddy) and SSH (2222, direct to
24the host).
25
26## 2. Caddy + index (in the hagrid repo)
27
28In `~/Code/hagrid/Caddyfile`, add:
29
30```
31anvil.richardscollin.com {
32 reverse_proxy anvil:3000
33}
34```
35
36In `~/Code/hagrid/sites.yaml`, add an entry:
37
38```yaml
39- name: anvil
40 host: anvil.richardscollin.com
41```
42
43Then reload Caddy (`./hagrid.sh reload`, or `./hagrid.sh deploy` to push to the
44host). Caddy resolves `anvil:3000` by container name over the `hagrid` network,
45so the anvil container must join that network (the run script does this).
46
47## 3. Build the image on your Mac, ship it to hagrid
48
49**Do not build on the VPS** — a release build needs ~2–4 GB peak and OOMs a
50cheap, swap-less droplet. Instead, cross-compile a static binary on your Mac
51(native speed, no QEMU) and copy it into a thin image.
52
53One-time toolchain setup:
54
55```sh
56brew install zig
57cargo install cargo-zigbuild
58rustup target add x86_64-unknown-linux-musl
59```
60
61Then, from a checkout of this repo on your Mac:
62
63```sh
64./deploy/build.sh
65```
66
67That:
681. `cargo zigbuild --release --target x86_64-unknown-linux-musl` — cross-compiles
69 a fully static `x86_64`-musl binary natively (~2 min, no emulation),
702. stages it at `deploy/anvild` and builds a thin image that just `COPY`s it in
71 (the `Dockerfile` does no compilation — fast),
723. ships it: `docker save | gzip | ssh hagrid 'docker load'`.
73
74The VPS never compiles anything.
75
76## 4. Run the container on hagrid
77
78On the hagrid host (only runs docker — no build):
79
80```sh
81./deploy/run.sh
82```
83
84which does:
85
86```sh
87docker run -d --name anvil --network hagrid --restart unless-stopped \
88 -p 2222:2222 \
89 -v anvil-data:/data \
90 -v /var/run/docker.sock:/var/run/docker.sock \
91 --group-add "$(stat -c '%g' /var/run/docker.sock)" \
92 anvil:latest
93```
94
95- `--network hagrid` — so Caddy can reach `anvil:3000`.
96- `-p 2222:2222` — publishes SSH to the host.
97- `-v anvil-data:/data` — a named volume holding the SQLite DB, the bare repos,
98 and the persistent SSH **host key**. Use a named volume (not a host bind
99 mount) so it's owned by the in-container `anvil` user.
100- `-v /var/run/docker.sock:/var/run/docker.sock` + `--group-add <sock gid>` —
101 lets the **CI runner** drive Docker on the host. The non-root `anvil` user
102 needs the socket's group to open it, hence `--group-add` with the socket's
103 gid (computed at run time by `run.sh`).
104
105> ⚠️ **Security:** mounting the Docker socket grants the container
106> **root-equivalent** control of the host. This is acceptable here because anvil
107> is **single-tenant and owner-operated** — CI only ever runs code *you* push.
108> Do **not** open this instance to untrusted users while the socket is mounted.
109> If you don't want CI, drop the `-v …docker.sock…` and `--group-add` flags;
110> the forge runs fine without them (CI runs just error out).
111
112The baked config lives at `/etc/anvil/anvil.toml` (see `deploy/anvil.toml`).
113Override it by bind-mounting your own file over that path.
114
115> **If you must build on the VPS anyway** (not recommended): give it swap and
116> cap parallelism, or it will OOM —
117> ```sh
118> sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile \
119> && sudo mkswap /swapfile && sudo swapon /swapfile # persist in /etc/fstab
120> # then build with CARGO_BUILD_JOBS=1 (slow, but survives 1 GB RAM)
121> ```
122
123## 5. First run: create your account, key, and the repo
124
125```sh
126# admin user
127docker exec anvil anvild -c /etc/anvil/anvil.toml \
128 user create collin --email you@example.com --password '<password>' --admin
129
130# your SSH public key (so you can push over SSH)
131docker exec -i anvil anvild -c /etc/anvil/anvil.toml \
132 user add-key collin --title laptop --key "$(cat ~/.ssh/id_ed25519.pub)"
133
134# the anvil repo itself
135docker exec anvil anvild -c /etc/anvil/anvil.toml repo create collin/anvil \
136 --description "a minimal git forge in Rust"
137```
138
139(You can also create the user, add keys, and create repos from the web UI once
140signed in — the CLI is just convenient for the first admin.)
141
142## 6. Self-host anvil on anvil
143
144From your local anvil checkout:
145
146```sh
147git remote add origin ssh://git@anvil.richardscollin.com:2222/collin/anvil.git
148git push -u origin main
149```
150
151Then browse it at `https://anvil.richardscollin.com/collin/anvil`. HTTPS clone
152also works: `git clone https://anvil.richardscollin.com/collin/anvil.git`
153(pushes over HTTPS require your account password as the git password).
154
155## 7. CI and the redeploy webhook (CD)
156
157Any repo with a `.anvil/ci.yml` runs CI on push (see `[ci]` requires the Docker
158socket mounted — section 4). A pipeline is just an image plus steps:
159
160```yaml
161image: rust:1.95-bookworm
162steps:
163 - name: test
164 run: cargo test --workspace
165 - run: cargo build --release
166```
167
168Runs show up at `/{owner}/{repo}/ci`, with a per-commit status badge on the
169commit list and a full log on each run's page.
170
171**Continuous deployment** is deliberately scoped to **one** repository. On a
172successful run of `deploy_branch` (default `main`) in the repo named by
173`[ci] deploy_repo`, anvil POSTs JSON to `[ci] deploy_webhook`:
174
175```json
176{ "repo": "collin/anvil", "ref": "main", "commit": "<oid>", "run_id": 42 }
177```
178
179No other repo can trigger this, even with passing CI. The webhook target is a
180**host-local plaintext** receiver (HTTPS is unsupported, to keep the build
181TLS-free) — typically a tiny script-runner on hagrid that, on a verified
182request, runs the actual redeploy. Verify the `X-Anvil-Deploy-Secret` header
183(set `[ci] deploy_secret`) before doing anything. Because anvil cross-compiles
184(section 3), "redeploy anvil" usually means: the receiver pulls the freshly
185built image and re-runs `deploy/run.sh` — it does **not** build in-place.
186
187> The deploy receiver runs with whatever privileges you give it — keep it
188> minimal, secret-gated, and bound to localhost / the Docker host gateway only.
189
190## Operations
191
192- **Update**: re-run `./deploy/run.sh` (rebuilds the image, recreates the
193 container; the `anvil-data` volume persists). NOTE: adding new DB tables in a
194 future version won't auto-apply to an existing database yet (Toasty migration
195 support is pending) — the git repos on disk are unaffected, but repo metadata
196 in SQLite may need recreating until migrations land.
197- **Backup**: snapshot the `anvil-data` volume, e.g.
198 `docker run --rm -v anvil-data:/data -v "$PWD":/out debian:bookworm-slim \
199 tar czf /out/anvil-data.tgz -C /data .`
200- **Logs**: `docker logs -f anvil`.
201- **System routes** live under `/-/` (e.g. sign in at
202 `https://anvil.richardscollin.com/-/login`); `/{username}` is the user/repo
203 namespace.