Build the React app during provisioning and serve it on :80 directly, proxying /api and /sanctum to an internal Laravel vhost on 127.0.0.1:8001. Fixes the login page not appearing at the container's root URL, since previously only the Laravel API vhost was configured and nothing served the built frontend.
430 lines
14 KiB
Bash
430 lines
14 KiB
Bash
#!/bin/bash
|
|
# webappAIO LXC Bootstrap
|
|
# One-shot setup for a fresh Proxmox LXC: SSH, Laravel API + React frontend,
|
|
# Mosquitto (Docker), code-server, Claude Code (npm).
|
|
#
|
|
# Usage (paste into a fresh, empty LXC as root):
|
|
#
|
|
# curl -fsSL https://git.innovator.bh/ghassan/webappAIO/raw/branch/main/bootstrap.sh -o bootstrap.sh
|
|
# bash bootstrap.sh
|
|
#
|
|
# All prompts can be skipped by exporting env vars before running, e.g.:
|
|
#
|
|
# APP_NAME=myapp SUPER_ADMIN_PASSWORD='change-me' \
|
|
# bash bootstrap.sh
|
|
#
|
|
# This script always clones https://git.innovator.bh/ghassan/webappAIO.git
|
|
# (the full RBAC/2FA/MQTT/Users-module app) and always uses SQLite — neither
|
|
# is prompted for.
|
|
#
|
|
# Supported env vars (all optional, will be prompted for if unset and a tty is available):
|
|
# APP_NAME Project directory name (default: laravel-app)
|
|
# SUPER_ADMIN_EMAIL Seeded Super Admin email (default: superadmin@example.com)
|
|
# SUPER_ADMIN_USERNAME Seeded Super Admin username (default: superadmin)
|
|
# SUPER_ADMIN_PASSWORD Seeded Super Admin password (default: random)
|
|
# INSTALL_CODE_SERVER yes|no (default: yes)
|
|
# INSTALL_CLAUDE_CODE yes|no (default: yes)
|
|
# FIX_SSHD yes|no - relax sshd_config for the common LXC issue (default: yes)
|
|
|
|
set -euo pipefail
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log() { echo -e "\n\033[1;32m==> $*\033[0m"; }
|
|
warn() { echo -e "\033[1;33m[warn] $*\033[0m"; }
|
|
|
|
# Prompt for a value only if it isn't already set via env var, and only if we
|
|
# have a real terminal to read from (works even when this script is piped
|
|
# straight into bash, by reading from /dev/tty instead of stdin).
|
|
prompt() {
|
|
local var_name="$1" prompt_text="$2" default_value="${3:-}" silent="${4:-no}"
|
|
|
|
# A var counts as "provided" if it's set at all, even to an intentionally
|
|
# blank value.
|
|
if declare -p "$var_name" &>/dev/null; then
|
|
return
|
|
fi
|
|
|
|
if [ ! -t 0 ]; then
|
|
if ! { [ -e /dev/tty ] && exec 0</dev/tty 2>/dev/null; }; then
|
|
warn "No terminal available to prompt for $var_name, using default: '$default_value'"
|
|
printf -v "$var_name" '%s' "$default_value"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
local current_value
|
|
if [ "$silent" = "yes" ]; then
|
|
read -r -s -p "$prompt_text" current_value
|
|
echo ""
|
|
else
|
|
read -r -p "$prompt_text" current_value
|
|
fi
|
|
printf -v "$var_name" '%s' "${current_value:-$default_value}"
|
|
}
|
|
|
|
if [ "$(id -u)" -ne 0 ]; then
|
|
echo "This script must be run as root." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 0. Gather configuration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "webappAIO LXC Bootstrap"
|
|
|
|
prompt APP_NAME "Project name (directory name) [laravel-app]: " "laravel-app"
|
|
|
|
# Fixed, not prompted for: this script always deploys the full webappAIO app
|
|
# against SQLite.
|
|
GIT_REPO="https://git.innovator.bh/ghassan/webappAIO.git"
|
|
DB_TYPE="sqlite"
|
|
|
|
prompt SUPER_ADMIN_EMAIL "Super Admin email [superadmin@example.com]: " "superadmin@example.com"
|
|
prompt SUPER_ADMIN_USERNAME "Super Admin username [superadmin]: " "superadmin"
|
|
prompt SUPER_ADMIN_PASSWORD "Super Admin password: " "$(openssl rand -hex 12)" "yes"
|
|
|
|
INSTALL_CODE_SERVER="${INSTALL_CODE_SERVER:-yes}"
|
|
INSTALL_CLAUDE_CODE="${INSTALL_CLAUDE_CODE:-yes}"
|
|
FIX_SSHD="${FIX_SSHD:-yes}"
|
|
|
|
IP_ADDR="$(hostname -I 2>/dev/null | awk '{print $1}')"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Base system update + tooling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Updating system and installing base tools"
|
|
apt-get update && apt-get upgrade -y
|
|
apt-get install -y sudo curl git wget unzip
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. SSH
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Installing and enabling SSH"
|
|
apt-get install -y openssh-server
|
|
systemctl enable ssh
|
|
systemctl start ssh
|
|
|
|
if [ "$FIX_SSHD" = "yes" ]; then
|
|
if [ -f /etc/ssh/sshd_config.d/turnkey.conf ]; then
|
|
log "TurnKey Core detected: enabling AllowTcpForwarding"
|
|
sed -i 's/^AllowTcpForwarding.*/AllowTcpForwarding yes/' /etc/ssh/sshd_config.d/turnkey.conf
|
|
grep -q '^AllowTcpForwarding' /etc/ssh/sshd_config.d/turnkey.conf || echo "AllowTcpForwarding yes" >> /etc/ssh/sshd_config.d/turnkey.conf
|
|
else
|
|
log "Enabling root login over SSH in /etc/ssh/sshd_config"
|
|
if grep -q '^PermitRootLogin' /etc/ssh/sshd_config; then
|
|
sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
|
else
|
|
echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
|
|
fi
|
|
fi
|
|
systemctl restart ssh
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. Docker + Mosquitto (MQTT broker)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Installing Docker Engine + Compose plugin"
|
|
if ! command -v docker &>/dev/null; then
|
|
curl -fsSL https://get.docker.com | sh
|
|
fi
|
|
apt-get install -y docker-compose-plugin
|
|
systemctl enable docker
|
|
systemctl start docker
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 4. Laravel + React stack
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Installing Apache, PHP and common extensions"
|
|
apt-get install -y apache2 php php-curl php-bcmath php-json php-mbstring php-xml php-tokenizer php-zip php-gd
|
|
|
|
log "Installing PHP SQLite extension"
|
|
apt-get install -y php-sqlite3
|
|
|
|
log "Installing Composer"
|
|
curl -sS https://getcomposer.org/installer | php
|
|
mv composer.phar /usr/local/bin/composer
|
|
|
|
log "Installing Node.js LTS"
|
|
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
|
|
apt-get install -y nodejs
|
|
|
|
mkdir -p /var/www
|
|
cd /var/www
|
|
|
|
log "Cloning $GIT_REPO into $APP_NAME"
|
|
git clone "$GIT_REPO" "$APP_NAME"
|
|
cd "$APP_NAME/backend"
|
|
composer install
|
|
cd ../frontend
|
|
npm install
|
|
log "Building the frontend for production"
|
|
npm run build
|
|
cd /var/www/"$APP_NAME"
|
|
|
|
cd "/var/www/$APP_NAME/backend"
|
|
|
|
log "Creating SQLite database file"
|
|
mkdir -p database
|
|
touch database/database.sqlite
|
|
|
|
log "Setting file permissions"
|
|
chown -R www-data:www-data "/var/www/$APP_NAME"
|
|
chmod -R 775 storage bootstrap/cache
|
|
|
|
log "Configuring Apache virtual hosts (frontend on :80, API proxied internally on 127.0.0.1:8001)"
|
|
a2enmod rewrite proxy proxy_http
|
|
|
|
cat > "/etc/apache2/sites-available/$APP_NAME-api.conf" <<EOF
|
|
Listen 127.0.0.1:8001
|
|
|
|
<VirtualHost 127.0.0.1:8001>
|
|
ServerAdmin webmaster@localhost
|
|
DocumentRoot /var/www/$APP_NAME/backend/public
|
|
|
|
<Directory /var/www/$APP_NAME/backend>
|
|
AllowOverride All
|
|
Require all granted
|
|
</Directory>
|
|
|
|
ErrorLog \${APACHE_LOG_DIR}/$APP_NAME-api-error.log
|
|
CustomLog \${APACHE_LOG_DIR}/$APP_NAME-api-access.log combined
|
|
</VirtualHost>
|
|
EOF
|
|
|
|
cat > "/etc/apache2/sites-available/$APP_NAME.conf" <<EOF
|
|
<VirtualHost *:80>
|
|
ServerAdmin webmaster@localhost
|
|
DocumentRoot /var/www/$APP_NAME/frontend/dist
|
|
|
|
<Directory /var/www/$APP_NAME/frontend/dist>
|
|
AllowOverride All
|
|
Require all granted
|
|
|
|
RewriteEngine On
|
|
RewriteBase /
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule . /index.html [L]
|
|
</Directory>
|
|
|
|
ProxyPreserveHost On
|
|
ProxyPass /api http://127.0.0.1:8001/api
|
|
ProxyPassReverse /api http://127.0.0.1:8001/api
|
|
ProxyPass /sanctum http://127.0.0.1:8001/sanctum
|
|
ProxyPassReverse /sanctum http://127.0.0.1:8001/sanctum
|
|
|
|
ErrorLog \${APACHE_LOG_DIR}/$APP_NAME-error.log
|
|
CustomLog \${APACHE_LOG_DIR}/$APP_NAME-access.log combined
|
|
</VirtualHost>
|
|
EOF
|
|
|
|
a2dissite 000-default.conf || true
|
|
a2ensite "$APP_NAME-api.conf"
|
|
a2ensite "$APP_NAME.conf"
|
|
systemctl reload apache2
|
|
systemctl restart apache2
|
|
|
|
log "Configuring Laravel .env"
|
|
if [ ! -f .env ] && [ -f .env.example ]; then
|
|
cp .env.example .env
|
|
fi
|
|
|
|
if [ -f .env ]; then
|
|
sed -i "s/^DB_CONNECTION=.*/DB_CONNECTION=sqlite/" .env
|
|
sed -i "s|^DB_DATABASE=.*|DB_DATABASE=$(pwd)/database/database.sqlite|" .env
|
|
sed -i "s/^DB_USERNAME=.*/DB_USERNAME=/" .env
|
|
sed -i "s/^DB_PASSWORD=.*/DB_PASSWORD=/" .env
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 5. Mosquitto config + matching backend secrets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Generating Mosquitto broker credentials"
|
|
cd "/var/www/$APP_NAME"
|
|
mkdir -p docker/mosquitto/config
|
|
|
|
if [ ! -f docker/mosquitto/config/mosquitto.conf ]; then
|
|
cat > docker/mosquitto/config/mosquitto.conf <<'EOF'
|
|
listener 1883
|
|
protocol mqtt
|
|
|
|
listener 9001
|
|
protocol websockets
|
|
|
|
allow_anonymous false
|
|
password_file /mosquitto/config/passwd
|
|
acl_file /mosquitto/config/acl.conf
|
|
|
|
persistence true
|
|
persistence_location /mosquitto/data/
|
|
|
|
log_dest file /mosquitto/log/mosquitto.log
|
|
log_dest stdout
|
|
EOF
|
|
fi
|
|
|
|
if [ ! -f docker/mosquitto/config/acl.conf ]; then
|
|
cat > docker/mosquitto/config/acl.conf <<'EOF'
|
|
user laravel_publisher
|
|
topic readwrite #
|
|
|
|
user role_admin
|
|
topic read broadcast/system
|
|
topic read role/admin/alerts
|
|
topic read module/+/events
|
|
topic read private/+/notifications
|
|
|
|
user role_manager
|
|
topic read broadcast/system
|
|
topic read role/manager/alerts
|
|
topic read module/+/events
|
|
topic read private/+/notifications
|
|
|
|
user role_user
|
|
topic read broadcast/system
|
|
topic read role/user/alerts
|
|
topic read private/+/notifications
|
|
EOF
|
|
fi
|
|
|
|
if [ ! -f docker-compose.yml ]; then
|
|
cat > docker-compose.yml <<'EOF'
|
|
services:
|
|
mosquitto:
|
|
image: eclipse-mosquitto:2
|
|
ports:
|
|
- "1883:1883"
|
|
- "9001:9001"
|
|
volumes:
|
|
- ./docker/mosquitto/config:/mosquitto/config:ro
|
|
- mosquitto-data:/mosquitto/data
|
|
- mosquitto-log:/mosquitto/log
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
mosquitto-data:
|
|
mosquitto-log:
|
|
EOF
|
|
fi
|
|
|
|
if [ ! -f docker/mosquitto/config/passwd ]; then
|
|
MQTT_PUBLISHER_PASSWORD="$(openssl rand -hex 16)"
|
|
MQTT_ROLE_ADMIN_PASSWORD="$(openssl rand -hex 16)"
|
|
MQTT_ROLE_MANAGER_PASSWORD="$(openssl rand -hex 16)"
|
|
MQTT_ROLE_USER_PASSWORD="$(openssl rand -hex 16)"
|
|
|
|
docker run --rm -v "$(pwd)/docker/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 mosquitto_passwd -b -c /mosquitto/config/passwd laravel_publisher "$MQTT_PUBLISHER_PASSWORD"
|
|
docker run --rm -v "$(pwd)/docker/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 mosquitto_passwd -b /mosquitto/config/passwd role_admin "$MQTT_ROLE_ADMIN_PASSWORD"
|
|
docker run --rm -v "$(pwd)/docker/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 mosquitto_passwd -b /mosquitto/config/passwd role_manager "$MQTT_ROLE_MANAGER_PASSWORD"
|
|
docker run --rm -v "$(pwd)/docker/mosquitto/config:/mosquitto/config" eclipse-mosquitto:2 mosquitto_passwd -b /mosquitto/config/passwd role_user "$MQTT_ROLE_USER_PASSWORD"
|
|
docker run --rm -v "$(pwd)/docker/mosquitto/config:/mosquitto/config" --entrypoint sh eclipse-mosquitto:2 -c "chmod 600 /mosquitto/config/passwd /mosquitto/config/acl.conf && chown mosquitto:mosquitto /mosquitto/config/passwd /mosquitto/config/acl.conf"
|
|
|
|
cat >> backend/.env <<EOF
|
|
|
|
MQTT_HOST=127.0.0.1
|
|
MQTT_PORT=1883
|
|
MQTT_WS_URL=ws://${IP_ADDR:-localhost}:9001
|
|
MQTT_TLS=false
|
|
MQTT_AUTH_USERNAME=laravel_publisher
|
|
MQTT_AUTH_PASSWORD=$MQTT_PUBLISHER_PASSWORD
|
|
MQTT_ROLE_ADMIN_USERNAME=role_admin
|
|
MQTT_ROLE_ADMIN_PASSWORD=$MQTT_ROLE_ADMIN_PASSWORD
|
|
MQTT_ROLE_MANAGER_USERNAME=role_manager
|
|
MQTT_ROLE_MANAGER_PASSWORD=$MQTT_ROLE_MANAGER_PASSWORD
|
|
MQTT_ROLE_USER_USERNAME=role_user
|
|
MQTT_ROLE_USER_PASSWORD=$MQTT_ROLE_USER_PASSWORD
|
|
EOF
|
|
fi
|
|
|
|
log "Starting Mosquitto"
|
|
docker compose up -d mosquitto
|
|
|
|
cd "/var/www/$APP_NAME/backend"
|
|
|
|
log "Configuring Super Admin seed credentials"
|
|
cat >> .env <<EOF
|
|
|
|
SUPER_ADMIN_EMAIL=$SUPER_ADMIN_EMAIL
|
|
SUPER_ADMIN_USERNAME=$SUPER_ADMIN_USERNAME
|
|
SUPER_ADMIN_PASSWORD=$SUPER_ADMIN_PASSWORD
|
|
|
|
FRONTEND_URL=http://${IP_ADDR:-localhost}
|
|
SANCTUM_STATEFUL_DOMAINS=${IP_ADDR:-localhost},localhost,127.0.0.1,localhost:5173
|
|
EOF
|
|
|
|
log "Generating application key, running migrations and seeders"
|
|
php artisan key:generate --force
|
|
php artisan migrate --force
|
|
if [ -f database/seeders/DatabaseSeeder.php ]; then
|
|
php artisan db:seed --force
|
|
fi
|
|
|
|
log "Installing a systemd unit for the queue worker (MQTT event publishing)"
|
|
cat > /etc/systemd/system/"$APP_NAME"-queue.service <<EOF
|
|
[Unit]
|
|
Description=$APP_NAME queue worker
|
|
After=network.target
|
|
|
|
[Service]
|
|
User=www-data
|
|
WorkingDirectory=/var/www/$APP_NAME/backend
|
|
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3
|
|
Restart=always
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
systemctl daemon-reload
|
|
systemctl enable --now "$APP_NAME"-queue.service
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 6. code-server (VS Code in the browser, over SSH)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [ "$INSTALL_CODE_SERVER" = "yes" ]; then
|
|
log "Installing code-server"
|
|
curl -fsSL https://code-server.dev/install.sh | sh
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 7. Claude Code (npm)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
if [ "$INSTALL_CLAUDE_CODE" = "yes" ]; then
|
|
log "Installing Claude Code via npm"
|
|
npm install -g @anthropic-ai/claude-code
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 8. Summary
|
|
# ---------------------------------------------------------------------------
|
|
|
|
log "Setup complete"
|
|
cat <<SUMMARY
|
|
|
|
Project: $APP_NAME
|
|
Location: /var/www/$APP_NAME (backend/, frontend/)
|
|
Database: $DB_TYPE
|
|
Web URL: http://${IP_ADDR:-<container-ip>}/ (login page — served by Apache; API is proxied internally, nothing extra to run)
|
|
MQTT broker: mqtt://${IP_ADDR:-<container-ip>}:1883, ws://${IP_ADDR:-<container-ip>}:9001
|
|
|
|
Super Admin: $SUPER_ADMIN_USERNAME / $SUPER_ADMIN_PASSWORD
|
|
|
|
Queue worker: running as systemd unit '$APP_NAME-queue' (needed for MQTT event publishing).
|
|
Active development: 'cd /var/www/$APP_NAME/frontend && npm run dev -- --host' gives hot-reload on :5173;
|
|
rerun 'npm run build' there and it's picked up at the Web URL above (no restart needed).
|
|
|
|
$( [ "$INSTALL_CODE_SERVER" = "yes" ] && echo "code-server: run 'code-server /var/www/$APP_NAME' then tunnel port 8080 over SSH" )
|
|
$( [ "$INSTALL_CLAUDE_CODE" = "yes" ] && echo "Claude Code: run 'claude' inside /var/www/$APP_NAME to start" )
|
|
|
|
SUMMARY
|