#!/bin/bash set -eo pipefail # WebsiteBox Install Script # Bootstrap script: installs Docker, clones repo, runs setup wizard # Usage: curl -fsSL /install.sh | bash # --- Colors and formatting --- BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m' RED='\033[1;31m' GREEN='\033[1;32m' YELLOW='\033[1;33m' ORANGE='\033[1;38;5;208m' CYAN='\033[1;36m' WHITE='\033[1;37m' header() { printf "\n${WHITE}═══════════════════════════════════════════════════════════${RESET}\n ${BOLD}%s${RESET}\n${WHITE}═══════════════════════════════════════════════════════════${RESET}\n\n" "$1"; } section() { printf "\n${CYAN}───────────────────────────────────────────────────────────${RESET}\n ${BOLD}%s${RESET}\n${CYAN}───────────────────────────────────────────────────────────${RESET}\n" "$1"; } step() { printf "${GREEN}▸${RESET} ${BOLD}%s${RESET}\n" "$1"; } info() { printf "${ORANGE} %s${RESET}\n" "$1"; } ok() { printf "${GREEN} ✓ %s${RESET}\n" "$1"; } warn() { printf "${YELLOW} ⚠ %s${RESET}\n" "$1"; } err() { printf "${RED} ✗ %s${RESET}\n" "$1"; } # --- Rolling preview helper --- # Shows last 4 lines of output in-place, then clears when done. # Keeps the terminal clean while still showing live progress. show_progress() { local n=4 buffer=() count=0 while IFS= read -r line; do buffer+=("$line") (( count++ )) || true if [ ${#buffer[@]} -gt $n ]; then buffer=("${buffer[@]:1}") fi if [ $count -gt 1 ]; then printf '\033[%dA' "${#buffer[@]}" 2>/dev/null || true fi for l in "${buffer[@]}"; do printf "\033[2K${DIM} %.60s${RESET}\n" "$l" done done # clear preview lines when done if [ ${#buffer[@]} -gt 0 ]; then printf '\033[%dA' "${#buffer[@]}" 2>/dev/null || true for _ in "${buffer[@]}"; do printf '\033[2K\n'; done printf '\033[%dA' "${#buffer[@]}" 2>/dev/null || true fi } header "WebsiteBox Installer" # --- Check for root/sudo --- if [ "$(id -u)" -eq 0 ]; then SUDO="" ACTUAL_USER="${SUDO_USER:-root}" else if ! command -v sudo &>/dev/null; then err "This script requires root or sudo access." exit 1 fi SUDO="sudo" ACTUAL_USER="$(whoami)" fi # --- Detect OS --- if [ -f /etc/os-release ]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" else err "Cannot detect OS. /etc/os-release not found." err "WebsiteBox supports Ubuntu 20.04+ and Debian 11+." exit 1 fi case "$OS_ID" in ubuntu) if [ "${OS_VERSION%%.*}" -lt 20 ]; then err "Ubuntu 20.04 or later is required (detected: ${OS_VERSION})." exit 1 fi ;; debian) if [ "${OS_VERSION%%.*}" -lt 11 ]; then err "Debian 11 or later is required (detected: ${OS_VERSION})." exit 1 fi ;; *) warn "Unsupported OS detected (${OS_ID} ${OS_VERSION})." warn "WebsiteBox is tested on Ubuntu 20.04+ and Debian 11+." read -rp "Continue anyway? (y/N) " cont < /dev/tty if [ "$cont" != "y" ] && [ "$cont" != "Y" ]; then exit 1 fi ;; esac ok "Detected: ${OS_ID} ${OS_VERSION}" # --- Secure the Server --- section "Securing your server" step "Updating system packages..." info "Downloading the latest security patches for your operating system." info "On a fresh server this can take 2-10 minutes. Sit tight." $SUDO apt-get update -qq DEBIAN_FRONTEND=noninteractive $SUDO apt-get upgrade -y -o Dpkg::Options::="--force-confold" 2>&1 | show_progress ok "System packages updated." step "Installing security tools (firewall, fail2ban, auto-updates)..." info "These protect your server from common attacks. Usually under a minute." DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y ufw fail2ban unattended-upgrades 2>&1 | show_progress # Configure firewall — allow SSH first to avoid lockout if ! $SUDO ufw status | grep -q "Status: active"; then step "Configuring firewall..." $SUDO ufw allow OpenSSH > /dev/null 2>&1 $SUDO ufw allow 80/tcp > /dev/null 2>&1 $SUDO ufw allow 443/tcp > /dev/null 2>&1 $SUDO ufw --force enable > /dev/null 2>&1 ok "Firewall enabled: SSH, HTTP, and HTTPS allowed. All other ports blocked." else # Firewall already active — just make sure our ports are open $SUDO ufw allow OpenSSH 2>/dev/null || true $SUDO ufw allow 80/tcp 2>/dev/null || true $SUDO ufw allow 443/tcp 2>/dev/null || true ok "Firewall already active. Verified SSH, HTTP, and HTTPS are allowed." fi # Enable fail2ban $SUDO systemctl enable fail2ban --quiet 2>/dev/null || true $SUDO systemctl start fail2ban 2>/dev/null || true ok "Fail2ban enabled: brute-force SSH protection active." # Enable automatic security updates (non-interactive) echo 'Unattended-Upgrade::Allowed-Origins { "${distro_id}:${distro_codename}"; "${distro_id}:${distro_codename}-security"; "${distro_id}ESMApps:${distro_codename}-apps-security"; "${distro_id}ESM:${distro_codename}-infra-security"; }; Unattended-Upgrade::AutoFixInterruptedDpkg "true"; Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; Unattended-Upgrade::Remove-Unused-Dependencies "true";' | $SUDO tee /etc/apt/apt.conf.d/50unattended-upgrades-websitebox > /dev/null echo 'APT::Periodic::Update-Package-Lists "1"; APT::Periodic::Unattended-Upgrade "1";' | $SUDO tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null ok "Automatic security updates enabled." # Configure Docker log rotation (create config before Docker install) $SUDO mkdir -p /etc/docker if [ ! -f /etc/docker/daemon.json ]; then echo '{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }' | $SUDO tee /etc/docker/daemon.json > /dev/null ok "Docker log rotation configured (10MB max per log, 3 files per container)." else ok "Docker daemon.json already exists. Skipping log rotation config." fi printf "\n${GREEN} Server secured:${RESET}\n" printf " ${BOLD}Firewall:${RESET} active (SSH, HTTP, HTTPS only)\n" printf " ${BOLD}Fail2ban:${RESET} active (SSH brute-force protection)\n" printf " ${BOLD}Auto-updates:${RESET} enabled (daily security patches)\n" printf " ${BOLD}Docker log limits:${RESET} configured (30MB max per container)\n\n" # --- Install Docker if needed --- DOCKER_JUST_INSTALLED=false if command -v docker &>/dev/null; then ok "Docker is already installed." # Restart to pick up daemon.json if it was just created if [ -f /etc/docker/daemon.json ]; then $SUDO systemctl restart docker 2>/dev/null || true fi else section "Installing Docker" step "Installing Docker..." info "Docker packages and runs your website in isolated containers." info "This is the largest download — usually takes 1-3 minutes." # Install prerequisites $SUDO apt-get update -qq DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y ca-certificates curl gnupg lsb-release 2>&1 | show_progress # Add Docker's GPG key $SUDO install -m 0755 -d /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/${OS_ID}/gpg" | $SUDO gpg --dearmor -o /etc/apt/keyrings/docker.gpg $SUDO chmod a+r /etc/apt/keyrings/docker.gpg # Add Docker repository echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${OS_ID} \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ $SUDO tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker $SUDO apt-get update -qq DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin 2>&1 | show_progress # Add user to docker group if [ "$ACTUAL_USER" != "root" ]; then $SUDO usermod -aG docker "$ACTUAL_USER" DOCKER_JUST_INSTALLED=true ok "Docker installed. User '${ACTUAL_USER}' added to docker group." fi # Start and enable Docker $SUDO systemctl start docker $SUDO systemctl enable docker ok "Docker installation complete." fi # --- Clone Repository --- INSTALL_DIR="${HOME}/websitebox" if [ -d "$INSTALL_DIR" ]; then ok "WebsiteBox directory already exists at ${INSTALL_DIR}" info "Pulling latest changes..." cd "$INSTALL_DIR" git pull || true else step "Cloning WebsiteBox..." info "Downloading the project files. Just a few seconds." git clone https://git.constantprojects.xyz/tankadmin/websitebox.git "$INSTALL_DIR" cd "$INSTALL_DIR" fi # Make scripts executable chmod +x setup.sh install.sh scripts/*.sh # --- Install shell alias --- # Adds "websitebox" command that auto-cds into the project directory. # Usage: websitebox up, websitebox logs, websitebox down, etc. SHELL_RC="${HOME}/.bashrc" [ -f "${HOME}/.zshrc" ] && SHELL_RC="${HOME}/.zshrc" if ! grep -qF 'websitebox()' "$SHELL_RC" 2>/dev/null; then cat >> "$SHELL_RC" <<'ALIASEOF' # WebsiteBox shortcut websitebox() { cd ~/websitebox || return case "$1" in up) shift; docker compose up -d --build "$@" ;; logs) shift; docker compose logs -f "$@" ;; *) docker compose "$@" ;; esac } ALIASEOF ok "Added 'websitebox' command" fi # --- Run Setup Wizard --- echo "" if [ "$DOCKER_JUST_INSTALLED" = true ] && [ "$ACTUAL_USER" != "root" ]; then # Activate docker group for this session without requiring logout/login info "Activating Docker permissions for current session..." sg docker -c "./setup.sh < /dev/tty" else ./setup.sh < /dev/tty fi echo "" printf "${ORANGE} Tip:${RESET} You can manage your site from anywhere using the ${BOLD}websitebox${RESET} command:\n" printf "${DIM} websitebox up ${RESET}${DIM}— start your site${RESET}\n" printf "${DIM} websitebox down ${RESET}${DIM}— stop your site${RESET}\n" printf "${DIM} websitebox logs ${RESET}${DIM}— watch live logs${RESET}\n" printf "${DIM} websitebox logs nginx ${RESET}${DIM}— logs for one service${RESET}\n" printf "${DIM} websitebox restart nginx ${RESET}${DIM}— restart a service${RESET}\n" echo "" info "Run this now to activate the websitebox command:" printf " ${CYAN}source ${SHELL_RC}${RESET}\n" echo "" warn "If 'docker compose' commands fail later, log out and back in" warn "to permanently activate Docker permissions, then try again." echo ""