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