#!/usr/bin/env bash # HostingOS node installer — served at https://get-hostingos.dhakatek.net # Usage (from Admin → Servers → Enroll new server): # curl -fsSL https://get-hostingos.dhakatek.net | sudo bash -s -- --token hn_xxx [--server https://api-hostingos.dhakatek.net] [--hostname node2.example.com] [--ip 203.0.113.5] set -euo pipefail SERVER="https://api-hostingos.dhakatek.net" GET_URL="https://get-hostingos.dhakatek.net" TOKEN="" NODE_HOSTNAME="" NODE_IP="" DRIVER="local" SKIP_STACK=0 while [ $# -gt 0 ]; do case "$1" in --token) TOKEN="$2"; shift 2 ;; --server) SERVER="${2%/}"; shift 2 ;; --hostname) NODE_HOSTNAME="$2"; shift 2 ;; --ip) NODE_IP="$2"; shift 2 ;; --driver) DRIVER="$2"; shift 2 ;; --skip-stack) SKIP_STACK=1; shift ;; *) echo "unknown option: $1" >&2; exit 2 ;; esac done log() { printf '\033[1;36m==> %s\033[0m\n' "$*"; } die() { printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || die "run as root (sudo)." [ -n "$TOKEN" ] || die "--token is required (generate one in Admin → Servers → Enroll new server)." command -v apt-get >/dev/null || die "only Debian-family systems are supported." . /etc/os-release case "${ID:-}:${VERSION_ID:-}" in debian:13) ;; ubuntu:26.04) log "Ubuntu 26.04 is best-effort; Debian 13 is the supported OS." ;; *) die "unsupported OS: ${PRETTY_NAME:-unknown}. HostingOS nodes run Debian 13." ;; esac ARCH=$(dpkg --print-architecture) case "$ARCH" in amd64|arm64) ;; *) die "unsupported architecture $ARCH";; esac for svc in nginx apache2 php-fpm mysqld; do if [ "$SKIP_STACK" -eq 0 ] && systemctl is-active --quiet "$svc" 2>/dev/null && [ ! -f /etc/hostingos/agent.json ]; then die "$svc is already running. HostingOS needs a clean server (or pass --skip-stack to only install the agent)." fi done export DEBIAN_FRONTEND=noninteractive CODENAME=$(. /etc/os-release; echo "${VERSION_CODENAME}") if [ "$SKIP_STACK" -eq 0 ]; then log "Installing base packages" apt-get update -qq apt-get install -y -qq curl gnupg2 ca-certificates lsb-release bzip2 cron quota unzip zip git rsync clamav clamav-freshclam fail2ban >/dev/null log "Adding vendor repositories (nginx.org, packages.sury.org, mariadb.org)" # --batch --yes: without them gpg asks before replacing an existing keyring, and the question # goes to a terminal that a remote installation does not have, so a re-run would hang or fail. curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --batch --yes --dearmor -o /usr/share/keyrings/nginx-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/debian $CODENAME nginx" > /etc/apt/sources.list.d/nginx.list curl -fsSL -o /usr/share/keyrings/deb.sury.org-php.gpg https://packages.sury.org/php/apt.gpg echo "deb [signed-by=/usr/share/keyrings/deb.sury.org-php.gpg] https://packages.sury.org/php/ $CODENAME main" > /etc/apt/sources.list.d/php.list curl -fsSL -o /usr/share/keyrings/mariadb-keyring.pgp https://mariadb.org/mariadb_release_signing_key.pgp echo "deb [signed-by=/usr/share/keyrings/mariadb-keyring.pgp] https://deb.mariadb.org/12.3/debian $CODENAME main" > /etc/apt/sources.list.d/mariadb.list if ! apt-get update -qq 2>/dev/null; then rm -f /etc/apt/sources.list.d/mariadb.list; apt-get update -qq; log "mariadb.org has no $CODENAME suite; using Debian's MariaDB"; fi log "Installing nginx, PHP 8.5/8.4, MariaDB, certbot" # Required: the node cannot host anything without these. apt-get install -y -qq nginx certbot php8.5-fpm php8.5-cli php8.4-fpm php8.4-cli mariadb-server >/dev/null # Extensions are installed one at a time. A vendor repository renames or folds packages from time # to time — opcache is built into PHP 8.5, for instance, so php8.5-opcache no longer exists — and # a single missing extension must not abort the installation of a whole server. missing="" for pkg in \ php8.5-mysql php8.5-pgsql php8.5-curl php8.5-gd php8.5-mbstring php8.5-xml php8.5-zip php8.5-intl php8.5-bcmath php8.5-opcache \ php8.4-mysql php8.4-pgsql php8.4-curl php8.4-gd php8.4-mbstring php8.4-xml php8.4-zip php8.4-intl php8.4-bcmath php8.4-opcache; do apt-get install -y -qq "$pkg" >/dev/null 2>&1 || missing="$missing $pkg" done [ -z "$missing" ] || log "not available in this repository, skipped:$missing" systemctl enable --now nginx php8.5-fpm php8.4-fpm mariadb cron >/dev/null 2>&1 || true log "Blocking repeated failed logins and probes" cat > /etc/fail2ban/jail.d/hostingos.local <<'JAIL' [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 backend = systemd [sshd] enabled = true [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/access.log JAIL systemctl enable --now fail2ban >/dev/null 2>&1 || true # A hosting node with no swap has no margin at all: one busy site can take the machine down # instead of slowing down. if [ "$(free -b | awk '/Swap/{print $2}')" = "0" ]; then log "Adding 2 GB of swap" fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile >/dev/null && swapon /swapfile grep -q '^/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab fi log "Installing restic 0.19.1" RESTIC_VER=0.19.1 curl -fsSL -o /tmp/restic.bz2 "https://github.com/restic/restic/releases/download/v${RESTIC_VER}/restic_${RESTIC_VER}_linux_${ARCH}.bz2" curl -fsSL -o /tmp/SHA256SUMS "https://github.com/restic/restic/releases/download/v${RESTIC_VER}/SHA256SUMS" (cd /tmp && grep "restic_${RESTIC_VER}_linux_${ARCH}.bz2" SHA256SUMS | sed "s/restic_${RESTIC_VER}_linux_${ARCH}.bz2/restic.bz2/" | sha256sum -c - >/dev/null) bzip2 -dkf /tmp/restic.bz2 && install -m 0755 /tmp/restic /usr/local/bin/restic install -d -m 0755 /etc/hostingos /etc/nginx/conf.d/sites /var/www/letsencrypt /var/lib/hostingos/backups [ -f /etc/hostingos/restic.pass ] || { openssl rand -hex 32 > /etc/hostingos/restic.pass; chmod 0600 /etc/hostingos/restic.pass; } echo "include /etc/nginx/conf.d/sites/*.conf;" > /etc/nginx/conf.d/00-hostingos-sites.conf rm -f /etc/nginx/conf.d/default.conf cat > /etc/nginx/conf.d/01-hostingos-default.conf <<'NGX' server { listen 80 default_server; listen [::]:80 default_server; server_name _; location ^~ /.well-known/acme-challenge/ { root /var/www/letsencrypt; default_type text/plain; } location / { return 444; } } NGX nginx -t >/dev/null && systemctl reload nginx fi log "Installing hostingos-agent" install -d -m 0755 /etc/hostingos curl -fsSL -o /tmp/hostingos-agent "$GET_URL/bin/hostingos-agent-linux-$ARCH" curl -fsSL -o /tmp/hostingos-agent.sha256 "$GET_URL/bin/hostingos-agent-linux-$ARCH.sha256" (cd /tmp && echo "$(cut -d' ' -f1 hostingos-agent.sha256) hostingos-agent" | sha256sum -c - >/dev/null) || die "agent checksum mismatch" install -m 0755 /tmp/hostingos-agent /usr/local/bin/hostingos-agent curl -fsSL -o /etc/systemd/system/hostingos-agent.service "$GET_URL/hostingos-agent.service" log "Enrolling with $SERVER" ENROLL_ARGS=(--server "$SERVER" --token "$TOKEN" --driver "$DRIVER") [ -n "$NODE_HOSTNAME" ] && ENROLL_ARGS+=(--hostname "$NODE_HOSTNAME") [ -n "$NODE_IP" ] && ENROLL_ARGS+=(--ip "$NODE_IP") /usr/local/bin/hostingos-agent enroll "${ENROLL_ARGS[@]}" systemctl daemon-reload systemctl enable --now hostingos-agent >/dev/null sleep 2 if systemctl is-active --quiet hostingos-agent; then log "Done. This server is now a HostingOS node ($(/usr/local/bin/hostingos-agent version)). It will appear online in Admin → Servers within 30 seconds." else journalctl -u hostingos-agent --no-pager -n 20 die "hostingos-agent failed to start; see the log above." fi