source: Klonkt/scripts/install.sh@ 54d2f98

main
Last change on this file since 54d2f98 was f99bbe8, checked in by Robin Genis <roboburr@…>, 3 months ago

security: bind to 127.0.0.1 by default behind a reverse proxy

New HOST env (default 0.0.0.0 for Docker/back-compat). The VPS installer now
writes HOST=127.0.0.1 and Docker maps the host port to loopback (127.0.0.1:3000:3000)
+ overrides HOST=0.0.0.0 inside the container — so the app is never reachable
directly on its port from the internet, only via the proxy. .env.example defaults
to 127.0.0.1 (manual installs); docs explain it. Existing installs hardened on
re-run of install.sh.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 10.8 KB
RevLine 
[83088b66]1#!/usr/bin/env bash
2#
[bb42dfb]3# Klonkt — installer for a Debian/Ubuntu VPS.
4# Installs Node 20, Caddy (automatic HTTPS) and Klonkt as a systemd service.
[83088b66]5#
[bb42dfb]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).
[83088b66]10#
[bb42dfb]11# Usage (as root), non-interactive:
[83088b66]12# curl -fsSL https://raw.githubusercontent.com/roboburr/klonkt/main/scripts/install.sh \
[bb42dfb]13# | sudo bash -s -- --domain klonkt.example.com
14# Or interactively from a downloaded file:
[83088b66]15# sudo bash install.sh
16#
[bb42dfb]17# Re-running on the same server = update (git pull + restart).
18# Fully isolated alternative: Docker (see docker-compose.yml in the repo).
[83088b66]19#
20set -euo pipefail
21
[bb42dfb]22# ── Settings (override via env var or flag) ────────────────────────────────
23KLONKT_REPO="${KLONKT_REPO:-https://github.com/roboburr/klonkt.git}"
[83088b66]24KLONKT_BRANCH="${KLONKT_BRANCH:-main}"
25KLONKT_DIR="${KLONKT_DIR:-/opt/klonkt}"
26KLONKT_USER="${KLONKT_USER:-klonkt}"
27KLONKT_PORT="${KLONKT_PORT:-3000}"
28KLONKT_DOMAIN="${KLONKT_DOMAIN:-}"
29KLONKT_LANG="${KLONKT_DEFAULT_LANG:-}"
30NODE_MAJOR="${NODE_MAJOR:-20}"
[bb42dfb]31NO_CADDY="${KLONKT_NO_CADDY:-}" # set to 1 to NEVER install Caddy (own proxy)
32NODE_FORCE="${NODE_FORCE:-}" # set to 1 to (re)install system Node anyway
[83088b66]33PORT_EXPLICIT=0
34
35while [ $# -gt 0 ]; do
36 case "$1" in
37 --domain) KLONKT_DOMAIN="$2"; shift 2;;
38 --repo) KLONKT_REPO="$2"; shift 2;;
39 --branch) KLONKT_BRANCH="$2"; shift 2;;
40 --dir) KLONKT_DIR="$2"; shift 2;;
41 --port) KLONKT_PORT="$2"; PORT_EXPLICIT=1; shift 2;;
42 --lang) KLONKT_LANG="$2"; shift 2;;
43 --no-caddy) NO_CADDY=1; shift;;
44 --force-node) NODE_FORCE=1; shift;;
45 -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
[bb42dfb]46 *) echo "Unknown option: $1" >&2; exit 1;;
[83088b66]47 esac
48done
49
50log() { printf '\n\033[1;33m▸ %s\033[0m\n' "$*"; }
51ok() { printf '\033[1;32m ✓ %s\033[0m\n' "$*"; }
52warn() { printf '\033[1;33m ! %s\033[0m\n' "$*"; }
53die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }
54as_klonkt() { runuser -u "$KLONKT_USER" -- env HOME="$KLONKT_DIR" "$@"; }
55port_busy() { ss -ltnH 2>/dev/null | awk '{print $4}' | grep -qE "[:.]${1}$"; }
56
[bb42dfb]57[ "$(id -u)" = 0 ] || die "Run this as root (sudo bash install.sh)."
58command -v apt-get >/dev/null || die "Debian/Ubuntu only (apt). On other systems use the Docker route."
[83088b66]59
60if [ -z "$KLONKT_DOMAIN" ]; then
[bb42dfb]61 read -rp "Domain for Klonkt (e.g. klonkt.example.com): " KLONKT_DOMAIN </dev/tty || true
[83088b66]62fi
[bb42dfb]63[ -n "$KLONKT_DOMAIN" ] || die "No domain given (--domain or KLONKT_DOMAIN)."
[83088b66]64case "$KLONKT_REPO" in
[bb42dfb]65 *OWNER/*) die "Set the real repo URL first: --repo https://github.com/<you>/klonkt.git (or KLONKT_REPO=...).";;
[83088b66]66esac
67
68export DEBIAN_FRONTEND=noninteractive
69
[bb42dfb]70# ── Preflight: see what's already running, adapt instead of clobbering ──────
71log "Preflight (what's already running?)…"
[83088b66]72apt-get update -y >/dev/null
73apt-get install -y iproute2 >/dev/null 2>&1 || true
74
[bb42dfb]75# Port: busy? With --port → error. Otherwise auto-pick a free one.
[83088b66]76if port_busy "$KLONKT_PORT"; then
77 if [ "$PORT_EXPLICIT" = 1 ]; then
[bb42dfb]78 die "Port ${KLONKT_PORT} is already in use. Pick a free port with --port."
[83088b66]79 fi
80 picked=""
81 for p in $(seq "$KLONKT_PORT" $((KLONKT_PORT+30))); do
82 port_busy "$p" || { picked="$p"; break; }
83 done
[bb42dfb]84 [ -n "$picked" ] || die "No free port found near ${KLONKT_PORT}. Provide one with --port."
85 warn "port ${KLONKT_PORT} busy → Klonkt uses ${picked}"
[83088b66]86 KLONKT_PORT="$picked"
87else
[bb42dfb]88 ok "port ${KLONKT_PORT} free"
[83088b66]89fi
90
[bb42dfb]91# Webserver on 80/443 that isn't Caddy? → skip Caddy, own-proxy mode.
[83088b66]92FOREIGN_PROXY=0
93if [ -z "$NO_CADDY" ] && command -v ss >/dev/null 2>&1; then
94 if ss -ltnpH 2>/dev/null | grep -E '[:.](80|443) ' | grep -viq 'caddy'; then
95 NO_CADDY=1; FOREIGN_PROXY=1
[bb42dfb]96 warn "something is already listening on port 80/443 (not Caddy) → NOT installing Caddy; you'll get proxy instructions"
[83088b66]97 fi
98fi
99
[bb42dfb]100# ── Node: respect an existing version, don't silently upgrade ──────────────
[83088b66]101log "Node ${NODE_MAJOR}.x…"
102if command -v node >/dev/null 2>&1 && [ -z "$NODE_FORCE" ]; then
103 CUR="$(node -v | sed 's/v//;s/\..*//')"
104 if [ "$CUR" -lt "$NODE_MAJOR" ]; then
[bb42dfb]105 die "Node $(node -v) is already installed on this server; Klonkt needs ≥${NODE_MAJOR}.
106 I will NOT auto-upgrade your system Node — that could break other apps.
107 Options: (a) use the Docker route (own Node, touches nothing), or
108 (b) upgrade Node yourself, or (c) force with NODE_FORCE=1 (at your own risk)."
[83088b66]109 fi
[bb42dfb]110 ok "using existing node $(node -v)"
[83088b66]111else
112 curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
113 apt-get install -y nodejs
[bb42dfb]114 ok "node $(node -v) installed"
[83088b66]115fi
116
[bb42dfb]117log "Other packages…"
[83088b66]118apt-get install -y curl ca-certificates git gnupg openssl build-essential python3
[bb42dfb]119apt-get install -y webp >/dev/null 2>&1 || true # cwebp = image→WebP (optional)
120ok "base packages"
[83088b66]121
122if [ -z "$NO_CADDY" ]; then
123 log "Caddy (reverse proxy + auto-HTTPS)…"
124 if ! command -v caddy >/dev/null 2>&1; then
125 curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
126 curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' > /etc/apt/sources.list.d/caddy-stable.list
127 apt-get update -y
128 apt-get install -y caddy
129 fi
[bb42dfb]130 ok "caddy present"
[83088b66]131fi
132
[bb42dfb]133log "Service user '${KLONKT_USER}'…"
[83088b66]134id -u "$KLONKT_USER" >/dev/null 2>&1 || useradd --system --home-dir "$KLONKT_DIR" --shell /usr/sbin/nologin "$KLONKT_USER"
[bb42dfb]135ok "user"
[83088b66]136
[bb42dfb]137log "Fetching Klonkt source…"
[83088b66]138if [ -d "$KLONKT_DIR/.git" ]; then
139 git -C "$KLONKT_DIR" remote set-url origin "$KLONKT_REPO"
140 git -C "$KLONKT_DIR" fetch --depth 1 origin "$KLONKT_BRANCH"
141 git -C "$KLONKT_DIR" reset --hard "origin/$KLONKT_BRANCH"
142else
[bb42dfb]143 [ -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."
[83088b66]144 mkdir -p "$KLONKT_DIR"
145 git clone --depth 1 --branch "$KLONKT_BRANCH" "$KLONKT_REPO" "$KLONKT_DIR"
146fi
147mkdir -p "$KLONKT_DIR/storage/media" "$KLONKT_DIR/storage/audio"
148chown -R "$KLONKT_USER:$KLONKT_USER" "$KLONKT_DIR"
149ok "code in $KLONKT_DIR"
150
[bb42dfb]151log "Installing dependencies (npm ci)…"
[83088b66]152as_klonkt bash -c "cd '$KLONKT_DIR' && npm ci --omit=dev"
153ok "node_modules"
154
155log ".env…"
156ENV="$KLONKT_DIR/.env"
157if [ ! -f "$ENV" ]; then
158 SECRET="$(openssl rand -hex 32)"
159 {
160 echo "NODE_ENV=production"
161 echo "PORT=${KLONKT_PORT}"
[f99bbe8]162 # Bind to loopback only: Caddy (this host) reaches it; the internet cannot
163 # hit the app directly on its port, bypassing HTTPS.
164 echo "HOST=127.0.0.1"
[83088b66]165 echo "SESSION_SECRET=${SECRET}"
166 echo "DATABASE_PATH=./storage/database.sqlite"
167 echo "MEDIA_PATH=./storage/media"
168 echo "AUDIO_PATH=./storage/audio"
169 echo "PUBLIC_BASE_URL=https://${KLONKT_DOMAIN}"
170 [ -n "$KLONKT_LANG" ] && echo "KLONKT_DEFAULT_LANG=${KLONKT_LANG}"
171 } > "$ENV"
172 chown "$KLONKT_USER:$KLONKT_USER" "$ENV"; chmod 600 "$ENV"
[f99bbe8]173 ok "new .env (random SESSION_SECRET, app bound to 127.0.0.1)"
[83088b66]174else
[bb42dfb]175 # sync the port in an existing .env with the chosen port
[83088b66]176 if grep -q '^PORT=' "$ENV"; then sed -i "s/^PORT=.*/PORT=${KLONKT_PORT}/" "$ENV"; fi
[f99bbe8]177 # harden older installs: bind to loopback if not already configured
178 grep -q '^HOST=' "$ENV" || echo "HOST=127.0.0.1" >> "$ENV"
179 ok "kept existing .env (port synced, bound to 127.0.0.1)"
[83088b66]180fi
181
[bb42dfb]182log "systemd service…"
[83088b66]183NODE_BIN="$(command -v node)"
184cat > /etc/systemd/system/klonkt.service <<EOF
185[Unit]
186Description=Klonkt
187After=network-online.target
188Wants=network-online.target
189
190[Service]
191Type=simple
192User=${KLONKT_USER}
193WorkingDirectory=${KLONKT_DIR}
194ExecStart=${NODE_BIN} src/server.js
195Environment=NODE_ENV=production
196Restart=always
197RestartSec=3
198NoNewPrivileges=true
199ProtectSystem=full
200PrivateTmp=true
201
202[Install]
203WantedBy=multi-user.target
204EOF
205systemctl daemon-reload
206systemctl enable --now klonkt
[bb42dfb]207ok "klonkt.service running on 127.0.0.1:${KLONKT_PORT}"
[83088b66]208
209if [ -z "$NO_CADDY" ]; then
[bb42dfb]210 log "Caddy config for ${KLONKT_DOMAIN}…"
[83088b66]211 CADDY=/etc/caddy/Caddyfile
212 SITE_BLOCK="${KLONKT_DOMAIN} {
213 reverse_proxy 127.0.0.1:${KLONKT_PORT}
214 encode gzip zstd
215}"
216 touch "$CADDY"
217 if grep -q '/usr/share/caddy' "$CADDY"; then
218 cp "$CADDY" "${CADDY}.bak.$(date +%s)"
219 printf '%s\n' "$SITE_BLOCK" > "$CADDY"
220 elif ! grep -q "^${KLONKT_DOMAIN} {" "$CADDY"; then
221 printf '\n%s\n' "$SITE_BLOCK" >> "$CADDY"
222 fi
[bb42dfb]223 caddy validate --config "$CADDY" --adapter caddyfile >/dev/null 2>&1 || die "Caddy config invalid — check $CADDY"
[83088b66]224 systemctl reload caddy 2>/dev/null || systemctl restart caddy
[bb42dfb]225 ok "caddy serving ${KLONKT_DOMAIN}"
[83088b66]226fi
227
[bb42dfb]228log "Update command 'klonkt-update'…"
[83088b66]229cat > /usr/local/bin/klonkt-update <<EOF
230#!/usr/bin/env bash
231set -euo pipefail
232D="${KLONKT_DIR}"
233B=\$(runuser -u ${KLONKT_USER} -- git -C "\$D" rev-parse HEAD 2>/dev/null || true)
234runuser -u ${KLONKT_USER} -- git -C "\$D" fetch --depth 1 origin ${KLONKT_BRANCH}
235runuser -u ${KLONKT_USER} -- git -C "\$D" reset --hard origin/${KLONKT_BRANCH}
236A=\$(runuser -u ${KLONKT_USER} -- git -C "\$D" rev-parse HEAD)
237if ! runuser -u ${KLONKT_USER} -- git -C "\$D" diff --quiet "\$B" "\$A" -- package-lock.json 2>/dev/null; then
238 runuser -u ${KLONKT_USER} -- env HOME="\$D" bash -c "cd '\$D' && npm ci --omit=dev"
239fi
240systemctl restart klonkt
[bb42dfb]241echo "Klonkt updated (\$A) + restarted."
[83088b66]242EOF
243chmod +x /usr/local/bin/klonkt-update
244ok "klonkt-update"
245
246echo
247echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
[bb42dfb]248echo " Klonkt is running! 🎉"
[83088b66]249echo
250if [ -n "$NO_CADDY" ]; then
[bb42dfb]251 echo " Klonkt listens on: http://127.0.0.1:${KLONKT_PORT}"
[83088b66]252 if [ "$FOREIGN_PROXY" = 1 ]; then
[bb42dfb]253 echo " A webserver is already running on 80/443 — put Klonkt behind it."
[83088b66]254 fi
[bb42dfb]255 echo " Example nginx:"
[83088b66]256 echo " location / { proxy_pass http://127.0.0.1:${KLONKT_PORT}; proxy_set_header Host \$host;"
257 echo " proxy_set_header X-Forwarded-Proto \$scheme; }"
[bb42dfb]258 echo " Example Caddy:"
[83088b66]259 echo " ${KLONKT_DOMAIN} { reverse_proxy 127.0.0.1:${KLONKT_PORT} }"
260else
[bb42dfb]261 echo " • Open your site: https://${KLONKT_DOMAIN}"
[83088b66]262fi
[bb42dfb]263echo " • First run: go to /auth/register and create your admin account."
[83088b66]264echo
[bb42dfb]265echo " Manage: systemctl status klonkt · journalctl -u klonkt -f · klonkt-update"
266echo " Lost password: cd ${KLONKT_DIR} && runuser -u ${KLONKT_USER} -- env HOME=${KLONKT_DIR} npm run reset-admin"
[83088b66]267echo
[bb42dfb]268echo " DNS: make sure A + AAAA of ${KLONKT_DOMAIN} point to this server."
[83088b66]269echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
Note: See TracBrowser for help on using the repository browser.