#!/bin/sh # Lumni: run your own, in one command. # # curl -fsSL https://get.lumniverse.com | sh this machine, http://localhost:5200 # curl -fsSL https://get.lumniverse.com | sh -s -- --domain acme.com a server: https://app.acme.com, https://api.acme.com # curl -fsSL https://get.lumniverse.com | sh -s -- --ip 203.0.113.7 a server with no domain yet: http://:5200 # # Needs Docker with Compose v2. Everything lives in ~/lumni (or --dir): the # compose file, a Caddyfile, and .env with secrets generated here, once. Run it # again to update; .env is never overwritten. Afterwards: ~/lumni/lumni help. set -eu GET_URL=${LUMNI_GET_URL:-https://get.lumniverse.com} DIR=${LUMNI_DIR:-$HOME/lumni} VERSION=${LUMNI_VERSION:-latest} DOMAIN="" IP="" START=1 OPEN=1 say() { printf '%s\n' "$*"; } ok() { printf ' \033[38;5;99m✓\033[0m %s\n' "$*"; } fail() { printf '\n \033[31m%s\033[0m\n\n' "$*" >&2; exit 1; } while [ $# -gt 0 ]; do case "$1" in --domain) DOMAIN=${2:-}; shift 2 ;; --ip) IP=${2:-}; shift 2 ;; --dir) DIR=${2:-}; shift 2 ;; --version) VERSION=${2:-}; shift 2 ;; --no-start) START=0; shift ;; --no-open) OPEN=0; shift ;; -h|--help) say "install.sh [--domain example.com] [--ip ADDRESS] [--dir DIR] [--version TAG] [--no-start] [--no-open]" say " --domain serve https://app. and https://api. (Caddy, automatic HTTPS)" say " --ip serve http://:5200 and :8080 (no domain yet)" say " neither this machine only: http://localhost:5200" exit 0 ;; *) fail "Unknown option: $1 (try --help)" ;; esac done say "" say " lumni setting up in $DIR" say "" # ─── what this machine needs ─────────────────────────── command -v docker >/dev/null 2>&1 || fail "Docker isn't installed. Get it from https://docs.docker.com/get-docker/ and run this again." docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 isn't available (docker compose). Update Docker and run this again." docker info >/dev/null 2>&1 || fail "Docker isn't running, or this user can't use it. Start Docker (or add yourself to the docker group) and run this again." command -v curl >/dev/null 2>&1 || fail "curl is needed." ok "Docker and Compose are ready" secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex "$1" else head -c "$1" /dev/urandom | od -An -tx1 | tr -d ' \n'; fi } mkdir -p "$DIR/sites" "$DIR/public" "$DIR/clickhouse/init" "$DIR/backups" cd "$DIR" # ─── the files (refreshed every run) ─────────────────── fetch() { curl -fsSL "$GET_URL/$1" -o "$2.tmp" && mv "$2.tmp" "$2" || fail "Couldn't download $GET_URL/$1. Check your connection, or set LUMNI_GET_URL."; } fetch compose.yaml compose.yaml fetch Caddyfile Caddyfile fetch clickhouse/001_schema.sql clickhouse/init/001_schema.sql fetch clickhouse/002_console.sql clickhouse/init/002_console.sql ok "Downloaded the compose file and Caddyfile" # ─── settings and secrets (once) ─────────────────────── if [ ! -f .env ]; then if [ -n "$DOMAIN" ]; then APP_HOST="app.$DOMAIN"; API_HOST="api.$DOMAIN" APP_URL="https://$APP_HOST"; API_URL="https://$API_HOST" PROFILES="https"; BIND="127.0.0.1" elif [ -n "$IP" ]; then APP_HOST="$IP:5200"; API_HOST="" APP_URL="http://$IP:5200"; API_URL="http://$IP:8080" PROFILES=""; BIND="0.0.0.0" else APP_HOST="localhost:5200"; API_HOST="" APP_URL="http://localhost:5200"; API_URL="http://localhost:8080" PROFILES=""; BIND="127.0.0.1" fi umask 077 cat > .env < lumni <<'EOF' #!/bin/sh # Run Lumni: ./lumni up | down | update | status | logs [service] | backup | url set -eu cd "$(dirname "$0")" dc() { docker compose --env-file .env -f compose.yaml "$@"; } url() { sed -n 's/^LUMNI_APP_URL=//p' .env; } case "${1:-help}" in up) dc up -d ;; down) dc down ;; update) curl -fsSL "${LUMNI_GET_URL:-https://get.lumniverse.com}" | LUMNI_DIR="$(pwd)" sh -s -- --no-open ;; status) dc ps ;; logs) shift; dc logs --tail 200 -f "$@" ;; backup) stamp=$(date +%Y%m%d-%H%M%S) dc exec -T postgres pg_dump -U lumni -d lumni | gzip > "backups/postgres-$stamp.sql.gz" dc exec -T console sh -c 'cat /data/console.db' > "backups/console-$stamp.db" echo "Saved backups/postgres-$stamp.sql.gz and backups/console-$stamp.db" ;; url) url ;; *) echo "./lumni up | down | update | status | logs [service] | backup | url" ;; esac EOF chmod +x lumni ok "Installed ./lumni (up, down, update, status, logs, backup)" [ "$START" = 1 ] || { say ""; say " Ready. Start it with: cd $DIR && ./lumni up"; say ""; exit 0; } # ─── start it ────────────────────────────────────────── say "" docker compose --env-file .env -f compose.yaml pull --quiet docker compose --env-file .env -f compose.yaml up -d --remove-orphans APP_URL=$(sed -n 's/^LUMNI_APP_URL=//p' .env) say "" printf ' Waiting for Lumni to start' i=0 until curl -fsS -o /dev/null "http://127.0.0.1:$(sed -n 's/^LUMNI_APP_PORT=//p' .env | grep . || echo 5200)/console/config.json" 2>/dev/null; do i=$((i + 1)) [ $i -lt 90 ] || { say ""; fail "Lumni didn't start in 3 minutes. See: cd $DIR && ./lumni logs"; } printf '.' sleep 2 done say "" ok "Lumni is running" say "" say " Open $APP_URL to create the owner account." if grep -q '^COMPOSE_PROFILES=https' .env; then say " DNS: point $(sed -n 's/^LUMNI_APP_HOST=//p' .env) and $(sed -n 's/^LUMNI_API_HOST=//p' .env) (A records) at this machine," say " and open ports 80 and 443. HTTPS certificates are fetched automatically." fi say " Manage it from $DIR: ./lumni status | logs | update | backup" say "" if [ "$OPEN" = 1 ] && [ -z "$DOMAIN" ] && [ -z "$IP" ]; then (command -v open >/dev/null 2>&1 && open "$APP_URL") || (command -v xdg-open >/dev/null 2>&1 && xdg-open "$APP_URL") || true fi