netgescon-day0/scripts/install/laravel-site-bootstrap.sh

500 lines
14 KiB
Bash

#!/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 <url> Git repository URL to clone
--db-password <password> MySQL password for the application user
Optional options:
--site-key <slug> Unique site key used for nginx/cron/supervisor names
--project-name <name> APP_NAME value (default: Laravel Site)
--branch <name> Git branch to checkout (default: main)
--app-dir <path> Laravel application directory (default: /var/www/<site-key>)
--app-url <url> Public APP_URL value (default: http://127.0.0.1)
--server-name <name> Nginx server_name value (default: _)
--db-name <name> MySQL database name (default derived from site key)
--db-user <user> MySQL app user (default derived from site key)
--db-host <host> MySQL host (default: 127.0.0.1)
--db-port <port> MySQL port (default: 3306)
--php-version <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 <cmd> 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 <<SQL
CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER IF NOT EXISTS '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
ALTER USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASSWORD_ESCAPED';
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';
FLUSH PRIVILEGES;
SQL
cd "$APP_DIR"
info "Installing PHP dependencies"
run_owner "$OWNER_USER" composer install --no-dev --optimize-autoloader
if [[ $RUN_NPM_BUILD -eq 1 && -f package.json ]]; then
info "Installing frontend dependencies"
if [[ -f package-lock.json ]]; then
run_owner "$OWNER_USER" npm ci
else
run_owner "$OWNER_USER" npm install
fi
run_owner "$OWNER_USER" npm run build
fi
if [[ ! -f .env ]]; then
if [[ -f .env.example ]]; then
cp .env.example .env
else
fail ".env and .env.example are both missing"
fi
fi
info "Configuring Laravel environment"
set_env_value APP_NAME "$PROJECT_NAME" .env
set_env_value APP_ENV production .env
set_env_value APP_DEBUG false .env
set_env_value APP_URL "$APP_URL" .env
set_env_value DB_CONNECTION mysql .env
set_env_value DB_HOST "$DB_HOST" .env
set_env_value DB_PORT "$DB_PORT" .env
set_env_value DB_DATABASE "$DB_NAME" .env
set_env_value DB_USERNAME "$DB_USER" .env
set_env_value DB_PASSWORD "$DB_PASSWORD" .env
php artisan key:generate --force
php artisan storage:link || true
php artisan optimize:clear
if [[ $RUN_MIGRATIONS -eq 1 ]]; then
info "Running migrations"
php artisan migrate --force
fi
info "Caching Laravel configuration"
php artisan config:cache
php artisan route:cache
php artisan view:cache
info "Setting permissions"
run_root chown -R www-data:www-data "$APP_DIR"
run_root chmod -R 775 "$APP_DIR/storage" "$APP_DIR/bootstrap/cache"
info "Writing nginx virtual host"
cat > "$TEMP_DIR/${SITE_KEY}.conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name $SERVER_NAME;
root $APP_DIR/public;
index index.php index.html;
client_max_body_size 64m;
access_log /var/log/nginx/${SITE_KEY}-access.log;
error_log /var/log/nginx/${SITE_KEY}-error.log;
location / {
try_files \$uri \$uri/ /index.php?\$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php${PHP_VERSION}-fpm.sock;
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
EOF
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}.conf" "$NGINX_SITE_FILE"
run_root ln -sfn "$NGINX_SITE_FILE" "$NGINX_SITE_ENABLED"
run_root nginx -t
run_root systemctl reload nginx
if [[ $CONFIGURE_SCHEDULER -eq 1 ]]; then
info "Configuring scheduler cron"
cat > "$TEMP_DIR/${SITE_KEY}-scheduler" <<EOF
* * * * * www-data cd $APP_DIR && php artisan schedule:run >> /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" <<EOF
[program:${SITE_KEY}-worker]
command=$QUEUE_COMMAND
directory=$APP_DIR
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/${SITE_KEY}-worker.log
stopwaitsecs=3600
EOF
run_root install -m 0644 "$TEMP_DIR/${SITE_KEY}-worker.conf" "$SUPERVISOR_FILE"
run_root supervisorctl reread
run_root supervisorctl update
run_root supervisorctl restart "${SITE_KEY}-worker" || true
fi
SUMMARY_FILE="$APP_DIR/install-site-summary.txt"
cat > "$TEMP_DIR/install-site-summary.txt" <<EOF
Laravel site bootstrap completed
Date: $(date -Is)
Site key: $SITE_KEY
Project name: $PROJECT_NAME
Application dir: $APP_DIR
Branch: $BRANCH
APP_URL: $APP_URL
Server name: $SERVER_NAME
Database: $DB_NAME
Database user: $DB_USER
Scheduler configured: $CONFIGURE_SCHEDULER
Queue worker configured: $CONFIGURE_QUEUE_WORKER
EOF
run_root install -o www-data -g www-data -m 0644 "$TEMP_DIR/install-site-summary.txt" "$SUMMARY_FILE"
info "Laravel site bootstrap completed"
printf '\nSummary file:\n'
printf ' %s\n' "$SUMMARY_FILE"