#!/usr/bin/env bash set -Eeuo pipefail LOG_FILE="${LOG_FILE:-/tmp/laravel-site-bootstrap.log}" SITE_KEY="${SITE_KEY:-}" PROJECT_NAME="${PROJECT_NAME:-Laravel Site}" APP_DIR="${APP_DIR:-}" BRANCH="${BRANCH:-main}" REPO_URL="${REPO_URL:-}" APP_URL="${APP_URL:-http://127.0.0.1}" SERVER_NAME="${SERVER_NAME:-_}" DB_NAME="${DB_NAME:-}" DB_USER="${DB_USER:-}" DB_PASSWORD="${DB_PASSWORD:-}" DB_HOST="${DB_HOST:-127.0.0.1}" DB_PORT="${DB_PORT:-3306}" PHP_VERSION="${PHP_VERSION:-8.3}" RUN_NPM_BUILD=1 RUN_MIGRATIONS=1 CONFIGURE_SCHEDULER=1 CONFIGURE_QUEUE_WORKER=0 ALLOW_EXISTING_APP_DIR=0 ALLOW_EXISTING_DATABASE=0 REPLACE_NGINX_SITE=0 QUEUE_COMMAND="${QUEUE_COMMAND:-php artisan queue:work --sleep=3 --tries=3 --max-time=3600}" OWNER_USER="${SUDO_USER:-$USER}" usage() { cat <<'EOF' Usage: laravel-site-bootstrap.sh [options] Required options: --repo-url Git repository URL to clone --db-password MySQL password for the application user Optional options: --site-key Unique site key used for nginx/cron/supervisor names --project-name APP_NAME value (default: Laravel Site) --branch Git branch to checkout (default: main) --app-dir Laravel application directory (default: /var/www/) --app-url Public APP_URL value (default: http://127.0.0.1) --server-name Nginx server_name value (default: _) --db-name MySQL database name (default derived from site key) --db-user MySQL app user (default derived from site key) --db-host MySQL host (default: 127.0.0.1) --db-port MySQL port (default: 3306) --php-version PHP FPM version for nginx upstream (default: 8.3) --skip-build Skip npm install/build --skip-migrations Skip artisan migrate --force --without-scheduler Do not create the cron scheduler entry --with-queue-worker Create a supervisor queue worker service --queue-command Queue worker command when --with-queue-worker is used --allow-existing-app-dir Reuse an existing git worktree in the target app dir --allow-existing-database Reuse an existing database/user instead of failing --replace-nginx-site Replace an existing nginx site file for this site key --help Show this message Example: sudo bash scripts/install/laravel-site-bootstrap.sh \ --site-key demo-netgescon \ --project-name "Demo NetGescon" \ --repo-url git@your-gitea:studio/demo-netgescon.git \ --branch main \ --app-url https://demo.example.it \ --server-name demo.example.it \ --db-password 'StrongDbPass123!' EOF } log() { local level="$1" shift printf '[%s] %s\n' "$level" "$*" >&2 printf '[%s] %s\n' "$level" "$*" >> "$LOG_FILE" 2>/dev/null || true } info() { log INFO "$@"; } warn() { log WARN "$@"; } fail() { log ERROR "$@"; exit 1; } on_error() { local exit_code="$1" local line_no="$2" local command_text="$3" log ERROR "Installer failed at line ${line_no}: ${command_text} (exit ${exit_code})" log ERROR "See log file: ${LOG_FILE}" exit "$exit_code" } trap 'on_error "$?" "$LINENO" "$BASH_COMMAND"' ERR run_root() { if [[ ${EUID} -eq 0 ]]; then "$@" else sudo "$@" fi } run_owner() { local target_user="$1" shift if [[ ${EUID} -eq 0 ]]; then runuser -u "$target_user" -- "$@" else sudo -u "$target_user" "$@" fi } sql_escape() { printf '%s' "$1" | sed "s/'/''/g" } set_env_value() { local key="$1" local value="$2" local file="$3" local escaped escaped=$(printf '%s' "$value" | sed -e 's/[\/&]/\\&/g') if grep -q "^${key}=" "$file"; then sed -i "s/^${key}=.*/${key}=${escaped}/" "$file" else printf '%s=%s\n' "$key" "$value" >> "$file" fi } slugify() { local value="$1" value=$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]') value=$(printf '%s' "$value" | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-{2,}/-/g') if [[ -z "$value" ]]; then value='laravel-site' fi printf '%s' "$value" } ensure_site_key() { if [[ -z "$SITE_KEY" ]]; then if [[ -n "$APP_DIR" ]]; then SITE_KEY="$(slugify "$(basename "$APP_DIR")")" elif [[ "$SERVER_NAME" != '_' ]]; then SITE_KEY="$(slugify "$SERVER_NAME")" elif [[ -n "$DB_NAME" ]]; then SITE_KEY="$(slugify "$DB_NAME")" else SITE_KEY='laravel-site' fi else SITE_KEY="$(slugify "$SITE_KEY")" fi if [[ -z "$APP_DIR" ]]; then APP_DIR="/var/www/${SITE_KEY}" fi if [[ -z "$DB_NAME" ]]; then DB_NAME="$(printf 'laravel_%s' "$SITE_KEY" | tr '-' '_' | cut -c1-64)" fi if [[ -z "$DB_USER" ]]; then DB_USER="$(printf '%s_user' "$DB_NAME" | cut -c1-32)" fi } mysql_database_exists() { run_root mysql -Nse "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '$1'" | grep -qx "$1" } mysql_user_exists() { run_root mysql -Nse "SELECT User FROM mysql.user WHERE User = '$1' AND Host = 'localhost'" | grep -qx "$1" } assert_safe_targets() { if [[ -d "$APP_DIR" ]] && [[ -n "$(find "$APP_DIR" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1)" ]]; then if [[ $ALLOW_EXISTING_APP_DIR -ne 1 ]]; then fail "App directory already exists and is not empty: $APP_DIR. Re-run with --allow-existing-app-dir only if you really want to reuse it." fi if [[ ! -d "$APP_DIR/.git" || ! -f "$APP_DIR/artisan" ]]; then fail "Existing app directory must already be a Laravel git worktree: $APP_DIR" fi fi if [[ -e "$NGINX_SITE_FILE" || -L "$NGINX_SITE_ENABLED" ]]; then if [[ $REPLACE_NGINX_SITE -ne 1 ]]; then fail "Nginx site already exists for ${SITE_KEY}. Re-run with --replace-nginx-site only if replacement is intentional." fi fi if mysql_database_exists "$DB_NAME"; then if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then fail "Database already exists: $DB_NAME. Re-run with --allow-existing-database only if reuse is intentional." fi fi if mysql_user_exists "$DB_USER"; then if [[ $ALLOW_EXISTING_DATABASE -ne 1 ]]; then fail "Database user already exists: $DB_USER. Re-run with --allow-existing-database only if reuse is intentional." fi fi } while [[ $# -gt 0 ]]; do case "$1" in --site-key) SITE_KEY="$2" shift 2 ;; --project-name) PROJECT_NAME="$2" shift 2 ;; --repo-url) REPO_URL="$2" shift 2 ;; --branch) BRANCH="$2" shift 2 ;; --app-dir) APP_DIR="$2" shift 2 ;; --app-url) APP_URL="$2" shift 2 ;; --server-name) SERVER_NAME="$2" shift 2 ;; --db-name) DB_NAME="$2" shift 2 ;; --db-user) DB_USER="$2" shift 2 ;; --db-password) DB_PASSWORD="$2" shift 2 ;; --db-host) DB_HOST="$2" shift 2 ;; --db-port) DB_PORT="$2" shift 2 ;; --php-version) PHP_VERSION="$2" shift 2 ;; --skip-build) RUN_NPM_BUILD=0 shift ;; --skip-migrations) RUN_MIGRATIONS=0 shift ;; --without-scheduler) CONFIGURE_SCHEDULER=0 shift ;; --with-queue-worker) CONFIGURE_QUEUE_WORKER=1 shift ;; --queue-command) QUEUE_COMMAND="$2" shift 2 ;; --allow-existing-app-dir) ALLOW_EXISTING_APP_DIR=1 shift ;; --allow-existing-database) ALLOW_EXISTING_DATABASE=1 shift ;; --replace-nginx-site) REPLACE_NGINX_SITE=1 shift ;; --help|-h) usage exit 0 ;; *) fail "Unknown option: $1" ;; esac done info "Starting Laravel site bootstrap" info "Execution user: $(id -un)" info "Log file: $LOG_FILE" if [[ -z "$REPO_URL" ]]; then fail "--repo-url is required" fi if [[ -z "$DB_PASSWORD" ]]; then fail "--db-password is required" fi for command in git php composer mysql nginx; do if ! command -v "$command" >/dev/null 2>&1; then fail "Missing required command: $command" fi done ensure_site_key APP_PARENT_DIR="$(dirname "$APP_DIR")" NGINX_SITE_FILE="/etc/nginx/sites-available/${SITE_KEY}.conf" NGINX_SITE_ENABLED="/etc/nginx/sites-enabled/${SITE_KEY}.conf" CRON_FILE="/etc/cron.d/${SITE_KEY}-scheduler" SUPERVISOR_FILE="/etc/supervisor/conf.d/${SITE_KEY}-worker.conf" TEMP_DIR="$(mktemp -d)" trap 'rm -rf "$TEMP_DIR"' EXIT assert_safe_targets info "Preparing directories" run_root install -d -o "$OWNER_USER" -g www-data -m 2775 "$APP_PARENT_DIR" if [[ -d "$APP_DIR/.git" ]]; then info "Refreshing application worktree" run_owner "$OWNER_USER" git -C "$APP_DIR" remote set-url origin "$REPO_URL" run_owner "$OWNER_USER" git -C "$APP_DIR" fetch --all --prune run_owner "$OWNER_USER" git -C "$APP_DIR" checkout "$BRANCH" run_owner "$OWNER_USER" git -C "$APP_DIR" reset --hard "origin/$BRANCH" else info "Cloning application worktree" run_owner "$OWNER_USER" git clone --branch "$BRANCH" "$REPO_URL" "$APP_DIR" fi if [[ ! -f "$APP_DIR/artisan" ]]; then fail "Laravel artisan file not found in $APP_DIR" fi info "Configuring MySQL database and user" DB_PASSWORD_ESCAPED="$(sql_escape "$DB_PASSWORD")" run_root mysql < "$TEMP_DIR/${SITE_KEY}.conf" < "$TEMP_DIR/${SITE_KEY}-scheduler" <> /var/log/${SITE_KEY}-scheduler.log 2>&1 EOF run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-scheduler" "$CRON_FILE" fi if [[ $CONFIGURE_QUEUE_WORKER -eq 1 ]]; then info "Configuring supervisor worker" cat > "$TEMP_DIR/${SITE_KEY}-worker.conf" < "$TEMP_DIR/install-site-summary.txt" <