- pricing.php: rename setInterval() to setBilling() to stop shadowing the native window.setInterval — the billing toggle was silently broken - deploy.sh: full automated deploy script for nixpanel.io - DocumentRoot set to public/ (fixes /pricing, /login 404s) - AllowOverride All so public/.htaccess handles all routing - PHP 8.3-FPM via Apache mod_proxy_fcgi - composer install, DB migrations, .env setup, SSL via certbot
296 lines
12 KiB
Bash
296 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# NixPanel Website — Deploy Script
|
|
# Deploys nixpanel.io (PHP frontend + customer portal) to a server.
|
|
# Run as root on the target server.
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m'
|
|
YELLOW='\033[1;33m'; BOLD='\033[1m'; RESET='\033[0m'
|
|
|
|
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
|
|
success() { echo -e "${GREEN}[ OK ]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
|
error() { echo -e "${RED}[ERR ]${RESET} $*" >&2; exit 1; }
|
|
step() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${RESET}"; }
|
|
|
|
[[ $EUID -ne 0 ]] && error "Must be run as root"
|
|
|
|
SITE_DIR="/var/www/nixpanel-website"
|
|
GITHUB_REPO="NixReaper/nixpanel-website"
|
|
SERVER_NAME="${1:-nixpanel.io}"
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${CYAN} NixPanel Website — Deployer${RESET}"
|
|
echo -e " Domain: ${SERVER_NAME}"
|
|
echo ""
|
|
|
|
# ── 1. System deps ────────────────────────────────────────────────────────────
|
|
step "Installing system dependencies"
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
|
|
# ondrej/php PPA for latest PHP on Ubuntu 24.04
|
|
apt-get install -y -qq ca-certificates gnupg software-properties-common
|
|
if ! grep -rq "ondrej/php" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null; then
|
|
add-apt-repository -y ppa:ondrej/php
|
|
apt-get update -qq
|
|
fi
|
|
|
|
apt-get install -y -qq \
|
|
apache2 \
|
|
php8.3 php8.3-fpm php8.3-cli php8.3-mysql php8.3-curl \
|
|
php8.3-mbstring php8.3-xml php8.3-zip php8.3-gd php8.3-intl \
|
|
mariadb-server mariadb-client \
|
|
certbot python3-certbot-apache \
|
|
curl wget git unzip
|
|
|
|
# Install Composer
|
|
if ! command -v composer &>/dev/null; then
|
|
info "Installing Composer…"
|
|
curl -fsSL https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
|
fi
|
|
success "Dependencies installed"
|
|
|
|
# ── 2. MariaDB ────────────────────────────────────────────────────────────────
|
|
step "Configuring MariaDB"
|
|
systemctl enable --now mariadb
|
|
|
|
if ! mysql -u root -e "SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='nixpanel_website'" 2>/dev/null | grep -q nixpanel_website; then
|
|
DB_PASS=$(openssl rand -hex 24)
|
|
|
|
mysql -u root <<SQL
|
|
CREATE DATABASE IF NOT EXISTS nixpanel_website CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER IF NOT EXISTS 'nixpanel_web'@'127.0.0.1' IDENTIFIED BY '${DB_PASS}';
|
|
GRANT ALL PRIVILEGES ON nixpanel_website.* TO 'nixpanel_web'@'127.0.0.1';
|
|
FLUSH PRIVILEGES;
|
|
SQL
|
|
|
|
echo "$DB_PASS" > /root/.nixpanel_website_db_pass
|
|
chmod 600 /root/.nixpanel_website_db_pass
|
|
success "Database created (password saved to /root/.nixpanel_website_db_pass)"
|
|
else
|
|
DB_PASS=$(cat /root/.nixpanel_website_db_pass 2>/dev/null || echo "")
|
|
info "Database already exists"
|
|
fi
|
|
|
|
# ── 3. Clone / update source ──────────────────────────────────────────────────
|
|
step "Deploying source code"
|
|
if [[ -d "$SITE_DIR/.git" ]]; then
|
|
info "Updating existing deployment…"
|
|
git -C "$SITE_DIR" pull --ff-only
|
|
else
|
|
git clone "https://github.com/${GITHUB_REPO}.git" "$SITE_DIR"
|
|
fi
|
|
success "Source at $SITE_DIR"
|
|
|
|
# ── 4. Composer dependencies ──────────────────────────────────────────────────
|
|
step "Installing PHP dependencies"
|
|
cd "$SITE_DIR"
|
|
composer install --no-dev --optimize-autoloader --quiet
|
|
success "Composer packages installed"
|
|
|
|
# ── 5. Run database migrations ────────────────────────────────────────────────
|
|
step "Running database migrations"
|
|
if [[ -f "$SITE_DIR/migrations/001_schema.sql" ]]; then
|
|
mysql -u nixpanel_web -p"${DB_PASS}" nixpanel_website < "$SITE_DIR/migrations/001_schema.sql"
|
|
success "Migrations applied"
|
|
else
|
|
warn "No migration file found at migrations/001_schema.sql"
|
|
fi
|
|
|
|
# ── 6. Environment file ───────────────────────────────────────────────────────
|
|
step "Configuring environment"
|
|
if [[ ! -f "$SITE_DIR/.env" ]]; then
|
|
echo ""
|
|
info "Setting up .env — press Enter to leave values blank (configure later)"
|
|
echo ""
|
|
|
|
read -rp " Stripe secret key (sk_live_...): " STRIPE_SK; STRIPE_SK="${STRIPE_SK:-}"
|
|
read -rp " Stripe publishable key (pk_live_...): " STRIPE_PK; STRIPE_PK="${STRIPE_PK:-}"
|
|
read -rp " Stripe webhook secret (whsec_...): " STRIPE_WH; STRIPE_WH="${STRIPE_WH:-}"
|
|
echo ""
|
|
info "Stripe Price IDs (leave blank to fill in later):"
|
|
read -rp " STRIPE_PRICE_SOLO_MONTHLY: " P_SOLO_M; P_SOLO_M="${P_SOLO_M:-}"
|
|
read -rp " STRIPE_PRICE_SOLO_ANNUAL: " P_SOLO_A; P_SOLO_A="${P_SOLO_A:-}"
|
|
read -rp " STRIPE_PRICE_HOST_MONTHLY: " P_HOST_M; P_HOST_M="${P_HOST_M:-}"
|
|
read -rp " STRIPE_PRICE_HOST_ANNUAL: " P_HOST_A; P_HOST_A="${P_HOST_A:-}"
|
|
read -rp " STRIPE_PRICE_AGENCY_MONTHLY: " P_AGCY_M; P_AGCY_M="${P_AGCY_M:-}"
|
|
read -rp " STRIPE_PRICE_AGENCY_ANNUAL: " P_AGCY_A; P_AGCY_A="${P_AGCY_A:-}"
|
|
echo ""
|
|
read -rp " SMTP host [smtp.mailgun.org]: " MAIL_HOST; MAIL_HOST="${MAIL_HOST:-smtp.mailgun.org}"
|
|
read -rp " SMTP user: " MAIL_USER; MAIL_USER="${MAIL_USER:-}"
|
|
read -rsp " SMTP password: " MAIL_PASS; MAIL_PASS="${MAIL_PASS:-}"; echo ""
|
|
echo ""
|
|
read -rp " Licensing server URL [https://license.nixpanel.io]: " LIC_URL
|
|
LIC_URL="${LIC_URL:-https://license.nixpanel.io}"
|
|
read -rp " Licensing server API key: " LIC_KEY; LIC_KEY="${LIC_KEY:-}"
|
|
|
|
SESSION_SECRET=$(openssl rand -hex 64)
|
|
|
|
cat > "$SITE_DIR/.env" <<EOF
|
|
APP_URL=https://${SERVER_NAME}
|
|
APP_ENV=production
|
|
|
|
# Database
|
|
DB_HOST=127.0.0.1
|
|
DB_PORT=3306
|
|
DB_NAME=nixpanel_website
|
|
DB_USER=nixpanel_web
|
|
DB_PASSWORD=${DB_PASS}
|
|
|
|
# Session
|
|
SESSION_SECRET=${SESSION_SECRET}
|
|
|
|
# Stripe
|
|
STRIPE_SECRET_KEY=${STRIPE_SK}
|
|
STRIPE_PUBLISHABLE_KEY=${STRIPE_PK}
|
|
STRIPE_WEBHOOK_SECRET=${STRIPE_WH}
|
|
|
|
STRIPE_PRICE_SOLO_MONTHLY=${P_SOLO_M}
|
|
STRIPE_PRICE_SOLO_ANNUAL=${P_SOLO_A}
|
|
STRIPE_PRICE_HOST_MONTHLY=${P_HOST_M}
|
|
STRIPE_PRICE_HOST_ANNUAL=${P_HOST_A}
|
|
STRIPE_PRICE_AGENCY_MONTHLY=${P_AGCY_M}
|
|
STRIPE_PRICE_AGENCY_ANNUAL=${P_AGCY_A}
|
|
|
|
# Mail (SMTP)
|
|
MAIL_HOST=${MAIL_HOST}
|
|
MAIL_PORT=587
|
|
MAIL_USER=${MAIL_USER}
|
|
MAIL_PASSWORD=${MAIL_PASS}
|
|
MAIL_FROM=noreply@${SERVER_NAME}
|
|
MAIL_FROM_NAME=NixPanel
|
|
|
|
# Internal licensing server
|
|
LICENSING_SERVER_URL=${LIC_URL}
|
|
LICENSING_SERVER_API_KEY=${LIC_KEY}
|
|
EOF
|
|
chmod 600 "$SITE_DIR/.env"
|
|
success ".env written"
|
|
else
|
|
info ".env already exists — skipping (edit manually: $SITE_DIR/.env)"
|
|
fi
|
|
|
|
# ── 7. File permissions ───────────────────────────────────────────────────────
|
|
step "Setting permissions"
|
|
chown -R www-data:www-data "$SITE_DIR"
|
|
chmod 750 "$SITE_DIR"
|
|
chmod 640 "$SITE_DIR/.env"
|
|
# Protect vendor and src from direct web access (handled by DocumentRoot=public/)
|
|
find "$SITE_DIR/public" -type d -exec chmod 755 {} \;
|
|
find "$SITE_DIR/public" -type f -exec chmod 644 {} \;
|
|
success "Permissions set"
|
|
|
|
# ── 8. Apache vhost ───────────────────────────────────────────────────────────
|
|
step "Configuring Apache"
|
|
a2enmod rewrite proxy proxy_http proxy_fcgi ssl headers expires deflate
|
|
a2enconf php8.3-fpm
|
|
|
|
# Remove default site
|
|
a2dissite 000-default 2>/dev/null || true
|
|
|
|
cat > /etc/apache2/sites-available/nixpanel-website.conf <<APACHEEOF
|
|
<VirtualHost *:80>
|
|
ServerName ${SERVER_NAME}
|
|
ServerAlias www.${SERVER_NAME}
|
|
|
|
# DocumentRoot points directly to public/ — only one .htaccess needed
|
|
DocumentRoot ${SITE_DIR}/public
|
|
|
|
<Directory ${SITE_DIR}/public>
|
|
Options -Indexes +FollowSymLinks
|
|
AllowOverride All
|
|
Require all granted
|
|
</Directory>
|
|
|
|
# Pass PHP to PHP-FPM
|
|
<FilesMatch "\.php$">
|
|
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
|
|
</FilesMatch>
|
|
|
|
# Security headers
|
|
Header always set X-Frame-Options "SAMEORIGIN"
|
|
Header always set X-Content-Type-Options "nosniff"
|
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
|
|
ErrorLog /var/log/apache2/nixpanel-website-error.log
|
|
CustomLog /var/log/apache2/nixpanel-website-access.log combined
|
|
</VirtualHost>
|
|
|
|
# HTTPS — populated by certbot
|
|
<VirtualHost *:443>
|
|
ServerName ${SERVER_NAME}
|
|
ServerAlias www.${SERVER_NAME}
|
|
|
|
DocumentRoot ${SITE_DIR}/public
|
|
|
|
<Directory ${SITE_DIR}/public>
|
|
Options -Indexes +FollowSymLinks
|
|
AllowOverride All
|
|
Require all granted
|
|
</Directory>
|
|
|
|
<FilesMatch "\.php$">
|
|
SetHandler "proxy:unix:/run/php/php8.3-fpm.sock|fcgi://localhost"
|
|
</FilesMatch>
|
|
|
|
Header always set X-Frame-Options "SAMEORIGIN"
|
|
Header always set X-Content-Type-Options "nosniff"
|
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
|
|
|
ErrorLog /var/log/apache2/nixpanel-website-error.log
|
|
CustomLog /var/log/apache2/nixpanel-website-access.log combined
|
|
|
|
# SSL — certbot will fill these in
|
|
# SSLEngine on
|
|
# SSLCertificateFile ...
|
|
# SSLCertificateKeyFile ...
|
|
</VirtualHost>
|
|
APACHEEOF
|
|
|
|
a2ensite nixpanel-website
|
|
systemctl enable --now apache2 php8.3-fpm
|
|
apache2ctl configtest && systemctl reload apache2
|
|
success "Apache configured"
|
|
|
|
# ── 9. SSL ────────────────────────────────────────────────────────────────────
|
|
step "SSL certificate (Let's Encrypt)"
|
|
echo ""
|
|
echo -e " ${BOLD}Obtain SSL cert now?${RESET} (requires DNS pointing ${SERVER_NAME} → this server)"
|
|
read -rp " Get SSL cert? [Y/n]: " DO_SSL
|
|
DO_SSL="${DO_SSL:-Y}"
|
|
|
|
if [[ "${DO_SSL,,}" == "y" ]]; then
|
|
read -rp " Email for Let's Encrypt notices: " LE_EMAIL
|
|
certbot --apache -d "$SERVER_NAME" -d "www.${SERVER_NAME}" \
|
|
--non-interactive --agree-tos -m "$LE_EMAIL" --redirect \
|
|
&& success "SSL certificate installed" \
|
|
|| warn "certbot failed — run manually: certbot --apache -d ${SERVER_NAME}"
|
|
else
|
|
info "Skipped — run later: certbot --apache -d ${SERVER_NAME}"
|
|
fi
|
|
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
|
SERVER_IP=$(hostname -I | awk '{print $1}')
|
|
|
|
echo ""
|
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
echo -e "${BOLD}${GREEN} NixPanel Website Deployed!${RESET}"
|
|
echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
echo ""
|
|
echo -e " ${BOLD}URL:${RESET} https://${SERVER_NAME}"
|
|
echo -e " ${BOLD}Root:${RESET} ${SITE_DIR}"
|
|
echo -e " ${BOLD}Config:${RESET} ${SITE_DIR}/.env"
|
|
echo -e " ${BOLD}Logs:${RESET} /var/log/apache2/nixpanel-website-*.log"
|
|
echo ""
|
|
echo -e " ${BOLD}To redeploy after code changes:${RESET}"
|
|
echo -e " git -C ${SITE_DIR} pull && composer install -d ${SITE_DIR} --no-dev -o"
|
|
echo ""
|
|
if [[ -z "$(grep 'STRIPE_SECRET_KEY=sk_' "$SITE_DIR/.env" 2>/dev/null || true)" ]]; then
|
|
echo -e " ${YELLOW}⚠ Stripe keys not yet configured — edit ${SITE_DIR}/.env${RESET}"
|
|
fi
|
|
echo ""
|