Skip to content

Repository files, collaboration, releases and project activity.

ARCHFORGE HISTORY

Blame · install.sh

Branch main · the Commit author is not necessarily a verified ArchForge account.

e59b7f7cc6archadmin2026-10-111 #!/usr/bin/env bash
e59b7f7cc6archadmin2026-10-112 # ArchForge External Status — Debian 13 standalone installer.
2109dd33b8archadmin2026-10-123 # Run from your ArchForge checkout: sudo ./install.sh
e59b7f7cc6archadmin2026-10-114 set -Eeuo pipefail
e59b7f7cc6archadmin2026-10-115 umask 027
e59b7f7cc6archadmin2026-10-116 abort(){ echo "[ERROR] $*" >&2;exit 1; }
e59b7f7cc6archadmin2026-10-117 [[ $EUID -eq 0 ]] || abort 'Run sudo ./install.sh'
e59b7f7cc6archadmin2026-10-118 [[ -f /etc/os-release ]] || abort 'Cannot determine operating system'
e59b7f7cc6archadmin2026-10-119 source /etc/os-release
e59b7f7cc6archadmin2026-10-1110 [[ ${ID:-} == debian && ${VERSION_ID:-} == 13 ]] || abort 'Only Debian 13 (trixie) is supported by the automatic installer'
e59b7f7cc6archadmin2026-10-1111 [[ -t 0 ]] || abort 'Use an interactive terminal to run the setup wizard'
e59b7f7cc6archadmin2026-10-1112 SRC=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
e59b7f7cc6archadmin2026-10-1113 [[ -f "$SRC/package.json" && -f "$SRC/src/server.js" ]] || abort 'Run installer from the project root'
2109dd33b8archadmin2026-10-1214 [[ "$SRC" != /opt/archforge-status/app ]] || abort 'Run from the ArchForge checkout, not installed app directory'
f1897d33c0archadmin2026-10-1115 if [[ -f /etc/systemd/system/archforge-status.service ]] && systemctl is-active --quiet archforge-status; then
f1897d33c0archadmin2026-10-1116  echo '[*] Existing service detected. Checking/recovering admin setup only; no reinstall.'
f1897d33c0archadmin2026-10-1117  exec bash "$SRC/recover-admin.sh"
f1897d33c0archadmin2026-10-1118 fi
2109dd33b8archadmin2026-10-1219 printf '\n══════════════════════════════════════════════\n ArchForge External Status 1.0.3 — Debian 13\n Independent ArchForge-installed status monitoring\n══════════════════════════════════════════════\n\n'
e59b7f7cc6archadmin2026-10-1120 echo 'This installer installs Node.js 24, Caddy, SQLite, msmtp, and a systemd service.'
e59b7f7cc6archadmin2026-10-1121 echo 'The existing archforge.nl website and MySQL database will NOT be changed.'
e59b7f7cc6archadmin2026-10-1122 read -rp 'Continue? Type INSTALL: ' OK
e59b7f7cc6archadmin2026-10-1123 [[ "$OK" == INSTALL ]] || abort 'Cancelled'
e59b7f7cc6archadmin2026-10-1124 export DEBIAN_FRONTEND=noninteractive
e59b7f7cc6archadmin2026-10-1125 apt-get update
e59b7f7cc6archadmin2026-10-1126 apt-get install -y ca-certificates curl gnupg git rsync sqlite3 caddy msmtp python3
e59b7f7cc6archadmin2026-10-1127 NODE_MAJOR=$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)
e59b7f7cc6archadmin2026-10-1128 if (( NODE_MAJOR < 24 )); then
e59b7f7cc6archadmin2026-10-1129  echo '[*] Installing Node.js 24 from the signed NodeSource apt repository'
e59b7f7cc6archadmin2026-10-1130  install -d -m 755 /etc/apt/keyrings
e59b7f7cc6archadmin2026-10-1131  curl --fail --silent --show-error --location https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor --yes -o /etc/apt/keyrings/nodesource.gpg
e59b7f7cc6archadmin2026-10-1132  chmod 644 /etc/apt/keyrings/nodesource.gpg
e59b7f7cc6archadmin2026-10-1133  ARCH=$(dpkg --print-architecture)
e59b7f7cc6archadmin2026-10-1134  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
e59b7f7cc6archadmin2026-10-1135  apt-get update
e59b7f7cc6archadmin2026-10-1136  apt-get install -y nodejs
e59b7f7cc6archadmin2026-10-1137 fi
e59b7f7cc6archadmin2026-10-1138 NODE_MAJOR=$(node -p 'Number(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)
e59b7f7cc6archadmin2026-10-1139 (( NODE_MAJOR >= 24 )) || abort 'Node.js 24+ is required'
e59b7f7cc6archadmin2026-10-1140 command -v caddy >/dev/null || abort 'Caddy installation failed'
e59b7f7cc6archadmin2026-10-1141 command -v sqlite3 >/dev/null || abort 'SQLite installation failed'
e59b7f7cc6archadmin2026-10-1142 command -v msmtp >/dev/null || abort 'msmtp installation failed'
e59b7f7cc6archadmin2026-10-1143 if ! id -u archforge-status >/dev/null 2>&1; then
e59b7f7cc6archadmin2026-10-1144  useradd --system --user-group --home-dir /var/lib/archforge-status --shell /usr/sbin/nologin archforge-status
e59b7f7cc6archadmin2026-10-1145 fi
e59b7f7cc6archadmin2026-10-1146 install -d -m 755 /opt/archforge-status /opt/archforge-status/app
e59b7f7cc6archadmin2026-10-1147 install -d -o archforge-status -g archforge-status -m 750 /var/lib/archforge-status
e59b7f7cc6archadmin2026-10-1148 install -d -m 750 /etc/archforge-status
e59b7f7cc6archadmin2026-10-1149 chown root:archforge-status /etc/archforge-status
e59b7f7cc6archadmin2026-10-1150 install -d -m 700 /var/backups/archforge-status
e59b7f7cc6archadmin2026-10-1151 rsync -a --delete --exclude='.git' --exclude='.env' --exclude='data' --exclude='node_modules' --exclude='backups' --chmod=D755,F644 "$SRC/" /opt/archforge-status/app/
e59b7f7cc6archadmin2026-10-1152 chown -R root:root /opt/archforge-status/app
e59b7f7cc6archadmin2026-10-1153 echo '[*] Running package validation before service installation'
e59b7f7cc6archadmin2026-10-1154 (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'
e59b7f7cc6archadmin2026-10-1155 chmod 755 /opt/archforge-status /opt/archforge-status/app
e59b7f7cc6archadmin2026-10-1156 printf '%s\n' "$SRC" > /etc/archforge-status/source-path
e59b7f7cc6archadmin2026-10-1157 chmod 600 /etc/archforge-status/source-path
e59b7f7cc6archadmin2026-10-1158 CFG=/etc/archforge-status/status.env
e59b7f7cc6archadmin2026-10-1159 if [[ -f "$CFG" ]]; then
e59b7f7cc6archadmin2026-10-1160  echo '[*] Existing configuration preserved (no secret rotation)'
e59b7f7cc6archadmin2026-10-1161 else
e59b7f7cc6archadmin2026-10-1162  echo
e59b7f7cc6archadmin2026-10-1163  echo '--- Domain and monitored targets ---'
e59b7f7cc6archadmin2026-10-1164  read -rp 'Public status domain [status.archforge.nl]: ' DOMAIN; DOMAIN=${DOMAIN:-status.archforge.nl}
e59b7f7cc6archadmin2026-10-1165  [[ "$DOMAIN" =~ ^[a-zA-Z0-9]([a-zA-Z0-9.-]{0,251}[a-zA-Z0-9])?$ && "$DOMAIN" == *.* ]] || abort 'Invalid domain'
e59b7f7cc6archadmin2026-10-1166  read -rp 'Main website HTTPS URL [https://archforge.nl]: ' WEBSITE; WEBSITE=${WEBSITE:-https://archforge.nl}
e59b7f7cc6archadmin2026-10-1167  [[ "$WEBSITE" =~ ^https://[a-zA-Z0-9.-]+/?$ ]] || abort 'Use https://example.com for website'
e59b7f7cc6archadmin2026-10-1168  read -rp 'Optional public API URL to monitor (Enter to disable): ' API
2109dd33b8archadmin2026-10-1269  read -rp 'Public ArchForge HTTPS URL ending /info/refs (leave empty to disable): ' GIT_HTTP
2109dd33b8archadmin2026-10-1270  read -rp 'ArchForge SSH host [archforge.nl]: ' SSH_HOST; SSH_HOST=${SSH_HOST:-archforge.nl}
2109dd33b8archadmin2026-10-1271  [[ "$SSH_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid ArchForge SSH host'
2109dd33b8archadmin2026-10-1272  read -rp 'ArchForge SSH port [22]: ' SSH_PORT; SSH_PORT=${SSH_PORT:-22}
e59b7f7cc6archadmin2026-10-1173  [[ "$SSH_PORT" =~ ^[0-9]{1,5}$ && "$SSH_PORT" -ge 1 && "$SSH_PORT" -le 65535 ]] || abort 'Invalid SSH port'
e59b7f7cc6archadmin2026-10-1174  echo
e59b7f7cc6archadmin2026-10-1175  echo '--- Notifications (optional, configurable later) ---'
e59b7f7cc6archadmin2026-10-1176  read -rp 'Discord webhook URL (Enter to skip): ' DISCORD
e59b7f7cc6archadmin2026-10-1177  [[ -z "$DISCORD" || "$DISCORD" =~ ^https://(discord.com|discordapp.com)/api/webhooks/ ]] || abort 'Discord URL must be an official webhook URL'
e59b7f7cc6archadmin2026-10-1178  read -rp 'SMTP server hostname (Enter to disable email): ' SMTP_HOST
e59b7f7cc6archadmin2026-10-1179  SMTP_PORT=587 SMTP_USER='' SMTP_PASS='' SMTP_FROM=''
e59b7f7cc6archadmin2026-10-1180  if [[ -n "$SMTP_HOST" ]]; then
e59b7f7cc6archadmin2026-10-1181  [[ "$SMTP_HOST" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid SMTP host'
e59b7f7cc6archadmin2026-10-1182  read -rp 'SMTP port [587]: ' SMTP_PORT; SMTP_PORT=${SMTP_PORT:-587}
e59b7f7cc6archadmin2026-10-1183  [[ "$SMTP_PORT" =~ ^[0-9]{1,5}$ && "$SMTP_PORT" -ge 1 && "$SMTP_PORT" -le 65535 ]] || abort 'Invalid SMTP port'
e59b7f7cc6archadmin2026-10-1184  read -rp 'SMTP username: ' SMTP_USER
e59b7f7cc6archadmin2026-10-1185  read -rsp 'SMTP password (hidden): ' SMTP_PASS;printf '\n'
e59b7f7cc6archadmin2026-10-1186  read -rp 'From email (e.g. status@archforge.nl): ' SMTP_FROM
e59b7f7cc6archadmin2026-10-1187  [[ "$SMTP_FROM" == *@* && "$SMTP_FROM" != *$'\n'* ]] || abort 'Invalid sender address'
e59b7f7cc6archadmin2026-10-1188  fi
e59b7f7cc6archadmin2026-10-1189  KEY=$(od -An -tx1 -N32 /dev/urandom | tr -d ' \n')
e59b7f7cc6archadmin2026-10-1190  # Never source this file as shell. Values are read as literal NAME=VALUE pairs by Node.
e59b7f7cc6archadmin2026-10-1191  (umask 077; cat > "$CFG" <<ENVFILE
e59b7f7cc6archadmin2026-10-1192 NODE_ENV=production
e59b7f7cc6archadmin2026-10-1193 PORT=3000
e59b7f7cc6archadmin2026-10-1194 STATUS_LISTEN_HOST=127.0.0.1
e59b7f7cc6archadmin2026-10-1195 STATUS_PUBLIC_URL=https://${DOMAIN}
e59b7f7cc6archadmin2026-10-1196 TARGET_WEBSITE=${WEBSITE}
e59b7f7cc6archadmin2026-10-1197 TARGET_API_URL=${API}
e59b7f7cc6archadmin2026-10-1198 TARGET_GIT_HTTP_URL=${GIT_HTTP}
e59b7f7cc6archadmin2026-10-1199 TARGET_GIT_SSH_HOST=${SSH_HOST}
e59b7f7cc6archadmin2026-10-11100 TARGET_GIT_SSH_PORT=${SSH_PORT}
e59b7f7cc6archadmin2026-10-11101 MONITOR_INTERVAL_SECONDS=60
e59b7f7cc6archadmin2026-10-11102 MONITOR_FAILURE_THRESHOLD=3
e59b7f7cc6archadmin2026-10-11103 MONITOR_RECOVERY_THRESHOLD=2
e59b7f7cc6archadmin2026-10-11104 MONITOR_TIMEOUT_MS=8000
e59b7f7cc6archadmin2026-10-11105 STATUS_DISCORD_WEBHOOK_URL=${DISCORD}
e59b7f7cc6archadmin2026-10-11106 SMTP_HOST=${SMTP_HOST}
e59b7f7cc6archadmin2026-10-11107 SMTP_PORT=${SMTP_PORT}
e59b7f7cc6archadmin2026-10-11108 SMTP_USER=${SMTP_USER}
e59b7f7cc6archadmin2026-10-11109 SMTP_PASSWORD=${SMTP_PASS}
e59b7f7cc6archadmin2026-10-11110 SMTP_FROM=${SMTP_FROM}
e59b7f7cc6archadmin2026-10-11111 SMTP_TLS=on
e59b7f7cc6archadmin2026-10-11112 STATUS_ENCRYPTION_KEY=${KEY}
e59b7f7cc6archadmin2026-10-11113 STATUS_DB_PATH=/var/lib/archforge-status/status.db
e59b7f7cc6archadmin2026-10-11114 ENVFILE
e59b7f7cc6archadmin2026-10-11115  )
e59b7f7cc6archadmin2026-10-11116  chown root:archforge-status "$CFG"
e59b7f7cc6archadmin2026-10-11117  chmod 640 "$CFG"
e59b7f7cc6archadmin2026-10-11118  unset KEY SMTP_PASS
e59b7f7cc6archadmin2026-10-11119 fi
2109dd33b8archadmin2026-10-12120 # Generate msmtp config from status.env only if enabled. Secrets never go into ArchForge.
e59b7f7cc6archadmin2026-10-11121 # Read settings without sourcing the dotenv file.
e59b7f7cc6archadmin2026-10-11122 get_conf(){ grep -m1 "^${1}=" "$CFG" | cut -d= -f2- || true; }
e59b7f7cc6archadmin2026-10-11123 SMTP_HOST_VALUE=$(get_conf SMTP_HOST)
e59b7f7cc6archadmin2026-10-11124 if [[ -n "$SMTP_HOST_VALUE" ]]; then
e59b7f7cc6archadmin2026-10-11125  smtp_quote(){ local x=$1;x=${x//\\/\\\\};x=${x//\"/\\\"};printf '"%s"' "$x"; }
e59b7f7cc6archadmin2026-10-11126  SMTP_P=$(get_conf SMTP_PORT);SMTP_U=$(get_conf SMTP_USER);SMTP_W=$(get_conf SMTP_PASSWORD);SMTP_F=$(get_conf SMTP_FROM)
e59b7f7cc6archadmin2026-10-11127  [[ "$SMTP_P" =~ ^[0-9]+$ ]] || abort 'Invalid SMTP port'
e59b7f7cc6archadmin2026-10-11128  {
e59b7f7cc6archadmin2026-10-11129  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'
e59b7f7cc6archadmin2026-10-11130  printf 'host %s\n' "$(smtp_quote "$SMTP_HOST_VALUE")"
e59b7f7cc6archadmin2026-10-11131  printf 'port %s\n' "$SMTP_P"
e59b7f7cc6archadmin2026-10-11132  printf 'from %s\n' "$(smtp_quote "$SMTP_F")"
e59b7f7cc6archadmin2026-10-11133  printf 'user %s\n' "$(smtp_quote "$SMTP_U")"
e59b7f7cc6archadmin2026-10-11134  printf 'password %s\n' "$(smtp_quote "$SMTP_W")"
e59b7f7cc6archadmin2026-10-11135  echo 'account default : status'
e59b7f7cc6archadmin2026-10-11136  } >/etc/archforge-status/msmtprc
e59b7f7cc6archadmin2026-10-11137  chown archforge-status:archforge-status /etc/archforge-status/msmtprc
e59b7f7cc6archadmin2026-10-11138  chmod 600 /etc/archforge-status/msmtprc
e59b7f7cc6archadmin2026-10-11139  unset SMTP_W
e59b7f7cc6archadmin2026-10-11140 else
e59b7f7cc6archadmin2026-10-11141  echo '[*] SMTP not configured. Email subscriptions will be disabled until configured.'
e59b7f7cc6archadmin2026-10-11142 fi
e59b7f7cc6archadmin2026-10-11143 install -m 644 "$SRC/scripts/archforge-status.service" /etc/systemd/system/archforge-status.service
e59b7f7cc6archadmin2026-10-11144 install -m 644 "$SRC/scripts/archforge-status-backup.service" /etc/systemd/system/archforge-status-backup.service
e59b7f7cc6archadmin2026-10-11145 install -m 644 "$SRC/scripts/archforge-status-backup.timer" /etc/systemd/system/archforge-status-backup.timer
e59b7f7cc6archadmin2026-10-11146 install -m 755 "$SRC/scripts/backup.sh" /usr/local/sbin/archforge-status-backup
e59b7f7cc6archadmin2026-10-11147 DOMAIN=$(get_conf STATUS_PUBLIC_URL);DOMAIN=${DOMAIN#https://};DOMAIN=${DOMAIN%%/*}
e59b7f7cc6archadmin2026-10-11148 [[ "$DOMAIN" =~ ^[a-zA-Z0-9.-]+$ ]] || abort 'Invalid domain in saved config'
e59b7f7cc6archadmin2026-10-11149 mkdir -p /etc/caddy/conf.d
e59b7f7cc6archadmin2026-10-11150 cat > /etc/caddy/conf.d/archforge-status.caddy <<CADDYFILE
e59b7f7cc6archadmin2026-10-11151 ${DOMAIN} {
e59b7f7cc6archadmin2026-10-11152  encode zstd gzip
e59b7f7cc6archadmin2026-10-11153  reverse_proxy 127.0.0.1:3000
e59b7f7cc6archadmin2026-10-11154 }
e59b7f7cc6archadmin2026-10-11155 CADDYFILE
e59b7f7cc6archadmin2026-10-11156 if ! grep -Fq 'import /etc/caddy/conf.d/*.caddy' /etc/caddy/Caddyfile; then
e59b7f7cc6archadmin2026-10-11157  cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.backup.$(date +%s)"
e59b7f7cc6archadmin2026-10-11158  printf '\nimport /etc/caddy/conf.d/*.caddy\n' >> /etc/caddy/Caddyfile
e59b7f7cc6archadmin2026-10-11159 fi
e59b7f7cc6archadmin2026-10-11160 caddy validate --config /etc/caddy/Caddyfile || abort 'Caddy configuration is invalid; inspect /etc/caddy/Caddyfile and its backup'
e59b7f7cc6archadmin2026-10-11161 systemctl daemon-reload
e59b7f7cc6archadmin2026-10-11162 # Create database and initial 2FA admin before starting the daemon.
e59b7f7cc6archadmin2026-10-11163 echo
e59b7f7cc6archadmin2026-10-11164 printf '\n--- Admin account + 2FA ---\n'
e59b7f7cc6archadmin2026-10-11165 cd /opt/archforge-status/app
e59b7f7cc6archadmin2026-10-11166 runuser -u archforge-status -- env STATUS_ENV_FILE="$CFG" NODE_ENV=production /usr/bin/node src/cli.js create-admin
e59b7f7cc6archadmin2026-10-11167 systemctl enable --now archforge-status.service
e59b7f7cc6archadmin2026-10-11168 systemctl enable --now archforge-status-backup.timer
e59b7f7cc6archadmin2026-10-11169 systemctl enable --now caddy
e59b7f7cc6archadmin2026-10-11170 systemctl reload caddy || systemctl restart caddy
e59b7f7cc6archadmin2026-10-11171 sleep 1
e59b7f7cc6archadmin2026-10-11172 systemctl is-active --quiet archforge-status || abort 'Service did not start: sudo journalctl -u archforge-status -n 50'
e59b7f7cc6archadmin2026-10-11173 echo
e59b7f7cc6archadmin2026-10-11174 printf '══════════════════════════════════════════════\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"
2109dd33b8archadmin2026-10-12175 echo 'For a guided domain and SSL check, run: sudo bash ./configure-domain.sh from your ArchForge checkout.'