documentation

building from source

Build GroundShade locally or for production. Local Docker images, multi-arch builds, Cargo profiles, cache strategies, and image layout.

updated

Three audiences, three paths.

  • Just need a binary. make build-release produces target/release/groundshade.
  • Self-hosting from Docker. Use the published image (codeberg.org/groundshade/groundshade:latest) or build your own with make docker.
  • Cutting a release (maintainer only). make release TAG=vX.Y.Z validates the working tree, runs CI, tags the commit, pushes the tag, and publishes the multi-arch image.

The builder image pins Rust 1.95 via the RUST_VERSION ARG in the Dockerfile.

Local Docker image

For local iteration:

make docker
docker run --rm -p 8080:8080 -p 127.0.0.1:9090:9090 \
  -e GROUNDSHADE_ADMIN_TOKEN=dev \
  groundshade/groundshade:dev

make docker uses the release-fast Cargo profile: release semantics, no fat LTO, 16-way codegen. Compiles 2-3x faster than full release with a small (under 5%) runtime cost. Fine for local; tagged releases use full release.

Multi-arch (arm64)

make release-push defaults to linux/amd64. Emulated arm64 via QEMU on an x86 host takes 4-5x longer than native amd64. To opt in:

# Emulated. Slow; run once at release time, not in a tight loop.
make release-push TAG=v0.1.2 PLATFORMS=linux/amd64,linux/arm64

# Native via buildx node on an Apple Silicon mac or arm64 runner.
docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap multi
make release-push TAG=v0.1.2 PLATFORMS=linux/amd64,linux/arm64

If you have a real arm64 machine on the network, attach it as a buildx node so each platform builds natively:

docker buildx create --name multi --use            # local node (amd64)
docker buildx create --append --name multi \
  ssh://you@arm64-host                              # remote arm64 node
docker buildx build --platform linux/amd64,linux/arm64 ...

Cache strategies

Cold cargo-chef build: 3-5 minutes for amd64 native, 10-20 minutes for multi-arch via QEMU. After the first build, edits to workspace crates rebuild in 30-60 seconds thanks to the dep layer cache.

Three ways to persist the cache:

  • Local layer cache (default). Works on the same machine. Cache lives in Docker’s layer store.

  • Registry-backed cache. Pushes the dep-build layer to a registry, so a fresh machine (CI runner, colleague’s laptop) can pull it.

    docker buildx build \
      --cache-to   type=registry,ref=codeberg.org/groundshade/groundshade-cache:buildcache,mode=max \
      --cache-from type=registry,ref=codeberg.org/groundshade/groundshade-cache:buildcache \
      --platform linux/amd64 \
      --tag codeberg.org/groundshade/groundshade:dev \
      --push .
  • sccache. Shared rustc cache across crates and projects. Most useful when you build several Rust projects on the same machine. Set RUSTC_WRAPPER=sccache and start sccache --start-server before cargo build.

For a single-project setup, the layer cache is enough. Registry cache pays off for ephemeral CI runners.

Cargo profiles

ProfileLTOcodegen-unitsUse
devoff256cargo run, fastest iteration
release-fastoff16Local Docker images, dev clusters
releasethin1Tagged production releases

release-fast is the right default for anyone building their own image. Use release when you cut a tag for real traffic.

Image layout

Two stages:

  • builder. rust:1.95-bookworm with cargo-chef installed. Plans the dependency graph from current sources, cooks deps in their own layer, then compiles the workspace.
  • runtime. debian:bookworm-slim with ca-certificates, wget (for the healthcheck), and tini (so docker kill -s HUP reaches the binary cleanly). Runs as non-root UID 65532. Final image: around 140 MB.

State persists in /var/lib/groundshade (the HMAC signing key for trust tokens, see operating). The image declares /var/lib/groundshade as a volume; mount it in production.

Default container env:

  • GROUNDSHADE_LISTEN_HTTP=0.0.0.0:8080
  • GROUNDSHADE_LISTEN_ADMIN=0.0.0.0:9090
  • GROUNDSHADE_TRUST_STATE_DIR=/var/lib/groundshade

The admin listener binds 0.0.0.0 inside the container; publish the host port to loopback only ("127.0.0.1:9090:9090").

Why not distroless

The runtime image needs wget for the HEALTHCHECK and tini for SIGHUP forwarding. A distroless base would drop both. Debian-slim is 25 MB heavier and worth it for the operational ergonomics. If you skip the Docker healthcheck and SIGHUP reload, building a distroless variant is a 10-line change to the runtime stage.