anvilsign in

collin/anvil

1#!/usr/bin/env bash
2# Run anvil locally in Docker, reachable at https://anvil.localhost.
3#
4# The same image shape as production (deploy/build.sh + run.sh), but built and
5# run on this machine: a container publishing 3000 to a fixed host port, with
6# portless reverse-proxying a stable `.localhost` name onto it. Running in
7# Docker rather than `cargo run` is what makes CI testable — the runner drives
8# the host's Docker socket, which is mounted in.
9#
10# Usage:
11# ./deploy/dev.sh build + (re)start, then print the URL
12# ./deploy/dev.sh --release optimized binary (slower build, faster server)
13# ./deploy/dev.sh --stop stop and remove the container
14# ./deploy/dev.sh --logs follow the container log
15#
16# State lives in the `anvil-dev-data` volume and survives restarts;
17# `docker volume rm anvil-dev-data` starts over.
18set -euo pipefail
19
20cd "$(dirname "$0")/.."
21
22NAME="${ANVIL_DEV_NAME:-anvil}"
23IMAGE="anvil-dev:latest"
24TARGET="x86_64-unknown-linux-musl"
25VOLUME="anvil-dev-data"
26# A stable, collision-resistant port for this project (see `devport -h`), so the
27# published port does not wander between runs. portless maps a name onto it.
28PORT="${ANVIL_DEV_PORT:-$(command -v devport >/dev/null && devport || echo 20640)}"
29SSH_PORT="${ANVIL_DEV_SSH_PORT:-$((PORT + 1))}"
30PROFILE=debug
31CARGO_FLAGS=()
32
33for arg in "$@"; do
34 case "$arg" in
35 --release)
36 PROFILE=release
37 CARGO_FLAGS+=(--release)
38 ;;
39 --stop)
40 docker rm -f "$NAME" >/dev/null 2>&1 || true
41 portless alias --remove "$NAME" >/dev/null 2>&1 || true
42 echo "stopped $NAME"
43 exit 0
44 ;;
45 --logs)
46 exec docker logs -f "$NAME"
47 ;;
48 *)
49 echo "unknown flag: $arg" >&2
50 exit 2
51 ;;
52 esac
53done
54
55echo "==> building anvild ($PROFILE, static musl)"
56# Static musl, exactly as in production: the runtime image is debian-slim and a
57# binary linked against Fedora's glibc would not run there.
58cargo zigbuild --target "$TARGET" --bin anvild "${CARGO_FLAGS[@]}"
59cp "target/$TARGET/$PROFILE/anvild" deploy/anvild
60trap 'rm -f deploy/anvild' EXIT
61
62echo "==> building $IMAGE"
63docker build --quiet --platform linux/amd64 \
64 --build-arg CONFIG=deploy/anvil.dev.toml -t "$IMAGE" . >/dev/null
65
66echo "==> (re)starting container $NAME"
67docker rm -f "$NAME" >/dev/null 2>&1 || true
68
69# Single sign-on against a provider on https://login.localhost (docs/oidc.md).
70# Two things the container does not get for free: the name resolves to its own
71# loopback rather than the host's portless proxy, and portless's CA — trusted
72# on the host by `portless trust` — is not in the image's root store. So point
73# the name at the host gateway, and hand the binary a bundle that is the host's
74# roots plus that CA (rustls reads SSL_CERT_FILE).
75SSO_ARGS=()
76if [[ -f "$HOME/.portless/ca.pem" ]]; then
77 HOST_ROOTS="$(ls /etc/ssl/certs/ca-bundle.crt /etc/ssl/certs/ca-certificates.crt 2>/dev/null | head -n1)"
78 cat "$HOST_ROOTS" "$HOME/.portless/ca.pem" >deploy/dev-ca.crt 2>/dev/null || true
79 if [[ -s deploy/dev-ca.crt ]]; then
80 SSO_ARGS+=(
81 --add-host "login.localhost:host-gateway"
82 -v "$PWD/deploy/dev-ca.crt:/etc/ssl/certs/anvil-dev-ca.crt:ro,z"
83 -e "SSL_CERT_FILE=/etc/ssl/certs/anvil-dev-ca.crt"
84 )
85 fi
86fi
87# The secret for the client registered at that provider, when there is one.
88if [[ -n "${ANVIL_OIDC_CLIENT_SECRET:-}" ]]; then
89 SSO_ARGS+=(-e "ANVIL_OIDC_CLIENT_SECRET=${ANVIL_OIDC_CLIENT_SECRET}")
90fi
91
92# The CI runner is a Docker client, so it needs the socket and the group that
93# owns it. `label=disable` rather than a `:z` relabel: :z would rewrite the
94# label on the *host's* socket, which every other container also uses.
95docker run -d --name "$NAME" --restart unless-stopped \
96 -p "127.0.0.1:$PORT:3000" \
97 -p "127.0.0.1:$SSH_PORT:2222" \
98 -v "$VOLUME:/data" \
99 -v /var/run/docker.sock:/var/run/docker.sock \
100 --security-opt label=disable \
101 --group-add "$(stat -c '%g' /var/run/docker.sock)" \
102 -e "ANVIL_BASE_URL=https://$NAME.localhost" \
103 "${SSO_ARGS[@]}" \
104 "$IMAGE" >/dev/null
105
106# Wait for the server to answer before handing over a URL that would 502.
107for _ in $(seq 1 50); do
108 if curl -fsS -o /dev/null "http://127.0.0.1:$PORT/-/healthz" 2>/dev/null; then
109 break
110 fi
111 sleep 0.2
112done
113
114if command -v portless >/dev/null; then
115 echo "==> routing https://$NAME.localhost -> 127.0.0.1:$PORT"
116 portless alias "$NAME" "$PORT" >/dev/null
117 URL="https://$NAME.localhost"
118else
119 echo "==> portless not installed; skipping the .localhost route"
120 URL="http://127.0.0.1:$PORT"
121fi
122
123cat <<EOF
124
125anvil is up.
126
127 web $URL
128 ssh ssh://git@localhost:$SSH_PORT/<owner>/<repo>.git
129 direct http://127.0.0.1:$PORT
130
131First run? Create an account and a repo:
132
133 docker exec $NAME anvild -c /etc/anvil/anvil.toml \\
134 user create <you> --password '<password>' --admin
135 docker exec -i $NAME anvild -c /etc/anvil/anvil.toml \\
136 user add-key <you> --title laptop --key "\$(cat ~/.ssh/id_ed25519.pub)"
137
138Logs: ./deploy/dev.sh --logs Stop: ./deploy/dev.sh --stop
139EOF