Skip to content

Repository files, collaboration, releases and project activity.

archforge-external-status / install.shmain
▤ install.sh10434 bytes
176 lines · Select a line number for a permalink
1#!/usr/bin/env bash
2# ArchForge External Status — Debian 13 standalone installer.
3# Run from your ArchForge checkout: sudo ./install.sh
4set -Eeuo pipefail
5umask 027
6abort(){ echo "[ERROR] $*" >&2;exit 1; }
7[[ $EUID -eq 0 ]] || abort 'Run sudo ./install.sh'
8[[ -f /etc/os-release ]] || abort 'Cannot determine operating system'
9source /etc/os-release
10[[ ${ID:-} == debian && ${VERSION_ID:-} == 13 ]] || abort 'Only Debian 13 (trixie) is supported by the automatic installer'
11[[ -t 0 ]] || abort 'Use an interactive terminal to run the setup wizard'
12SRC=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
13[[ -f "$SRC/package.json" && -f "$SRC/src/server.js" ]] || abort 'Run installer from the project root'
14[[ "$SRC" != /opt/archforge-status/app ]] || abort 'Run from the ArchForge checkout, not installed app directory'
15if [[ -f /etc/systemd/system/archforge-status.service ]] && systemctl is-active --quiet archforge-status; then
16 echo '[*] Existing service detected. Checking/recovering admin setup only; no reinstall.'
17 exec bash "$SRC/recover-admin.sh"
18fi
19printf '\n══════════════════════════════════════════════\n ArchForge External Status 1.0.3 — Debian 13\n Independent ArchForge-installed status monitoring\n══════════════════════════════════════════════\n\n'
20echo 'This installer installs Node.js 24, Caddy, SQLite, msmtp, and a systemd service.'
21echo 'The existing archforge.nl website and MySQL database will NOT be changed.'
22read -rp 'Continue? Type INSTALL: ' OK
23[[ "$OK" == INSTALL ]] || abort 'Cancelled'
24export DEBIAN_FRONTEND=noninteractive
25apt-get update
26apt-get install -y ca-certificates curl gnupg git rsync sqlite3 caddy msmtp python3
27NODE_MAJOR=$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)
28if (( NODE_MAJOR < 24 )); then
29 echo '[*] Installing Node.js 24 from the signed NodeSource apt repository'
30 install -d -m 755 /etc/apt/keyrings
31 curl --fail --silent --show-error --location https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
32 chmod 644 /etc/apt/keyrings/nodesource.gpg
33 ARCH=$(dpkg --print-architecture)
34 printf 'deb [arch=%s signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main\n' "$ARCH" > /etc/apt/sources.list.d/nodesource.list
35 apt-get update
36 apt-get install -y nodejs
37fi
38NODE_MAJOR=$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)
39(( NODE_MAJOR >= 24 )) || abort 'Node.js 24+ is required'
40command -v caddy >/dev/null || abort 'Caddy installation failed'
41command -v sqlite3 >/dev/null || abort 'SQLite installation failed'
42command -v msmtp >/dev/null || abort 'msmtp installation failed'
43if ! id -u archforge-status >/dev/null 2>&1; then
44 useradd --system --user-group --home-dir /var/lib/archforge-status --shell /usr/sbin/nologin archforge-status
45fi
46install -d -m 755 /opt/archforge-status /opt/archforge-status/app
47install -d -o archforge-status -g archforge-status -m 750 /var/lib/archforge-status
48install -d -m 750 /etc/archforge-status
49chown root:archforge-status /etc/archforge-status
50install -d -m 700 /var/backups/archforge-status
51rsync -a --delete --exclude='.git' --exclude='.env' --exclude='data' --exclude='node_modules' --exclude='backups' --chmod=D755,F644 "$SRC/" /opt/archforge-status/app/
52chown -R root:root /opt/archforge-status/app
53echo '[*] Running package validation before service installation'
54(cd /opt/archforge-status/app && /usr/bin/node --check src/server.js && /usr/bin/node --test tests/*.test.js) || abort 'Application self-tests failed; installation halted'
55chmod 755 /opt/archforge-status /opt/archforge-status/app
56printf '%s\n' "$SRC" > /etc/archforge-status/source-path
57chmod 600 /etc/archforge-status/source-path
58CFG=/etc/archforge-status/status.env
59if [[ -f "$CFG" ]]; then
60 echo '[*] Existing configuration preserved (no secret rotation)'
61else
62 echo
63 echo '--- Domain and monitored targets ---'
64 read -rp 'Public status domain [status.archforge.nl]: ' DOMAIN; DOMAIN=${DOMAIN:-status.archforge.nl}
65 [[ "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]{0,251}[a-zA-Z0-9])?$ && "$DOMAIN" == *.* ]] || abort 'Invalid domain'
66 read -rp 'Main website HTTPS URL [https://archforge.nl]: ' WEBSITE; WEBSITE=${WEBSITE:-https://archforge.nl}
67 [[ "$WEBSITE" =~ ^https://[a-zA-Z0-9.-]+/?$ ]] || abort 'Use https://example.com for website'
68 read -rp 'Optional public API URL to monitor (Enter to disable): ' API
69 read -rp 'Public ArchForge HTTPS URL ending /info/refs (leave empty to disable): ' GIT_HTTP
70 read -rp 'ArchForge SSH host [archforge.nl]: ' SSH_HOST; SSH_HOST=${SSH_HOST:-archforge.nl}
71 [[ "$SSH_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid ArchForge SSH host'
72 read -rp 'ArchForge SSH port [22]: ' SSH_PORT; SSH_PORT=${SSH_PORT:-22}
73 [[ "$SSH_PORT" =~ ^[0-9]{1,5}$ && "$SSH_PORT" -ge 1 && "$SSH_PORT" -le 65535 ]] || abort 'Invalid SSH port'
74 echo
75 echo '--- Notifications (optional, configurable later) ---'
76 read -rp 'Discord webhook URL (Enter to skip): ' DISCORD
77 [[ -z "$DISCORD" || "$DISCORD" =~ ^https://(discord.com|discordapp.com)/api/webhooks/ ]] || abort 'Discord URL must be an official webhook URL'
78 read -rp 'SMTP server hostname (Enter to disable email): ' SMTP_HOST
79 SMTP_PORT=587 SMTP_USER='' SMTP_PASS='' SMTP_FROM=''
80 if [[ -n "$SMTP_HOST" ]]; then
81 [[ "$SMTP_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid SMTP host'
82 read -rp 'SMTP port [587]: ' SMTP_PORT; SMTP_PORT=${SMTP_PORT:-587}
83 [[ "$SMTP_PORT" =~ ^[0-9]{1,5}$ && "$SMTP_PORT" -ge 1 && "$SMTP_PORT" -le 65535 ]] || abort 'Invalid SMTP port'
84 read -rp 'SMTP username: ' SMTP_USER
85 read -rsp 'SMTP password (hidden): ' SMTP_PASS;printf '\n'
86 read -rp 'From email (e.g. status@archforge.nl): ' SMTP_FROM
87 [[ "$SMTP_FROM" == *@* && "$SMTP_FROM" != *$'\n'* ]] || abort 'Invalid sender address'
88 fi
89 KEY=$(od -An -tx1 -N32 /dev/urandom | tr -d ' \n')
90 # Never source this file as shell. Values are read as literal NAME=VALUE pairs by Node.
91 (umask 077; cat > "$CFG" <<ENVFILE
92NODE_ENV=production
93PORT=3000
94STATUS_LISTEN_HOST=127.0.0.1
95STATUS_PUBLIC_URL=https://${DOMAIN}
96TARGET_WEBSITE=${WEBSITE}
97TARGET_API_URL=${API}
98TARGET_GIT_HTTP_URL=${GIT_HTTP}
99TARGET_GIT_SSH_HOST=${SSH_HOST}
100TARGET_GIT_SSH_PORT=${SSH_PORT}
101MONITOR_INTERVAL_SECONDS=60
102MONITOR_FAILURE_THRESHOLD=3
103MONITOR_RECOVERY_THRESHOLD=2
104MONITOR_TIMEOUT_MS=8000
105STATUS_DISCORD_WEBHOOK_URL=${DISCORD}
106SMTP_HOST=${SMTP_HOST}
107SMTP_PORT=${SMTP_PORT}
108SMTP_USER=${SMTP_USER}
109SMTP_PASSWORD=${SMTP_PASS}
110SMTP_FROM=${SMTP_FROM}
111SMTP_TLS=on
112STATUS_ENCRYPTION_KEY=${KEY}
113STATUS_DB_PATH=/var/lib/archforge-status/status.db
114ENVFILE
115 )
116 chown root:archforge-status "$CFG"
117 chmod 640 "$CFG"
118 unset KEY SMTP_PASS
119fi
120# Generate msmtp config from status.env only if enabled. Secrets never go into ArchForge.
121# Read settings without sourcing the dotenv file.
122get_conf(){ grep -m1 "^${1}=" "$CFG" | cut -d= -f2- || true; }
123SMTP_HOST_VALUE=$(get_conf SMTP_HOST)
124if [[ -n "$SMTP_HOST_VALUE" ]]; then
125 smtp_quote(){ local x=$1;x=${x//\\/\\\\};x=${x//\"/\\\"};printf '"%s"' "$x"; }
126 SMTP_P=$(get_conf SMTP_PORT);SMTP_U=$(get_conf SMTP_USER);SMTP_W=$(get_conf SMTP_PASSWORD);SMTP_F=$(get_conf SMTP_FROM)
127 [[ "$SMTP_P" =~ ^[0-9]+$ ]] || abort 'Invalid SMTP port'
128 {
129 echo 'defaults';echo 'auth on';echo 'tls on';echo 'tls_starttls on';echo 'tls_trust_file /etc/ssl/certs/ca-certificates.crt';echo 'account status'
130 printf 'host %s\n' "$(smtp_quote "$SMTP_HOST_VALUE")"
131 printf 'port %s\n' "$SMTP_P"
132 printf 'from %s\n' "$(smtp_quote "$SMTP_F")"
133 printf 'user %s\n' "$(smtp_quote "$SMTP_U")"
134 printf 'password %s\n' "$(smtp_quote "$SMTP_W")"
135 echo 'account default : status'
136 } >/etc/archforge-status/msmtprc
137 chown archforge-status:archforge-status /etc/archforge-status/msmtprc
138 chmod 600 /etc/archforge-status/msmtprc
139 unset SMTP_W
140else
141 echo '[*] SMTP not configured. Email subscriptions will be disabled until configured.'
142fi
143install -m 644 "$SRC/scripts/archforge-status.service" /etc/systemd/system/archforge-status.service
144install -m 644 "$SRC/scripts/archforge-status-backup.service" /etc/systemd/system/archforge-status-backup.service
145install -m 644 "$SRC/scripts/archforge-status-backup.timer" /etc/systemd/system/archforge-status-backup.timer
146install -m 755 "$SRC/scripts/backup.sh" /usr/local/sbin/archforge-status-backup
147DOMAIN=$(get_conf STATUS_PUBLIC_URL);DOMAIN=${DOMAIN#https://};DOMAIN=${DOMAIN%%/*}
148[[ "$DOMAIN" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid domain in saved config'
149mkdir -p /etc/caddy/conf.d
150cat > /etc/caddy/conf.d/archforge-status.caddy <<CADDYFILE
151${DOMAIN} {
152 encode zstd gzip
153 reverse_proxy 127.0.0.1:3000
155CADDYFILE
156if ! grep -Fq 'import /etc/caddy/conf.d/*.caddy' /etc/caddy/Caddyfile; then
157 cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.backup.$(date +%s)"
158 printf '\nimport /etc/caddy/conf.d/*.caddy\n' >> /etc/caddy/Caddyfile
159fi
160caddy validate --config /etc/caddy/Caddyfile || abort 'Caddy configuration is invalid; inspect /etc/caddy/Caddyfile and its backup'
161systemctl daemon-reload
162# Create database and initial 2FA admin before starting the daemon.
163echo
164printf '\n--- Admin account + 2FA ---\n'
165cd /opt/archforge-status/app
166runuser -u archforge-status -- env STATUS_ENV_FILE="$CFG" NODE_ENV=production /usr/bin/node src/cli.js create-admin
167systemctl enable --now archforge-status.service
168systemctl enable --now archforge-status-backup.timer
169systemctl enable --now caddy
170systemctl reload caddy || systemctl restart caddy
171sleep 1
172systemctl is-active --quiet archforge-status || abort 'Service did not start: sudo journalctl -u archforge-status -n 50'
173echo
174printf '══════════════════════════════════════════════\n Installation finished.\n Public status: https://%s\n Admin login: https://%s/admin/login\n Local health: http://127.0.0.1:3000/healthz\n Logs: sudo journalctl -u archforge-status -f\n══════════════════════════════════════════════\n' "$DOMAIN" "$DOMAIN"
175echo 'For a guided domain and SSL check, run: sudo bash ./configure-domain.sh from your ArchForge checkout.'