collin/anvil · 7f97cf80
Cross-compile deploy via zigbuild (thin copy-binary image)
Collin Richards · 2026-06-09 20:36 UTC · 7f97cf80a075478e4316766b86bd524baafcb61a · parent b26efa9c · browse files
modified.gitignore+1 −0
| ⋯ 3 unchanged lines | |||
| 4 | 4 | *.db-wal | |
| 5 | 5 | *.db-shm | |
| 6 | 6 | anvil.toml | |
| 7 | + | /deploy/anvild | |
modifiedDEPLOY.md+42 −5
| ⋯ 43 unchanged lines | |||
| 44 | 44 | host). Caddy resolves `anvil:3000` by container name over the `hagrid` network, | |
| 45 | 45 | so the anvil container must join that network (the run script does this). | |
| 46 | 46 | ||
| 47 | - | ## 3. Build & run the container | |
| 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 | |
| 50 | + | cheap, 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 | + | ||
| 53 | + | One-time toolchain setup: | |
| 54 | + | ||
| 55 | + | ```sh | |
| 56 | + | brew install zig | |
| 57 | + | cargo install cargo-zigbuild | |
| 58 | + | rustup target add x86_64-unknown-linux-musl | |
| 59 | + | ``` | |
| 48 | 60 | ||
| 49 | - | On the hagrid host, from a checkout of this repo: | |
| 61 | + | Then, from a checkout of this repo on your Mac: | |
| 50 | 62 | ||
| 51 | 63 | ```sh | |
| 64 | + | ./deploy/build.sh | |
| 65 | + | ``` | |
| 66 | + | ||
| 67 | + | That: | |
| 68 | + | 1. `cargo zigbuild --release --target x86_64-unknown-linux-musl` — cross-compiles | |
| 69 | + | a fully static `x86_64`-musl binary natively (~2 min, no emulation), | |
| 70 | + | 2. stages it at `deploy/anvild` and builds a thin image that just `COPY`s it in | |
| 71 | + | (the `Dockerfile` does no compilation — fast), | |
| 72 | + | 3. ships it: `docker save | gzip | ssh hagrid 'docker load'`. | |
| 73 | + | ||
| 74 | + | The VPS never compiles anything. | |
| 75 | + | ||
| 76 | + | ## 4. Run the container on hagrid | |
| 77 | + | ||
| 78 | + | On the hagrid host (only runs docker — no build): | |
| 79 | + | ||
| 80 | + | ```sh | |
| 52 | 81 | ./deploy/run.sh | |
| 53 | 82 | ``` | |
| 54 | 83 | ||
| 55 | - | That builds `anvil:latest` and runs: | |
| 84 | + | which does: | |
| 56 | 85 | ||
| 57 | 86 | ```sh | |
| 58 | 87 | docker run -d --name anvil --network hagrid --restart unless-stopped \ | |
| ⋯ 11 unchanged lines | |||
| 70 | 99 | The baked config lives at `/etc/anvil/anvil.toml` (see `deploy/anvil.toml`). | |
| 71 | 100 | Override it by bind-mounting your own file over that path. | |
| 72 | 101 | ||
| 73 | - | ## 4. First run: create your account, key, and the repo | |
| 102 | + | > **If you must build on the VPS anyway** (not recommended): give it swap and | |
| 103 | + | > cap parallelism, or it will OOM — | |
| 104 | + | > ```sh | |
| 105 | + | > sudo fallocate -l 4G /swapfile && sudo chmod 600 /swapfile \ | |
| 106 | + | > && sudo mkswap /swapfile && sudo swapon /swapfile # persist in /etc/fstab | |
| 107 | + | > # then build with CARGO_BUILD_JOBS=1 (slow, but survives 1 GB RAM) | |
| 108 | + | > ``` | |
| 109 | + | ||
| 110 | + | ## 5. First run: create your account, key, and the repo | |
| 74 | 111 | ||
| 75 | 112 | ```sh | |
| 76 | 113 | # admin user | |
| ⋯ 12 unchanged lines | |||
| 89 | 126 | (You can also create the user, add keys, and create repos from the web UI once | |
| 90 | 127 | signed in — the CLI is just convenient for the first admin.) | |
| 91 | 128 | ||
| 92 | - | ## 5. Self-host anvil on anvil | |
| 129 | + | ## 6. Self-host anvil on anvil | |
| 93 | 130 | ||
| 94 | 131 | From your local anvil checkout: | |
| 95 | 132 | ||
| ⋯ 23 unchanged lines | |||
modifiedDockerfile+8 −20
| 1 | - | # Multi-stage build for anvil. | |
| 1 | + | # anvil runtime image — just the prebuilt binary, no compilation in Docker. | |
| 2 | 2 | # | |
| 3 | - | # anvil is self-contained at runtime: SQLite (rusqlite) and the SSH crypto | |
| 4 | - | # (aws-lc-rs, via russh) compile into the binary, and gix is pure-Rust — so the | |
| 5 | - | # runtime image needs no git, no OpenSSH, and no system sqlite. | |
| 6 | - | ||
| 7 | - | # ---- build stage ---- | |
| 8 | - | FROM rust:1.95-bookworm AS build | |
| 9 | - | ||
| 10 | - | # aws-lc-sys (russh crypto) needs cmake; rusqlite "bundled" needs a C compiler | |
| 11 | - | # (present in the full rust image). perl is used by the aws-lc build scripts. | |
| 12 | - | RUN apt-get update \ | |
| 13 | - | && apt-get install -y --no-install-recommends cmake clang perl \ | |
| 14 | - | && rm -rf /var/lib/apt/lists/* | |
| 15 | - | ||
| 16 | - | WORKDIR /src | |
| 17 | - | COPY . . | |
| 18 | - | RUN cargo build --release --bin anvild | |
| 3 | + | # The binary is cross-compiled on the build host into a fully static | |
| 4 | + | # x86_64-musl executable (see deploy/build.sh: `cargo zigbuild --target | |
| 5 | + | # x86_64-unknown-linux-musl`), then staged at deploy/anvild and copied in here. | |
| 6 | + | # So building this image is a fast `COPY` — no QEMU-emulated release build, and | |
| 7 | + | # the VPS never compiles anything. | |
| 19 | 8 | ||
| 20 | - | # ---- runtime stage ---- | |
| 21 | 9 | FROM debian:bookworm-slim | |
| 22 | 10 | ||
| 23 | 11 | RUN apt-get update \ | |
| ⋯ 3 unchanged lines | |||
| 27 | 15 | && mkdir -p /data /etc/anvil \ | |
| 28 | 16 | && chown -R anvil:anvil /data | |
| 29 | 17 | ||
| 30 | - | COPY --from=build /src/target/release/anvild /usr/local/bin/anvild | |
| 18 | + | # Prebuilt static binary staged by deploy/build.sh. | |
| 19 | + | COPY deploy/anvild /usr/local/bin/anvild | |
| 31 | 20 | COPY deploy/anvil.toml /etc/anvil/anvil.toml | |
| 32 | 21 | ||
| 33 | - | # Web (proxied by Caddy over the internal network) and SSH (published to host). | |
| 34 | 22 | EXPOSE 3000 2222 | |
| 35 | 23 | VOLUME /data | |
| 36 | 24 | USER anvil | |
| ⋯ 3 unchanged lines | |||
addeddeploy/build.sh+34 −0
| 1 | + | #!/usr/bin/env bash | |
| 2 | + | # Cross-compile anvil natively on the build host (Mac) and ship the image to | |
| 3 | + | # hagrid. NOT run on the VPS — it never compiles anything. | |
| 4 | + | # | |
| 5 | + | # Uses cargo-zigbuild to cross-compile a fully static x86_64-musl binary at | |
| 6 | + | # native speed (no QEMU), stages it at deploy/anvild, builds a thin image that | |
| 7 | + | # just COPYs it in, and pipes the image to hagrid via docker load. | |
| 8 | + | # | |
| 9 | + | # Prereqs (one-time): | |
| 10 | + | # brew install zig | |
| 11 | + | # cargo install cargo-zigbuild | |
| 12 | + | # rustup target add x86_64-unknown-linux-musl | |
| 13 | + | set -euo pipefail | |
| 14 | + | ||
| 15 | + | IMAGE="${ANVIL_IMAGE:-anvil:latest}" | |
| 16 | + | REMOTE="${ANVIL_REMOTE:-hagrid}" | |
| 17 | + | TARGET="x86_64-unknown-linux-musl" | |
| 18 | + | ||
| 19 | + | cd "$(dirname "$0")/.." | |
| 20 | + | ||
| 21 | + | echo "==> cross-compiling anvild for $TARGET (native, via zig)" | |
| 22 | + | cargo zigbuild --release --target "$TARGET" --bin anvild | |
| 23 | + | ||
| 24 | + | echo "==> staging binary at deploy/anvild" | |
| 25 | + | cp "target/$TARGET/release/anvild" deploy/anvild | |
| 26 | + | ||
| 27 | + | echo "==> building $IMAGE (just COPYs the binary — fast)" | |
| 28 | + | docker build --platform linux/amd64 -t "$IMAGE" . | |
| 29 | + | ||
| 30 | + | echo "==> shipping $IMAGE to $REMOTE" | |
| 31 | + | docker save "$IMAGE" | gzip | ssh "$REMOTE" 'docker load' | |
| 32 | + | ||
| 33 | + | rm -f deploy/anvild | |
| 34 | + | echo "==> done. On $REMOTE, run ./deploy/run.sh to (re)start the container." |
modifieddeploy/run.sh+5 −11
| 1 | 1 | #!/usr/bin/env bash | |
| 2 | - | # Build and (re)start the anvil container on hagrid. | |
| 2 | + | # (Re)start the anvil container on hagrid from an ALREADY-LOADED image. | |
| 3 | 3 | # | |
| 4 | - | # Run from the repo root, on the hagrid host (or anywhere with the `hagrid` | |
| 5 | - | # Docker network). See DEPLOY.md for the full procedure (Caddy + DNS + admin). | |
| 4 | + | # Build and ship the image first with deploy/build.sh on a capable machine | |
| 5 | + | # (the VPS can't compile it). This script only runs docker — no build — so it's | |
| 6 | + | # safe on the low-RAM box. Standalone: needs only docker + the loaded image. | |
| 6 | 7 | set -euo pipefail | |
| 7 | 8 | ||
| 8 | 9 | IMAGE="${ANVIL_IMAGE:-anvil:latest}" | |
| 9 | 10 | NETWORK="${ANVIL_NETWORK:-hagrid}" | |
| 10 | 11 | SSH_PORT="${ANVIL_SSH_PORT:-2222}" | |
| 11 | 12 | ||
| 12 | - | cd "$(dirname "$0")/.." | |
| 13 | - | ||
| 14 | - | echo "==> building $IMAGE" | |
| 15 | - | docker build -t "$IMAGE" . | |
| 16 | - | ||
| 17 | - | echo "==> (re)starting anvil container" | |
| 18 | 13 | docker rm -f anvil 2>/dev/null || true | |
| 19 | 14 | docker run -d \ | |
| 20 | 15 | --name anvil \ | |
| ⋯ 3 unchanged lines | |||
| 24 | 19 | -v anvil-data:/data \ | |
| 25 | 20 | "$IMAGE" | |
| 26 | 21 | ||
| 27 | - | echo "==> anvil is running (web: anvil:3000 via Caddy, ssh: host :${SSH_PORT})" | |
| 28 | - | echo " Next: add the Caddy block + DNS, then create your admin user — see DEPLOY.md." | |
| 22 | + | echo "==> anvil (re)started from $IMAGE (web: anvil:3000 via Caddy, ssh: host :${SSH_PORT})" | |