diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..da228fc --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# ============================================ +# 生产环境 docker-compose 环境变量示例 +# 复制此文件为 .env,填入实际值 +# ============================================ + +# 后端镜像地址(阿里云容器镜像服务) +BACKEND_IMAGE=registry.cn-hangzhou.aliyuncs.com/your-namespace/idc-backend:latest + +# 前端镜像地址(阿里云容器镜像服务) +FRONTEND_IMAGE=registry.cn-hangzhou.aliyuncs.com/your-namespace/idc-frontend:latest + +# 指定特定版本(可选,默认 latest) +# BACKEND_VERSION=v1.0.0 +# FRONTEND_VERSION=v1.0.0 + +# ---- MySQL 容器配置(如启用本地 MySQL) ---- +# MYSQL_ROOT_PASSWORD=your_root_password +# MYSQL_DATABASE=idc_management +# MYSQL_USER=idc_user +# MYSQL_PASSWORD=your_mysql_password diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..490eb26 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,122 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - master + tags: + - 'v*' + pull_request: + branches: + - master + workflow_dispatch: + +env: + REGISTRY: registry.cn-hangzhou.aliyuncs.com + NAMESPACE: ${{ secrets.ALIYUN_NAMESPACE }} + BACKEND_IMAGE: idc-backend + FRONTEND_IMAGE: idc-frontend + +jobs: + build-backend: + name: Build Backend Image + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 登录阿里云 Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: 提取版本号 + id: meta + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + VERSION=${GITHUB_REF#refs/tags/} + else + VERSION=latest + fi + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "COMMIT_SHA=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: 设置 Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: 构建并推送后端镜像 + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.BACKEND_IMAGE }}:${{ steps.meta.outputs.VERSION }} + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.BACKEND_IMAGE }}:${{ steps.meta.outputs.COMMIT_SHA }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-frontend: + name: Build Frontend Image + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + + steps: + - name: 检出代码 + uses: actions/checkout@v4 + + - name: 登录阿里云 Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.ALIYUN_USERNAME }} + password: ${{ secrets.ALIYUN_PASSWORD }} + + - name: 提取版本号 + id: meta + run: | + if [[ "${{ github.ref_type }}" == "tag" ]]; then + VERSION=${GITHUB_REF#refs/tags/} + else + VERSION=latest + fi + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + echo "COMMIT_SHA=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: 设置 Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: 构建并推送前端镜像 + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.FRONTEND_IMAGE }}:${{ steps.meta.outputs.VERSION }} + ${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.FRONTEND_IMAGE }}:${{ steps.meta.outputs.COMMIT_SHA }} + cache-from: type=gha + cache-to: type=gha,mode=max + + notify: + name: Notify Build Complete + runs-on: ubuntu-latest + needs: [build-backend, build-frontend] + if: always() && github.event_name != 'pull_request' + + steps: + - name: 输出构建结果 + run: | + echo "=== 构建结果 ===" + echo "后端镜像: ${{ needs.build-backend.result }}" + echo "前端镜像: ${{ needs.build-frontend.result }}" + if [[ "${{ needs.build-backend.result }}" == "success" && "${{ needs.build-frontend.result }}" == "success" ]]; then + echo "所有镜像构建成功" + else + echo "部分镜像构建失败" + exit 1 + fi diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..21a2447 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,41 @@ +# ============================================ +# 后端 Docker 构建忽略文件 +# ============================================ + +# 依赖 +node_modules + +# 环境配置(运行时挂载) +.env + +# 运行时数据(运行时挂载) +uploads +logs +backups +temp + +# 数据库文件 +*.db +*.sqlite + +# 版本控制 +.git +.gitignore + +# 测试 +tests +coverage +*.test.js + +# IDE +.idea +.vscode +*.swp +*.swo + +# 系统 +.DS_Store +Thumbs.db + +# npm +npm-debug.log* diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d561b6a --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,34 @@ +# ============================================ +# IDC 设备管理系统 - 后端 Dockerfile +# 基于 Node.js 20 Alpine 构建 +# ============================================ + +FROM node:20-alpine + +# 设置工作目录 +WORKDIR /app + +# 安装系统依赖(如需编译原生模块) +RUN apk add --no-cache tzdata + +# 设置时区 +ENV TZ=Asia/Shanghai + +# 复制依赖配置文件 +COPY package.json ./ +COPY package-lock.json* yarn.lock* ./ + +# 安装生产依赖(不安装 devDependencies) +RUN npm install --omit=dev --ignore-scripts + +# 复制后端代码 +COPY . . + +# 确保日志和上传目录存在 +RUN mkdir -p logs uploads temp backups + +# 暴露端口 +EXPOSE 8000 + +# 启动命令 +CMD ["node", "server.js"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..9e90ed0 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,81 @@ +# ============================================ +# IDC 设备管理系统 - 开发环境配置 +# 使用 build 指令本地构建镜像 +# ============================================ + +version: '3.8' + +services: + # ---------- 后端服务 ---------- + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: idc-backend-dev + volumes: + # 配置映射(核心) + - ./backend/.env:/app/.env + # 数据持久化 + - ./backend/uploads:/app/uploads + - ./backend/logs:/app/logs + - ./backend/backups:/app/backups + - ./backend/temp:/app/temp + env_file: + - ./backend/.env + environment: + - NODE_ENV=development + # 连接 docker-compose 内的 MySQL 时,注释掉下方 MYSQL_HOST 的注释 + # 连接远程 MySQL 时,保持 .env 中的配置即可 + networks: + - idc-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # ---------- 前端(Nginx)服务 ---------- + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + container_name: idc-frontend-dev + ports: + - "80:80" + networks: + - idc-network + depends_on: + - backend + restart: unless-stopped + + # ---------- MySQL 数据库 ---------- + # 如需使用本地 MySQL 容器,取消下方注释 + # mysql: + # image: mysql:8.0 + # container_name: idc-mysql-dev + # volumes: + # - mysql_data:/var/lib/mysql + # environment: + # MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123} + # MYSQL_DATABASE: idc_management + # MYSQL_USER: idc_user + # MYSQL_PASSWORD: ${MYSQL_PASSWORD:-idc_pass} + # ports: + # - "3306:3306" + # networks: + # - idc-network + # restart: unless-stopped + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 5 + +networks: + idc-network: + driver: bridge + +# volumes: +# mysql_data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..fcc433b --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,77 @@ +# ============================================ +# IDC 设备管理系统 - 生产环境配置 +# 从镜像仓库拉取预构建镜像,无 build 指令 +# ============================================ + +version: '3.8' + +services: + # ---------- 后端服务 ---------- + backend: + # 从镜像仓库拉取,需替换为实际的镜像名 + image: ${BACKEND_IMAGE:-your-registry/idc-backend:latest} + container_name: idc-backend + volumes: + # 配置映射(核心) + - ./backend/.env:/app/.env + # 数据持久化 + - ./backend/uploads:/app/uploads + - ./backend/logs:/app/logs + - ./backend/backups:/app/backups + - ./backend/temp:/app/temp + env_file: + - ./backend/.env + environment: + - NODE_ENV=production + # 生产环境使用 Docker 内 MySQL 时,MYSQL_HOST 需设为 mysql + # 使用远程 MySQL 时,保持 .env 中的配置 + networks: + - idc-network + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + # ---------- 前端(Nginx)服务 ---------- + frontend: + image: ${FRONTEND_IMAGE:-your-registry/idc-frontend:latest} + pull_policy: always + container_name: idc-frontend + ports: + - "80:80" + networks: + - idc-network + depends_on: + - backend + restart: unless-stopped + + # ---------- MySQL 数据库 ---------- + # 如需使用本地 MySQL 容器,取消下方注释 + # mysql: + # image: mysql:8.0 + # container_name: idc-mysql + # volumes: + # - mysql_data:/var/lib/mysql + # environment: + # MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + # MYSQL_DATABASE: ${MYSQL_DATABASE:-idc_management} + # MYSQL_USER: ${MYSQL_USER:-idc_user} + # MYSQL_PASSWORD: ${MYSQL_PASSWORD} + # networks: + # - idc-network + # restart: unless-stopped + # healthcheck: + # test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + # interval: 10s + # timeout: 5s + # retries: 5 + +networks: + idc-network: + driver: bridge + +# volumes: +# mysql_data: diff --git a/docs/docker-deployment-plan.md b/docs/docker-deployment-plan.md new file mode 100644 index 0000000..8017d2d --- /dev/null +++ b/docs/docker-deployment-plan.md @@ -0,0 +1,182 @@ +# Docker 化部署方案 + +> 讨论时间: 2026-06-12 + +## 一、架构设计 + +### 3 个容器 + +| 容器 | 角色 | 说明 | +|------|------|------| +| **Nginx(含前端)** | Web 服务器 + 前端静态文件 | serve 前端构建产物,反向代理 `/api` 到后端 | +| **Backend** | Node.js 后端服务 | Express API 服务 | +| **MySQL** | 数据库 | MySQL 8.0 | + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Nginx 容器 │ │ Backend 容器 │ │ MySQL 容器 │ +│ │ │ │ │ │ +│ / ──────▶ │ │ Node.js │ │ 数据库 │ +│ 前端静态文件 │ │ :8000 │ │ :3306 │ +│ │────▶│ │────▶│ │ +│ /api ────▶ │ │ │ │ │ +│ 反向代理 │ │ │ │ │ +│ :80 │ │ │ │ │ +└──────────────┘ └──────────────┘ └──────────────┘ +``` + +> 注意:前端不单独占一个容器。前端代码构建成静态文件后,直接打包进 Nginx 镜像中。 + +--- + +## 二、主机需要映射的目录 + +| 路径 | 说明 | 必要性 | +|------|------|--------| +| `backend/.env` | 核心配置(数据库、JWT、日志等) | **必选** | +| `backend/uploads/` | 用户上传文件持久化 | **必选** | +| `backend/logs/` | 运行日志持久化 | **必选** | +| `backend/backups/` | 数据库备份文件持久化 | **必选** | +| `backend/temp/` | 临时文件 | 可选 | +| `mysql_data/` | MySQL 数据库文件持久化(Docker 数据卷) | **必选** | + +--- + +## 三、构建与部署方案 + +### 3.1 CI/CD 自动化流程 + +``` +开发 → git push + │ + ▼ + GitHub Actions + │ + ├── 构建 idc-backend:latest 镜像 + ├── 构建 idc-frontend:latest 镜像(Nginx + 前端静态文件) + ├── 推送到镜像仓库(Docker Hub / 阿里云 / GitHub Container Registry) + │ + ▼ + 生产服务器 + docker compose pull # 拉取最新镜像 + docker compose up -d # 重启容器 +``` + +### 3.2 环境区分 + +| 环境 | docker-compose 文件 | 镜像来源 | +|------|--------------------|----------| +| 开发环境 | `docker-compose.dev.yml`(含 `build:` 指令) | 本地构建 | +| 生产环境 | `docker-compose.prod.yml`(用 `image:` 指令) | 从镜像仓库拉取 | + +生产环境的 `docker-compose.yml` 不写 `build:`,直接拉取远程镜像: + +```yaml +services: + backend: + image: yourname/idc-backend:latest + # 没有 build 指令,直接从仓库拉取 + + frontend: + image: yourname/idc-frontend:latest + # 同上,服务器不需要装 Node.js + + mysql: + image: mysql:8.0 +``` + +### 版本回滚 + +构建时打版本标签,支持快速回滚: + +```bash +# 部署新版本 +docker compose pull && docker compose up -d + +# 回滚到旧版本 +BACKEND_VERSION=v1.2.0 FRONTEND_VERSION=v1.2.0 docker compose up -d +``` + +--- + +## 四、后端关键配置变化 + +| 配置项 | 开发环境(远程 MySQL) | Docker 生产环境 | +|--------|----------------------|----------------| +| `MYSQL_HOST` | `1p.debian.zhaoyi520.tk` | `mysql`(Docker 服务名) | +| `MYSQL_PORT` | `33060` | `3306` | +| `JWT_SECRET` | 自动生成 | 部署前必须手动配置 | + +### 注意点 + +1. **JWT_SECRET**:生产环境必须在 `.env` 中预先配置好,因为容器内无法写回只读挂载的 `.env` +2. **数据库初始化**:MySQL 容器首次启动时,后端 `sequelize.sync()` 会自动建表,无需手动初始化 +3. **Nginx 反向代理**:Nginx 容器内配置 `proxy_pass http://backend:8000`,用 Docker 服务名通信 + +### 已创建的文件(2026-06-12) + +| 文件 | 说明 | +|------|------| +| `backend/Dockerfile` | 后端镜像构建(Node.js 20 Alpine) | +| `backend/.dockerignore` | 后端构建忽略配置 | +| `frontend/Dockerfile` | 前端镜像构建(多阶段构建:先 npm build,再 Nginx) | +| `frontend/nginx.conf` | Nginx 配置(serve 静态文件 + 反向代理 /api) | +| `frontend/.dockerignore` | 前端构建忽略配置 | +| `docker-compose.dev.yml` | 开发环境 Compose(含 `build:` 指令,本地构建) | +| `docker-compose.prod.yml` | 生产环境 Compose(用 `image:`,从仓库拉取) | + +### 待实现 + +- ~~`.github/workflows/deploy.yml` — CI/CD 自动构建推送~~ ✅ 已完成 + +## 五、CI/CD 配置 + +### 5.1 镜像仓库:阿里云容器镜像服务 + +阿里云控制台开通容器镜像服务后: +1. 创建命名空间(如 `idc-assest`) +2. 创建镜像仓库:`idc-backend`、`idc-frontend` +3. 设置访问凭证(用户名 + 密码) + +### 5.2 GitHub Secrets 配置 + +在 GitHub 仓库 `Settings → Secrets and variables → Actions` 中配置: + +| Secret 名称 | 说明 | +|------------|------| +| `ALIYUN_USERNAME` | 阿里云 Docker Registry 用户名 | +| `ALIYUN_PASSWORD` | 阿里云 Docker Registry 密码 | +| `ALIYUN_NAMESPACE` | 阿里云命名空间 | + +### 5.3 触发条件 + +- 推送到 `master` 分支 → 构建 `latest` 镜像 +- 创建 `v*` 标签(如 `v1.0.0`)→ 构建对应版本镜像 +- 手动触发(Actions 页面 `Run workflow`) + +### 5.4 镜像标签策略 + +每次构建会同时打两个标签: +- `latest`(或 tag 名称):最新版本 +- ``:精确回溯到某次提交 + +### 5.5 部署流程 + +``` +开发者:git push origin master + ↓ + GitHub Actions 自动构建 + ↓ + 推送镜像到阿里云仓库 + ↓ + 管理员 SSH 到生产服务器 + ↓ + cd /opt/idc_assest + docker compose -f docker-compose.prod.yml pull + docker compose -f docker-compose.prod.yml up -d +``` + +### 5.6 已创建文件 + +- `.github/workflows/docker-build.yml` — CI/CD 工作流定义 +- `.env.example` — 生产环境 docker-compose 环境变量示例 diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..4f83c35 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,30 @@ +# ============================================ +# 前端 Docker 构建忽略文件 +# ============================================ + +# 依赖 +node_modules + +# 构建产物(由构建阶段生成) +dist + +# 本地环境 +.env +.frontend-port + +# 版本控制 +.git +.gitignore + +# IDE +.idea +.vscode +*.swp +*.swo + +# 系统 +.DS_Store +Thumbs.db + +# npm +npm-debug.log* diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..42e62f1 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,41 @@ +# ============================================ +# IDC 设备管理系统 - 前端(Nginx)Dockerfile +# 第一阶段:构建前端静态文件 +# 第二阶段:Nginx 运行 +# ============================================ + +# ---------- 第一阶段:构建 ---------- +FROM node:20-alpine AS build + +WORKDIR /app + +# 复制依赖配置 +COPY package.json package-lock.json* yarn.lock* ./ + +# 安装所有依赖(含 devDependencies,vite build 需要) +RUN npm install --ignore-scripts + +# 复制前端源码 +COPY . . + +# 构建生产版本 +RUN npm run build + +# ---------- 第二阶段:Nginx 运行 ---------- +FROM nginx:alpine + +# 设置时区 +RUN apk add --no-cache tzdata +ENV TZ=Asia/Shanghai + +# 复制 Nginx 配置 +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# 复制前端构建产物到 Nginx 静态目录 +COPY --from=build /app/dist /usr/share/nginx/html + +# 暴露端口 +EXPOSE 80 + +# Nginx 前台运行 +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..96d2726 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,77 @@ +# ============================================ +# IDC 设备管理系统 - Nginx 配置 +# 功能:serve 前端静态文件 + 反向代理 API +# ============================================ + +server { + listen 80; + server_name localhost; + + # 前端静态文件 + root /usr/share/nginx/html; + index index.html; + + # Gzip 压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml; + gzip_min_length 1024; + gzip_vary on; + + # 前端路由(SPA 支持) + location / { + try_files $uri $uri/ /index.html; + } + + # API 反向代理到后端 + location /api/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + 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_cache_bypass $http_upgrade; + + # 超时设置 + proxy_read_timeout 120s; + proxy_send_timeout 120s; + } + + # 上传文件访问 + location /uploads/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # 健康检查端点 + location /health { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + # API 文档 + location /api-docs { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + } + + # 禁止访问隐藏文件 + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # 静态资源缓存策略 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf|eot)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } +}