832 lines
30 KiB
Bash
832 lines
30 KiB
Bash
|
|
#!/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)
|
||
|
|
# --admin-user Admin username to create with SSH key access
|
||
|
|
# --admin-ssh-key Public SSH key for the admin user
|
||
|
|
#
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
# Prevent interactive prompts during apt install
|
||
|
|
export DEBIAN_FRONTEND=noninteractive
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# 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=""
|
||
|
|
|
||
|
|
# Admin user setup (optional - replaces hardcoded user)
|
||
|
|
ADMIN_USER=""
|
||
|
|
ADMIN_SSH_KEY=""
|
||
|
|
|
||
|
|
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
|
||
|
|
;;
|
||
|
|
--admin-user)
|
||
|
|
ADMIN_USER="$2"
|
||
|
|
shift 2
|
||
|
|
;;
|
||
|
|
--admin-ssh-key)
|
||
|
|
ADMIN_SSH_KEY="$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 " --admin-user Admin username to create with SSH key access"
|
||
|
|
echo " --admin-ssh-key Public SSH key for the admin user"
|
||
|
|
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
|
||
|
|
|
||
|
|
# Open mail ports only if Poste mail server is being deployed
|
||
|
|
if [[ "$TOOLS_TO_DEPLOY" == *"poste"* || "$TOOLS_TO_DEPLOY" == "all" ]]; then
|
||
|
|
echo "Opening mail ports for Poste..."
|
||
|
|
ufw allow 25
|
||
|
|
ufw allow 587
|
||
|
|
ufw allow 143
|
||
|
|
ufw allow 110
|
||
|
|
ufw allow 4190
|
||
|
|
ufw allow 465
|
||
|
|
ufw allow 993
|
||
|
|
ufw allow 995
|
||
|
|
fi
|
||
|
|
|
||
|
|
ufw --force enable
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# ADMIN USER SETUP
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
if [[ -n "$ADMIN_USER" && -n "$ADMIN_SSH_KEY" ]]; then
|
||
|
|
echo "[6/10] Configuring admin user '$ADMIN_USER'..."
|
||
|
|
|
||
|
|
if ! id -u "$ADMIN_USER" > /dev/null 2>&1; then
|
||
|
|
echo "User $ADMIN_USER does not exist, will be created."
|
||
|
|
useradd -m -s /bin/bash "$ADMIN_USER"
|
||
|
|
fi
|
||
|
|
|
||
|
|
mkdir -p /home/$ADMIN_USER/.ssh
|
||
|
|
chmod 700 /home/$ADMIN_USER/.ssh
|
||
|
|
|
||
|
|
echo "$ADMIN_SSH_KEY" >> /home/$ADMIN_USER/.ssh/authorized_keys
|
||
|
|
chmod 600 /home/$ADMIN_USER/.ssh/authorized_keys
|
||
|
|
chown -R $ADMIN_USER:$ADMIN_USER /home/$ADMIN_USER/.ssh
|
||
|
|
|
||
|
|
usermod -aG docker "$ADMIN_USER"
|
||
|
|
|
||
|
|
echo "Public key was added for user $ADMIN_USER."
|
||
|
|
else
|
||
|
|
echo "[6/10] Skipping admin user setup (no --admin-user and --admin-ssh-key provided)"
|
||
|
|
fi
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# 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 prohibit-password
|
||
|
|
#StrictModes yes
|
||
|
|
MaxAuthTries 6
|
||
|
|
MaxSessions 10
|
||
|
|
|
||
|
|
PasswordAuthentication no
|
||
|
|
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
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
echo "Setting up backup script and cron..."
|
||
|
|
chmod 750 /opt/letsbe/scripts/backups.sh 2>/dev/null || true
|
||
|
|
chmod 750 /opt/letsbe/scripts/restore.sh 2>/dev/null || true
|
||
|
|
mkdir -p /root/.config/rclone
|
||
|
|
mkdir -p /opt/letsbe/logs
|
||
|
|
|
||
|
|
# Install backup cron non-interactively (daily at 2am)
|
||
|
|
BACKUP_CRON="0 2 * * * /bin/bash /opt/letsbe/scripts/backups.sh >> /opt/letsbe/logs/backup.log 2>&1"
|
||
|
|
( crontab -l 2>/dev/null | grep -v "backups.sh"; echo "$BACKUP_CRON" ) | crontab -
|
||
|
|
echo "Backup cron installed (daily at 2:00 AM)"
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# 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
|
||
|
|
|
||
|
|
# Ensure orchestrator is FIRST (creates network that sysadmin needs)
|
||
|
|
if [[ -f "/opt/letsbe/stacks/orchestrator/docker-compose.yml" ]]; then
|
||
|
|
# Remove orchestrator from current position if present
|
||
|
|
declare -a new_list=()
|
||
|
|
for tool in "${tools_list[@]}"; do
|
||
|
|
if [[ "$tool" != "orchestrator" ]]; then
|
||
|
|
new_list+=("$tool")
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
# Prepend orchestrator to front
|
||
|
|
tools_list=("orchestrator" "${new_list[@]}")
|
||
|
|
echo "Orchestrator moved to front (creates network for sysadmin)"
|
||
|
|
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 600 "$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" == "portainer" ]]; then
|
||
|
|
echo "Configuring Portainer local Docker endpoint..."
|
||
|
|
|
||
|
|
# Get Portainer container name
|
||
|
|
PORTAINER_CONTAINER=$(docker ps --format '{{.Names}}' | grep portainer | head -1)
|
||
|
|
|
||
|
|
if [[ -n "$PORTAINER_CONTAINER" ]]; then
|
||
|
|
# Wait for Portainer to be ready
|
||
|
|
echo " Waiting for Portainer to be ready..."
|
||
|
|
for i in {1..30}; do
|
||
|
|
if curl -ks https://localhost:9443/api/system/status >/dev/null 2>&1; then
|
||
|
|
echo " Portainer is ready."
|
||
|
|
break
|
||
|
|
fi
|
||
|
|
sleep 2
|
||
|
|
done
|
||
|
|
|
||
|
|
# Read admin password from file
|
||
|
|
PORTAINER_PASSWORD=$(cat /opt/letsbe/env/portainer_admin_password.txt 2>/dev/null)
|
||
|
|
|
||
|
|
if [[ -n "$PORTAINER_PASSWORD" ]]; then
|
||
|
|
# Authenticate and get JWT token
|
||
|
|
echo " Authenticating with Portainer..."
|
||
|
|
JWT_RESPONSE=$(curl -ks -X POST https://localhost:9443/api/auth \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d "{\"username\":\"admin\",\"password\":\"${PORTAINER_PASSWORD}\"}" 2>/dev/null)
|
||
|
|
|
||
|
|
JWT=$(echo "$JWT_RESPONSE" | grep -o '"jwt":"[^"]*"' | cut -d'"' -f4)
|
||
|
|
|
||
|
|
if [[ -n "$JWT" ]]; then
|
||
|
|
echo " Creating local Docker endpoint..."
|
||
|
|
# Create local Docker socket endpoint
|
||
|
|
ENDPOINT_RESPONSE=$(curl -ks -X POST https://localhost:9443/api/endpoints \
|
||
|
|
-H "Authorization: Bearer $JWT" \
|
||
|
|
-H "Content-Type: multipart/form-data" \
|
||
|
|
-F "Name=local" \
|
||
|
|
-F "EndpointCreationType=1" 2>/dev/null)
|
||
|
|
|
||
|
|
if echo "$ENDPOINT_RESPONSE" | grep -q '"Id"'; then
|
||
|
|
echo " Local Docker endpoint created successfully."
|
||
|
|
else
|
||
|
|
echo " Warning: Endpoint creation response: $ENDPOINT_RESPONSE"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
echo " Warning: Could not authenticate with Portainer"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
echo " Warning: Could not read Portainer password file"
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
echo " Warning: Could not find Portainer container"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
|
||
|
|
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
|
||
|
|
|
||
|
|
# =============================================================================
|
||
|
|
# LOCAL ORCHESTRATOR BOOTSTRAP (License Validation + Agent Registration)
|
||
|
|
# =============================================================================
|
||
|
|
|
||
|
|
echo "[9.6/10] Running local orchestrator bootstrap..."
|
||
|
|
|
||
|
|
BOOTSTRAP_SCRIPT="/opt/letsbe/scripts/local_bootstrap.sh"
|
||
|
|
CREDENTIALS_FILE="/opt/letsbe/env/credentials.env"
|
||
|
|
|
||
|
|
if [[ -f "$BOOTSTRAP_SCRIPT" && -f "$CREDENTIALS_FILE" ]]; then
|
||
|
|
# Source credentials to get required variables
|
||
|
|
source "$CREDENTIALS_FILE"
|
||
|
|
|
||
|
|
echo " Validating license and setting up local orchestrator..."
|
||
|
|
echo " Instance ID: ${INSTANCE_ID:-unknown}"
|
||
|
|
echo " Hub URL: ${HUB_URL:-unknown}"
|
||
|
|
|
||
|
|
# Run bootstrap script with required environment variables
|
||
|
|
HUB_URL="${HUB_URL}" \
|
||
|
|
LICENSE_KEY="${LICENSE_KEY}" \
|
||
|
|
INSTANCE_ID="${INSTANCE_ID}" \
|
||
|
|
ORCHESTRATOR_URL="http://localhost:8100" \
|
||
|
|
ADMIN_API_KEY="${ADMIN_API_KEY}" \
|
||
|
|
CUSTOMER="$(echo ${INSTANCE_ID} | sed 's/-orchestrator$//')" \
|
||
|
|
CREDENTIALS_DIR="/opt/letsbe/env" \
|
||
|
|
bash "$BOOTSTRAP_SCRIPT"
|
||
|
|
|
||
|
|
BOOTSTRAP_EXIT=$?
|
||
|
|
|
||
|
|
if [[ $BOOTSTRAP_EXIT -ne 0 ]]; then
|
||
|
|
echo ""
|
||
|
|
echo "=============================================="
|
||
|
|
echo " BOOTSTRAP FAILED"
|
||
|
|
echo "=============================================="
|
||
|
|
echo ""
|
||
|
|
echo "License validation or agent registration failed."
|
||
|
|
echo "Check the error messages above for details."
|
||
|
|
echo ""
|
||
|
|
echo "Common issues:"
|
||
|
|
echo " - Invalid license_key in config.json"
|
||
|
|
echo " - Network connectivity to Hub (${HUB_URL})"
|
||
|
|
echo " - Instance not registered in LetsBe Hub"
|
||
|
|
echo ""
|
||
|
|
echo "The stack has been deployed but is NOT properly configured."
|
||
|
|
echo "Please fix the issue and re-run: bash /opt/letsbe/scripts/local_bootstrap.sh"
|
||
|
|
echo "=============================================="
|
||
|
|
# Don't exit - let the rest of setup complete, but warn
|
||
|
|
else
|
||
|
|
echo " Bootstrap completed successfully!"
|
||
|
|
echo " Agent should register with local orchestrator within 30 seconds."
|
||
|
|
fi
|
||
|
|
else
|
||
|
|
if [[ ! -f "$BOOTSTRAP_SCRIPT" ]]; then
|
||
|
|
echo "Warning: Bootstrap script not found at $BOOTSTRAP_SCRIPT"
|
||
|
|
fi
|
||
|
|
if [[ ! -f "$CREDENTIALS_FILE" ]]; then
|
||
|
|
echo "Warning: Credentials file not found at $CREDENTIALS_FILE"
|
||
|
|
echo "Run env_setup.sh first to generate credentials."
|
||
|
|
fi
|
||
|
|
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"
|
||
|
|
if [[ -n "$ADMIN_USER" ]]; then
|
||
|
|
echo "SSH User: $ADMIN_USER (key-based auth only)"
|
||
|
|
else
|
||
|
|
echo "SSH User: root (key-based auth only, no admin user configured)"
|
||
|
|
fi
|
||
|
|
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"
|
||
|
|
if [[ -n "$ADMIN_USER" ]]; then
|
||
|
|
echo " - User '$ADMIN_USER' has Docker access (key in /home/$ADMIN_USER/.ssh/)"
|
||
|
|
fi
|
||
|
|
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)"
|
||
|
|
if [[ -n "$ADMIN_USER" ]]; then
|
||
|
|
echo "Reconnect with: ssh -i id_ed25519 -p 22022 $ADMIN_USER@$SERVER_IP"
|
||
|
|
else
|
||
|
|
echo "Reconnect with: ssh -p 22022 root@$SERVER_IP"
|
||
|
|
fi
|
||
|
|
echo ""
|
||
|
|
|
||
|
|
# Small delay to ensure output is sent before disconnect
|
||
|
|
sleep 2
|
||
|
|
|
||
|
|
systemctl restart sshd
|