重大变更: - 移除CI/CD中的Docker镜像构建和推送 - 改为在CI中构建产物,通过rsync同步到生产服务器 - 生产服务器本地构建镜像并部署 新增文件: - Dockerfile.prod: 生产服务器专用Dockerfile - docker-compose.server.yml: 生产服务器docker-compose配置 - scripts/deploy-production.sh: 生产服务器部署脚本 优势: 1. 减少CI/CD服务器负载(无需构建镜像) 2. 加快部署速度(直接同步产物) 3. 降低镜像仓库存储成本 4. 生产服务器可自主控制构建和部署
This commit is contained in:
+39
-103
@@ -16,7 +16,7 @@
|
|||||||
#
|
#
|
||||||
# 3. release/** 分支:生产环境代码
|
# 3. release/** 分支:生产环境代码
|
||||||
# - 触发:push
|
# - 触发:push
|
||||||
# - 执行:完整测试 + 构建 + 部署 + 归档
|
# - 执行:代码检查 + 构建产物 + 同步部署
|
||||||
# - 部署到:生产环境(139.155.109.62)
|
# - 部署到:生产环境(139.155.109.62)
|
||||||
# - 归档到:main分支
|
# - 归档到:main分支
|
||||||
#
|
#
|
||||||
@@ -24,25 +24,23 @@
|
|||||||
# - 只读分支
|
# - 只读分支
|
||||||
# - 仅接收来自release的自动归档
|
# - 仅接收来自release的自动归档
|
||||||
#
|
#
|
||||||
# 流水线阶段(严格顺序执行):
|
# 流水线阶段:
|
||||||
# 阶段0: 依赖安装(统一缓存)
|
# 阶段0: 依赖安装(统一缓存)
|
||||||
# 阶段1: 并行代码质量检查 (lint, type-check, security-scan)
|
# 阶段1: 并行代码质量检查 (lint, type-check, security-scan)
|
||||||
# 阶段2: 单元测试 -> E2E测试
|
# 阶段2: 单元测试 -> E2E测试 (允许失败)
|
||||||
# 阶段3: 构建Docker镜像 (仅release分支,依赖E2E测试通过)
|
# 阶段3: 构建生产产物 (仅release分支)
|
||||||
# 阶段4: 部署到生产环境 (仅release分支,依赖镜像构建成功)
|
# 阶段4: 同步产物到生产服务器并部署 (仅release分支)
|
||||||
# 阶段5: 归档到main分支 (仅release分支,依赖部署成功)
|
# 阶段5: 归档到main分支 (仅release分支)
|
||||||
# 阶段6: 企业微信通知
|
# 阶段6: 企业微信通知
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# 全局环境变量
|
|
||||||
variables:
|
variables:
|
||||||
- &node_image node:20-alpine
|
- &node_image node:20-alpine
|
||||||
- &docker_image docker:24-cli
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 阶段0: 依赖安装(统一缓存)
|
|
||||||
# ============================================
|
|
||||||
steps:
|
steps:
|
||||||
|
# ============================================
|
||||||
|
# 阶段0: 依赖安装(统一缓存)
|
||||||
|
# ============================================
|
||||||
install-deps:
|
install-deps:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
environment:
|
environment:
|
||||||
@@ -129,6 +127,9 @@ steps:
|
|||||||
- release
|
- release
|
||||||
- release/**
|
- release/**
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 阶段2: 测试 (允许失败)
|
||||||
|
# ============================================
|
||||||
unit-tests:
|
unit-tests:
|
||||||
image: *node_image
|
image: *node_image
|
||||||
environment:
|
environment:
|
||||||
@@ -139,7 +140,7 @@ steps:
|
|||||||
- type-check
|
- type-check
|
||||||
commands:
|
commands:
|
||||||
- npm run test:unit -- --coverage --coverageReporters=text-summary --forceExit 2>&1 | tee test-results.txt || true
|
- npm run test:unit -- --coverage --coverageReporters=text-summary --forceExit 2>&1 | tee test-results.txt || true
|
||||||
- echo "Unit tests completed. Check test-results.txt for details."
|
- echo "Unit tests completed."
|
||||||
failure: ignore
|
failure: ignore
|
||||||
volumes:
|
volumes:
|
||||||
- /tmp/npm-cache:/root/.npm
|
- /tmp/npm-cache:/root/.npm
|
||||||
@@ -153,9 +154,6 @@ steps:
|
|||||||
- release
|
- release
|
||||||
- release/**
|
- release/**
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 阶段2: E2E测试 (分层测试)
|
|
||||||
# ============================================
|
|
||||||
e2e-tests:
|
e2e-tests:
|
||||||
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
image: mcr.microsoft.com/playwright:v1.48.0-jammy
|
||||||
environment:
|
environment:
|
||||||
@@ -183,28 +181,24 @@ steps:
|
|||||||
- release/**
|
- release/**
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 阶段3: 构建Docker镜像 (release分支)
|
# 阶段3: 构建生产产物 (release分支)
|
||||||
# ============================================
|
# ============================================
|
||||||
build-image:
|
build-artifacts:
|
||||||
image: *docker_image
|
image: *node_image
|
||||||
environment:
|
environment:
|
||||||
REGISTRY_PASSWORD:
|
NODE_ENV: production
|
||||||
from_secret: registry_password
|
NEXT_TELEMETRY_DISABLED: 1
|
||||||
depends_on:
|
depends_on:
|
||||||
- lint
|
- lint
|
||||||
- type-check
|
- type-check
|
||||||
commands:
|
commands:
|
||||||
- echo "Building Docker image..."
|
- echo "Building production artifacts..."
|
||||||
- docker build -t registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} .
|
- npm run build
|
||||||
- docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:latest
|
- echo "✅ Build completed"
|
||||||
- docker tag registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA} registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7}
|
- ls -la dist/
|
||||||
- echo "Pushing to registry..."
|
|
||||||
- echo "$REGISTRY_PASSWORD" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn
|
|
||||||
- docker push registry.f.novalon.cn/novalon-website:${CI_COMMIT_SHA}
|
|
||||||
- docker push registry.f.novalon.cn/novalon-website:latest
|
|
||||||
- docker push registry.f.novalon.cn/novalon-website:release-${CI_COMMIT_SHA:0:7}
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /tmp/npm-cache:/root/.npm
|
||||||
|
- /tmp/node-modules-cache:/woodpecker/src/node_modules
|
||||||
when:
|
when:
|
||||||
- event: push
|
- event: push
|
||||||
branch:
|
branch:
|
||||||
@@ -220,10 +214,8 @@ steps:
|
|||||||
DEPLOY_ENV: production
|
DEPLOY_ENV: production
|
||||||
SSH_PRIVATE_KEY:
|
SSH_PRIVATE_KEY:
|
||||||
from_secret: ssh_private_key
|
from_secret: ssh_private_key
|
||||||
REGISTRY_PASSWORD:
|
|
||||||
from_secret: registry_password
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- build-image
|
- build-artifacts
|
||||||
commands:
|
commands:
|
||||||
- echo "Deploying to production environment..."
|
- echo "Deploying to production environment..."
|
||||||
- mkdir -p ~/.ssh
|
- mkdir -p ~/.ssh
|
||||||
@@ -231,76 +223,29 @@ steps:
|
|||||||
- chmod 600 ~/.ssh/id_rsa
|
- chmod 600 ~/.ssh/id_rsa
|
||||||
- ssh-keyscan -H 139.155.109.62 >> ~/.ssh/known_hosts
|
- ssh-keyscan -H 139.155.109.62 >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# 前置检查
|
|
||||||
- echo "Pre-deployment checks..."
|
- echo "Pre-deployment checks..."
|
||||||
- ssh root@139.155.109.62 "echo 'Server connection OK'"
|
- ssh root@139.155.109.62 "echo 'Server connection OK'"
|
||||||
- ssh root@139.155.109.62 "df -h | grep -E '/$|/home'"
|
- ssh root@139.155.109.62 "df -h | grep -E '/$|/home'"
|
||||||
- ssh root@139.155.109.62 "docker ps | grep novalon-website || echo 'No existing container'"
|
|
||||||
|
|
||||||
# 部署
|
- echo "Syncing build artifacts to production server..."
|
||||||
|
- rsync -avz --delete dist/ root@139.155.109.62:/home/novalon/docker-app/novalon-website/dist/
|
||||||
|
- rsync -avz public/ root@139.155.109.62:/home/novalon/docker-app/novalon-website/public/
|
||||||
|
- rsync -avz package.json package-lock.json root@139.155.109.62:/home/novalon/docker-app/novalon-website/
|
||||||
|
- rsync -avz Dockerfile.prod docker-compose.server.yml root@139.155.109.62:/home/novalon/docker-app/novalon-website/
|
||||||
|
- rsync -avz scripts/deploy-production.sh root@139.155.109.62:/home/novalon/docker-app/novalon-website/scripts/
|
||||||
|
- rsync -avz .env.production root@139.155.109.62:/home/novalon/docker-app/novalon-website/ 2>/dev/null || echo "No .env.production file"
|
||||||
|
|
||||||
- |
|
- |
|
||||||
ssh root@139.155.109.62 << EOF
|
ssh root@139.155.109.62 << 'EOF'
|
||||||
set -e # 任何命令失败立即退出
|
set -e
|
||||||
cd /home/novalon/docker-app/novalon-website
|
cd /home/novalon/docker-app/novalon-website
|
||||||
|
|
||||||
echo "=== Step 1: Login to Registry ==="
|
if [ -f docker-compose.server.yml ]; then
|
||||||
if ! echo "${REGISTRY_PASSWORD}" | docker login -u novalon-admin --password-stdin registry.f.novalon.cn; then
|
mv docker-compose.server.yml docker-compose.yml
|
||||||
echo "❌ Registry login failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== Step 2: Backup current version ==="
|
chmod +x scripts/deploy-production.sh
|
||||||
BACKUP_TIME=\$(date +%Y%m%d_%H%M%S)
|
./scripts/deploy-production.sh
|
||||||
docker tag registry.f.novalon.cn/novalon-website:latest registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} 2>/dev/null || echo "No existing image to backup"
|
|
||||||
|
|
||||||
echo "=== Step 3: Pull new image ==="
|
|
||||||
if ! docker-compose pull novalon-website; then
|
|
||||||
echo "❌ Image pull failed!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Step 4: Rolling update ==="
|
|
||||||
docker-compose up -d --no-deps novalon-website
|
|
||||||
|
|
||||||
echo "=== Step 5: Wait for service startup ==="
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
echo "=== Step 6: Database migration ==="
|
|
||||||
if ! docker-compose exec -T novalon-website npm run db:migrate; then
|
|
||||||
echo "❌ Database migration failed, rolling back..."
|
|
||||||
docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true
|
|
||||||
docker-compose pull novalon-website
|
|
||||||
docker-compose up -d --no-deps novalon-website
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "=== Step 7: Health check ==="
|
|
||||||
for i in {1..30}; do
|
|
||||||
if wget -q --spider https://novalon.cn/api/health; then
|
|
||||||
echo "✅ Health check passed!"
|
|
||||||
|
|
||||||
echo "=== Step 8: Cleanup old images ==="
|
|
||||||
docker image prune -f
|
|
||||||
docker images registry.f.novalon.cn/novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print \$1}' | xargs -r docker rmi -f || true
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Waiting for service to be ready... (\$i/30)"
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "❌ Health check failed, rolling back..."
|
|
||||||
docker tag registry.f.novalon.cn/novalon-website:backup-\${BACKUP_TIME} registry.f.novalon.cn/novalon-website:latest 2>/dev/null || true
|
|
||||||
docker-compose pull novalon-website
|
|
||||||
docker-compose up -d --no-deps novalon-website
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# 验证回滚
|
|
||||||
if wget -q --spider https://novalon.cn/api/health; then
|
|
||||||
echo "✅ Rollback succeeded, but deployment failed"
|
|
||||||
else
|
|
||||||
echo "❌ Rollback also failed!"
|
|
||||||
fi
|
|
||||||
exit 1
|
|
||||||
EOF
|
EOF
|
||||||
- echo "✅ Production deployment completed!"
|
- echo "✅ Production deployment completed!"
|
||||||
when:
|
when:
|
||||||
@@ -332,7 +277,6 @@ steps:
|
|||||||
git config --global user.name "Woodpecker CI"
|
git config --global user.name "Woodpecker CI"
|
||||||
|
|
||||||
git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git
|
git remote set-url origin git@git.f.novalon.cn:novalon/novalon-website.git
|
||||||
|
|
||||||
git fetch origin
|
git fetch origin
|
||||||
|
|
||||||
CURRENT_BRANCH="${CI_COMMIT_BRANCH}"
|
CURRENT_BRANCH="${CI_COMMIT_BRANCH}"
|
||||||
@@ -340,7 +284,6 @@ steps:
|
|||||||
|
|
||||||
git checkout main
|
git checkout main
|
||||||
git pull origin main
|
git pull origin main
|
||||||
|
|
||||||
git merge "$CURRENT_BRANCH" --no-ff -m "chore: 归档${CURRENT_BRANCH} ${CI_COMMIT_SHA:0:7}"
|
git merge "$CURRENT_BRANCH" --no-ff -m "chore: 归档${CURRENT_BRANCH} ${CI_COMMIT_SHA:0:7}"
|
||||||
|
|
||||||
VERSION_TAG="v$(date +%Y.%m.%d)-${CI_COMMIT_SHA:0:7}"
|
VERSION_TAG="v$(date +%Y.%m.%d)-${CI_COMMIT_SHA:0:7}"
|
||||||
@@ -356,7 +299,6 @@ steps:
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "⚠️ Archive failed, but deployment succeeded"
|
echo "⚠️ Archive failed, but deployment succeeded"
|
||||||
echo "Manual archive may be needed"
|
|
||||||
exit 0
|
exit 0
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
@@ -406,16 +348,10 @@ steps:
|
|||||||
status:
|
status:
|
||||||
- failure
|
- failure
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 工作区配置
|
|
||||||
# ============================================
|
|
||||||
workspace:
|
workspace:
|
||||||
base: /woodpecker
|
base: /woodpecker
|
||||||
path: src
|
path: src
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# 克隆配置
|
|
||||||
# ============================================
|
|
||||||
clone:
|
clone:
|
||||||
git:
|
git:
|
||||||
image: woodpeckerci/plugin-git
|
image: woodpeckerci/plugin-git
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs && \
|
||||||
|
adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY dist/standalone ./
|
||||||
|
COPY dist/static ./dist/static
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
RUN chown -R nextjs:nodejs /app
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
novalon-website:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
image: novalon-website:latest
|
||||||
|
container_name: novalon-website
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
env_file:
|
||||||
|
- .env.production
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- novalon-network
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
novalon-network:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
DEPLOY_DIR="/home/novalon/docker-app/novalon-website"
|
||||||
|
BACKUP_DIR="/home/novalon/backups/novalon-website"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Novalon Website 部署脚本"
|
||||||
|
echo "时间: $(date)"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
cd $DEPLOY_DIR
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 1: 备份当前版本 ==="
|
||||||
|
mkdir -p $BACKUP_DIR
|
||||||
|
if [ -d "dist" ]; then
|
||||||
|
tar -czf $BACKUP_DIR/dist_$TIMESTAMP.tar.gz dist public package.json package-lock.json 2>/dev/null || echo "备份完成(部分文件可能不存在)"
|
||||||
|
echo "✅ 备份已保存到: $BACKUP_DIR/dist_$TIMESTAMP.tar.gz"
|
||||||
|
else
|
||||||
|
echo "⚠️ 没有找到dist目录,跳过备份"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 2: 构建Docker镜像 ==="
|
||||||
|
docker build -t novalon-website:$TIMESTAMP -t novalon-website:latest -f Dockerfile.prod .
|
||||||
|
echo "✅ 镜像构建完成: novalon-website:$TIMESTAMP"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 3: 停止旧容器 ==="
|
||||||
|
if docker ps -a | grep -q novalon-website; then
|
||||||
|
docker-compose down
|
||||||
|
echo "✅ 旧容器已停止"
|
||||||
|
else
|
||||||
|
echo "⚠️ 没有找到旧容器"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 4: 启动新容器 ==="
|
||||||
|
docker-compose up -d
|
||||||
|
echo "✅ 新容器已启动"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 5: 等待服务启动 ==="
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 6: 健康检查 ==="
|
||||||
|
for i in {1..30}; do
|
||||||
|
if wget -q --spider http://localhost:3000/api/health 2>/dev/null; then
|
||||||
|
echo "✅ 健康检查通过!"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Step 7: 清理旧镜像 ==="
|
||||||
|
docker image prune -f
|
||||||
|
docker images novalon-website --format "{{.ID}} {{.CreatedAt}}" | tail -n +4 | awk '{print $1}' | xargs -r docker rmi -f 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "✅ 部署成功!"
|
||||||
|
echo "版本: $TIMESTAMP"
|
||||||
|
echo "时间: $(date)"
|
||||||
|
echo "=========================================="
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "等待服务就绪... ($i/30)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "❌ 健康检查失败,开始回滚..."
|
||||||
|
if [ -f "$BACKUP_DIR/dist_$TIMESTAMP.tar.gz" ]; then
|
||||||
|
tar -xzf $BACKUP_DIR/dist_$TIMESTAMP.tar.gz -C $DEPLOY_DIR
|
||||||
|
docker-compose down
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 10
|
||||||
|
if wget -q --spider http://localhost:3000/api/health 2>/dev/null; then
|
||||||
|
echo "✅ 回滚成功"
|
||||||
|
else
|
||||||
|
echo "❌ 回滚也失败了!"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ 没有找到备份文件,无法回滚"
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user