The exec < /dev/tty approach silently killed the script under set -e if /dev/tty wasn't accessible. Replace with surgical redirects on just the interactive reads and setup.sh invocation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
262 lines
9.5 KiB
Bash
Executable File
262 lines
9.5 KiB
Bash
Executable File
#!/bin/bash
|
|
set -eo pipefail
|
|
|
|
# WebsiteBox Install Script
|
|
# Bootstrap script: installs Docker, clones repo, runs setup wizard
|
|
# Usage: curl -fsSL <url>/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
|
|
|
|
# --- 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 ""
|
|
warn "If 'docker compose' commands fail later, log out and back in"
|
|
warn "to permanently activate Docker permissions, then try again."
|
|
echo ""
|