feat(cli): introduce hdg global CLI tool with self-install, self-update and shorthand syntax

- hdg: Global executable with auto-symlinking to /usr/local/bin, self-update, and auto-setup

- cli.py: Unified Python CLI with setup, repair, doctor, update, and install commands

- add-gunicorn-service.sh: Kept as backward-compatible wrapper redirecting to hdg
This commit is contained in:
2026-09-14 13:55:14 +03:30
parent 11608523c9
commit aebae3b604
3 changed files with 738 additions and 91 deletions

View File

@@ -1,92 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────
# File: add-gunicorn-service.sh # Backward-compatibility wrapper -> redirects to 'hdg'
# Usage: ./add-gunicorn-service.sh domain.com hestiauser projectname # ──────────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
set -e exec "$SCRIPT_DIR/hdg" "$@"
if [ "$#" -ne 3 ]; then
echo "Usage: $0 domain hestia_user project_name"
echo "Example: $0 example.com admin myproject"
exit 1
fi
DOMAIN="$1"
USER="$2"
PROJECT="$3"
HOMEDIR="/home/$USER"
WEBDIR="$HOMEDIR/web/$DOMAIN/public_html"
VENV_BIN="$WEBDIR/venv/bin"
SOCK_PATH="$WEBDIR/gunicorn.sock"
SERVICE_NAME="gunicorn-$DOMAIN"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
if [ ! -d "$WEBDIR" ]; then
echo "Error: Domain directory not found → $WEBDIR"
exit 1
fi
if [ ! -d "$VENV_BIN" ]; then
echo "Warning: Virtualenv not found → $VENV_BIN"
echo "Assuming gunicorn is installed inside the venv anyway"
fi
echo "Creating service for: $DOMAIN"
echo "User: $USER"
echo "Project: $PROJECT"
echo "Socket path: $SOCK_PATH"
echo "Service name: $SERVICE_NAME"
echo ""
cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null
[Unit]
Description=Gunicorn instance for $DOMAIN (Django)
After=network.target
[Service]
User=$USER
Group=www-data
WorkingDirectory=$WEBDIR
Environment="PATH=$VENV_BIN:/usr/local/bin:/usr/bin"
Environment="PYTHONUNBUFFERED=1"
ExecStart=$VENV_BIN/gunicorn \\
--access-logfile - \\
--workers 3 \\
--threads 2 \\
--worker-class=gthread \\
--bind unix:$SOCK_PATH \\
--log-level=info \\
$PROJECT.wsgi:application
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
echo "Service file created: $SERVICE_FILE"
sudo systemctl daemon-reload
echo "→ daemon-reload completed"
sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true
sudo systemctl enable "$SERVICE_NAME"
sudo systemctl start "$SERVICE_NAME"
echo ""
echo "Service status:"
sudo systemctl status "$SERVICE_NAME" --no-pager -l | head -n 12
echo ""
echo "Socket check:"
ls -l "$SOCK_PATH" 2>/dev/null || echo "Socket not yet created (service must be running)"
echo ""
echo "If you get 502 Bad Gateway:"
echo "1. Make sure gunicorn is installed in venv: $VENV_BIN/pip install gunicorn"
echo "2. Run migrate & collectstatic if needed"
echo "3. Restart service: sudo systemctl restart $SERVICE_NAME"

391
cli.py Normal file
View File

@@ -0,0 +1,391 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
HestiaCP Django-Gunicorn Deployment CLI.
A clean, modular command-line interface that orchestrates all deployment,
repair, and diagnostic operations.
Usage:
python3 cli.py setup <domain> <user> [project]
python3 cli.py repair <domain> <user>
python3 cli.py doctor <domain> <user>
python3 cli.py install-templates
python3 cli.py status <domain>
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
# Ensure the project root is on sys.path so ``core`` can be imported
# regardless of where the script is invoked from.
PROJECT_ROOT = Path(__file__).resolve().parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.detector import DjangoProjectDetector
from core.doctor import DeploymentDoctor, print_report
from core.executor import LinuxExecutor
from core.migrator import ProjectMigrator
from core.models import DomainConfig, ServiceConfig
from core.nginx_manager import HestiaNginxManager
from core.service_generator import SystemdServiceGenerator
# ──────────────────────────────────────────────────────────────────────────
# ANSI helpers
# ──────────────────────────────────────────────────────────────────────────
RESET = "\033[0m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
BOLD = "\033[1m"
CYAN = "\033[96m"
def info(msg: str) -> None:
print(f" {CYAN}→{RESET} {msg}")
def success(msg: str) -> None:
print(f" {GREEN}✔{RESET} {msg}")
def warn(msg: str) -> None:
print(f" {YELLOW}⚠{RESET} {msg}")
def error(msg: str) -> None:
print(f" {RED}✘{RESET} {msg}")
def banner() -> None:
print(f"""
{BOLD}┌──────────────────────────────────────────────────────────┐
│ HestiaCP + Django + Gunicorn — Deployment Engine │
│ Prepared by MohammadAmin Baranzehi (amin.baranzehi.com) │
└──────────────────────────────────────────────────────────┘{RESET}
""")
# ──────────────────────────────────────────────────────────────────────────
# Shared factory
# ──────────────────────────────────────────────────────────────────────────
def _build_components():
"""Instantiate shared components with dependency injection."""
executor = LinuxExecutor()
detector = DjangoProjectDetector()
generator = SystemdServiceGenerator()
nginx_mgr = HestiaNginxManager(executor)
migrator = ProjectMigrator(executor, detector, generator)
doctor = DeploymentDoctor(executor)
return executor, detector, generator, nginx_mgr, migrator, doctor
# ──────────────────────────────────────────────────────────────────────────
# Sub-command handlers
# ──────────────────────────────────────────────────────────────────────────
def cmd_setup(args: argparse.Namespace) -> int:
"""Deploy a new Django project on HestiaCP."""
banner()
executor, detector, generator, nginx_mgr, _, doctor = _build_components()
dc = DomainConfig(domain=args.domain, user=args.user)
info(f"Domain : {dc.domain}")
info(f"User : {dc.user}")
info(f"Web dir: {dc.web_dir}")
print()
# 1. Detect project
info("Detecting Django project structure...")
project_info = detector.detect(dc.web_dir, project_hint=args.project)
if not project_info.is_valid:
error("Project detection failed:")
for err in project_info.errors:
error(f" {err}")
return 1
success(f"Layout : {project_info.layout.name}")
success(f"WSGI : {project_info.wsgi_module}")
success(f"Venv : {project_info.venv_path or 'NOT FOUND'}")
success(f"Static : {project_info.static_root_name}")
success(f"DB : {project_info.database_engine.name}")
if project_info.has_dot_env:
success(".env file detected — will load via EnvironmentFile")
print()
# 2. Install templates if not present
if not nginx_mgr.is_template_installed():
info("Installing NGINX templates...")
tpl_dir = PROJECT_ROOT / "templates"
nginx_mgr.install_templates(
tpl_dir / "django-gunicorn.tpl",
tpl_dir / "django-gunicorn.stpl",
)
success("NGINX templates installed.")
else:
success("NGINX templates already installed.")
# 3. Generate service file
info("Generating systemd service...")
service_config = ServiceConfig(domain_config=dc, project_info=project_info)
content = generator.generate(service_config)
# Backup if exists
if executor.file_exists(dc.service_file):
bak = executor.backup_file(dc.service_file)
warn(f"Existing service backed up to: {bak}")
executor.write_file(dc.service_file, content, mode=0o644)
success(f"Service file: {dc.service_file}")
success(f"Workers: {service_config.workers} | Threads: {service_config.threads} | umask: {service_config.umask}")
print()
# 4. Enable and start
info("Starting Gunicorn service...")
executor.run("systemctl daemon-reload")
executor.run(f"systemctl enable {dc.service_name}", check=False)
executor.run(f"systemctl restart {dc.service_name}", check=False)
# 5. Rebuild NGINX
info("Rebuilding NGINX for domain...")
nginx_mgr.rebuild_domain(dc)
print()
# 6. Fix permissions
info("Fixing file permissions...")
db_file = dc.web_dir / "db.sqlite3"
if executor.file_exists(db_file):
executor.run(f"chown {dc.user}:www-data {db_file} && chmod 664 {db_file}", check=False)
success("db.sqlite3 permissions fixed.")
static_dir = dc.web_dir / project_info.static_root_name
if executor.file_exists(static_dir):
executor.run(f"chown -R {dc.user}:www-data {static_dir}", check=False)
success(f"{project_info.static_root_name}/ ownership fixed.")
print()
# 7. Run doctor
info("Running health checks...")
report = doctor.diagnose(dc)
print_report(report)
return 0 if report.is_healthy else 1
def cmd_repair(args: argparse.Namespace) -> int:
"""Repair / upgrade a previously deployed project."""
banner()
executor, detector, generator, nginx_mgr, migrator, _ = _build_components()
dc = DomainConfig(domain=args.domain, user=args.user)
info(f"Repairing deployment for: {dc.domain}")
print()
if not migrator.needs_migration(dc):
success("No migration needed — deployment looks up-to-date.")
return 0
# Update templates first
info("Updating NGINX templates...")
tpl_dir = PROJECT_ROOT / "templates"
nginx_mgr.install_templates(
tpl_dir / "django-gunicorn.tpl",
tpl_dir / "django-gunicorn.stpl",
)
success("NGINX templates updated.")
# Run migration
info("Migrating service configuration...")
report = migrator.migrate(dc)
# Rebuild NGINX
info("Rebuilding NGINX configuration...")
nginx_mgr.rebuild_domain(dc)
print_report(report)
return 0 if report.is_healthy else 1
def cmd_doctor(args: argparse.Namespace) -> int:
"""Run diagnostics on a deployment."""
banner()
_, _, _, _, _, doctor = _build_components()
dc = DomainConfig(domain=args.domain, user=args.user)
report = doctor.diagnose(dc)
print_report(report)
return 0 if report.is_healthy else 1
def cmd_install_templates(args: argparse.Namespace) -> int:
"""Install NGINX templates to HestiaCP."""
banner()
executor, _, _, nginx_mgr, _, _ = _build_components()
tpl_dir = PROJECT_ROOT / "templates"
info("Installing NGINX templates...")
nginx_mgr.install_templates(
tpl_dir / "django-gunicorn.tpl",
tpl_dir / "django-gunicorn.stpl",
)
success("Templates installed to HestiaCP successfully.")
return 0
def cmd_status(args: argparse.Namespace) -> int:
"""Show status of a Gunicorn service."""
banner()
executor, _, _, _, _, _ = _build_components()
service_name = f"gunicorn-{args.domain}"
rc, stdout, stderr = executor.run(
f"systemctl status {service_name} --no-pager -l",
check=False,
)
print(stdout)
if stderr:
print(stderr)
return rc
def cmd_update(args: argparse.Namespace) -> int:
"""Self-update the deployment engine and refresh templates."""
banner()
executor, _, _, nginx_mgr, _, _ = _build_components()
info("Updating HestiaCP Django-Gunicorn Deployment Engine...")
# 1. Pull latest from git if repository
git_dir = PROJECT_ROOT / ".git"
if git_dir.exists():
info("Pulling latest updates from git...")
rc, out, err = executor.run(f"git -C {PROJECT_ROOT} pull --ff-only", check=False)
if rc == 0:
success("Repository updated to latest commit.")
else:
rc2, out2, err2 = executor.run(f"git -C {PROJECT_ROOT} pull", check=False)
if rc2 == 0:
success("Repository updated.")
else:
warn(f"Could not pull updates from git: {err or err2}")
# 2. Update templates in HestiaCP
tpl_dir = PROJECT_ROOT / "templates"
info("Updating NGINX templates...")
nginx_mgr.install_templates(
tpl_dir / "django-gunicorn.tpl",
tpl_dir / "django-gunicorn.stpl",
)
success("NGINX templates updated in HestiaCP.")
# 3. Ensure global symlink
cmd_install(args)
success("Update completed successfully! 'hdg' is up to date.")
return 0
def cmd_install(args: argparse.Namespace) -> int:
"""Install hdg globally to /usr/local/bin."""
executor, _, _, nginx_mgr, _, _ = _build_components()
bin_dir = Path("/usr/local/bin")
hdg_script = PROJECT_ROOT / "hdg"
if bin_dir.exists() and hdg_script.exists():
for link_name in ["hdg", "v-django", "hestia-django"]:
target = bin_dir / link_name
executor.run(f"ln -sf {hdg_script} {target}", check=False)
executor.run(f"chmod +x {hdg_script}", check=False)
success("Global commands installed: 'hdg', 'v-django', 'hestia-django'")
return 0
# ──────────────────────────────────────────────────────────────────────────
# Argument parser
# ──────────────────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="hdg",
description="HestiaCP + Django + Gunicorn Deployment Engine (hdg)",
)
sub = parser.add_subparsers(dest="command", help="Available commands")
# setup
p_setup = sub.add_parser("setup", help="Deploy a new Django project")
p_setup.add_argument("domain", help="Domain name (e.g. example.com)")
p_setup.add_argument("user", help="HestiaCP system user")
p_setup.add_argument("project", nargs="?", default=None,
help="Project name (auto-detected if omitted)")
# repair
p_repair = sub.add_parser("repair", help="Repair a previously deployed project")
p_repair.add_argument("domain", help="Domain name")
p_repair.add_argument("user", help="HestiaCP system user")
# doctor
p_doctor = sub.add_parser("doctor", help="Run deployment health checks")
p_doctor.add_argument("domain", help="Domain name")
p_doctor.add_argument("user", help="HestiaCP system user")
# install-templates
sub.add_parser("install-templates", help="Install NGINX templates to HestiaCP")
# update
sub.add_parser("update", help="Update hdg and NGINX templates to latest version")
# install
sub.add_parser("install", help="Install global symlinks to /usr/local/bin")
# status
p_status = sub.add_parser("status", help="Show Gunicorn service status")
p_status.add_argument("domain", help="Domain name")
return parser
def main() -> int:
# Convenience shorthand: if first arg is a domain or not a command, default to 'setup'
known_commands = {
"setup", "repair", "doctor", "install-templates",
"update", "install", "status", "-h", "--help",
}
if len(sys.argv) > 1 and sys.argv[1] not in known_commands:
sys.argv.insert(1, "setup")
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
return 0
dispatch = {
"setup": cmd_setup,
"repair": cmd_repair,
"doctor": cmd_doctor,
"install-templates": cmd_install_templates,
"update": cmd_update,
"install": cmd_install,
"status": cmd_status,
}
handler = dispatch.get(args.command)
if handler is None:
parser.print_help()
return 1
try:
return handler(args)
except Exception as exc:
error(f"Unexpected error: {exc}")
return 1
if __name__ == "__main__":
sys.exit(main())

342
hdg Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────────────────────
# hdg — HestiaCP + Django + Gunicorn Global CLI
# ──────────────────────────────────────────────────────────────────────────
# A fast, global tool for deploying and managing Django on HestiaCP.
#
# Usage from ANYWHERE on your system:
# hdg <domain> <user> [project] # Quick deploy (auto-detects project)
# hdg setup <domain> <user> [project] # Explicit deploy
# hdg repair <domain> <user> # Fix & migrate existing setup
# hdg doctor <domain> <user> # Run health checks
# hdg status <domain> # Check service status
# hdg update # Self-update to latest version
# hdg install # Re-link global commands
#
# Prepared by: MohammadAmin Baranzehi — amin.baranzehi.com
# ──────────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Resolve real project directory even through symlinks ──────────────────
SOURCE="${BASH_SOURCE[0]}"
while [ -h "$SOURCE" ]; do
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
SOURCE="$(readlink "$SOURCE")"
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
done
PROJECT_ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)"
CLI_PY="${PROJECT_ROOT}/cli.py"
# ── Colours ───────────────────────────────────────────────────────────────
RED='\033[0;91m'
GREEN='\033[0;92m'
YELLOW='\033[0;93m'
CYAN='\033[0;96m'
BOLD='\033[1m'
NC='\033[0m'
info() { echo -e " ${CYAN}→${NC} $*"; }
success() { echo -e " ${GREEN}✔${NC} $*"; }
warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
error() { echo -e " ${RED}✘${NC} $*"; }
# ── Self-installation to /usr/local/bin ───────────────────────────────────
install_global_links() {
local bin_dir="/usr/local/bin"
[ ! -d "$bin_dir" ] && return 0
local links=("hdg" "v-django" "hestia-django")
local need_install=0
for link in "${links[@]}"; do
local target="${bin_dir}/${link}"
if [ ! -L "$target" ] || [ "$(readlink -f "$target" 2>/dev/null)" != "${PROJECT_ROOT}/hdg" ]; then
need_install=1
break
fi
done
if [ "$need_install" -eq 1 ]; then
local SUDO=""
[ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &>/dev/null && SUDO="sudo"
for link in "${links[@]}"; do
$SUDO ln -sf "${PROJECT_ROOT}/hdg" "${bin_dir}/${link}" 2>/dev/null || true
done
$SUDO chmod +x "${PROJECT_ROOT}/hdg" 2>/dev/null || true
if [ ! -f "${PROJECT_ROOT}/.installed" ]; then
touch "${PROJECT_ROOT}/.installed" 2>/dev/null || true
echo ""
success "Installed globally as '${BOLD}hdg${NC}' (also '${BOLD}v-django${NC}')"
info "You can now run '${BOLD}hdg <domain> <user>${NC}' from ANY directory!"
echo ""
fi
fi
}
# ── Self-update command ───────────────────────────────────────────────────
self_update() {
echo ""
echo -e "${BOLD}Updating hdg (HestiaCP Django-Gunicorn Engine)...${NC}"
if [ -d "${PROJECT_ROOT}/.git" ] && command -v git &>/dev/null; then
info "Pulling latest changes from git repository..."
git -C "$PROJECT_ROOT" pull --ff-only 2>/dev/null || {
warn "git pull --ff-only failed, trying standard pull..."
git -C "$PROJECT_ROOT" pull || warn "Could not pull git updates (check network or local changes)"
}
else
info "Git repository not detected, skipping git pull."
fi
local hestia_tpl_dir="/usr/local/hestia/data/templates/web/nginx"
if [ -d "$hestia_tpl_dir" ]; then
info "Updating NGINX templates in HestiaCP..."
local SUDO=""
[ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &>/dev/null && SUDO="sudo"
$SUDO cp -f "${PROJECT_ROOT}/templates/django-gunicorn.tpl" "$hestia_tpl_dir/" 2>/dev/null || true
$SUDO cp -f "${PROJECT_ROOT}/templates/django-gunicorn.stpl" "$hestia_tpl_dir/" 2>/dev/null || true
success "NGINX templates updated in ${hestia_tpl_dir}"
fi
install_global_links
success "hdg is up to date and ready!"
echo ""
exit 0
}
# ── Handle self-install / update directly ─────────────────────────────────
case "${1:-}" in
install|self-install)
install_global_links
success "Global installation complete! Run '${BOLD}hdg${NC}' from anywhere."
exit 0
;;
update|self-update)
self_update
;;
esac
# ── Automatically install or update global symlink on run ────────────────
install_global_links 2>/dev/null || true
# ── Try Python engine first ──────────────────────────────────────────────
if command -v python3 &>/dev/null && [ -f "$CLI_PY" ]; then
python3 "$CLI_PY" "$@"
exit $?
fi
# ══════════════════════════════════════════════════════════════════════════
# Bash fallback (self-contained, for servers without full Python setup)
# ══════════════════════════════════════════════════════════════════════════
usage() {
echo -e "${BOLD}hdg${NC} — HestiaCP + Django + Gunicorn CLI"
echo ""
echo "Usage:"
echo " hdg <domain> <user> [project] # Auto-deploy"
echo " hdg repair <domain> <user> # Fix existing deployment"
echo " hdg doctor <domain> <user> # Check deployment health"
echo " hdg update # Self-update"
echo ""
echo "Examples:"
echo " hdg example.com admin myproject"
echo " hdg example.com admin # auto-detects config/wsgi.py"
echo " hdg repair example.com admin"
echo " hdg update"
exit 1
}
# ── Handle 'repair' ───────────────────────────────────────────────────────
if [ "${1:-}" = "repair" ]; then
[ "$#" -lt 3 ] && usage
DOMAIN="$2"
USER="$3"
SERVICE_NAME="gunicorn-${DOMAIN}"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ ! -f "$SERVICE_FILE" ]; then
error "Service file not found: $SERVICE_FILE"
exit 1
fi
info "Repairing deployment for: $DOMAIN"
BACKUP="${SERVICE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "$SERVICE_FILE" "$BACKUP"
success "Backed up to: $BACKUP"
"$0" "$DOMAIN" "$USER"
exit $?
fi
# ── Handle 'setup' explicit keyword ──────────────────────────────────────
if [ "${1:-}" = "setup" ]; then
shift
fi
# ── Parse arguments ──────────────────────────────────────────────────────
[ "$#" -lt 2 ] && usage
DOMAIN="$1"
USER="$2"
PROJECT="${3:-}"
HOMEDIR="/home/$USER"
WEBDIR="$HOMEDIR/web/$DOMAIN/public_html"
SERVICE_NAME="gunicorn-${DOMAIN}"
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
if [ ! -d "$WEBDIR" ]; then
error "Domain directory not found → $WEBDIR"
exit 1
fi
# ── Virtualenv detection ─────────────────────────────────────────────────
if [ -d "$WEBDIR/venv" ]; then
VENV_BIN="$WEBDIR/venv/bin"
elif [ -d "$WEBDIR/.venv" ]; then
VENV_BIN="$WEBDIR/.venv/bin"
elif [ -d "$HOMEDIR/.virtualenvs/$DOMAIN" ]; then
VENV_BIN="$HOMEDIR/.virtualenvs/$DOMAIN/bin"
else
error "Virtual environment not found in:"
error " $WEBDIR/venv"
error " $WEBDIR/.venv"
error " $HOMEDIR/.virtualenvs/$DOMAIN"
exit 1
fi
if [ ! -f "$VENV_BIN/gunicorn" ]; then
error "Gunicorn is not installed in the virtualenv ($VENV_BIN/gunicorn missing)."
error "Install it: $VENV_BIN/pip install gunicorn"
exit 1
fi
# ── WSGI auto-detection ──────────────────────────────────────────────────
WSGI_MODULE=""
if [ -n "$PROJECT" ] && [ -f "$WEBDIR/$PROJECT/wsgi.py" ]; then
WSGI_MODULE="${PROJECT}.wsgi:application"
info "WSGI module (specified): $WSGI_MODULE"
elif [ -f "$WEBDIR/config/wsgi.py" ]; then
WSGI_MODULE="config.wsgi:application"
info "WSGI module (detected modular): $WSGI_MODULE"
else
DETECTED_WSGI=$(find "$WEBDIR" -maxdepth 3 -name "wsgi.py" \
-not -path "*/venv/*" \
-not -path "*/.venv/*" \
-not -path "*/site-packages/*" \
| head -n 1)
if [ -n "$DETECTED_WSGI" ]; then
REL_WSGI="${DETECTED_WSGI#$WEBDIR/}"
MODULE_PATH="${REL_WSGI%/wsgi.py}"
MODULE_DOT=$(echo "$MODULE_PATH" | tr '/' '.')
if [ "$REL_WSGI" = "wsgi.py" ]; then
WSGI_MODULE="wsgi:application"
else
WSGI_MODULE="${MODULE_DOT}.wsgi:application"
fi
info "WSGI module (auto-detected): $WSGI_MODULE"
else
error "Could not auto-detect wsgi.py in $WEBDIR"
error "Please specify the project name: $0 $DOMAIN $USER <project_name>"
exit 1
fi
fi
# ── Environment & Workers ────────────────────────────────────────────────
ENV_FILE_LINE=""
if [ -f "$WEBDIR/.env" ]; then
ENV_FILE_LINE="EnvironmentFile=$WEBDIR/.env"
info "Environment file detected: $WEBDIR/.env"
fi
CPU_COUNT=$(nproc 2>/dev/null || echo 1)
WORKERS=$(( (2 * CPU_COUNT) + 1 ))
[ "$WORKERS" -gt 9 ] && WORKERS=9
SOCK_PATH="$WEBDIR/gunicorn.sock"
# ── Print summary ────────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}Deployment Summary${NC}"
echo " Domain : $DOMAIN"
echo " User : $USER"
echo " WSGI : $WSGI_MODULE"
echo " Workers : $WORKERS"
echo " Socket : $SOCK_PATH"
echo " Service : $SERVICE_NAME"
echo ""
# ── Generate service file ────────────────────────────────────────────────
if [ -f "$SERVICE_FILE" ]; then
BACKUP="${SERVICE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "$SERVICE_FILE" "$BACKUP"
warn "Existing service backed up to: $BACKUP"
fi
cat <<EOF | sudo tee "$SERVICE_FILE" > /dev/null
# ──────────────────────────────────────────────────────────────────
# Gunicorn service for $DOMAIN
# Auto-generated by hdg (HestiaCP Django-Gunicorn Engine)
# ──────────────────────────────────────────────────────────────────
[Unit]
Description=Gunicorn instance for $DOMAIN (Django)
After=network.target
[Service]
User=$USER
Group=www-data
WorkingDirectory=$WEBDIR
Environment="PATH=$VENV_BIN:/usr/local/bin:/usr/bin"
Environment="PYTHONUNBUFFERED=1"
$ENV_FILE_LINE
ExecStart=$VENV_BIN/gunicorn \\
--access-logfile - \\
--error-logfile - \\
--workers $WORKERS \\
--threads 2 \\
--worker-class=gthread \\
--bind unix:$SOCK_PATH \\
--umask 007 \\
--log-level=info \\
$WSGI_MODULE
Restart=always
RestartSec=3
ProtectSystem=full
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
success "Generated: $SERVICE_FILE"
# ── Fix database permissions ─────────────────────────────────────────────
if [ -f "$WEBDIR/db.sqlite3" ]; then
sudo chown "$USER:www-data" "$WEBDIR/db.sqlite3"
sudo chmod 664 "$WEBDIR/db.sqlite3"
success "Fixed SQLite permissions on db.sqlite3"
fi
# ── Enable & start service ───────────────────────────────────────────────
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME"
sudo systemctl restart "$SERVICE_NAME"
sleep 1
if sudo systemctl is-active --quiet "$SERVICE_NAME"; then
success "Service $SERVICE_NAME is running!"
else
error "Service failed to start. Logs:"
sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager
exit 1
fi
echo ""
echo -e "${GREEN}${BOLD}Deployment completed successfully!${NC}"