| 1 | #!/usr/bin/env bash
|
|---|
| 2 | #
|
|---|
| 3 | # Klonkt — installer for a Debian/Ubuntu VPS.
|
|---|
| 4 | # Installs Node 20, Caddy (automatic HTTPS) and Klonkt as a systemd service.
|
|---|
| 5 | #
|
|---|
| 6 | # Safe on a server that ALREADY runs things: it won't upgrade your system Node,
|
|---|
| 7 | # auto-picks a free port, and skips Caddy if a webserver/reverse-proxy is already
|
|---|
| 8 | # listening on port 80/443 (you then get instructions to put Klonkt behind your
|
|---|
| 9 | # own proxy).
|
|---|
| 10 | #
|
|---|
| 11 | # Usage (as root), non-interactive:
|
|---|
| 12 | # curl -fsSL https://raw.githubusercontent.com/roboburr/klonkt/main/scripts/install.sh \
|
|---|
| 13 | # | sudo bash -s -- --domain klonkt.example.com
|
|---|
| 14 | # Or interactively from a downloaded file:
|
|---|
| 15 | # sudo bash install.sh
|
|---|
| 16 | #
|
|---|
| 17 | # Re-running on the same server = update (git pull + restart).
|
|---|
| 18 | # Fully isolated alternative: Docker (see docker-compose.yml in the repo).
|
|---|
| 19 | #
|
|---|
| 20 | set -euo pipefail
|
|---|
| 21 |
|
|---|
| 22 | # ── Settings (override via env var or flag) ────────────────────────────────
|
|---|
| 23 | KLONKT_REPO="${KLONKT_REPO:-https://github.com/roboburr/klonkt.git}"
|
|---|
| 24 | # `stable` = the release channel: it only moves forward to a version that has been verified,
|
|---|
| 25 | # so a self-host auto-update (klonkt-update) never pulls work-in-progress. Use `--branch main`
|
|---|
| 26 | # for the bleeding-edge dev branch instead.
|
|---|
| 27 | KLONKT_BRANCH_SET="${KLONKT_BRANCH:+1}" # channel chosen via env? (empty = no, "1" = yes)
|
|---|
| 28 | KLONKT_BRANCH="${KLONKT_BRANCH:-stable}"
|
|---|
| 29 | KLONKT_DIR="${KLONKT_DIR:-/opt/klonkt}"
|
|---|
| 30 | # Where instance data lives, one directory per slug. The code in KLONKT_DIR is
|
|---|
| 31 | # shared; everything an instance writes stays under here.
|
|---|
| 32 | KLONKT_DATA_ROOT="${KLONKT_DATA_ROOT:-/var/lib/klonkt}"
|
|---|
| 33 | # Short name for this instance: its directory under the data root and its
|
|---|
| 34 | # systemd unit (klonkt@<slug>). Derived from the domain when left empty.
|
|---|
| 35 | KLONKT_SLUG="${KLONKT_SLUG:-}"
|
|---|
| 36 | KLONKT_USER="${KLONKT_USER:-klonkt}"
|
|---|
| 37 | KLONKT_PORT="${KLONKT_PORT:-3000}"
|
|---|
| 38 | KLONKT_DOMAIN="${KLONKT_DOMAIN:-}"
|
|---|
| 39 | KLONKT_LANG="${KLONKT_DEFAULT_LANG:-}"
|
|---|
| 40 | NODE_MAJOR="${NODE_MAJOR:-20}"
|
|---|
| 41 | NO_CADDY="${KLONKT_NO_CADDY:-}" # set to 1 to NEVER install Caddy (own proxy)
|
|---|
| 42 | NODE_FORCE="${NODE_FORCE:-}" # set to 1 to (re)install system Node anyway
|
|---|
| 43 | PORT_EXPLICIT=0
|
|---|
| 44 | BRANCH_EXPLICIT="${KLONKT_BRANCH_SET:-0}" # 1 = operator chose the channel (env or --branch)
|
|---|
| 45 |
|
|---|
| 46 | while [ $# -gt 0 ]; do
|
|---|
| 47 | case "$1" in
|
|---|
| 48 | --domain) KLONKT_DOMAIN="$2"; shift 2;;
|
|---|
| 49 | --repo) KLONKT_REPO="$2"; shift 2;;
|
|---|
| 50 | --branch) KLONKT_BRANCH="$2"; BRANCH_EXPLICIT=1; shift 2;;
|
|---|
| 51 | --dir) KLONKT_DIR="$2"; shift 2;;
|
|---|
| 52 | --port) KLONKT_PORT="$2"; PORT_EXPLICIT=1; shift 2;;
|
|---|
| 53 | --lang) KLONKT_LANG="$2"; shift 2;;
|
|---|
| 54 | --no-caddy) NO_CADDY=1; shift;;
|
|---|
| 55 | --force-node) NODE_FORCE=1; shift;;
|
|---|
| 56 | -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
|
|---|
| 57 | *) echo "Unknown option: $1" >&2; exit 1;;
|
|---|
| 58 | esac
|
|---|
| 59 | done
|
|---|
| 60 |
|
|---|
| 61 | log() { printf '\n\033[1;33m▸ %s\033[0m\n' "$*"; }
|
|---|
| 62 | ok() { printf '\033[1;32m ✓ %s\033[0m\n' "$*"; }
|
|---|
| 63 | warn() { printf '\033[1;33m ! %s\033[0m\n' "$*"; }
|
|---|
| 64 | die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
|
|---|
| 65 | as_klonkt() { runuser -u "$KLONKT_USER" -- env HOME="$KLONKT_DIR" "$@"; }
|
|---|
| 66 | port_busy() { ss -ltnH 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${1}$"; }
|
|---|
| 67 |
|
|---|
| 68 | [ "$(id -u)" = 0 ] || die "Run this as root (sudo bash install.sh)."
|
|---|
| 69 | command -v apt-get >/dev/null || die "Debian/Ubuntu only (apt). On other systems use the Docker route."
|
|---|
| 70 |
|
|---|
| 71 | if [ -z "$KLONKT_DOMAIN" ]; then
|
|---|
| 72 | read -rp "Domain for Klonkt (e.g. klonkt.example.com): " KLONKT_DOMAIN </dev/tty || true
|
|---|
| 73 | fi
|
|---|
| 74 | [ -n "$KLONKT_DOMAIN" ] || die "No domain given (--domain or KLONKT_DOMAIN)."
|
|---|
| 75 | case "$KLONKT_REPO" in
|
|---|
| 76 | *OWNER/*) die "Set the real repo URL first: --repo https://github.com/<you>/klonkt.git (or KLONKT_REPO=...).";;
|
|---|
| 77 | esac
|
|---|
| 78 |
|
|---|
| 79 | export DEBIAN_FRONTEND=noninteractive
|
|---|
| 80 |
|
|---|
| 81 | # ── Preflight: see what's already running, adapt instead of clobbering ──────
|
|---|
| 82 | log "Preflight (what's already running?)…"
|
|---|
| 83 | apt-get update -y >/dev/null
|
|---|
| 84 | apt-get install -y iproute2 >/dev/null 2>&1 || true
|
|---|
| 85 |
|
|---|
| 86 | # Port: busy? With --port → error. Otherwise auto-pick a free one.
|
|---|
| 87 | if port_busy "$KLONKT_PORT"; then
|
|---|
| 88 | if [ "$PORT_EXPLICIT" = 1 ]; then
|
|---|
| 89 | die "Port ${KLONKT_PORT} is already in use. Pick a free port with --port."
|
|---|
| 90 | fi
|
|---|
| 91 | picked=""
|
|---|
| 92 | for p in $(seq "$KLONKT_PORT" $((KLONKT_PORT+30))); do
|
|---|
| 93 | port_busy "$p" || { picked="$p"; break; }
|
|---|
| 94 | done
|
|---|
| 95 | [ -n "$picked" ] || die "No free port found near ${KLONKT_PORT}. Provide one with --port."
|
|---|
| 96 | warn "port ${KLONKT_PORT} busy → Klonkt uses ${picked}"
|
|---|
| 97 | KLONKT_PORT="$picked"
|
|---|
| 98 | else
|
|---|
| 99 | ok "port ${KLONKT_PORT} free"
|
|---|
| 100 | fi
|
|---|
| 101 |
|
|---|
| 102 | # Webserver on 80/443 that isn't Caddy? → skip Caddy, own-proxy mode.
|
|---|
| 103 | FOREIGN_PROXY=0
|
|---|
| 104 | if [ -z "$NO_CADDY" ] && command -v ss >/dev/null 2>&1; then
|
|---|
| 105 | if ss -ltnpH 2>/dev/null | grep -E '[:.](80|443) ' | grep -viq 'caddy'; then
|
|---|
| 106 | NO_CADDY=1; FOREIGN_PROXY=1
|
|---|
| 107 | warn "something is already listening on port 80/443 (not Caddy) → NOT installing Caddy; you'll get proxy instructions"
|
|---|
| 108 | fi
|
|---|
| 109 | fi
|
|---|
| 110 |
|
|---|
| 111 | # ── Node: respect an existing version, don't silently upgrade ──────────────
|
|---|
| 112 | log "Node ${NODE_MAJOR}.x…"
|
|---|
| 113 | if command -v node >/dev/null 2>&1 && [ -z "$NODE_FORCE" ]; then
|
|---|
| 114 | CUR="$(node -v | sed 's/v//;s/\..*//')"
|
|---|
| 115 | if [ "$CUR" -lt "$NODE_MAJOR" ]; then
|
|---|
| 116 | die "Node $(node -v) is already installed on this server; Klonkt needs ≥${NODE_MAJOR}.
|
|---|
| 117 | I will NOT auto-upgrade your system Node — that could break other apps.
|
|---|
| 118 | Options: (a) use the Docker route (own Node, touches nothing), or
|
|---|
| 119 | (b) upgrade Node yourself, or (c) force with NODE_FORCE=1 (at your own risk)."
|
|---|
| 120 | fi
|
|---|
| 121 | ok "using existing node $(node -v)"
|
|---|
| 122 | else
|
|---|
| 123 | curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
|
|---|
| 124 | apt-get install -y nodejs
|
|---|
| 125 | ok "node $(node -v) installed"
|
|---|
| 126 | fi
|
|---|
| 127 |
|
|---|
| 128 | log "Other packages…"
|
|---|
| 129 | apt-get install -y curl ca-certificates git gnupg openssl build-essential python3
|
|---|
| 130 | apt-get install -y webp >/dev/null 2>&1 || true # cwebp = image→WebP (optional)
|
|---|
| 131 | ok "base packages"
|
|---|
| 132 |
|
|---|
| 133 | if [ -z "$NO_CADDY" ]; then
|
|---|
| 134 | log "Caddy (reverse proxy + auto-HTTPS)…"
|
|---|
| 135 | if ! command -v caddy >/dev/null 2>&1; then
|
|---|
| 136 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|---|
| 137 | curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list
|
|---|
| 138 | apt-get update -y
|
|---|
| 139 | apt-get install -y caddy
|
|---|
| 140 | fi
|
|---|
| 141 | ok "caddy present"
|
|---|
| 142 | fi
|
|---|
| 143 |
|
|---|
| 144 | log "Service user '${KLONKT_USER}'…"
|
|---|
| 145 | id -u "$KLONKT_USER" >/dev/null 2>&1 || useradd --system --home-dir "$KLONKT_DIR" --shell /usr/sbin/nologin "$KLONKT_USER"
|
|---|
| 146 | ok "user"
|
|---|
| 147 |
|
|---|
| 148 | log "Fetching Klonkt source…"
|
|---|
| 149 | if [ -d "$KLONKT_DIR/.git" ]; then
|
|---|
| 150 | git -C "$KLONKT_DIR" remote set-url origin "$KLONKT_REPO"
|
|---|
| 151 | # Re-run on an EXISTING install: keep the channel this install already tracks — never
|
|---|
| 152 | # silently switch it to the stable default. Only an explicit --branch / KLONKT_BRANCH
|
|---|
| 153 | # overrides; a fresh install (else-branch) uses the stable default.
|
|---|
| 154 | if [ "$BRANCH_EXPLICIT" != "1" ]; then
|
|---|
| 155 | _cur=$(git -C "$KLONKT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
|---|
| 156 | [ -n "$_cur" ] && [ "$_cur" != "HEAD" ] && KLONKT_BRANCH="$_cur"
|
|---|
| 157 | fi
|
|---|
| 158 | log "Channel: $KLONKT_BRANCH"
|
|---|
| 159 | git -C "$KLONKT_DIR" fetch --depth 1 origin "$KLONKT_BRANCH"
|
|---|
| 160 | # Check out FETCH_HEAD AS the target branch — not `reset --hard origin/$KLONKT_BRANCH`
|
|---|
| 161 | # (a single-branch/shallow clone, or one that started on a different branch like main,
|
|---|
| 162 | # has no origin/<branch> ref → "ambiguous argument 'origin/stable'"), and not a plain
|
|---|
| 163 | # `reset --hard FETCH_HEAD` (that would leave the OLD local branch, e.g. main, pointing at
|
|---|
| 164 | # a stable commit → `git status` reports it as diverged from origin/main). `checkout -f -B`
|
|---|
| 165 | # makes the local branch BE $KLONKT_BRANCH at the fetched tip: robust, forced, no divergence.
|
|---|
| 166 | git -C "$KLONKT_DIR" checkout -qf -B "$KLONKT_BRANCH" FETCH_HEAD
|
|---|
| 167 | else
|
|---|
| 168 | [ -e "$KLONKT_DIR" ] && [ -n "$(ls -A "$KLONKT_DIR" 2>/dev/null)" ] && die "$KLONKT_DIR already exists and is not a git checkout. Pick --dir, or clean it up."
|
|---|
| 169 | mkdir -p "$KLONKT_DIR"
|
|---|
| 170 | git clone --depth 1 --branch "$KLONKT_BRANCH" "$KLONKT_REPO" "$KLONKT_DIR"
|
|---|
| 171 | fi
|
|---|
| 172 | chown -R "$KLONKT_USER:$KLONKT_USER" "$KLONKT_DIR"
|
|---|
| 173 | ok "code in $KLONKT_DIR"
|
|---|
| 174 |
|
|---|
| 175 | # --- where this instance keeps its data -------------------------------------
|
|---|
| 176 | # New installs put data in /var/lib/klonkt/<slug> so the checkout stays free of
|
|---|
| 177 | # user data and can be shared by more instances later. An install that already
|
|---|
| 178 | # has its .env inside the checkout is left exactly as it is: re-running the
|
|---|
| 179 | # installer must never move a live database. Convert those deliberately with
|
|---|
| 180 | # scripts/klonkt-migrate-data.sh.
|
|---|
| 181 | if [ -z "$KLONKT_SLUG" ]; then
|
|---|
| 182 | KLONKT_SLUG="$(printf '%s' "${KLONKT_DOMAIN:-default}" | sed 's/^www\.//' | cut -d. -f1 \
|
|---|
| 183 | | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9._-')"
|
|---|
| 184 | [ -n "$KLONKT_SLUG" ] || KLONKT_SLUG=default
|
|---|
| 185 | fi
|
|---|
| 186 | # A database in the checkout but no .env is too ambiguous to guess at: refuse,
|
|---|
| 187 | # rather than start a fresh empty instance beside data nobody is reading.
|
|---|
| 188 | if [ ! -f "$KLONKT_DIR/.env" ] && [ -f "$KLONKT_DIR/storage/database.sqlite" ]; then
|
|---|
| 189 | die "found $KLONKT_DIR/storage/database.sqlite but no .env next to it.
|
|---|
| 190 | Put the .env back and re-run, or move the old storage/ aside first."
|
|---|
| 191 | fi
|
|---|
| 192 | if [ -f "$KLONKT_DIR/.env" ]; then
|
|---|
| 193 | LAYOUT=legacy
|
|---|
| 194 | ENV="$KLONKT_DIR/.env"
|
|---|
| 195 | DATA_DIR="$KLONKT_DIR/storage"
|
|---|
| 196 | SERVICE="klonkt"
|
|---|
| 197 | mkdir -p "$DATA_DIR/media" "$DATA_DIR/audio"
|
|---|
| 198 | chown -R "$KLONKT_USER:$KLONKT_USER" "$DATA_DIR"
|
|---|
| 199 | ok "existing layout kept (data inside $KLONKT_DIR; split it with scripts/klonkt-migrate-data.sh)"
|
|---|
| 200 | else
|
|---|
| 201 | LAYOUT=split
|
|---|
| 202 | DATA_DIR="$KLONKT_DATA_ROOT/$KLONKT_SLUG"
|
|---|
| 203 | ENV="$DATA_DIR/.env"
|
|---|
| 204 | SERVICE="klonkt@${KLONKT_SLUG}"
|
|---|
| 205 | mkdir -p "$DATA_DIR/media" "$DATA_DIR/audio"
|
|---|
| 206 | chown -R "$KLONKT_USER:$KLONKT_USER" "$DATA_DIR"
|
|---|
| 207 | chmod 750 "$DATA_DIR"
|
|---|
| 208 | ok "data in $DATA_DIR (instance '$KLONKT_SLUG')"
|
|---|
| 209 | fi
|
|---|
| 210 |
|
|---|
| 211 | log "Installing dependencies (npm ci)…"
|
|---|
| 212 | as_klonkt bash -c "cd '$KLONKT_DIR' && npm ci --omit=dev"
|
|---|
| 213 | ok "node_modules"
|
|---|
| 214 |
|
|---|
| 215 | log ".env…"
|
|---|
| 216 | if [ ! -f "$ENV" ]; then
|
|---|
| 217 | SECRET="$(openssl rand -hex 32)"
|
|---|
| 218 | {
|
|---|
| 219 | echo "NODE_ENV=production"
|
|---|
| 220 | echo "PORT=${KLONKT_PORT}"
|
|---|
| 221 | # Bind to loopback only: Caddy (this host) reaches it; the internet cannot
|
|---|
| 222 | # hit the app directly on its port, bypassing HTTPS.
|
|---|
| 223 | echo "HOST=127.0.0.1"
|
|---|
| 224 | echo "SESSION_SECRET=${SECRET}"
|
|---|
| 225 | # Absolute, so the app does not depend on its working directory and the
|
|---|
| 226 | # data can sit outside the checkout. Media subdirectories (avatars,
|
|---|
| 227 | # post-images, ...) follow MEDIA_PATH by themselves.
|
|---|
| 228 | echo "DATABASE_PATH=${DATA_DIR}/database.sqlite"
|
|---|
| 229 | echo "MEDIA_PATH=${DATA_DIR}/media"
|
|---|
| 230 | echo "AUDIO_PATH=${DATA_DIR}/audio"
|
|---|
| 231 | echo "PUBLIC_BASE_URL=https://${KLONKT_DOMAIN}"
|
|---|
| 232 | [ -n "$KLONKT_LANG" ] && echo "KLONKT_DEFAULT_LANG=${KLONKT_LANG}"
|
|---|
| 233 | } > "$ENV"
|
|---|
| 234 | chown "$KLONKT_USER:$KLONKT_USER" "$ENV"; chmod 600 "$ENV"
|
|---|
| 235 | ok "new .env (random SESSION_SECRET, app bound to 127.0.0.1)"
|
|---|
| 236 | else
|
|---|
| 237 | # sync the port in an existing .env with the chosen port
|
|---|
| 238 | if grep -q '^PORT=' "$ENV"; then sed -i "s/^PORT=.*/PORT=${KLONKT_PORT}/" "$ENV"; fi
|
|---|
| 239 | # harden older installs: bind to loopback if not already configured
|
|---|
| 240 | grep -q '^HOST=' "$ENV" || echo "HOST=127.0.0.1" >> "$ENV"
|
|---|
| 241 | ok "kept existing .env (port synced, bound to 127.0.0.1)"
|
|---|
| 242 | fi
|
|---|
| 243 |
|
|---|
| 244 | log "systemd service…"
|
|---|
| 245 | NODE_BIN="$(command -v node)"
|
|---|
| 246 | if [ "$LAYOUT" = split ]; then
|
|---|
| 247 | # One template, one service per instance. Adding a site later is a data
|
|---|
| 248 | # directory plus an .env, with no second copy of the code.
|
|---|
| 249 | sed -e "s#^User=klonkt\$#User=${KLONKT_USER}#" \
|
|---|
| 250 | -e "s#^Group=klonkt\$#Group=${KLONKT_USER}#" \
|
|---|
| 251 | -e "s#^WorkingDirectory=/opt/klonkt\$#WorkingDirectory=${KLONKT_DIR}#" \
|
|---|
| 252 | -e "s#^EnvironmentFile=/var/lib/klonkt/%i/.env\$#EnvironmentFile=${KLONKT_DATA_ROOT}/%i/.env#" \
|
|---|
| 253 | -e "s#^ReadWritePaths=/var/lib/klonkt/%i\$#ReadWritePaths=${KLONKT_DATA_ROOT}/%i#" \
|
|---|
| 254 | -e "s#^ExecStart=/usr/bin/node src/server.js\$#ExecStart=${NODE_BIN} src/server.js#" \
|
|---|
| 255 | "$KLONKT_DIR/deploy/klonkt@.service" > /etc/systemd/system/klonkt@.service
|
|---|
| 256 | chmod 0644 /etc/systemd/system/klonkt@.service
|
|---|
| 257 | systemctl daemon-reload
|
|---|
| 258 | systemctl enable --now "klonkt@${KLONKT_SLUG}"
|
|---|
| 259 | ok "klonkt@${KLONKT_SLUG} running on 127.0.0.1:${KLONKT_PORT}"
|
|---|
| 260 | else
|
|---|
| 261 | cat > /etc/systemd/system/klonkt.service <<EOF
|
|---|
| 262 | [Unit]
|
|---|
| 263 | Description=Klonkt
|
|---|
| 264 | After=network-online.target
|
|---|
| 265 | Wants=network-online.target
|
|---|
| 266 |
|
|---|
| 267 | [Service]
|
|---|
| 268 | Type=simple
|
|---|
| 269 | User=${KLONKT_USER}
|
|---|
| 270 | WorkingDirectory=${KLONKT_DIR}
|
|---|
| 271 | ExecStart=${NODE_BIN} src/server.js
|
|---|
| 272 | Environment=NODE_ENV=production
|
|---|
| 273 | Restart=always
|
|---|
| 274 | RestartSec=3
|
|---|
| 275 | NoNewPrivileges=true
|
|---|
| 276 | ProtectSystem=full
|
|---|
| 277 | PrivateTmp=true
|
|---|
| 278 |
|
|---|
| 279 | [Install]
|
|---|
| 280 | WantedBy=multi-user.target
|
|---|
| 281 | EOF
|
|---|
| 282 | systemctl daemon-reload
|
|---|
| 283 | systemctl enable --now klonkt
|
|---|
| 284 | ok "klonkt.service running on 127.0.0.1:${KLONKT_PORT}"
|
|---|
| 285 | fi
|
|---|
| 286 |
|
|---|
| 287 | if [ -z "$NO_CADDY" ]; then
|
|---|
| 288 | log "Caddy config for ${KLONKT_DOMAIN}…"
|
|---|
| 289 | CADDY=/etc/caddy/Caddyfile
|
|---|
| 290 | SITE_BLOCK="${KLONKT_DOMAIN} {
|
|---|
| 291 | reverse_proxy 127.0.0.1:${KLONKT_PORT}
|
|---|
| 292 | encode gzip zstd
|
|---|
| 293 | }"
|
|---|
| 294 | touch "$CADDY"
|
|---|
| 295 | if grep -q '/usr/share/caddy' "$CADDY"; then
|
|---|
| 296 | cp "$CADDY" "${CADDY}.bak.$(date +%s)"
|
|---|
| 297 | printf '%s\n' "$SITE_BLOCK" > "$CADDY"
|
|---|
| 298 | elif ! grep -q "^${KLONKT_DOMAIN} {" "$CADDY"; then
|
|---|
| 299 | printf '\n%s\n' "$SITE_BLOCK" >> "$CADDY"
|
|---|
| 300 | fi
|
|---|
| 301 | caddy validate --config "$CADDY" --adapter caddyfile >/dev/null 2>&1 || die "Caddy config invalid — check $CADDY"
|
|---|
| 302 | systemctl reload caddy 2>/dev/null || systemctl restart caddy
|
|---|
| 303 | ok "caddy serving ${KLONKT_DOMAIN}"
|
|---|
| 304 | fi
|
|---|
| 305 |
|
|---|
| 306 | log "Update command 'klonkt-update'…"
|
|---|
| 307 | cat > /usr/local/bin/klonkt-update <<EOF
|
|---|
| 308 | #!/usr/bin/env bash
|
|---|
| 309 | set -euo pipefail
|
|---|
| 310 | D="${KLONKT_DIR}"
|
|---|
| 311 | B=\$(runuser -u ${KLONKT_USER} -- git -C "\$D" rev-parse HEAD 2>/dev/null || true)
|
|---|
| 312 | runuser -u ${KLONKT_USER} -- git -C "\$D" fetch --depth 1 origin ${KLONKT_BRANCH}
|
|---|
| 313 | runuser -u ${KLONKT_USER} -- git -C "\$D" checkout -qf -B ${KLONKT_BRANCH} FETCH_HEAD
|
|---|
| 314 | A=\$(runuser -u ${KLONKT_USER} -- git -C "\$D" rev-parse HEAD)
|
|---|
| 315 | if [ "\$B" = "\$A" ]; then
|
|---|
| 316 | echo "Klonkt is already up to date (\$A) — nothing to do."
|
|---|
| 317 | exit 0
|
|---|
| 318 | fi
|
|---|
| 319 | if ! runuser -u ${KLONKT_USER} -- git -C "\$D" diff --quiet "\$B" "\$A" -- package-lock.json 2>/dev/null; then
|
|---|
| 320 | runuser -u ${KLONKT_USER} -- env HOME="\$D" bash -c "cd '\$D' && npm ci --omit=dev"
|
|---|
| 321 | fi
|
|---|
| 322 | # Restart every instance. Each directory under the data root with an .env is one
|
|---|
| 323 | # instance sharing this checkout. An install that has not been split yet has no
|
|---|
| 324 | # such directories and still runs the single klonkt.service.
|
|---|
| 325 | N=0
|
|---|
| 326 | for d in ${KLONKT_DATA_ROOT}/*/; do
|
|---|
| 327 | [ -f "\$d/.env" ] || continue
|
|---|
| 328 | s=\$(basename "\$d")
|
|---|
| 329 | systemctl restart "klonkt@\$s" && N=\$((N+1))
|
|---|
| 330 | done
|
|---|
| 331 | if [ "\$N" = 0 ]; then
|
|---|
| 332 | systemctl restart klonkt
|
|---|
| 333 | echo "Klonkt updated (\$A) + restarted."
|
|---|
| 334 | else
|
|---|
| 335 | echo "Klonkt updated (\$A) + restarted \$N instance(s)."
|
|---|
| 336 | fi
|
|---|
| 337 | EOF
|
|---|
| 338 | chmod +x /usr/local/bin/klonkt-update
|
|---|
| 339 | ok "klonkt-update"
|
|---|
| 340 |
|
|---|
| 341 | echo
|
|---|
| 342 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|---|
| 343 | echo " Klonkt is running! 🎉"
|
|---|
| 344 | echo
|
|---|
| 345 | if [ -n "$NO_CADDY" ]; then
|
|---|
| 346 | echo " Klonkt listens on: http://127.0.0.1:${KLONKT_PORT}"
|
|---|
| 347 | if [ "$FOREIGN_PROXY" = 1 ]; then
|
|---|
| 348 | echo " A webserver is already running on 80/443 — put Klonkt behind it."
|
|---|
| 349 | fi
|
|---|
| 350 | echo " Example nginx:"
|
|---|
| 351 | echo " location / { proxy_pass http://127.0.0.1:${KLONKT_PORT}; proxy_set_header Host \$host;"
|
|---|
| 352 | echo " proxy_set_header X-Forwarded-Proto \$scheme; }"
|
|---|
| 353 | echo " Example Caddy:"
|
|---|
| 354 | echo " ${KLONKT_DOMAIN} { reverse_proxy 127.0.0.1:${KLONKT_PORT} }"
|
|---|
| 355 | else
|
|---|
| 356 | echo " • Open your site: https://${KLONKT_DOMAIN}"
|
|---|
| 357 | fi
|
|---|
| 358 | echo " • First run: go to /auth/register and create your admin account."
|
|---|
| 359 | echo
|
|---|
| 360 | echo " Manage: systemctl status ${SERVICE} · journalctl -u ${SERVICE} -f · klonkt-update"
|
|---|
| 361 | echo " Lost password: cd ${KLONKT_DIR} && runuser -u ${KLONKT_USER} -- env HOME=${KLONKT_DIR} npm run reset-admin"
|
|---|
| 362 | if [ "$LAYOUT" = split ]; then
|
|---|
| 363 | echo
|
|---|
| 364 | echo " Code: ${KLONKT_DIR} shared, nothing of yours lives here"
|
|---|
| 365 | echo " Data: ${DATA_DIR} database, uploads and .env — back up this one"
|
|---|
| 366 | echo " Another site on this server, sharing the same code:"
|
|---|
| 367 | echo " sudo bash ${KLONKT_DIR}/scripts/klonkt-add-instance.sh <slug> <domain>"
|
|---|
| 368 | fi
|
|---|
| 369 | echo
|
|---|
| 370 | echo " DNS: make sure A + AAAA of ${KLONKT_DOMAIN} point to this server."
|
|---|
| 371 | echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|---|