automated-setup/script/setup.sh

664 lines
23 KiB
Bash
Raw Normal View History

#!/bin/bash
#
# LetsBe Server Setup Script
# This script sets up the server and deploys selected tools.
#
# Usage:
# ./setup.sh --tools "all" --domain "example.com"
# ./setup.sh --tools "portainer,n8n,baserow" --domain "example.com"
# ./setup.sh --tools "1,2,3"
# ./setup.sh # Foundation only, no tools deployed
#
# Arguments:
# --tools Comma-separated list of tools to deploy, "all", or tool numbers
# --domain Domain name for SSL email (administrator@domain)
# --skip-ssl Skip SSL certificate setup (useful for testing)
#
set -euo pipefail
# =============================================================================
# ARGUMENT PARSING
# =============================================================================
TOOLS_TO_DEPLOY=""
SKIP_SSL=false
ROOT_SSL=false
DOMAIN=""
# Docker registry authentication (optional)
DOCKER_USER=""
DOCKER_TOKEN=""
DOCKER_REGISTRY=""
# Gitea registry authentication (for private images from code.letsbe.solutions)
GITEA_REGISTRY=""
GITEA_USER=""
GITEA_TOKEN=""
while [[ $# -gt 0 ]]; do
case $1 in
--tools)
TOOLS_TO_DEPLOY="$2"
shift 2
;;
--domain)
DOMAIN="$2"
shift 2
;;
--skip-ssl)
SKIP_SSL=true
shift
;;
--root-ssl)
ROOT_SSL=true
shift
;;
--docker-user)
DOCKER_USER="$2"
shift 2
;;
--docker-token)
DOCKER_TOKEN="$2"
shift 2
;;
--docker-registry)
DOCKER_REGISTRY="$2"
shift 2
;;
--gitea-registry)
GITEA_REGISTRY="$2"
shift 2
;;
--gitea-user)
GITEA_USER="$2"
shift 2
;;
--gitea-token)
GITEA_TOKEN="$2"
shift 2
;;
--help|-h)
echo "Usage: $0 [--tools \"tool1,tool2,...\"|\"all\"] [--domain DOMAIN] [--skip-ssl] [--root-ssl]"
echo ""
echo "Options:"
echo " --tools Comma-separated list of tools, 'all', or tool numbers"
echo " --domain Domain name for SSL email (administrator@domain)"
echo " --skip-ssl Skip SSL certificate setup"
echo " --root-ssl Include root domain in SSL certificate"
echo " --docker-user Docker registry username (optional)"
echo " --docker-token Docker registry password/token (optional)"
echo " --docker-registry Docker registry URL (optional, defaults to Docker Hub)"
echo ""
echo "Examples:"
echo " $0 --tools \"all\" --domain \"example.com\""
echo " $0 --tools \"portainer,n8n,baserow\""
echo " $0 --tools \"1,5,10\""
echo " $0 # Foundation only"
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
echo "=== LetsBe Server Setup ==="
echo ""
# =============================================================================
# PACKAGE INSTALLATION
# =============================================================================
echo "[1/10] Installing system packages..."
sudo apt update && sudo apt upgrade -y
sudo apt install -y build-essential net-tools tree wget jq nano curl htop ufw fail2ban unattended-upgrades apt-listchanges apticron git gnupg ca-certificates apache2-utils acl certbot python3-certbot-nginx rsync rclone s3cmd zip sudo iptables htop dstat openssl
# =============================================================================
# DOCKER INSTALLATION
# =============================================================================
echo "[2/10] Installing Docker..."
sudo install -m 0755 -d /etc/apt/keyrings
# Use --batch and --yes for non-interactive gpg (required for nohup/background execution)
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg
sudo gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
rm -f /tmp/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
sudo echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable docker
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-linux-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod 755 /usr/local/bin/docker-compose
# Docker registry login (optional - for private registries or to bypass rate limits)
if [[ -n "$DOCKER_USER" && -n "$DOCKER_TOKEN" ]]; then
if [[ -n "$DOCKER_REGISTRY" ]]; then
echo "Logging into Docker registry: $DOCKER_REGISTRY..."
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USER" --password-stdin "$DOCKER_REGISTRY"
else
echo "Logging into Docker Hub..."
echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USER" --password-stdin
fi
fi
# Gitea registry login (for private images from code.letsbe.solutions)
if [[ -n "$GITEA_REGISTRY" && -n "$GITEA_USER" && -n "$GITEA_TOKEN" ]]; then
echo "Logging into Gitea registry: $GITEA_REGISTRY..."
echo "$GITEA_TOKEN" | docker login -u "$GITEA_USER" --password-stdin "$GITEA_REGISTRY"
fi
# =============================================================================
# DISABLE CONFLICTING SERVICES
# =============================================================================
echo "[3/10] Disabling conflicting services..."
sudo systemctl stop exim4 2>/dev/null || true
sudo systemctl disable exim4 2>/dev/null || true
sudo systemctl stop apache2 2>/dev/null || true
sudo systemctl disable apache2 2>/dev/null || true
sudo apt remove -y apache2 2>/dev/null || true
# =============================================================================
# NGINX INSTALLATION & CONFIGURATION
# =============================================================================
echo "[4/10] Installing and configuring nginx..."
sudo apt install -y nginx
sudo systemctl enable nginx
sudo rm -f /etc/nginx/sites-enabled/default
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout /etc/nginx/placeholder.key -out /etc/nginx/placeholder.crt
cat <<EOF > /etc/nginx/conf.d/fallback.conf
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
server_name _;
return 444;
ssl_certificate /etc/nginx/placeholder.crt;
ssl_certificate_key /etc/nginx/placeholder.key;
}
EOF
sudo systemctl restart nginx
# =============================================================================
# FIREWALL CONFIGURATION
# =============================================================================
echo "[5/10] Configuring UFW firewall..."
ufw allow 22
ufw allow 22022
ufw allow 80
ufw allow 443
ufw allow 25
ufw allow 587
ufw allow 143
ufw allow 110
ufw allow 4190
ufw allow 465
ufw allow 993
ufw allow 995
ufw --force enable
# =============================================================================
# USER SETUP - STEFAN (DO NOT MODIFY)
# =============================================================================
echo "[6/10] Configuring user 'stefan'..."
USER="stefan"
PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINV9ptnNgA4+haqLWh9bOilydlX2LIlAZqjfaDN5qEPf calorie_preset200@simplelogin.com"
if ! id -u $USER > /dev/null 2>&1; then
echo "User $USER does not exist, will be created."
useradd -m -s /bin/bash $USER
fi
mkdir -p /home/$USER/.ssh
chmod 700 /home/$USER/.ssh
echo "$PUBLIC_KEY" >> /home/$USER/.ssh/authorized_keys
chmod 600 /home/$USER/.ssh/authorized_keys
chown -R $USER:$USER /home/$USER/.ssh
usermod -aG docker $USER
echo "Public key was added for user $USER."
# =============================================================================
# SSH SECURITY HARDENING
# =============================================================================
echo "[7/10] Hardening SSH configuration..."
cat <<EOF > /etc/ssh/sshd_config
Include /etc/ssh/sshd_config.d/*.conf
Port 22022
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
#HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key
#HostKey /etc/ssh/ssh_host_ed25519_key
SyslogFacility AUTH
LogLevel VERBOSE
LoginGraceTime 2m
PermitRootLogin yes
#StrictModes yes
MaxAuthTries 6
MaxSessions 10
PasswordAuthentication yes
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
PrintLastLog yes
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
UsePrivilegeSeparation sandbox
AuthenticationMethods publickey
EOF
# NOTE: SSH restart moved to end of script to keep connection alive
# =============================================================================
# AUTOMATIC SECURITY UPDATES
# =============================================================================
echo "[8/10] Configuring automatic security updates..."
cat <<EOF > /etc/apt/apt.conf.d/20auto-upgrades
// Enable the update/upgrade script (0=disable)
APT::Periodic::Enable "1";
// Do "apt-get update" automatically every n-days (0=disable)
APT::Periodic::Update-Package-Lists "1";
// Do "apt-get upgrade --download-only" every n-days (0=disable)
APT::Periodic::Download-Upgradeable-Packages "1";
// Do "apt-get autoclean" every n-days (0=disable)
APT::Periodic::AutocleanInterval "7";
// Send report mail to root
// 0: no report (or null string)
// 1: progress report (actually any string)
// 2: + command outputs (remove -qq, remove 2>/dev/null, add -d)
APT::Periodic::Unattended-Upgrade "1";
// Automatically upgrade packages from these
Unattended-Upgrade::Origins-Pattern {
// "o=Debian,a=stable";
// "o=Debian,a=stable-updates";
"origin=Debian,codename=\${distro_codename},label=Debian-Security";
};
// You can specify your own packages to NOT automatically upgrade here
Unattended-Upgrade::Package-Blacklist {
};
// Run dpkg --force-confold --configure -a if a unclean dpkg state is detected to true to ensure that updates get installed even when the system got interrupted during a previous run
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
// Perform the upgrade when the machine is running because we wont be shutting our server down often
Unattended-Upgrade::InstallOnShutdown "false";
// Send an email to this address with information about the packages upgraded.
Unattended-Upgrade::Mail "administrator@letsbe.biz";
// Always send an e-mail
Unattended-Upgrade::MailOnlyOnError "true";
// Remove all unused dependencies after the upgrade has finished
Unattended-Upgrade::Remove-Unused-Dependencies "true";
// Remove any new unused dependencies after the upgrade has finished
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
// Automatically reboot WITHOUT CONFIRMATION if the file /var/run/reboot-required is found after the upgrade.
Unattended-Upgrade::Automatic-Reboot "false";
// Automatically reboot even if users are logged in.
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
EOF
# =============================================================================
# BACKUP SCRIPT & CRON (DISABLED - TODO: fix for non-interactive mode)
# =============================================================================
echo "Skipping backup script setup (will be configured manually later)..."
chmod 750 /opt/letsbe/scripts/backups.sh 2>/dev/null || true
mkdir -p /root/.config/rclone
# NOTE: Backup cron disabled - crontab hangs in non-interactive mode
# To enable manually after setup:
# crontab -e
# Add: 0 2 * * * /bin/bash /opt/letsbe/scripts/backups.sh >> /var/log/letsbe-backups.log 2>&1
# =============================================================================
# TOOL DEPLOYMENT
# =============================================================================
echo "[9/10] Deploying tools..."
# Get list of available tools
mapfile -t available_tools < <(ls /opt/letsbe/stacks/*/docker-compose.yml 2>/dev/null | xargs -I {} dirname {} | xargs -I {} basename {})
if [[ -z "$TOOLS_TO_DEPLOY" ]]; then
echo "No tools specified. Skipping tool deployment."
echo "Available tools: ${available_tools[*]}"
echo "Use --tools to deploy tools later."
else
# Determine which tools to deploy
declare -a tools_list=()
if [[ "$TOOLS_TO_DEPLOY" == "all" || "$TOOLS_TO_DEPLOY" == "a" ]]; then
tools_list=("${available_tools[@]}")
else
# Parse comma-separated list
IFS=',' read -ra requested_tools <<< "$TOOLS_TO_DEPLOY"
for tool in "${requested_tools[@]}"; do
tool=$(echo "$tool" | xargs) # Trim whitespace
# Check if it's a number (index)
if [[ "$tool" =~ ^[0-9]+$ ]]; then
idx=$((tool - 1))
if [[ $idx -ge 0 && $idx -lt ${#available_tools[@]} ]]; then
tools_list+=("${available_tools[$idx]}")
else
echo "Warning: Tool index $tool out of range, skipping."
fi
else
# It's a tool name
if [[ " ${available_tools[*]} " =~ " ${tool} " ]]; then
tools_list+=("$tool")
else
echo "Warning: Tool '$tool' not found, skipping."
fi
fi
done
fi
echo "Deploying tools: ${tools_list[*]}"
# Track deployed tools for SSL setup
DEPLOYED_TOOLS=()
for tool_name in "${tools_list[@]}"; do
compose_file="/opt/letsbe/stacks/${tool_name}/docker-compose.yml"
if [[ -f "$compose_file" ]]; then
# Copy .env file to centralized env directory if it exists
stack_env="/opt/letsbe/stacks/${tool_name}/.env"
central_env="/opt/letsbe/env/${tool_name}.env"
if [[ -f "$stack_env" ]]; then
cp "$stack_env" "$central_env"
chmod 640 "$central_env"
echo "Copied env file for $tool_name"
fi
# Tool-specific pre-deployment setup
if [[ "$tool_name" == "nextcloud" ]]; then
echo "Creating Nextcloud bind mount directories..."
mkdir -p /opt/letsbe/config/nextcloud
mkdir -p /opt/letsbe/data/nextcloud
# Set appropriate ownership for www-data (uid 33 in Nextcloud container)
chown -R 33:33 /opt/letsbe/config/nextcloud
chown -R 33:33 /opt/letsbe/data/nextcloud
fi
if [[ "$tool_name" == "sysadmin" ]]; then
echo " Pulling latest sysadmin agent image..."
docker-compose -f "$compose_file" pull
fi
echo "Starting $tool_name..."
docker-compose -f "$compose_file" up -d
# Tool-specific post-deployment initialization
if [[ "$tool_name" == "chatwoot" ]]; then
echo "Initializing Chatwoot database (pgvector + migrations)..."
# Get the customer prefix from the container name
CHATWOOT_POSTGRES=$(docker ps --format '{{.Names}}' | grep chatwoot-postgres | head -1)
CHATWOOT_RAILS=$(docker ps --format '{{.Names}}' | grep chatwoot-rails | head -1)
if [[ -n "$CHATWOOT_POSTGRES" && -n "$CHATWOOT_RAILS" ]]; then
# Wait for Postgres to be ready
echo " Waiting for Postgres to be ready..."
for i in {1..30}; do
if docker exec "$CHATWOOT_POSTGRES" pg_isready -U chatwoot -d chatwoot_production >/dev/null 2>&1; then
echo " Postgres is ready."
break
fi
sleep 2
done
# Create pgvector extension
echo " Creating pgvector extension..."
docker exec "$CHATWOOT_POSTGRES" psql -U chatwoot -d chatwoot_production -c "CREATE EXTENSION IF NOT EXISTS vector;" 2>/dev/null || true
# Wait for Rails container to be fully up
echo " Waiting for Rails container..."
sleep 10
# Run database migrations
echo " Running Chatwoot database prepare..."
docker exec "$CHATWOOT_RAILS" bundle exec rails db:chatwoot_prepare 2>&1 || echo " Note: db:chatwoot_prepare may have already run"
echo " Chatwoot initialization complete."
else
echo " Warning: Could not find Chatwoot containers for initialization"
fi
fi
# Link nginx config if exists
nginx_conf="/opt/letsbe/nginx/${tool_name}.conf"
if [[ -f "$nginx_conf" ]]; then
cp "$nginx_conf" /etc/nginx/sites-available/
ln -sf /etc/nginx/sites-available/${tool_name}.conf /etc/nginx/sites-enabled/
echo "Nginx config linked for $tool_name"
DEPLOYED_TOOLS+=("$tool_name")
else
echo "No nginx config for $tool_name (may not need one)"
fi
else
echo "Warning: docker-compose.yml not found for $tool_name"
fi
done
# Restart nginx to apply new configs
systemctl restart nginx
fi
# =============================================================================
# SYSADMIN AGENT (Always deployed)
# =============================================================================
echo "[9.5/10] Deploying sysadmin agent..."
SYSADMIN_COMPOSE="/opt/letsbe/stacks/sysadmin/docker-compose.yml"
if [[ -f "$SYSADMIN_COMPOSE" ]]; then
# Check if sysadmin is already running
if docker ps --format '{{.Names}}' | grep -q "agent$"; then
echo " Sysadmin agent already running, updating..."
fi
echo " Pulling latest sysadmin agent image..."
docker-compose -f "$SYSADMIN_COMPOSE" pull
echo " Starting sysadmin agent..."
docker-compose -f "$SYSADMIN_COMPOSE" up -d
echo " Sysadmin agent deployed successfully."
else
echo "Warning: Sysadmin docker-compose.yml not found at $SYSADMIN_COMPOSE"
fi
# Collect domains from deployed tools' nginx configs (for SSL)
SSL_DOMAINS=()
if [[ ${#DEPLOYED_TOOLS[@]} -gt 0 ]]; then
for tool_name in "${DEPLOYED_TOOLS[@]}"; do
tool_conf="/etc/nginx/sites-enabled/${tool_name}.conf"
if [[ -f "$tool_conf" ]]; then
# Extract server_name values (excluding placeholders and _)
while IFS= read -r domain; do
if [[ -n "$domain" && "$domain" != "_" && ! "$domain" =~ \{\{ ]]; then
SSL_DOMAINS+=("$domain")
fi
done < <(grep -h "server_name" "$tool_conf" 2>/dev/null | awk '{print $2}' | tr -d ';' | sort -u)
fi
done
fi
# =============================================================================
# SSL CERTIFICATE SETUP
# =============================================================================
echo "[10/10] Setting up SSL certificates..."
# NOTE: Certbot cron disabled - crontab hangs in non-interactive mode
# Certbot installs its own systemd timer, so manual cron not needed
echo "Certbot renewal handled by systemd timer (certbot.timer)"
if [[ "$SKIP_SSL" == "true" ]]; then
echo "Skipping SSL setup (--skip-ssl flag set)"
elif [[ ${#SSL_DOMAINS[@]} -eq 0 ]]; then
echo "No deployed tools with valid domains found."
echo "Skipping SSL setup. Either:"
echo " - No tools were deployed, or"
echo " - Templates not replaced (run env_setup.sh first with --domain parameter)"
echo "To manually setup SSL later: certbot --nginx -d yourdomain.com"
else
# Remove duplicates from SSL_DOMAINS
SSL_DOMAINS=($(printf '%s\n' "${SSL_DOMAINS[@]}" | sort -u))
# Add root domain if --root-ssl flag is set
if [[ "$ROOT_SSL" == "true" && -n "$DOMAIN" ]]; then
# Check if root domain is not already in the list
if [[ ! " ${SSL_DOMAINS[*]} " =~ " ${DOMAIN} " ]]; then
SSL_DOMAINS+=("$DOMAIN")
echo "Including root domain: $DOMAIN"
fi
fi
echo "----"
echo "Setting up SSL certificates for deployed tools:"
for domain in "${SSL_DOMAINS[@]}"; do
echo " - $domain"
done
echo ""
echo "Make sure DNS entries point to this server IP before proceeding."
# Derive email from domain parameter or use default
if [[ -n "$DOMAIN" ]]; then
SSL_EMAIL="administrator@${DOMAIN}"
else
# Try to extract base domain from first SSL domain
FIRST_DOMAIN="${SSL_DOMAINS[0]}"
# Extract base domain (remove subdomain)
BASE_DOMAIN=$(echo "$FIRST_DOMAIN" | awk -F. '{if(NF>2) print $(NF-1)"."$NF; else print $0}')
SSL_EMAIL="administrator@${BASE_DOMAIN}"
fi
echo "Using email: $SSL_EMAIL"
# Build domain arguments for certbot
DOMAIN_ARGS=""
for domain in "${SSL_DOMAINS[@]}"; do
DOMAIN_ARGS="$DOMAIN_ARGS -d $domain"
done
# Run certbot non-interactively with specific domains
sudo certbot --nginx \
--non-interactive \
--agree-tos \
--email "$SSL_EMAIL" \
--redirect \
$DOMAIN_ARGS \
|| echo "Certbot completed (some domains may have failed - check DNS)"
fi
# =============================================================================
# COMPLETION SUMMARY
# =============================================================================
echo ""
echo "----"
echo "Configured domains:"
for conf_file in /etc/nginx/sites-enabled/*.conf; do
if [[ -f "$conf_file" ]]; then
server_names=$(grep -E "^\s*server_name\s+" "$conf_file" 2>/dev/null | awk '{print $2}' | tr -d ';' | sort | uniq)
for server_name in $server_names; do
if [[ "$server_name" != "_" ]]; then
echo " - $server_name ($conf_file)"
fi
done
fi
done
SERVER_IP=$(curl -4 -s ifconfig.co)
echo ""
echo "=============================================="
echo " LetsBe Server Setup Complete"
echo "=============================================="
echo ""
echo "Server IP: $SERVER_IP"
echo "SSH Port: 22022"
echo "SSH User: stefan (key-based auth only)"
echo ""
echo "Portainer (if deployed): https://$SERVER_IP:9443"
echo ""
echo "Important:"
echo " - Configure rclone for backups: rclone config"
echo " - SSH port changed to 22022"
echo " - User 'stefan' has sudo access (key in /home/stefan/.ssh/)"
echo ""
echo "=============================================="
# =============================================================================
# MARK SETUP AS COMPLETE (before SSH restart)
# =============================================================================
touch /opt/letsbe/.setup_installed
echo "Setup marked as complete."
# =============================================================================
# RESTART SSH (MUST BE LAST - This will disconnect the session!)
# =============================================================================
echo ""
echo "Restarting SSH on port 22022... (connection will drop)"
echo "Reconnect with: ssh -i id_ed25519 -p 22022 stefan@$SERVER_IP"
echo ""
# Small delay to ensure output is sent before disconnect
sleep 2
systemctl restart sshd