#!/bin/bash # Spug 自动部署脚本 - Docker 版本 # 用途:医院绩效考核系统 Docker 自动化部署 # 执行方式:在 Spug 的"执行任务"中调用此脚本 set -e # ==================== 配置参数 ==================== # 项目配置 PROJECT_NAME="${SPUG_APP_NAME:-hospital-performance}" PROJECT_DIR="${SPUG_DEPLOY_DIR:-/var/www/hospital-performance}" DOCKER_REGISTRY="${DOCKER_REGISTRY:-}" # 可选:私有仓库地址 DOCKER_IMAGE_NAME="${DOCKER_IMAGE_NAME:-hospital-performance}" DOCKER_TAG="${DOCKER_TAG:-latest}" # Git 配置 if [ -n "${SPUG_GIT_URL}" ]; then GIT_REPO="${SPUG_GIT_URL}" elif [ -n "${SPUG_GIT_REPO}" ]; then GIT_REPO="${SPUG_GIT_REPO}" else GIT_REPO="https://gitea.gentronhealth.com/chenqi/hospital_performance.git" fi GIT_BRANCH="${SPUG_GIT_BRANCH:-main}" # Docker 配置 DOCKER_NETWORK="${DOCKER_NETWORK:-hospital-network}" DOCKER_BACKEND_PORT="${DOCKER_BACKEND_PORT:-8000}" DOCKER_FRONTEND_PORT="${DOCKER_FRONTEND_PORT:-80}" CONTAINER_NAME="${CONTAINER_NAME:-hospital-performance}" # 数据库配置(从环境变量获取) DATABASE_HOST="${DATABASE_HOST:-192.168.110.252}" DATABASE_PORT="${DATABASE_PORT:-15432}" DATABASE_USER="${DATABASE_USER:-postgresql}" DATABASE_PASSWORD="${DATABASE_PASSWORD:-Jchl1528}" DATABASE_NAME="${DATABASE_NAME:-hospital_performance}" # JWT 配置 SECRET_KEY="${SECRET_KEY:-change-this-secret-key-in-production}" DEBUG="${DEBUG:-False}" # 日志配置 if [ -z "${LOG_FILE}" ]; then LOG_FILE="/var/log/spug/deploy-docker.log" mkdir -p /var/log/spug 2>/dev/null || true fi DEPLOY_TIME=$(date +"%Y%m%d_%H%M%S") # ==================== 工具函数 ==================== log() { local level=$1 shift local message="$*" local timestamp=$(date '+%Y-%m-%d %H:%M:%S') echo "[${timestamp}] [${level}] ${message}" | tee -a "${LOG_FILE}" } info() { log "INFO" "$@"; } error() { log "ERROR" "$@"; } success() { log "SUCCESS" "$@"; } check_command() { if ! command -v $1 &> /dev/null; then error "命令 $1 未安装,请先安装" exit 1 fi } # ==================== 前置检查 ==================== pre_check() { info "========== 开始前置检查 ==========" # 检查必要命令 check_command git check_command docker # 检查 Docker 是否运行 if ! docker info > /dev/null 2>&1; then error "Docker 未运行,请启动 Docker 服务" exit 1 fi # 检查部署目录 if [ ! -d "${PROJECT_DIR}" ]; then info "创建部署目录:${PROJECT_DIR}" mkdir -p "${PROJECT_DIR}" fi info "✓ 前置检查通过" } # ==================== 更新代码 ==================== update_code() { info "========== 更新代码 ==========" cd "${PROJECT_DIR}" if [ ! -d ".git" ]; then info "首次部署,克隆仓库..." if [ "$(ls -A ${PROJECT_DIR} 2>/dev/null)" ]; then info "目录非空,先备份现有文件..." mkdir -p "${PROJECT_DIR}/backup_$(date +%Y%m%d_%H%M%S)_non_git" find "${PROJECT_DIR}" -maxdepth 1 -type f -o -type d ! -name '.' -exec mv {} "${PROJECT_DIR}/backup_$(date +%Y%m%d_%H%M%S)_non_git/" \; 2>/dev/null || true fi git clone "${GIT_REPO}" . git checkout "${GIT_BRANCH}" else info "更新现有代码..." git fetch origin "${GIT_BRANCH}" git reset --hard "origin/${GIT_BRANCH}" git clean -fd fi local commit_hash=$(git rev-parse --short HEAD) local commit_msg=$(git log -1 --pretty=format:"%s") success "✓ 代码更新完成,当前版本:${commit_hash} - ${commit_msg}" } # ==================== 构建 Docker 镜像 ==================== build_docker_image() { info "========== 构建 Docker 镜像 ==========" cd "${PROJECT_DIR}" # 检查 Dockerfile 是否存在 if [ ! -f "Dockerfile" ]; then error "Dockerfile 不存在,无法构建镜像" exit 1 fi # 构建后端镜像 info "构建后端 Docker 镜像..." docker build -t ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} \ -f Dockerfile.backend . \ --build-arg DATABASE_HOST=${DATABASE_HOST} \ --build-arg DATABASE_PORT=${DATABASE_PORT} \ --build-arg SECRET_KEY=${SECRET_KEY} \ --build-arg DEBUG=${DEBUG} # 构建前端镜像(如果有) if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then info "构建前端 Docker 镜像..." docker build -t ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} \ -f Dockerfile.frontend . fi # 推送到私有仓库(如果配置了) if [ -n "${DOCKER_REGISTRY}" ]; then info "推送镜像到私有仓库:${DOCKER_REGISTRY}" docker tag ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then docker tag ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} \ ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} fi fi success "✓ Docker 镜像构建完成" } # ==================== 停止旧容器 ==================== stop_old_containers() { info "========== 停止旧容器 ==========" # 停止并删除旧容器 if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-backend$"; then info "停止后端容器:${CONTAINER_NAME}-backend" docker stop "${CONTAINER_NAME}-backend" || true docker rm "${CONTAINER_NAME}-backend" || true fi if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-frontend$"; then info "停止前端容器:${CONTAINER_NAME}-frontend" docker stop "${CONTAINER_NAME}-frontend" || true docker rm "${CONTAINER_NAME}-frontend" || true fi success "✓ 旧容器已清理" } # ==================== 启动新容器 ==================== start_new_containers() { info "========== 启动新容器 ==========" # 创建网络(如果不存在) if ! docker network ls --format '{{.Name}}' | grep -q "^${DOCKER_NETWORK}$"; then info "创建 Docker 网络:${DOCKER_NETWORK}" docker network create ${DOCKER_NETWORK} fi # 启动后端容器 info "启动后端容器..." docker run -d \ --name ${CONTAINER_NAME}-backend \ --network ${DOCKER_NETWORK} \ -p ${DOCKER_BACKEND_PORT}:8000 \ -e DATABASE_URL="postgresql+asyncpg://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}" \ -e SECRET_KEY=${SECRET_KEY} \ -e DEBUG=${DEBUG} \ -v ${PROJECT_DIR}/backend/logs:/app/logs \ --restart unless-stopped \ ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} # 等待后端启动 info "等待后端服务启动..." sleep 10 # 启动前端容器(如果有) if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then info "启动前端容器..." docker run -d \ --name ${CONTAINER_NAME}-frontend \ --network ${DOCKER_NETWORK} \ -p ${DOCKER_FRONTEND_PORT}:80 \ --link ${CONTAINER_NAME}-backend:backend \ --restart unless-stopped \ ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} fi success "✓ 容器启动成功" } # ==================== 健康检查 ==================== health_check() { info "========== 执行健康检查 ==========" # 等待服务启动 sleep 5 # 检查后端 API local backend_url="http://localhost:${DOCKER_BACKEND_PORT}/api/v1/health" local max_attempts=10 local attempt=1 while [ $attempt -le $max_attempts ]; do if curl -f -s "${backend_url}" > /dev/null 2>&1; then success "✓ 后端 API 健康检查通过" break else if [ $attempt -eq $max_attempts ]; then error "✗ 后端 API 健康检查失败(尝试 ${attempt}/${max_attempts})" exit 1 fi info "后端服务未就绪,等待 5 秒后重试... (${attempt}/${max_attempts})" sleep 5 attempt=$((attempt + 1)) fi done # 检查容器状态 if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-backend$"; then success "✓ 后端容器运行正常" else error "✗ 后端容器未运行" exit 1 fi if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-frontend$"; then success "✓ 前端容器运行正常" fi fi info "✓ 所有健康检查通过" } # ==================== 清理旧镜像 ==================== cleanup_old_images() { info "========== 清理旧 Docker 镜像 ==========" # 保留最近 3 个版本的镜像 docker images ${DOCKER_IMAGE_NAME}-backend --format "{{.Tag}}" | \ tail -n +4 | xargs -r docker rmi 2>/dev/null || true if [ -f "Dockerfile.frontend" ]; then docker images ${DOCKER_IMAGE_NAME}-frontend --format "{{.Tag}}" | \ tail -n +4 | xargs -r docker rmi 2>/dev/null || true fi # 清理悬空镜像 docker image prune -f info "✓ 清理完成" } # ==================== 主流程 ==================== main() { info "========================================" info "Spug Docker 自动部署开始" info "项目名称:${PROJECT_NAME}" info "部署目录:${PROJECT_DIR}" info "Git 分支:${GIT_BRANCH}" info "Docker 镜像:${DOCKER_IMAGE_NAME}:${DOCKER_TAG}" info "========================================" pre_check update_code build_docker_image stop_old_containers start_new_containers health_check cleanup_old_images info "========================================" success "🎉 Docker 部署成功完成!" info "后端地址:http://localhost:${DOCKER_BACKEND_PORT}" if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then info "前端地址:http://localhost:${DOCKER_FRONTEND_PORT}" fi info "========================================" } main "$@"