chore: initialize repository with deployment baseline
This commit is contained in:
76
infra/deploy/docker-compose.production.yml
Normal file
76
infra/deploy/docker-compose.production.yml
Normal 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:
|
||||
345
infra/deploy/install-proxpanel.sh
Normal file
345
infra/deploy/install-proxpanel.sh
Normal 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
29
infra/nginx/default.conf
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user