Files
ServerKit/serverkit
T
Juan Denis e0492f54f0 Move nginx private URLs to location snippets
Generate private URL configs as location-only snippets in /etc/nginx/serverkit-locations instead of full server blocks. nginx_service.py: write location snippets (.conf) to LOCATIONS_DIR, ensure the locations directory, remove legacy separate server blocks, and add get_location_config to read these snippets (used for private URLs, Gitea, WordPress). install.sh & serverkit: only remove conflicting catch-all (server_name _) configs that are not ServerKit-managed (skip serverkit.conf and serverkit-*), strengthen the server_name regex, and create the serverkit-locations dir. cmd_doctor: add docker presence checks and avoid frontend checks when docker is missing; require root in --fix mode. Misc cleanup and minor refactors around enabling/disabling configs and reload handling.
2026-03-28 13:47:43 -04:00

1382 lines
40 KiB
Bash

#!/bin/bash
#
# ServerKit CLI - Management tool for ServerKit
#
# Architecture:
# - Backend: systemd service (runs directly on host)
# - Frontend: Docker container (nginx)
#
# Usage: serverkit <command> [options]
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration
INSTALL_DIR="${SERVERKIT_DIR:-/opt/serverkit}"
VENV_DIR="$INSTALL_DIR/venv"
BACKEND_SERVICE="serverkit"
FRONTEND_CONTAINER="serverkit-frontend"
# Helper functions
print_header() {
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} ServerKit CLI${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
}
print_success() { echo -e "${GREEN}✓ $1${NC}"; }
print_error() { echo -e "${RED}✗ $1${NC}"; }
print_warning() { echo -e "${YELLOW}! $1${NC}"; }
print_info() { echo -e "${BLUE}→ $1${NC}"; }
check_root() {
if [ "$EUID" -ne 0 ]; then
print_error "This command requires root privileges. Run with sudo."
exit 1
fi
}
check_installed() {
if [ ! -d "$INSTALL_DIR" ]; then
print_error "ServerKit is not installed in $INSTALL_DIR"
print_info "Run the install script first"
exit 1
fi
}
run_cli() {
# Run CLI commands directly on host using the virtual environment
cd "$INSTALL_DIR/backend"
source "$VENV_DIR/bin/activate"
python cli.py "$@"
}
# Version management
VERSION_FILE="$INSTALL_DIR/VERSION"
REMOTE_VERSION_URL="https://raw.githubusercontent.com/jhd3197/serverkit/main/VERSION"
get_local_version() {
if [ -f "$VERSION_FILE" ]; then
cat "$VERSION_FILE" | tr -d '\n\r '
else
echo "unknown"
fi
}
get_remote_version() {
curl -sf "$REMOTE_VERSION_URL" 2>/dev/null | tr -d '\n\r ' || echo "unknown"
}
compare_versions() {
# Returns: 0 if equal, 1 if v1 > v2, 2 if v1 < v2
local v1="$1"
local v2="$2"
if [ "$v1" = "$v2" ]; then
return 0
fi
# Compare version components
local v1_major=$(echo "$v1" | cut -d. -f1)
local v1_minor=$(echo "$v1" | cut -d. -f2)
local v1_patch=$(echo "$v1" | cut -d. -f3)
local v2_major=$(echo "$v2" | cut -d. -f1)
local v2_minor=$(echo "$v2" | cut -d. -f2)
local v2_patch=$(echo "$v2" | cut -d. -f3)
if [ "$v1_major" -gt "$v2_major" ] 2>/dev/null; then return 1; fi
if [ "$v1_major" -lt "$v2_major" ] 2>/dev/null; then return 2; fi
if [ "$v1_minor" -gt "$v2_minor" ] 2>/dev/null; then return 1; fi
if [ "$v1_minor" -lt "$v2_minor" ] 2>/dev/null; then return 2; fi
if [ "$v1_patch" -gt "$v2_patch" ] 2>/dev/null; then return 1; fi
if [ "$v1_patch" -lt "$v2_patch" ] 2>/dev/null; then return 2; fi
return 0
}
cmd_version() {
local version=$(get_local_version)
echo "ServerKit version $version"
}
cmd_check_update() {
print_info "Checking for updates..."
local local_version=$(get_local_version)
local remote_version=$(get_remote_version)
echo ""
echo " Installed version: $local_version"
echo " Latest version: $remote_version"
echo ""
if [ "$remote_version" = "unknown" ]; then
print_warning "Could not check remote version (network error?)"
return 1
fi
set +e # Temporarily disable exit on error
compare_versions "$local_version" "$remote_version"
local result=$?
set -e # Re-enable exit on error
if [ $result -eq 2 ]; then
print_success "Update available! Run 'sudo serverkit update' to upgrade."
return 0
elif [ $result -eq 0 ]; then
print_success "You are running the latest version."
return 0
else
print_info "You are running a newer version than released."
return 0
fi
}
# Commands
cmd_help() {
print_header
echo ""
echo "Usage: serverkit <command> [options]"
echo ""
echo "Service Commands:"
echo " start Start all services"
echo " stop Stop all services"
echo " restart Restart all services"
echo " status Show service status"
echo " logs [service] View logs (backend, frontend, or all)"
echo " update [options] Update ServerKit"
echo " --branch <name> Switch to specific branch (e.g., dev)"
echo " --force Skip version check"
echo " branch Show current branch"
echo " uninstall Uninstall ServerKit"
echo ""
echo "User Management:"
echo " create-admin Create a new admin user"
echo " reset-password Reset a user's password"
echo " unlock-user Unlock a locked user account"
echo " list-users List all users"
echo " make-admin Promote user to admin"
echo " deactivate-user Deactivate a user account"
echo " activate-user Activate a user account"
echo ""
echo "Database Commands:"
echo " init-db Initialize the database"
echo " migrate-db Apply database migrations"
echo " backup-db Backup the database"
echo " restore-db Restore database from backup"
echo ""
echo "Cleanup Commands:"
echo " cleanup-apps Delete all apps and containers"
echo " factory-reset Complete reset (delete everything)"
echo ""
echo "App Management:"
echo " create-app Show how to create apps (via web UI)"
echo " list-apps [--all] List deployed apps (--all shows Docker containers)"
echo " start-app Start an app"
echo " stop-app Stop an app"
echo " app-logs View app logs"
echo ""
echo "Site Management:"
echo " setup-proxy Setup host nginx as reverse proxy (run once)"
echo " add-site Add a new site/domain"
echo " list-sites List all configured sites"
echo " remove-site Remove a site"
echo ""
echo "Version Commands:"
echo " version Show current version"
echo " check-update Check if updates are available"
echo ""
echo "Diagnostics:"
echo " doctor Check for common issues"
echo " doctor --fix Auto-fix detected issues"
echo ""
echo "Utility Commands:"
echo " generate-keys Generate secure keys for .env"
echo " config Edit configuration"
echo ""
echo "Examples:"
echo " sudo serverkit start"
echo " sudo serverkit create-admin"
echo " sudo serverkit add-site test.mysite.com 3000"
echo " serverkit logs backend"
echo ""
}
cmd_start() {
check_root
check_installed
print_info "Starting ServerKit..."
# Start backend (systemd)
systemctl start $BACKEND_SERVICE
# Start frontend (Docker)
cd "$INSTALL_DIR"
docker compose up -d
sleep 3
print_success "ServerKit started"
cmd_status_brief
}
cmd_stop() {
check_root
check_installed
print_info "Stopping ServerKit..."
# Stop backend (systemd)
systemctl stop $BACKEND_SERVICE || true
# Stop frontend (Docker)
cd "$INSTALL_DIR"
docker compose down || true
print_success "ServerKit stopped"
}
cmd_restart() {
check_root
check_installed
print_info "Restarting ServerKit..."
# Restart backend (systemd)
systemctl restart $BACKEND_SERVICE
# Restart frontend (Docker)
cd "$INSTALL_DIR"
docker compose restart
sleep 3
print_success "ServerKit restarted"
cmd_status_brief
}
cmd_status_brief() {
# Quick status check
echo ""
# Backend status
if systemctl is-active --quiet $BACKEND_SERVICE; then
print_success "Backend: running"
else
print_error "Backend: stopped"
fi
# Frontend status
if docker ps --format '{{.Names}}' | grep -q $FRONTEND_CONTAINER; then
print_success "Frontend: running"
else
print_error "Frontend: stopped"
fi
# API health
if curl -s http://127.0.0.1:5000/api/v1/system/health > /dev/null 2>&1; then
print_success "API: healthy"
else
print_warning "API: not responding"
fi
}
cmd_status() {
check_installed
print_header
echo ""
echo "Backend Service (systemd):"
echo "─────────────────────────────"
systemctl status $BACKEND_SERVICE --no-pager -l 2>/dev/null || print_warning "Backend service not found"
echo ""
echo "Frontend Container (Docker):"
echo "─────────────────────────────"
cd "$INSTALL_DIR"
docker compose ps 2>/dev/null || print_warning "Frontend container not found"
echo ""
cmd_status_brief
}
cmd_logs() {
check_installed
service="${1:-all}"
case "$service" in
backend)
journalctl -u $BACKEND_SERVICE -f
;;
frontend)
cd "$INSTALL_DIR"
docker compose logs -f
;;
all|*)
print_info "Showing backend logs (Ctrl+C to switch to frontend)..."
echo ""
# Show both in split view isn't easy in bash, so show backend first
journalctl -u $BACKEND_SERVICE -n 50 --no-pager
echo ""
print_info "Showing frontend logs..."
echo ""
cd "$INSTALL_DIR"
docker compose logs --tail=50
echo ""
print_info "For live logs, use: serverkit logs backend OR serverkit logs frontend"
;;
esac
}
cmd_branch() {
check_installed
cd "$INSTALL_DIR"
local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
local current_commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
echo ""
echo " Current branch: $current_branch"
echo " Current commit: $current_commit"
echo " Version: $(get_local_version)"
echo ""
# Show available remote branches
print_info "Available branches:"
git branch -r 2>/dev/null | grep -v HEAD | sed 's/origin\// /' | head -10
echo ""
}
cmd_update() {
local force_update=false
local target_branch=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--force|-f)
force_update=true
shift
;;
--branch|-b)
target_branch="$2"
shift 2
;;
*)
shift
;;
esac
done
check_root
check_installed
print_header
echo ""
cd "$INSTALL_DIR"
# Get current branch
local current_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
# If no branch specified, use current branch (or main if detached)
if [ -z "$target_branch" ]; then
target_branch="$current_branch"
if [ "$target_branch" = "HEAD" ]; then
target_branch="main"
fi
fi
# Get versions
CURRENT_VERSION=$(get_local_version)
# For non-main branches, skip version comparison
if [ "$target_branch" != "main" ]; then
force_update=true
print_warning "Updating to branch: $target_branch (dev mode)"
echo ""
else
REMOTE_VERSION=$(get_remote_version)
echo " Installed version: $CURRENT_VERSION"
echo " Latest version: $REMOTE_VERSION"
echo ""
fi
# Check if update is needed (skip if --force or non-main branch)
if [ "$force_update" = false ] && [ "$REMOTE_VERSION" != "unknown" ]; then
set +e # Temporarily disable exit on error
compare_versions "$CURRENT_VERSION" "$REMOTE_VERSION"
local result=$?
set -e # Re-enable exit on error
if [ $result -eq 0 ]; then
print_success "Already running the latest version."
echo ""
read -p "Force update anyway? [y/N] " -n 1 -r
echo ""
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
fi
fi
print_info "Updating ServerKit to branch: $target_branch..."
# Stop services
print_info "Stopping services..."
systemctl stop $BACKEND_SERVICE || true
docker compose down || true
# Fetch all branches and pull changes
print_info "Fetching latest changes..."
git fetch origin --prune
# Checkout target branch
print_info "Switching to branch: $target_branch..."
git checkout "$target_branch" 2>/dev/null || git checkout -b "$target_branch" "origin/$target_branch"
git reset --hard "origin/$target_branch"
# Restore executable permissions
chmod +x "$INSTALL_DIR/serverkit"
chmod +x "$INSTALL_DIR/scripts/"*.sh 2>/dev/null || true
# Get new version from VERSION file
NEW_VERSION=$(get_local_version)
NEW_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
# Update Python dependencies
print_info "Updating Python dependencies..."
source "$VENV_DIR/bin/activate"
pip install -r "$INSTALL_DIR/backend/requirements.txt" --quiet
# Sync app templates
print_info "Syncing app templates..."
mkdir -p /etc/serverkit/templates
cp -r "$INSTALL_DIR/backend/templates/"*.yaml /etc/serverkit/templates/ 2>/dev/null || true
cp -r "$INSTALL_DIR/backend/templates/"*.yml /etc/serverkit/templates/ 2>/dev/null || true
# Remove any nginx configs that use server_name _ besides serverkit-managed ones
mkdir -p /etc/nginx/serverkit-locations
for conf in /etc/nginx/sites-enabled/*; do
[ -f "$conf" ] || continue
name=$(basename "$conf")
# Skip all ServerKit-managed configs
case "$name" in serverkit-*) continue ;; esac
if grep -Eq 'server_name[[:space:]]+_;' "$conf" 2>/dev/null; then
print_warning "Removing conflicting catch-all config: $name"
rm -f "/etc/nginx/sites-enabled/$name"
rm -f "/etc/nginx/sites-available/$name"
fi
done
# Update main nginx config
if [ -f "$INSTALL_DIR/nginx/sites-available/serverkit.conf" ]; then
cp "$INSTALL_DIR/nginx/sites-available/serverkit.conf" /etc/nginx/sites-available/
fi
# Clean up Docker to prevent networking issues
print_info "Cleaning up Docker..."
docker network prune -f 2>/dev/null || true
docker system prune -f 2>/dev/null || true
# Rebuild frontend (no cache to ensure fresh build)
print_info "Rebuilding frontend..."
docker compose build --no-cache
# Reload systemd in case service file changed
systemctl daemon-reload
# Restart services
print_info "Starting services..."
systemctl start $BACKEND_SERVICE
docker compose up -d
sleep 5
# Track successful update
curl -s "https://serverkit.ai/track/update?v=${NEW_VERSION}" >/dev/null 2>&1 || true
echo ""
print_success "ServerKit updated!"
echo " Branch: $target_branch"
echo " Commit: $NEW_COMMIT"
echo " Version: $NEW_VERSION"
echo ""
if [ "$target_branch" != "main" ]; then
print_warning "Running on '$target_branch' branch. Use 'serverkit update --branch main' to return to stable."
fi
cmd_status_brief
}
cmd_uninstall() {
check_root
print_header
echo ""
print_warning "This will remove ServerKit and all data!"
echo ""
echo "The following will be removed:"
echo " - ServerKit backend service (systemd)"
echo " - Frontend Docker container and images"
echo " - Installation directory ($INSTALL_DIR)"
echo " - Configuration (/etc/serverkit)"
echo " - App data (/var/serverkit)"
echo " - Logs (/var/log/serverkit)"
echo " - Nginx site configs for ServerKit"
echo " - CLI symlink (/usr/local/bin/serverkit)"
echo ""
read -p "Are you sure? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Track uninstall before removing files
UNINSTALL_VERSION=$(get_local_version)
curl -s "https://serverkit.ai/track/uninstall?v=${UNINSTALL_VERSION}" >/dev/null 2>&1 || true
print_info "Stopping services..."
systemctl stop $BACKEND_SERVICE 2>/dev/null || true
systemctl disable $BACKEND_SERVICE 2>/dev/null || true
cd "$INSTALL_DIR" 2>/dev/null && docker compose down -v 2>/dev/null || true
# Move to safe directory before deleting install dir
cd /tmp 2>/dev/null || cd / || true
print_info "Removing files..."
rm -f /etc/systemd/system/serverkit.service
rm -f /usr/local/bin/serverkit
rm -rf "$INSTALL_DIR"
rm -rf /var/log/serverkit
rm -rf /var/lib/serverkit
# Remove ServerKit config and app data
rm -rf /etc/serverkit
rm -rf /var/serverkit
# Remove nginx site configs installed by ServerKit
print_info "Removing nginx configs..."
rm -f /etc/nginx/sites-enabled/serverkit.conf
rm -f /etc/nginx/sites-available/serverkit.conf
rm -f /etc/nginx/sites-available/example.conf.template
# Reload nginx if it's still running (don't stop it — user may use it for other sites)
if systemctl is-active --quiet nginx 2>/dev/null; then
systemctl reload nginx 2>/dev/null || true
fi
systemctl daemon-reload
print_success "ServerKit uninstalled"
echo ""
echo "Note: System packages (Docker, Node.js, nginx, Python) were NOT removed."
echo "Remove them manually if no longer needed."
echo ""
echo "To reinstall, run:"
echo " cd ~ && curl -fsSL https://serverkit.ai/install.sh | sudo bash"
fi
}
cmd_create_admin() {
check_installed
print_info "Creating admin user..."
run_cli create-admin
}
cmd_reset_password() {
check_installed
print_info "Resetting password..."
run_cli reset-password
}
cmd_unlock_user() {
check_installed
run_cli unlock-user
}
cmd_list_users() {
check_installed
run_cli list-users
}
cmd_make_admin() {
check_installed
run_cli make-admin
}
cmd_deactivate_user() {
check_installed
run_cli deactivate-user
}
cmd_activate_user() {
check_installed
run_cli activate-user
}
cmd_init_db() {
check_installed
run_cli init-db
}
cmd_migrate_db() {
check_installed
run_cli migrate-db
}
cmd_cleanup_apps() {
check_root
check_installed
DELETE_VOLUMES=""
KEEP_DB=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--delete-volumes|-v)
DELETE_VOLUMES="--delete-volumes"
shift
;;
--keep-db|-k)
KEEP_DB="--keep-db"
shift
;;
*)
shift
;;
esac
done
run_cli cleanup-apps $DELETE_VOLUMES $KEEP_DB
}
cmd_factory_reset() {
check_root
check_installed
run_cli factory-reset
}
cmd_backup_db() {
check_installed
BACKUP_DIR="$INSTALL_DIR/backups"
BACKUP_FILE="$BACKUP_DIR/serverkit-$(date +%Y%m%d-%H%M%S).db"
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
mkdir -p "$BACKUP_DIR"
if [ -f "$DB_FILE" ]; then
cp "$DB_FILE" "$BACKUP_FILE"
print_success "Database backed up to $BACKUP_FILE"
else
print_error "Database file not found: $DB_FILE"
exit 1
fi
}
cmd_restore_db() {
check_root
check_installed
BACKUP_DIR="$INSTALL_DIR/backups"
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
if [ -z "$1" ]; then
echo "Available backups:"
ls -la "$BACKUP_DIR"/*.db 2>/dev/null || echo "No backups found"
echo ""
read -p "Enter backup file path: " BACKUP_FILE
else
BACKUP_FILE="$1"
fi
if [ ! -f "$BACKUP_FILE" ]; then
print_error "Backup file not found: $BACKUP_FILE"
exit 1
fi
print_warning "This will overwrite the current database!"
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
# Stop backend before restore
systemctl stop $BACKEND_SERVICE
cp "$BACKUP_FILE" "$DB_FILE"
# Restart backend
systemctl start $BACKEND_SERVICE
print_success "Database restored"
fi
}
cmd_generate_keys() {
print_header
echo ""
echo "Add these to your .env file:"
echo ""
SECRET_KEY=$(openssl rand -hex 32)
JWT_SECRET_KEY=$(openssl rand -hex 32)
echo "SECRET_KEY=$SECRET_KEY"
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY"
echo ""
}
cmd_config() {
check_installed
${EDITOR:-nano} "$INSTALL_DIR/.env"
print_info "Restart ServerKit to apply changes: sudo serverkit restart"
}
cmd_setup_proxy() {
check_root
check_installed
print_info "Setting up host nginx as reverse proxy..."
if [ -f "$INSTALL_DIR/scripts/setup-nginx-proxy.sh" ]; then
chmod +x "$INSTALL_DIR/scripts/setup-nginx-proxy.sh"
"$INSTALL_DIR/scripts/setup-nginx-proxy.sh"
else
print_error "Setup script not found. Run 'serverkit update' first."
exit 1
fi
}
cmd_create_app() {
print_header
echo ""
echo "To create a new app, use the ServerKit web interface:"
echo ""
echo " 1. Open ServerKit in your browser"
echo " 2. Go to Templates in the sidebar"
echo " 3. Choose from 60+ available templates:"
echo " - PHP Application (custom PHP projects)"
echo " - Python Application (Flask, FastAPI, Django)"
echo " - Node.js Application (Express, Next.js)"
echo " - WordPress, Ghost, Nextcloud, and many more"
echo " 4. Click Install and configure your app"
echo ""
echo "The web interface provides:"
echo " ✓ One-click installation"
echo " ✓ Automatic port assignment"
echo " ✓ Configuration UI"
echo " ✓ Update management"
echo ""
}
cmd_list_apps() {
check_installed
# Pass --all flag if provided
if [ "$1" = "--all" ] || [ "$1" = "-a" ]; then
run_cli list-apps --all
else
run_cli list-apps
fi
}
cmd_start_app() {
check_root
NAME="$1"
if [ -z "$NAME" ]; then
echo "Usage: serverkit start-app <name>"
cmd_list_apps
exit 1
fi
APP_DIR="/var/www/$NAME"
if [ ! -d "$APP_DIR" ]; then
print_error "App not found: $NAME"
exit 1
fi
print_info "Starting $NAME..."
cd "$APP_DIR"
docker compose up -d
print_success "$NAME started"
}
cmd_stop_app() {
check_root
NAME="$1"
if [ -z "$NAME" ]; then
echo "Usage: serverkit stop-app <name>"
cmd_list_apps
exit 1
fi
APP_DIR="/var/www/$NAME"
if [ ! -d "$APP_DIR" ]; then
print_error "App not found: $NAME"
exit 1
fi
print_info "Stopping $NAME..."
cd "$APP_DIR"
docker compose down
print_success "$NAME stopped"
}
cmd_app_logs() {
NAME="$1"
if [ -z "$NAME" ]; then
echo "Usage: serverkit app-logs <name>"
cmd_list_apps
exit 1
fi
APP_DIR="/var/www/$NAME"
if [ ! -d "$APP_DIR" ]; then
print_error "App not found: $NAME"
exit 1
fi
cd "$APP_DIR"
docker compose logs -f
}
cmd_add_site() {
check_root
DOMAIN="$1"
PORT="${2:-3000}"
if [ -z "$DOMAIN" ]; then
echo "Usage: serverkit add-site <domain> [port]"
echo ""
echo "Examples:"
echo " serverkit add-site test.builditdesign.com"
echo " serverkit add-site myapp.builditdesign.com 3000"
exit 1
fi
SITE_FILE="/etc/nginx/sites-available/${DOMAIN}.conf"
if [ -f "$SITE_FILE" ]; then
print_warning "Site config already exists: $SITE_FILE"
read -p "Overwrite? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 0
fi
fi
print_info "Creating site config for $DOMAIN..."
cat > "$SITE_FILE" << EOF
# Site: $DOMAIN
# Created: $(date)
server {
listen 80;
server_name $DOMAIN;
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection "upgrade";
}
}
EOF
# Enable the site
ln -sf "$SITE_FILE" /etc/nginx/sites-enabled/
# Test and reload nginx
if nginx -t 2>/dev/null; then
systemctl reload nginx
print_success "Site added: $DOMAIN → localhost:$PORT"
echo ""
echo "Next steps:"
echo " 1. Add DNS record in Cloudflare:"
echo " Type: A, Name: ${DOMAIN%%.*}, Content: $(curl -s ifconfig.me)"
echo " 2. Start your app on port $PORT"
echo ""
else
print_error "Nginx config error. Check: $SITE_FILE"
rm -f "/etc/nginx/sites-enabled/${DOMAIN}.conf"
exit 1
fi
}
cmd_list_sites() {
print_header
echo ""
echo "Configured Sites:"
echo "─────────────────────────────────────────"
if [ -d /etc/nginx/sites-enabled ]; then
for site in /etc/nginx/sites-enabled/*.conf; do
if [ -f "$site" ]; then
DOMAIN=$(grep -m1 "server_name" "$site" | awk '{print $2}' | tr -d ';')
PORT=$(grep -m1 "proxy_pass" "$site" | grep -oP ':\K[0-9]+' || echo "static")
ENABLED="✓"
printf " %s %-35s → port %s\n" "$ENABLED" "$DOMAIN" "$PORT"
fi
done
else
echo " No sites configured. Run 'serverkit setup-proxy' first."
fi
echo ""
}
cmd_doctor() {
check_installed
print_header
echo ""
print_info "Running diagnostics..."
echo ""
ISSUES=0
FIXED=0
FIX_MODE="${1:-}" # --fix to auto-fix
if [ "$FIX_MODE" = "--fix" ]; then
check_root
fi
doctor_issue() {
ISSUES=$((ISSUES + 1))
print_error "$1"
}
doctor_ok() {
print_success "$1"
}
doctor_fixed() {
FIXED=$((FIXED + 1))
print_success "FIXED: $1"
}
doctor_skip() {
print_warning "SKIP: $1 (run with --fix to auto-fix)"
}
# ── 1. Backend service ──────────────────────────────────────
echo "Backend"
echo "───────────────────────────────────────"
if systemctl is-active --quiet $BACKEND_SERVICE 2>/dev/null; then
doctor_ok "systemd service is running"
else
doctor_issue "systemd service is not running"
if [ "$FIX_MODE" = "--fix" ]; then
systemctl start $BACKEND_SERVICE 2>/dev/null && doctor_fixed "Started backend service" || print_error " Could not start service"
else
doctor_skip "systemctl start $BACKEND_SERVICE"
fi
fi
if curl -sf http://127.0.0.1:5000/api/v1/system/health > /dev/null 2>&1; then
doctor_ok "API responding on port 5000"
else
if systemctl is-active --quiet $BACKEND_SERVICE 2>/dev/null; then
doctor_issue "Service running but API not responding on port 5000"
echo " Check logs: journalctl -u serverkit -n 30"
else
doctor_issue "API not responding (service is down)"
fi
fi
echo ""
# ── 2. Frontend container ───────────────────────────────────
FRONTEND_RUNNING=false
FRONTEND_PORT=""
if command -v docker >/dev/null 2>&1; then
echo "Frontend"
echo "───────────────────────────────────────"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$FRONTEND_CONTAINER"; then
FRONTEND_RUNNING=true
# Get the actual host port the container is mapped to
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | grep -oP ':\K[0-9]+' || echo "")
doctor_ok "Container running (port $FRONTEND_PORT)"
else
doctor_issue "Container '$FRONTEND_CONTAINER' is not running"
if [ "$FIX_MODE" = "--fix" ]; then
cd "$INSTALL_DIR"
docker compose up -d 2>/dev/null && {
sleep 3
FRONTEND_PORT=$(docker port "$FRONTEND_CONTAINER" 80 2>/dev/null | head -1 | grep -oP ':\K[0-9]+' || echo "")
FRONTEND_RUNNING=true
doctor_fixed "Started frontend container (port $FRONTEND_PORT)"
} || print_error " Could not start container"
else
doctor_skip "docker compose up -d"
fi
fi
if [ "$FRONTEND_RUNNING" = true ] && [ -n "$FRONTEND_PORT" ]; then
if curl -sf "http://127.0.0.1:$FRONTEND_PORT" > /dev/null 2>&1; then
doctor_ok "Frontend responding on port $FRONTEND_PORT"
else
doctor_issue "Container running but not responding on port $FRONTEND_PORT"
fi
fi
else
echo "Frontend"
echo "───────────────────────────────────────"
doctor_issue "Docker not installed or not in PATH; skipping frontend checks"
fi
echo ""
# ── 3. Nginx ────────────────────────────────────────────────
echo "Nginx"
echo "───────────────────────────────────────"
if systemctl is-active --quiet nginx 2>/dev/null; then
doctor_ok "nginx is running"
else
doctor_issue "nginx is not running"
if [ "$FIX_MODE" = "--fix" ]; then
systemctl start nginx 2>/dev/null && doctor_fixed "Started nginx" || print_error " Could not start nginx"
else
doctor_skip "systemctl start nginx"
fi
fi
if nginx -t 2>/dev/null; then
doctor_ok "nginx config syntax OK"
else
doctor_issue "nginx config has errors"
echo " Run: nginx -t"
fi
# Scan ALL enabled nginx configs for competing server_name _
# Only serverkit.conf should use server_name _ (the catch-all panel block)
for SITE_CONF in /etc/nginx/sites-enabled/*; do
[ -f "$SITE_CONF" ] || continue
SITE_NAME=$(basename "$SITE_CONF")
# Skip all ServerKit-managed configs
case "$SITE_NAME" in serverkit-*|serverkit.conf) continue ;; esac
if grep -Eq 'server_name[[:space:]]+_;' "$SITE_CONF" 2>/dev/null; then
doctor_issue "'$SITE_NAME' uses server_name _ (conflicts with panel, can hijack all traffic)"
if [ "$FIX_MODE" = "--fix" ]; then
rm -f "/etc/nginx/sites-enabled/$SITE_NAME"
rm -f "/etc/nginx/sites-available/$SITE_NAME"
doctor_fixed "Removed conflicting '$SITE_NAME'"
else
doctor_skip "Remove conflicting '$SITE_NAME' from sites-enabled"
fi
fi
done
# Ensure serverkit-locations directory exists
if [ ! -d /etc/nginx/serverkit-locations ]; then
print_warning "Missing /etc/nginx/serverkit-locations directory"
if [ "$FIX_MODE" = "--fix" ]; then
mkdir -p /etc/nginx/serverkit-locations
doctor_fixed "Created /etc/nginx/serverkit-locations"
else
doctor_skip "mkdir -p /etc/nginx/serverkit-locations"
fi
fi
# Check for port mismatch between nginx and frontend container
NGINX_CONF="/etc/nginx/sites-available/serverkit.conf"
if [ -f "$NGINX_CONF" ]; then
NGINX_FRONTEND_PORT=$(grep -m1 'proxy_pass.*127.0.0.1' "$NGINX_CONF" | head -1 | grep -oP ':\K[0-9]+' || echo "")
if [ -n "$FRONTEND_PORT" ] && [ -n "$NGINX_FRONTEND_PORT" ]; then
if [ "$NGINX_FRONTEND_PORT" = "$FRONTEND_PORT" ]; then
doctor_ok "nginx proxy port ($NGINX_FRONTEND_PORT) matches frontend container ($FRONTEND_PORT)"
else
doctor_issue "PORT MISMATCH: nginx proxies to :$NGINX_FRONTEND_PORT but frontend is on :$FRONTEND_PORT"
if [ "$FIX_MODE" = "--fix" ]; then
sed -i "s|proxy_pass http://127.0.0.1:${NGINX_FRONTEND_PORT}|proxy_pass http://127.0.0.1:${FRONTEND_PORT}|" "$NGINX_CONF"
if nginx -t 2>/dev/null; then
nginx -s reload 2>/dev/null
doctor_fixed "Updated nginx to proxy to port $FRONTEND_PORT"
else
print_error " nginx config broken after edit — reverting"
sed -i "s|proxy_pass http://127.0.0.1:${FRONTEND_PORT}|proxy_pass http://127.0.0.1:${NGINX_FRONTEND_PORT}|" "$NGINX_CONF"
fi
else
doctor_skip "Update nginx proxy_pass from :$NGINX_FRONTEND_PORT to :$FRONTEND_PORT"
fi
fi
fi
# Check API proxy port
NGINX_API_PORT=$(grep -A1 'location /api' "$NGINX_CONF" | grep proxy_pass | grep -oP ':\K[0-9]+' || echo "")
if [ -n "$NGINX_API_PORT" ]; then
if ss -tlnp 2>/dev/null | grep -q ":${NGINX_API_PORT}.*gunicorn\|:${NGINX_API_PORT}.*python"; then
doctor_ok "nginx API proxy port ($NGINX_API_PORT) has backend listening"
else
doctor_issue "nginx proxies API to :$NGINX_API_PORT but nothing is listening there"
fi
fi
else
doctor_issue "nginx config not found: $NGINX_CONF"
fi
echo ""
# ── 4. Docker ───────────────────────────────────────────────
echo "Docker"
echo "───────────────────────────────────────"
if command -v docker &>/dev/null; then
doctor_ok "Docker installed"
else
doctor_issue "Docker not found"
fi
if docker info &>/dev/null; then
doctor_ok "Docker daemon running"
else
doctor_issue "Docker daemon not running"
if [ "$FIX_MODE" = "--fix" ]; then
systemctl start docker 2>/dev/null && doctor_fixed "Started Docker daemon" || print_error " Could not start Docker"
else
doctor_skip "systemctl start docker"
fi
fi
echo ""
# ── 5. Disk space ───────────────────────────────────────────
echo "System"
echo "───────────────────────────────────────"
DISK_USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$DISK_USAGE" -lt 90 ]; then
doctor_ok "Disk usage: ${DISK_USAGE}%"
elif [ "$DISK_USAGE" -lt 95 ]; then
print_warning "Disk usage high: ${DISK_USAGE}%"
else
doctor_issue "Disk almost full: ${DISK_USAGE}%"
fi
RAM_AVAIL=$(free -m | awk '/Mem:/ {print $7}')
if [ "$RAM_AVAIL" -gt 100 ]; then
doctor_ok "Available RAM: ${RAM_AVAIL}MB"
else
doctor_issue "Low RAM: ${RAM_AVAIL}MB available"
fi
# Check swap
SWAP_TOTAL=$(free -m | awk '/^Swap:/ {print $2}')
if [ "$SWAP_TOTAL" -gt 0 ]; then
doctor_ok "Swap: ${SWAP_TOTAL}MB"
else
print_warning "No swap configured (recommended for low-RAM servers)"
fi
echo ""
# ── 6. Database ─────────────────────────────────────────────
echo "Database"
echo "───────────────────────────────────────"
DB_FILE="$INSTALL_DIR/backend/instance/serverkit.db"
if [ -f "$DB_FILE" ]; then
DB_SIZE=$(du -h "$DB_FILE" | cut -f1)
doctor_ok "SQLite database exists ($DB_SIZE)"
else
doctor_issue "Database file not found: $DB_FILE"
fi
echo ""
# ── 7. Connectivity ─────────────────────────────────────────
echo "Connectivity"
echo "───────────────────────────────────────"
if curl -sf http://127.0.0.1 > /dev/null 2>&1; then
doctor_ok "localhost:80 responding"
else
doctor_issue "localhost:80 not responding (nginx → frontend broken)"
fi
if curl -sf http://127.0.0.1/api/v1/system/health > /dev/null 2>&1; then
doctor_ok "localhost:80/api proxying to backend"
else
doctor_issue "localhost:80/api not reaching backend"
fi
echo ""
# ── Summary ─────────────────────────────────────────────────
echo "───────────────────────────────────────"
if [ "$ISSUES" -eq 0 ]; then
print_success "All checks passed!"
else
if [ "$FIX_MODE" = "--fix" ]; then
echo -e "${YELLOW}Found $ISSUES issue(s), fixed $FIXED${NC}"
REMAINING=$((ISSUES - FIXED))
if [ "$REMAINING" -gt 0 ]; then
echo "$REMAINING issue(s) need manual attention"
fi
else
echo -e "${RED}Found $ISSUES issue(s)${NC}"
echo "Run 'sudo serverkit doctor --fix' to auto-fix"
fi
fi
echo ""
}
cmd_remove_site() {
check_root
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
echo "Usage: serverkit remove-site <domain>"
cmd_list_sites
exit 1
fi
SITE_FILE="/etc/nginx/sites-available/${DOMAIN}.conf"
ENABLED_FILE="/etc/nginx/sites-enabled/${DOMAIN}.conf"
if [ ! -f "$SITE_FILE" ]; then
print_error "Site not found: $DOMAIN"
exit 1
fi
print_warning "This will remove the site config for $DOMAIN"
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -f "$ENABLED_FILE"
rm -f "$SITE_FILE"
systemctl reload nginx
print_success "Site removed: $DOMAIN"
fi
}
# Main
case "${1:-help}" in
help|--help|-h)
cmd_help
;;
start)
cmd_start
;;
stop)
cmd_stop
;;
restart)
cmd_restart
;;
status)
cmd_status
;;
logs)
cmd_logs "$2"
;;
update)
shift
cmd_update "$@"
;;
branch)
cmd_branch
;;
version|--version|-v)
cmd_version
;;
check-update)
cmd_check_update
;;
uninstall)
cmd_uninstall
;;
create-admin)
cmd_create_admin
;;
reset-password)
cmd_reset_password
;;
unlock-user)
cmd_unlock_user
;;
list-users)
cmd_list_users
;;
make-admin)
cmd_make_admin
;;
deactivate-user)
cmd_deactivate_user
;;
activate-user)
cmd_activate_user
;;
init-db)
cmd_init_db
;;
migrate-db)
cmd_migrate_db
;;
cleanup-apps)
shift
cmd_cleanup_apps "$@"
;;
factory-reset)
cmd_factory_reset
;;
backup-db)
cmd_backup_db
;;
restore-db)
cmd_restore_db "$2"
;;
generate-keys)
cmd_generate_keys
;;
config)
cmd_config
;;
setup-proxy)
cmd_setup_proxy
;;
create-app)
cmd_create_app "$2" "$3" "$4"
;;
list-apps)
cmd_list_apps "$2"
;;
start-app)
cmd_start_app "$2"
;;
stop-app)
cmd_stop_app "$2"
;;
app-logs)
cmd_app_logs "$2"
;;
add-site)
cmd_add_site "$2" "$3"
;;
list-sites)
cmd_list_sites
;;
remove-site)
cmd_remove_site "$2"
;;
doctor)
cmd_doctor "$2"
;;
*)
print_error "Unknown command: $1"
echo "Run 'serverkit help' for usage"
exit 1
;;
esac