#!/bin/sh # Kestrove self-host setup: asks for your domain and email, writes .env, checks DNS and starts Kestrove. # Safe to run again: it keeps an existing .env unless you choose to replace it. set -eu cd "$(dirname "$0")" say() { printf '%s\n' "$*"; } ask() { # prompt default -> REPLY if [ -n "$2" ]; then printf '%s [%s]: ' "$1" "$2"; else printf '%s: ' "$1"; fi read -r REPLY || REPLY="" [ -n "$REPLY" ] || REPLY=$2 } die() { say "setup: $*" >&2; exit 1; } command -v docker >/dev/null 2>&1 || die "Docker is not installed. See https://docs.docker.com/engine/install/" docker compose version >/dev/null 2>&1 || die "Docker Compose v2 is missing (the 'docker compose' command)." docker info >/dev/null 2>&1 || die "Docker is not running, or this user can't use it (try sudo)." if [ -f .env ]; then ask "A .env already exists. Keep it? (Y/n)" "Y" case "$REPLY" in [nN]*) ;; *) KEEP=1 ;; esac fi if [ -z "${KEEP:-}" ]; then say "" say "Kestrove needs a domain name, e.g. vault.example.com, with a DNS record pointing at this server." ask "Domain" "" DOMAIN=$(printf '%s' "$REPLY" | tr 'A-Z' 'a-z' | sed -e 's#^https\{0,1\}://##' -e 's#/.*$##') printf '%s' "$DOMAIN" | grep -Eq '^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$' || die "'$DOMAIN' is not a domain name" ask "Email for Let's Encrypt certificate notices" "" ACME_EMAIL=$REPLY printf '%s' "$ACME_EMAIL" | grep -Eq '^[^@ ]+@[^@ ]+\.[^@ ]+$' || die "'$ACME_EMAIL' is not an email address" # 24 characters from an unambiguous alphabet (no 0/O, 1/I/L), in groups of four. CODE=$(LC_ALL=C tr -dc 'ABCDEFGHJKMNPQRSTUVWXYZ23456789' .env say "Wrote .env (readable only by you)." fi # shellcheck disable=SC1091 . ./.env [ -n "${DOMAIN:-}" ] || die ".env has no DOMAIN" # DNS check: the domain should resolve to one of this machine's public addresses (warn only; NAT and # proxies make this check imperfect). PUBLIC_IP=$(curl -fsS -4 --max-time 5 https://api.ipify.org 2>/dev/null || true) RESOLVED=$( (getent ahostsv4 "$DOMAIN" 2>/dev/null || true) | awk '{print $1; exit}') [ -n "$RESOLVED" ] || RESOLVED=$( (nslookup "$DOMAIN" 2>/dev/null || true) | awk '/^Address: /{print $2}' | grep -v ':' | tail -1) if [ -z "$RESOLVED" ]; then say "Warning: $DOMAIN does not resolve yet. Create its DNS A record, wait a minute, then run this again." ask "Start anyway? (y/N)" "N" case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac elif [ -n "$PUBLIC_IP" ] && [ "$RESOLVED" != "$PUBLIC_IP" ]; then say "Warning: $DOMAIN points to $RESOLVED, but this server's public address is $PUBLIC_IP." say " If you use Cloudflare, set the record to \"DNS only\" (grey cloud). HTTPS setup fails otherwise." ask "Start anyway? (y/N)" "N" case "$REPLY" in [yY]*) ;; *) exit 1 ;; esac fi say "Starting Kestrove…" docker compose pull -q docker compose up -d say "" say "Waiting for the HTTPS certificate (up to a minute)…" i=0 until curl -fsS -o /dev/null --max-time 5 "https://$DOMAIN/" 2>/dev/null; do i=$((i + 1)) if [ "$i" -ge 24 ]; then say "Not reachable over HTTPS yet. Check: docker compose logs web" say "Ports 80 and 443 must be open to the internet for Let's Encrypt." break fi sleep 5 done say "" say "Kestrove is running at https://$DOMAIN" say "" say "Next: open https://$DOMAIN, choose Create account, and enter this setup code:" say "" if [ -n "${KV_SETUP_CODE:-}" ]; then say " $KV_SETUP_CODE" else say " (printed in: docker compose logs server | grep SETUP_TOKEN)" fi say "" say "Save your Emergency Kit when the web vault offers it. Then add your phone and computers from the" say "web vault (Devices > Add a device) by scanning the QR code in the Kestrove apps."