Host everything on the website: hosting guide, setup files, downloads; source code button
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# Kestrove: your own end-to-end encrypted password manager server.
|
||||
#
|
||||
# ./setup.sh asks two questions, writes .env and starts everything
|
||||
# docker compose up -d (after filling in .env yourself)
|
||||
#
|
||||
# Two containers: `server` (the sync API; it only ever stores encrypted data) and `web` (Caddy with the
|
||||
# web vault: automatic HTTPS from Let's Encrypt, strict security headers, and /v1 proxied to `server`).
|
||||
# Full guide: https://kestrove.samialabri.com/host.html
|
||||
name: kestrove
|
||||
|
||||
x-server-image: &server-image ${KESTROVE_REGISTRY:-gitea.samialabri.com/kestrove}/kestrove-server:${KESTROVE_VERSION:-latest}
|
||||
|
||||
services:
|
||||
server:
|
||||
image: *server-image
|
||||
restart: unless-stopped
|
||||
read_only: true
|
||||
tmpfs: [/tmp]
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
volumes:
|
||||
- data:/data
|
||||
environment:
|
||||
RUST_LOG: info
|
||||
KV_PUBLIC_URL: https://${DOMAIN:?Set DOMAIN in .env (run ./setup.sh)}
|
||||
KV_TRUST_PROXY: "1"
|
||||
# One-time code for creating your account in the web vault. Empty: a random one is printed in
|
||||
# `docker compose logs server` on first start.
|
||||
KV_SETUP_CODE: ${KV_SETUP_CODE:-}
|
||||
networks: [internal]
|
||||
|
||||
web:
|
||||
image: ${KESTROVE_REGISTRY:-gitea.samialabri.com/kestrove}/kestrove-web:${KESTROVE_VERSION:-latest}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "443:443/udp"
|
||||
environment:
|
||||
DOMAIN: ${DOMAIN:?Set DOMAIN in .env (run ./setup.sh)}
|
||||
ACME_EMAIL: ${ACME_EMAIL:?Set ACME_EMAIL in .env (run ./setup.sh)}
|
||||
volumes:
|
||||
- caddy-data:/data
|
||||
- caddy-config:/config
|
||||
networks: [internal]
|
||||
depends_on: [server]
|
||||
|
||||
# docker compose run --rm -T backup > kestrove-$(date +%F).db
|
||||
# A consistent, integrity-checked copy of the database, taken while the server runs. Your vault inside it
|
||||
# is still end-to-end encrypted: restoring it needs your master password and Secret Key.
|
||||
backup:
|
||||
image: *server-image
|
||||
profiles: [tools]
|
||||
command: ["backup"]
|
||||
read_only: true
|
||||
tmpfs: [/tmp]
|
||||
cap_drop: [ALL]
|
||||
security_opt: ["no-new-privileges:true"]
|
||||
network_mode: none
|
||||
volumes:
|
||||
- data:/data
|
||||
|
||||
volumes:
|
||||
data:
|
||||
caddy-data:
|
||||
caddy-config:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copy to .env (./setup.sh does this for you), then: docker compose up -d
|
||||
|
||||
# The address of your Kestrove, e.g. vault.example.com. Create a DNS A record (and AAAA for IPv6) pointing at
|
||||
# this server first. Using Cloudflare? Set the record to "DNS only" (grey cloud).
|
||||
DOMAIN=
|
||||
|
||||
# Let's Encrypt sends certificate expiry warnings here. Nothing else.
|
||||
ACME_EMAIL=
|
||||
|
||||
# One-time code you enter in the web vault to create your account. At least 16 characters; keep it private.
|
||||
# It stops working as soon as your account exists. Leave empty to have one printed in the server log.
|
||||
KV_SETUP_CODE=
|
||||
|
||||
# Where the images come from, and which version. "latest" follows releases; pin a version (e.g. 1.1.0)
|
||||
# to update only when you choose.
|
||||
KESTROVE_REGISTRY=gitea.samialabri.com/kestrove
|
||||
KESTROVE_VERSION=latest
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/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' </dev/urandom | head -c 24 | sed 's/..../&-/g; s/-$//')
|
||||
umask 077
|
||||
sed -e "s|^DOMAIN=.*|DOMAIN=$DOMAIN|" -e "s|^ACME_EMAIL=.*|ACME_EMAIL=$ACME_EMAIL|" -e "s|^KV_SETUP_CODE=.*|KV_SETUP_CODE=$CODE|" \
|
||||
.env.example > .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."
|
||||
Reference in New Issue
Block a user