Switch production deployment from Docker to PM2 on Ubuntu.
Add Express gateway, ecosystem config, and one-click install with native PostgreSQL, Node, and Python venv on port 23566. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+22
-25
@@ -1,25 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 备份 PostgreSQL 与 uploads 目录
|
||||
# 版权所有 (c) 马建军
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
cd "${INSTALL_DIR}"
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
echo "[INFO] 备份数据库…"
|
||||
docker compose --env-file .env exec -T db \
|
||||
pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-student_archive}" \
|
||||
> "${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||
|
||||
echo "[INFO] 备份 uploads…"
|
||||
tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/
|
||||
|
||||
echo "[INFO] 备份完成:"
|
||||
echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||
echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
cd "${INSTALL_DIR}"
|
||||
# shellcheck disable=SC1090
|
||||
source .env
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
echo "[INFO] 备份数据库…"
|
||||
PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h 127.0.0.1 -U "${POSTGRES_USER}" "${POSTGRES_DB}" \
|
||||
> "${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||
|
||||
echo "[INFO] 备份 uploads…"
|
||||
tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/
|
||||
|
||||
echo "[INFO] 完成:"
|
||||
echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||
echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz"
|
||||
|
||||
+235
-231
@@ -1,231 +1,235 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 中学成绩档案系统 — Ubuntu 一键部署脚本
|
||||
# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125
|
||||
#
|
||||
# 用法(root):
|
||||
# curl -fsSL .../deploy/install.sh | bash
|
||||
# 或
|
||||
# bash deploy/install.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
WEB_PORT="${WEB_PORT:-23566}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
log_error "请使用 root 用户运行本脚本,例如: sudo bash deploy/install.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
log_error "无法识别操作系统,本脚本仅支持 Ubuntu/Debian 系 Linux"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}"
|
||||
case "${ID:-}" in
|
||||
ubuntu|debian)
|
||||
;;
|
||||
*)
|
||||
log_warn "当前系统为 ${ID:-unknown},未经完整测试,继续安装…"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
check_resources() {
|
||||
local mem_kb disk_kb
|
||||
mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
disk_kb=$(df -k "${INSTALL_DIR%/*}" 2>/dev/null | tail -1 | awk '{print $4}')
|
||||
if [[ "${mem_kb:-0}" -lt 1800000 ]]; then
|
||||
log_warn "内存不足 2GB,PaddleOCR 首次运行可能较慢"
|
||||
fi
|
||||
if [[ "${disk_kb:-0}" -lt 5242880 ]]; then
|
||||
log_warn "可用磁盘空间不足 5GB,请确保有足够空间存放镜像与 OCR 模型"
|
||||
fi
|
||||
}
|
||||
|
||||
check_port() {
|
||||
if command -v ss &>/dev/null; then
|
||||
if ss -tln | grep -q ":${WEB_PORT} "; then
|
||||
log_error "端口 ${WEB_PORT} 已被占用,请修改 WEB_PORT 环境变量后重试"
|
||||
exit 1
|
||||
fi
|
||||
elif command -v netstat &>/dev/null; then
|
||||
if netstat -tln | grep -q ":${WEB_PORT} "; then
|
||||
log_error "端口 ${WEB_PORT} 已被占用"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_info "端口 ${WEB_PORT} 可用"
|
||||
}
|
||||
|
||||
install_packages() {
|
||||
log_info "安装基础依赖…"
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl ca-certificates openssl
|
||||
}
|
||||
|
||||
install_docker() {
|
||||
if command -v docker &>/dev/null; then
|
||||
log_info "Docker 已安装: $(docker --version)"
|
||||
else
|
||||
log_info "正在安装 Docker…"
|
||||
apt-get install -y -qq apt-transport-https gnupg lsb-release
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
||||
chmod a+r /etc/apt/keyrings/docker.asc
|
||||
echo \
|
||||
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
|
||||
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \
|
||||
> /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
systemctl enable docker
|
||||
systemctl start docker
|
||||
log_info "Docker 安装完成"
|
||||
fi
|
||||
|
||||
if ! docker compose version &>/dev/null; then
|
||||
log_error "未检测到 docker compose 插件,请手动安装 docker-compose-plugin"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Docker Compose: $(docker compose version)"
|
||||
}
|
||||
|
||||
clone_or_update_repo() {
|
||||
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
||||
log_info "更新代码: ${INSTALL_DIR}"
|
||||
git -C "${INSTALL_DIR}" fetch origin
|
||||
git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true
|
||||
git -C "${INSTALL_DIR}" pull --ff-only origin "${BRANCH}" || git -C "${INSTALL_DIR}" pull origin "${BRANCH}"
|
||||
elif [[ -d "${INSTALL_DIR}" ]]; then
|
||||
log_error "目录 ${INSTALL_DIR} 已存在但不是 git 仓库,请备份后删除或修改 INSTALL_DIR"
|
||||
exit 1
|
||||
else
|
||||
log_info "克隆仓库到 ${INSTALL_DIR}"
|
||||
mkdir -p "$(dirname "${INSTALL_DIR}")"
|
||||
git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}"
|
||||
fi
|
||||
}
|
||||
|
||||
generate_env() {
|
||||
local env_file="${INSTALL_DIR}/.env"
|
||||
local server_ip
|
||||
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
server_ip="${server_ip:-127.0.0.1}"
|
||||
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
log_info "保留已有 .env 配置"
|
||||
# shellcheck source=/dev/null
|
||||
source "${env_file}"
|
||||
WEB_PORT="${WEB_PORT:-23566}"
|
||||
return
|
||||
fi
|
||||
|
||||
log_info "生成 .env 配置文件"
|
||||
local secret pg_pass
|
||||
secret=$(openssl rand -hex 32)
|
||||
pg_pass=$(openssl rand -hex 16)
|
||||
|
||||
cat > "${env_file}" <<EOF
|
||||
# 由 deploy/install.sh 自动生成 — $(date -Iseconds)
|
||||
WEB_PORT=${WEB_PORT}
|
||||
SECRET_KEY=${secret}
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=${pg_pass}
|
||||
POSTGRES_DB=student_archive
|
||||
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
|
||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||||
OLLAMA_MODEL=qwen2.5:7b
|
||||
FLUCTUATION_THRESHOLD=0.08
|
||||
EOF
|
||||
chmod 600 "${env_file}"
|
||||
log_info ".env 已写入 ${env_file}"
|
||||
}
|
||||
|
||||
start_services() {
|
||||
log_info "构建并启动 Docker 服务(首次可能需 10–30 分钟)…"
|
||||
cd "${INSTALL_DIR}"
|
||||
mkdir -p uploads backups
|
||||
docker compose --env-file .env pull 2>/dev/null || true
|
||||
docker compose --env-file .env up -d --build
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local i max=60
|
||||
log_info "等待 API 就绪…"
|
||||
for ((i=1; i<=max; i++)); do
|
||||
if docker compose --env-file "${INSTALL_DIR}/.env" exec -T api \
|
||||
python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')" &>/dev/null; then
|
||||
log_info "API 健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
log_warn "API 启动超时,请执行: cd ${INSTALL_DIR} && docker compose logs api"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
local ip
|
||||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
ip="${ip:-127.0.0.1}"
|
||||
# shellcheck source=/dev/null
|
||||
source "${INSTALL_DIR}/.env"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 中学成绩档案系统 部署完成"
|
||||
echo " 版权所有 (c) 马建军"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo " 访问地址: http://${ip}:${WEB_PORT}"
|
||||
echo " 本地访问: http://127.0.0.1:${WEB_PORT}"
|
||||
echo " 安装目录: ${INSTALL_DIR}"
|
||||
echo ""
|
||||
echo " 常用命令:"
|
||||
echo " 查看状态: cd ${INSTALL_DIR} && docker compose ps"
|
||||
echo " 查看日志: cd ${INSTALL_DIR} && docker compose logs -f"
|
||||
echo " 停止服务: cd ${INSTALL_DIR} && docker compose down"
|
||||
echo " 更新版本: bash ${INSTALL_DIR}/deploy/update.sh"
|
||||
echo " 数据备份: bash ${INSTALL_DIR}/deploy/backup.sh"
|
||||
echo ""
|
||||
echo " 反向代理(Nginx/Caddy 等)请自行配置,本项目不包含。"
|
||||
echo " 详见文档: ${INSTALL_DIR}/docs/DEPLOY.md"
|
||||
echo ""
|
||||
echo " 技术支持: 微信 dekun03 手机 18364911125"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
main() {
|
||||
echo ""
|
||||
log_info "中学成绩档案系统 — 一键部署开始"
|
||||
require_root
|
||||
check_os
|
||||
check_resources
|
||||
check_port
|
||||
install_packages
|
||||
install_docker
|
||||
clone_or_update_repo
|
||||
generate_env
|
||||
start_services
|
||||
wait_healthy
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 中学成绩档案系统 — Ubuntu PM2 一键部署
|
||||
# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
WEB_PORT="${WEB_PORT:-23566}"
|
||||
API_PORT="${API_PORT:-8000}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
NODE_MAJOR="${NODE_MAJOR:-20}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
|
||||
require_root() {
|
||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||
log_error "请使用 root 用户运行: sudo bash deploy/install.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_os() {
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
log_error "无法识别操作系统"
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}"
|
||||
}
|
||||
|
||||
check_port() {
|
||||
if command -v ss &>/dev/null && ss -tln | grep -q ":${WEB_PORT} "; then
|
||||
log_error "端口 ${WEB_PORT} 已被占用"
|
||||
exit 1
|
||||
fi
|
||||
log_info "端口 ${WEB_PORT} 可用"
|
||||
}
|
||||
|
||||
install_base_packages() {
|
||||
log_info "安装系统依赖…"
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||||
git curl ca-certificates openssl \
|
||||
python3 python3-venv python3-pip python3-dev \
|
||||
build-essential libpq-dev \
|
||||
postgresql postgresql-contrib \
|
||||
libgomp1 libglib2.0-0 libsm6 libxrender1 libxext6
|
||||
}
|
||||
|
||||
install_node_pm2() {
|
||||
if ! command -v node &>/dev/null || [[ "$(node -v | cut -d. -f1 | tr -d v)" -lt "${NODE_MAJOR}" ]]; then
|
||||
log_info "安装 Node.js ${NODE_MAJOR}.x…"
|
||||
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
|
||||
apt-get install -y -qq nodejs
|
||||
fi
|
||||
log_info "Node: $(node -v) npm: $(npm -v)"
|
||||
|
||||
if ! command -v pm2 &>/dev/null; then
|
||||
log_info "安装 PM2…"
|
||||
npm install -g pm2
|
||||
fi
|
||||
log_info "PM2: $(pm2 -v)"
|
||||
}
|
||||
|
||||
clone_or_update_repo() {
|
||||
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
||||
log_info "更新代码: ${INSTALL_DIR}"
|
||||
git -C "${INSTALL_DIR}" fetch origin
|
||||
git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true
|
||||
git -C "${INSTALL_DIR}" pull origin "${BRANCH}"
|
||||
else
|
||||
log_info "克隆仓库到 ${INSTALL_DIR}"
|
||||
mkdir -p "$(dirname "${INSTALL_DIR}")"
|
||||
git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}"
|
||||
fi
|
||||
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
|
||||
}
|
||||
|
||||
generate_env() {
|
||||
local env_file="${INSTALL_DIR}/.env"
|
||||
local server_ip
|
||||
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
server_ip="${server_ip:-127.0.0.1}"
|
||||
|
||||
if [[ -f "${env_file}" ]]; then
|
||||
log_info "保留已有 .env"
|
||||
# shellcheck disable=SC1090
|
||||
set -a && source "${env_file}" && set +a
|
||||
return
|
||||
fi
|
||||
|
||||
local secret pg_pass pg_user
|
||||
secret=$(openssl rand -hex 32)
|
||||
pg_pass=$(openssl rand -hex 16)
|
||||
pg_user="gradeapp"
|
||||
|
||||
cat > "${env_file}" <<EOF
|
||||
# generated by deploy/install.sh — $(date -Iseconds)
|
||||
WEB_PORT=${WEB_PORT}
|
||||
API_PORT=${API_PORT}
|
||||
API_TARGET=http://127.0.0.1:${API_PORT}
|
||||
SECRET_KEY=${secret}
|
||||
POSTGRES_USER=${pg_user}
|
||||
POSTGRES_PASSWORD=${pg_pass}
|
||||
POSTGRES_DB=student_archive
|
||||
DATABASE_URL=postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/student_archive
|
||||
UPLOAD_DIR=${INSTALL_DIR}/uploads
|
||||
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
|
||||
OLLAMA_BASE_URL=http://127.0.0.1:11434
|
||||
OLLAMA_MODEL=qwen2.5:7b
|
||||
FLUCTUATION_THRESHOLD=0.08
|
||||
EOF
|
||||
chmod 600 "${env_file}"
|
||||
log_info ".env 已生成"
|
||||
}
|
||||
|
||||
setup_postgresql() {
|
||||
# shellcheck disable=SC1090
|
||||
source "${INSTALL_DIR}/.env"
|
||||
log_info "配置 PostgreSQL…"
|
||||
systemctl enable postgresql
|
||||
systemctl start postgresql
|
||||
|
||||
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${POSTGRES_USER}'" | grep -q 1; then
|
||||
sudo -u postgres psql -c "CREATE USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
|
||||
else
|
||||
sudo -u postgres psql -c "ALTER USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
|
||||
fi
|
||||
|
||||
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" | grep -q 1; then
|
||||
sudo -u postgres psql -c "CREATE DATABASE ${POSTGRES_DB} OWNER ${POSTGRES_USER};"
|
||||
fi
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_USER};"
|
||||
}
|
||||
|
||||
setup_backend() {
|
||||
log_info "安装 Python 依赖…"
|
||||
cd "${INSTALL_DIR}/backend"
|
||||
python3 -m venv venv
|
||||
# shellcheck disable=SC1091
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
deactivate
|
||||
}
|
||||
|
||||
setup_frontend() {
|
||||
log_info "构建前端…"
|
||||
cd "${INSTALL_DIR}/frontend"
|
||||
npm ci --silent
|
||||
npm run build
|
||||
}
|
||||
|
||||
setup_gateway() {
|
||||
log_info "安装 Web 网关依赖…"
|
||||
cd "${INSTALL_DIR}/deploy/pm2"
|
||||
npm ci --silent
|
||||
}
|
||||
|
||||
start_pm2() {
|
||||
log_info "启动 PM2 服务…"
|
||||
cd "${INSTALL_DIR}"
|
||||
mkdir -p uploads backups
|
||||
pm2 delete grade-api grade-web 2>/dev/null || true
|
||||
pm2 start deploy/pm2/ecosystem.config.cjs
|
||||
pm2 save
|
||||
env PATH="$PATH" pm2 startup systemd -u root --hp /root >/tmp/pm2-startup.sh 2>/dev/null || true
|
||||
if [[ -f /tmp/pm2-startup.sh ]]; then
|
||||
bash /tmp/pm2-startup.sh || true
|
||||
fi
|
||||
}
|
||||
|
||||
wait_healthy() {
|
||||
local i
|
||||
for i in $(seq 1 40); do
|
||||
if curl -sf "http://127.0.0.1:${WEB_PORT}/api/health" >/dev/null; then
|
||||
log_info "健康检查通过"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
log_warn "健康检查超时,请查看: pm2 logs"
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
local ip
|
||||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
ip="${ip:-127.0.0.1}"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 中学成绩档案系统 PM2 部署完成"
|
||||
echo " 版权所有 (c) 马建军"
|
||||
echo "=========================================="
|
||||
echo " 访问: http://${ip}:${WEB_PORT}"
|
||||
echo " 目录: ${INSTALL_DIR}"
|
||||
echo ""
|
||||
echo " pm2 status"
|
||||
echo " pm2 logs"
|
||||
echo " bash ${INSTALL_DIR}/deploy/update.sh"
|
||||
echo " bash ${INSTALL_DIR}/deploy/backup.sh"
|
||||
echo ""
|
||||
echo " 反向代理请自行配置,本项目不包含"
|
||||
echo " 微信 dekun03 手机 18364911125"
|
||||
echo "=========================================="
|
||||
}
|
||||
|
||||
main() {
|
||||
log_info "PM2 一键部署开始"
|
||||
require_root
|
||||
check_os
|
||||
check_port
|
||||
install_base_packages
|
||||
install_node_pm2
|
||||
clone_or_update_repo
|
||||
generate_env
|
||||
setup_postgresql
|
||||
setup_backend
|
||||
setup_frontend
|
||||
setup_gateway
|
||||
start_pm2
|
||||
wait_healthy
|
||||
print_summary
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -0,0 +1,61 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
const root = path.resolve(__dirname, '../..')
|
||||
const envPath = path.join(root, '.env')
|
||||
|
||||
function loadEnv() {
|
||||
const env = { NODE_ENV: 'production' }
|
||||
if (!fs.existsSync(envPath)) return env
|
||||
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
const idx = trimmed.indexOf('=')
|
||||
if (idx === -1) continue
|
||||
const key = trimmed.slice(0, idx).trim()
|
||||
let value = trimmed.slice(idx + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
env[key] = value
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
const env = loadEnv()
|
||||
const webPort = env.WEB_PORT || '23566'
|
||||
const apiPort = env.API_PORT || '8000'
|
||||
const venvPython = path.join(root, 'backend', 'venv', 'bin', 'python')
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'grade-api',
|
||||
cwd: path.join(root, 'backend'),
|
||||
script: venvPython,
|
||||
args: `-m uvicorn app.main:app --host 127.0.0.1 --port ${apiPort}`,
|
||||
interpreter: 'none',
|
||||
env: {
|
||||
...env,
|
||||
UPLOAD_DIR: env.UPLOAD_DIR || path.join(root, 'uploads'),
|
||||
},
|
||||
max_restarts: 10,
|
||||
restart_delay: 5000,
|
||||
},
|
||||
{
|
||||
name: 'grade-web',
|
||||
cwd: path.join(root, 'deploy', 'pm2'),
|
||||
script: 'server.js',
|
||||
env: {
|
||||
...env,
|
||||
WEB_PORT: webPort,
|
||||
API_TARGET: env.API_TARGET || `http://127.0.0.1:${apiPort}`,
|
||||
},
|
||||
max_restarts: 10,
|
||||
restart_delay: 3000,
|
||||
},
|
||||
],
|
||||
}
|
||||
Generated
+1052
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "grade-archive-gateway",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"description": "Static web + API reverse proxy for PM2 deployment",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const express = require('express')
|
||||
const { createProxyMiddleware } = require('http-proxy-middleware')
|
||||
|
||||
const envPath = path.join(__dirname, '../../.env')
|
||||
if (fs.existsSync(envPath)) {
|
||||
require('dotenv').config({ path: envPath })
|
||||
}
|
||||
|
||||
const PORT = Number(process.env.WEB_PORT || 23566)
|
||||
const API_TARGET = process.env.API_TARGET || 'http://127.0.0.1:8000'
|
||||
const STATIC_ROOT = path.join(__dirname, '../../frontend/dist')
|
||||
|
||||
const app = express()
|
||||
app.use(
|
||||
'/api',
|
||||
createProxyMiddleware({
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
}),
|
||||
)
|
||||
app.use(express.static(STATIC_ROOT, { maxAge: '1h', index: false }))
|
||||
app.get(/^\/(?!api).*/, (_req, res) => {
|
||||
res.sendFile(path.join(STATIC_ROOT, 'index.html'))
|
||||
})
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Grade archive web gateway listening on http://0.0.0.0:${PORT}`)
|
||||
console.log(`API proxy target: ${API_TARGET}`)
|
||||
})
|
||||
+11
-28
@@ -1,28 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 停止并移除容器(默认保留数据卷与 uploads)
|
||||
# 版权所有 (c) 马建军
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
REMOVE_VOLUMES="${REMOVE_VOLUMES:-0}"
|
||||
|
||||
cd "${INSTALL_DIR}" || exit 1
|
||||
|
||||
echo "将停止 Docker 服务…"
|
||||
if [[ "${REMOVE_VOLUMES}" == "1" ]]; then
|
||||
echo "警告: 将删除数据库卷(所有成绩数据会丢失)"
|
||||
read -r -p "确认删除数据卷? 输入 yes: " ans
|
||||
if [[ "${ans}" == "yes" ]]; then
|
||||
docker compose --env-file .env down -v
|
||||
else
|
||||
echo "已取消"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
docker compose --env-file .env down
|
||||
echo "数据卷与 ${INSTALL_DIR}/uploads 已保留"
|
||||
fi
|
||||
|
||||
echo "卸载完成。源码目录 ${INSTALL_DIR} 未自动删除,如需删除请手动 rm -rf"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
|
||||
cd "${INSTALL_DIR}" || exit 1
|
||||
pm2 delete grade-api grade-web 2>/dev/null || true
|
||||
pm2 save --force
|
||||
|
||||
echo "PM2 服务已停止。PostgreSQL 数据与 ${INSTALL_DIR}/uploads 仍保留。"
|
||||
echo "如需删除源码: rm -rf ${INSTALL_DIR}"
|
||||
|
||||
+39
-29
@@ -1,29 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# 更新已部署实例:拉取代码并重建容器
|
||||
# 版权所有 (c) 马建军
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
|
||||
if [[ ! -d "${INSTALL_DIR}/.git" ]]; then
|
||||
echo "未找到部署目录 ${INSTALL_DIR},请先运行 deploy/install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "${INSTALL_DIR}"
|
||||
log_info "拉取最新代码…"
|
||||
git fetch origin
|
||||
git checkout "${BRANCH}" 2>/dev/null || true
|
||||
git pull origin "${BRANCH}"
|
||||
|
||||
log_info "重建并重启服务…"
|
||||
docker compose --env-file .env up -d --build
|
||||
|
||||
log_info "更新完成。访问端口见 .env 中 WEB_PORT"
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||
BRANCH="${BRANCH:-main}"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
|
||||
cd "${INSTALL_DIR}" || exit 1
|
||||
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
|
||||
|
||||
log_info "拉取最新代码…"
|
||||
git fetch origin
|
||||
git checkout "${BRANCH}" 2>/dev/null || true
|
||||
git pull origin "${BRANCH}"
|
||||
|
||||
log_info "更新后端依赖…"
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt -q
|
||||
deactivate
|
||||
|
||||
log_info "重建前端…"
|
||||
cd ../frontend
|
||||
npm ci --silent
|
||||
npm run build
|
||||
|
||||
log_info "更新网关…"
|
||||
cd ../deploy/pm2
|
||||
npm ci --silent
|
||||
|
||||
log_info "重启 PM2…"
|
||||
cd "${INSTALL_DIR}"
|
||||
pm2 reload deploy/pm2/ecosystem.config.cjs --update-env || pm2 start deploy/pm2/ecosystem.config.cjs
|
||||
pm2 save
|
||||
|
||||
log_info "更新完成"
|
||||
|
||||
Reference in New Issue
Block a user