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