chore: initialize repository with deployment baseline

This commit is contained in:
Austin A
2026-04-17 23:03:00 +01:00
parent f02ddf42aa
commit 5def26e0df
166 changed files with 43065 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
services:
postgres:
image: postgres:16-alpine
container_name: proxpanel-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-proxpanel}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB:-proxpanel}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-proxpanel} -d ${POSTGRES_DB:-proxpanel}"]
interval: 10s
timeout: 5s
retries: 10
backend:
build:
context: ../../backend
container_name: proxpanel-backend
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
environment:
NODE_ENV: production
PORT: 8080
DATABASE_URL: postgresql://${POSTGRES_USER:-proxpanel}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-proxpanel}
JWT_SECRET: ${JWT_SECRET}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
JWT_EXPIRES_IN: ${JWT_EXPIRES_IN:-15m}
JWT_REFRESH_EXPIRES_IN: ${JWT_REFRESH_EXPIRES_IN:-30d}
CORS_ORIGIN: ${CORS_ORIGIN}
RATE_LIMIT_WINDOW_MS: ${RATE_LIMIT_WINDOW_MS:-60000}
RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-600}
AUTH_RATE_LIMIT_WINDOW_MS: ${AUTH_RATE_LIMIT_WINDOW_MS:-60000}
AUTH_RATE_LIMIT_MAX: ${AUTH_RATE_LIMIT_MAX:-20}
ENABLE_SCHEDULER: ${ENABLE_SCHEDULER:-true}
BILLING_CRON: ${BILLING_CRON:-0 * * * *}
BACKUP_CRON: ${BACKUP_CRON:-*/15 * * * *}
POWER_SCHEDULE_CRON: ${POWER_SCHEDULE_CRON:-* * * * *}
MONITORING_CRON: ${MONITORING_CRON:-*/5 * * * *}
PROXMOX_TIMEOUT_MS: ${PROXMOX_TIMEOUT_MS:-15000}
ADMIN_EMAIL: ${ADMIN_EMAIL}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
expose:
- "8080"
ports:
- "127.0.0.1:${BACKEND_PORT:-8080}:8080"
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost:8080/api/health >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 5s
retries: 10
frontend:
build:
context: ../../
args:
VITE_API_BASE_URL: ""
container_name: proxpanel-frontend
restart: unless-stopped
depends_on:
backend:
condition: service_healthy
ports:
- "127.0.0.1:${FRONTEND_PORT:-80}:80"
healthcheck:
test: ["CMD-SHELL", "nginx -t >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 5s
retries: 10
volumes:
postgres_data:

View File

@@ -0,0 +1,345 @@
#!/usr/bin/env bash
set -Eeuo pipefail
APP_DIR="/opt/proxpanel"
REPO_URL=""
BRANCH="main"
PUBLIC_URL=""
ADMIN_EMAIL="admin@proxpanel.local"
ADMIN_PASSWORD=""
POSTGRES_PASSWORD=""
FRONTEND_PORT="80"
BACKEND_PORT="8080"
CONFIGURE_UFW="false"
log() {
printf '\n[%s] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*"
}
die() {
printf '\n[ERROR] %s\n' "$*" >&2
exit 1
}
usage() {
cat <<'EOF'
Usage:
bash infra/deploy/install-proxpanel.sh [options]
Options:
--repo-url <url> Git repository URL (required if app is not already in /opt/proxpanel)
--branch <name> Git branch to deploy (default: main)
--app-dir <path> Deployment directory (default: /opt/proxpanel)
--public-url <url> Public base URL (example: http://102.69.243.167)
--admin-email <email> Initial admin email (default: admin@proxpanel.local)
--admin-password <pass> Initial admin password (auto-generated if omitted)
--postgres-password <pass> Postgres password (auto-generated if omitted)
--frontend-port <port> Public frontend port (default: 80)
--backend-port <port> Local backend bind port (default: 8080 on 127.0.0.1)
--configure-ufw Allow OpenSSH + frontend port via UFW (if available)
-h, --help Show this help
Examples:
bash infra/deploy/install-proxpanel.sh \
--repo-url https://github.com/your-org/proxpanel.git \
--branch main \
--public-url http://102.69.243.167 \
--admin-email admin@yourdomain.com
EOF
}
random_secret() {
openssl rand -base64 72 | tr -d '\n'
}
random_db_password() {
# URL-safe hex string to avoid DATABASE_URL parsing issues.
openssl rand -hex 32 | tr -d '\n'
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--repo-url)
REPO_URL="${2:-}"
shift 2
;;
--branch)
BRANCH="${2:-}"
shift 2
;;
--app-dir)
APP_DIR="${2:-}"
shift 2
;;
--public-url)
PUBLIC_URL="${2:-}"
shift 2
;;
--admin-email)
ADMIN_EMAIL="${2:-}"
shift 2
;;
--admin-password)
ADMIN_PASSWORD="${2:-}"
shift 2
;;
--postgres-password)
POSTGRES_PASSWORD="${2:-}"
shift 2
;;
--frontend-port)
FRONTEND_PORT="${2:-}"
shift 2
;;
--backend-port)
BACKEND_PORT="${2:-}"
shift 2
;;
--configure-ufw)
CONFIGURE_UFW="true"
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
}
ensure_root() {
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
die "Run as root (or with sudo)."
fi
}
install_prereqs() {
log "Installing OS prerequisites..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -y
apt-get install -y ca-certificates curl git openssl jq rsync
}
install_docker_if_needed() {
if command -v docker >/dev/null 2>&1; then
log "Docker already installed."
else
log "Installing Docker..."
curl -fsSL https://get.docker.com | sh
fi
systemctl enable docker >/dev/null 2>&1 || true
systemctl start docker
docker compose version >/dev/null 2>&1 || die "Docker Compose plugin is required but not available."
}
sync_source() {
if [[ -d "${APP_DIR}/.git" ]]; then
log "Updating existing repository in ${APP_DIR}..."
git -C "${APP_DIR}" fetch --all --prune
git -C "${APP_DIR}" checkout "${BRANCH}"
git -C "${APP_DIR}" pull --ff-only origin "${BRANCH}"
return
fi
if [[ -n "${REPO_URL}" ]]; then
log "Cloning repository into ${APP_DIR}..."
mkdir -p "$(dirname "${APP_DIR}")"
git clone --branch "${BRANCH}" --single-branch "${REPO_URL}" "${APP_DIR}"
return
fi
if [[ -f "./package.json" && -d "./backend" && -d "./infra" ]]; then
log "No repo URL provided; copying current directory into ${APP_DIR}..."
mkdir -p "${APP_DIR}"
rsync -a --delete --exclude .git --exclude node_modules --exclude backend/node_modules ./ "${APP_DIR}/"
return
fi
die "Could not determine source. Provide --repo-url or run this script from project root."
}
validate_project_layout() {
[[ -f "${APP_DIR}/infra/deploy/docker-compose.production.yml" ]] || die "Missing infra/deploy/docker-compose.production.yml"
[[ -f "${APP_DIR}/backend/Dockerfile" ]] || die "Missing backend/Dockerfile"
[[ -f "${APP_DIR}/Dockerfile" ]] || die "Missing frontend Dockerfile"
}
infer_public_url() {
if [[ -n "${PUBLIC_URL}" ]]; then
return
fi
local ip
ip="$(hostname -I | awk '{print $1}')"
[[ -n "${ip}" ]] || ip="127.0.0.1"
PUBLIC_URL="http://${ip}"
}
write_env_file() {
[[ -n "${ADMIN_PASSWORD}" ]] || ADMIN_PASSWORD="$(openssl rand -base64 18 | tr -d '\n' | tr '/+' 'ab')A9!"
[[ -n "${POSTGRES_PASSWORD}" ]] || POSTGRES_PASSWORD="$(random_db_password)"
local jwt_secret jwt_refresh_secret env_file
jwt_secret="$(random_secret)"
jwt_refresh_secret="$(random_secret)"
env_file="${APP_DIR}/.env.production"
log "Writing production env file..."
umask 077
cat > "${env_file}" <<EOF
POSTGRES_USER=proxpanel
POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
POSTGRES_DB=proxpanel
JWT_SECRET=${jwt_secret}
JWT_REFRESH_SECRET=${jwt_refresh_secret}
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=30d
CORS_ORIGIN=${PUBLIC_URL}
ADMIN_EMAIL=${ADMIN_EMAIL}
ADMIN_PASSWORD=${ADMIN_PASSWORD}
ENABLE_SCHEDULER=true
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=600
AUTH_RATE_LIMIT_WINDOW_MS=60000
AUTH_RATE_LIMIT_MAX=20
PROXMOX_TIMEOUT_MS=15000
FRONTEND_PORT=${FRONTEND_PORT}
BACKEND_PORT=${BACKEND_PORT}
EOF
}
deploy_stack() {
log "Building and starting production stack..."
cd "${APP_DIR}"
docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml pull || true
docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml up -d --build
}
wait_for_health() {
log "Waiting for API health..."
local max_tries=60
local i
for ((i=1; i<=max_tries; i++)); do
if curl -fsS "http://127.0.0.1:${BACKEND_PORT}/api/health" >/dev/null 2>&1; then
log "Backend is healthy."
return
fi
sleep 3
done
die "Backend health check failed."
}
apply_database_schema() {
log "Applying database schema..."
cd "${APP_DIR}"
if docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml exec -T backend npm run prisma:deploy; then
log "Schema migration deploy completed."
return
fi
log "prisma:deploy failed or no migrations found. Falling back to prisma:push..."
docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml exec -T backend npm run prisma:push
log "Schema push completed."
}
seed_database() {
log "Running Prisma seed (idempotent)..."
cd "${APP_DIR}"
docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml exec -T backend npm run prisma:seed
}
verify_login() {
log "Verifying login endpoint with seeded admin credentials..."
local status
status="$(curl -s -o /tmp/proxpanel-login.json -w "%{http_code}" \
-X POST "http://127.0.0.1:${FRONTEND_PORT}/api/auth/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${ADMIN_EMAIL}\",\"password\":\"${ADMIN_PASSWORD}\"}")"
if [[ "${status}" != "200" ]]; then
cat /tmp/proxpanel-login.json >&2 || true
die "Login verification failed with status ${status}."
fi
jq -e '.token and .refresh_token' /tmp/proxpanel-login.json >/dev/null 2>&1 || die "Login response missing token."
log "Login verification passed."
}
configure_ufw_if_requested() {
if [[ "${CONFIGURE_UFW}" != "true" ]]; then
return
fi
if ! command -v ufw >/dev/null 2>&1; then
log "UFW not installed; skipping firewall configuration."
return
fi
log "Configuring UFW rules..."
ufw allow OpenSSH >/dev/null 2>&1 || true
ufw allow "${FRONTEND_PORT}/tcp" >/dev/null 2>&1 || true
ufw --force enable >/dev/null 2>&1 || true
}
write_summary() {
local summary_file="/root/proxpanel-install-summary.txt"
cat > "${summary_file}" <<EOF
ProxPanel production deployment completed.
Public URL: ${PUBLIC_URL}
Server IP: $(echo "${PUBLIC_URL}" | sed -E 's#^https?://##')
Admin Email: ${ADMIN_EMAIL}
Admin Password: ${ADMIN_PASSWORD}
Deployment Directory: ${APP_DIR}
Compose File: infra/deploy/docker-compose.production.yml
Env File: ${APP_DIR}/.env.production
Quick checks:
curl -fsS http://127.0.0.1:${BACKEND_PORT}/api/health
docker compose --env-file .env.production -f infra/deploy/docker-compose.production.yml ps
IMPORTANT:
1) Change admin password immediately after first login.
2) Configure Proxmox credentials in Settings -> Proxmox.
3) Add TLS/reverse-proxy (Nginx/Caddy) if exposing publicly.
EOF
chmod 600 "${summary_file}"
log "Summary saved to ${summary_file}"
}
main() {
parse_args "$@"
ensure_root
install_prereqs
install_docker_if_needed
sync_source
validate_project_layout
infer_public_url
write_env_file
deploy_stack
wait_for_health
apply_database_schema
seed_database
verify_login
configure_ufw_if_requested
write_summary
log "Deployment finished successfully."
printf '\nOpen: %s\n' "${PUBLIC_URL}"
printf 'Admin email: %s\n' "${ADMIN_EMAIL}"
printf 'Admin password: %s\n' "${ADMIN_PASSWORD}"
}
main "$@"

29
infra/nginx/default.conf Normal file
View File

@@ -0,0 +1,29 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://backend:8080/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 90s;
}
location = /api/health {
proxy_pass http://backend:8080/api/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri /index.html;
}
}