CI/CD (持续集成/持续部署) GitHub Actions 自动构建
好的,这是一个相当全面的 CI/CD (持续集成/持续部署) 流程,涉及到 GitHub Actions 自动构建 Docker 镜像并将其部署到你的阿里云 ECS (Elastic Compute Service) 服务器上。下面是详细的步骤:
这个方案假设:
- 你已经在阿里云上有一台 ECS 服务器,并且可以 SSH 访问。
- 你的 Spring Boot 后端和 React 前端分别是两个独立的 GitHub 公共仓库。
- 你希望每次向这两个仓库的特定分支(例如
main
)推送代码时,都能自动更新部署在 ECS 上的应用。
我们将使用 Alibaba Cloud Container Registry (ACR) 来存储我们构建的 Docker 镜像,这是一个推荐的做法,比直接在 ECS 上构建镜像更规范和高效。
阶段一:阿里云 ECS 服务器准备
- 安装 Docker 和 Docker Compose:
- 通过 SSH 连接到你的阿里云 ECS。
- 安装 Docker:
sudo apt-get update sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io sudo systemctl start docker sudo systemctl enable docker * 安装 Docker Compose:
bash sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose docker-compose --version # 验证安装 * (可选但推荐) 将当前用户添加到 `docker` 组,以避免每次都使用 `sudo`:
bash sudo usermod -aG docker $USER newgrp docker # 或者重新登录使组生效 ```
-
配置安全组:
- 登录阿里云控制台,找到你的 ECS 实例。
- 配置其安全组规则:
- 添加入方向规则以允许 HTTP (端口 80) 和 HTTPS (端口 443) 流量。
- 确保 SSH (端口 22) 对你的 IP 地址或 GitHub Actions 的 IP 范围开放 (如果需要更严格的限制,但通常使用 SSH Key 更安全)。
- MySQL 端口 (3306) 不建议对公网开放。它将在 Docker 内部网络中被后端访问。
-
创建部署目录和准备
db.sql
:- 在 ECS 上创建一个用于存放
docker-compose.yml
和数据库初始化脚本的目录: Bashmkdir -p ~/my-fullstack-app/db_init cd ~/my-fullstack-app
- 重要: 将你后端项目中的
db.sql
文件上传到 ECS 的~/my-fullstack-app/db_init/
目录下。你可以使用scp
命令: Bash# 从你的本地机器执行 scp path/to/your/local/db.sql your_ecs_user@your_ecs_ip:~/my-fullstack-app/db_init/db.sql
- 在 ECS 上创建一个用于存放
-
准备
YAMLdocker-compose.yml
文件在 ECS 上: 在 ECS 的~/my-fullstack-app/
目录下创建一个docker-compose.yml
文件。这个文件将引用我们稍后推送到 ACR 的镜像。# ~/my-fullstack-app/docker-compose.yml on your ECS server version: '3.8'services:backend:# 镜像名称将从 ACR 拉取,例如: registry.cn-hangzhou.aliyuncs.com/your-acr-namespace/springboot-backend:latestimage: ACR_REGISTRY_URL/YOUR_ACR_NAMESPACE/springboot-backend:latest # <--- 稍后替换container_name: my-springboot-apprestart: unless-stoppedports:- "8080:8080"environment:- SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydatabase?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC- SPRING_DATASOURCE_USERNAME=myuser- SPRING_DATASOURCE_PASSWORD=mypassword- SPRING_JPA_HIBERNATE_DDL_AUTO=update # 或者根据你的 db.sql 决定depends_on:db:condition: service_healthynetworks:- my-app-networkfrontend:# 镜像名称将从 ACR 拉取,例如: registry.cn-hangzhou.aliyuncs.com/your-acr-namespace/react-frontend:latestimage: ACR_REGISTRY_URL/YOUR_ACR_NAMESPACE/react-frontend:latest # <--- 稍后替换container_name: my-react-apprestart: unless-stoppedports:- "80:80"depends_on:- backendnetworks:- my-app-networkdb:image: mysql:8.0container_name: my-mysql-dbrestart: unless-stoppedenvironment:MYSQL_DATABASE: mydatabaseMYSQL_USER: myuserMYSQL_PASSWORD: mypasswordMYSQL_ROOT_PASSWORD: rootpassword # 请使用强密码volumes:- mysql_data_ecs:/var/lib/mysql- ./db_init/db.sql:/docker-entrypoint-initdb.d/init.sql # 从 ECS 本地挂载# 不建议将 3306 映射到公网,除非你有特定的安全措施# ports:# - "3306:3306"healthcheck:test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-u$$MYSQL_USER", "-p$$MYSQL_PASSWORD"]interval: 10stimeout: 5sretries: 5start_period: 30snetworks:- my-app-networknetworks:my-app-network:driver: bridgevolumes:mysql_data_ecs: # Docker volume on ECS for MySQL data persistence
注意:
ACR_REGISTRY_URL/YOUR_ACR_NAMESPACE/springboot-backend:latest
和ACR_REGISTRY_URL/YOUR_ACR_NAMESPACE/react-frontend:latest
是占位符。你需要在创建 ACR 仓库后替换它们。- 数据库密码等敏感信息,更安全的做法是使用 Docker secrets 或 ECS 参数存储,但这里为了简化,直接写在环境变量中。
阶段二:阿里云容器镜像服务 (ACR) 设置
- 开通 ACR 服务: 如果你还没有开通,请在阿里云控制台开通“容器镜像服务 ACR”。选择个人版(免费)或企业版。
- 创建命名空间 (Namespace): 命名空间用于组织你的镜像。例如,你可以创建一个叫
my-apps
或你公司/项目名的命名空间。 - 创建镜像仓库 (Repository):
- 在你的命名空间下,分别为后端和前端创建两个镜像仓库。
- 例如:
springboot-backend
- 例如:
react-frontend
- 例如:
- 记下你的 ACR Registry URL (例如
registry.cn-hangzhou.aliyuncs.com
) 和完整的仓库名称 (例如registry.cn-hangzhou.aliyuncs.com/your-namespace/springboot-backend
)。
- 在你的命名空间下,分别为后端和前端创建两个镜像仓库。
- 获取 ACR 访问凭证:
- 在 ACR 控制台的“访问凭证”部分,设置固定密码或创建临时令牌。GitHub Actions 将使用这些凭证来推送镜像。
阶段三:GitHub Secrets 配置
在你的每一个 GitHub 仓库(后端和前端)中,都需要配置以下 Secrets。进入仓库的 Settings
-> Secrets and variables
-> Actions
-> New repository secret
。
ACR_REGISTRY_URL
: 你的 ACR 实例的访问域名 (例如registry.cn-hangzhou.aliyuncs.com
)。ACR_USERNAME
: 你的 ACR 登录用户名。ACR_PASSWORD
: 你的 ACR 登录密码。ECS_HOST
: 你阿里云 ECS 的公网 IP 地址。ECS_USER
: 你用于 SSH 登录 ECS 的用户名 (例如root
或你创建的部署用户)。ECS_SSH_PRIVATE_KEY
: 用于 SSH 登录 ECS 的私钥。- 生成 SSH 密钥对: 如果没有,可以在本地用
ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_ecs
生成。 - 将公钥 (
~/.ssh/id_rsa_ecs.pub
的内容) 添加到 ECS 服务器上对应用户的~/.ssh/authorized_keys
文件中。 - 将私钥 (
~/.ssh/id_rsa_ecs
的内容) 复制到 GitHub SecretECS_SSH_PRIVATE_KEY
中。确保复制完整内容,包括-----BEGIN RSA PRIVATE KEY-----
和-----END RSA PRIVATE KEY-----
。
- 生成 SSH 密钥对: 如果没有,可以在本地用
DOCKER_COMPOSE_PATH
: ECS 上docker-compose.yml
文件所在的绝对路径 (例如/home/your_ecs_user/my-fullstack-app
)。
阶段四:Dockerfile 修改 (不再从 Dockerfile 内部 clone)
GitHub Actions 会先 checkout 代码,所以 Dockerfile 不需要再 git clone
。它们应该假设源代码已经在构建上下文中。
1. 后端 Spring Boot ( backend/Dockerfile
- 在你的后端仓库中)
Dockerfile
# 使用一个包含 JDK 的基础镜像
FROM openjdk:17-jdk-slim# 设置工作目录 (源代码将被 GitHub Actions checkout 到这里)
WORKDIR /app# 复制 Maven Wrapper 相关文件
COPY .mvn/ .mvn
COPY mvnw .
COPY pom.xml .# 下载依赖 (利用 Docker 缓存机制)
RUN ./mvnw dependency:go-offline -B# 复制源代码
COPY src ./src# 打包应用
RUN ./mvnw package -DskipTests# ---- 运行阶段 ----
# 使用一个更小的 JRE 镜像来运行应用
FROM openjdk:17-jre-slimWORKDIR /app# 从构建阶段复制 JAR 文件
COPY --from=0 /app/target/*.jar app.jar # 注意 --from=0 (指向第一个 FROM 指令)# 暴露 Spring Boot 应用的端口
EXPOSE 8080# 运行应用
ENTRYPOINT ["java", "-jar", "app.jar"]
2. 前端 React ( frontend/Dockerfile
- 在你的前端仓库中)
Dockerfile
# ---- 构建阶段 ----
FROM node:18-alpineWORKDIR /app# 复制 package.json 和 package-lock.json (或 yarn.lock)
COPY package*.json ./# 安装依赖
RUN npm install
# 如果使用 yarn:
# COPY yarn.lock ./
# RUN yarn install# 复制所有项目文件
COPY . .# 构建应用
RUN npm run build
# 如果使用 yarn:
# RUN yarn build# ---- 运行阶段 ----
FROM nginx:stable-alpine# 复制 Nginx 配置文件 (这个文件也应该在你的前端仓库中,例如 frontend/nginx.conf)
COPY nginx.conf /etc/nginx/conf.d/default.conf# 从构建阶段复制构建好的静态文件到 Nginx 的 www 目录
COPY --from=0 /app/dist /usr/share/nginx/html # 注意 --from=0# 暴露 Nginx 端口
EXPOSE 80# 启动 Nginx
CMD ["nginx", "-g", "daemon off;"]
确保 frontend/nginx.conf
文件也在你的前端仓库中 (与 Dockerfile
同级或在可被 COPY
的路径)。内容与之前提供的 nginx.conf
相同。
阶段五:GitHub Actions Workflow 文件
在你的后端 GitHub 仓库的 .github/workflows/
目录下创建一个 deploy-backend.yml
文件:
backend/.github/workflows/deploy-backend.yml
YAML
name: Deploy Backend to Alibaba Cloud ECSon:push:branches:- main # 或者你的主部署分支,例如 masterjobs:build-and-deploy-backend:runs-on: ubuntu-lateststeps:- name: Checkout codeuses: actions/checkout@v3- name: Set up JDK 17uses: actions/setup-java@v3with:java-version: '17'distribution: 'temurin' # 或其他你偏好的 JDK 发行版- name: Login to Alibaba Cloud Container Registryuses: docker/login-action@v2with:registry: ${{ secrets.ACR_REGISTRY_URL }}username: ${{ secrets.ACR_USERNAME }}password: ${{ secrets.ACR_PASSWORD }}- name: Build and push Docker image for Backenduses: docker/build-push-action@v4with:context: ./backend # 确保这是你后端 Dockerfile 所在的目录相对于仓库根目录file: ./backend/Dockerfile # Dockerfile 的路径push: truetags: |${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/springboot-backend:${{ github.sha }}${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/springboot-backend:latest# YOUR_ACR_NAMESPACE 替换为你的ACR命名空间- name: Deploy to ECS via SSHuses: appleboy/ssh-action@masterwith:host: ${{ secrets.ECS_HOST }}username: ${{ secrets.ECS_USER }}key: ${{ secrets.ECS_SSH_PRIVATE_KEY }}script: |cd ${{ secrets.DOCKER_COMPOSE_PATH }}echo "Pulling latest backend image..."docker pull ${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/springboot-backend:latestecho "Updating backend service..."# 更新 docker-compose.yml 中的 backend image tag (如果需要动态指定 github.sha)# sed -i "s|image: ${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/springboot-backend:.*|image: ${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/springboot-backend:${{ github.sha }}|g" docker-compose.yml# 或者直接使用 latest 标签,如当前配置docker-compose -f docker-compose.yml up -d --no-deps backendecho "Cleaning up old Docker images..."docker image prune -af
在你的前端 GitHub 仓库的 .github/workflows/
目录下创建一个 deploy-frontend.yml
文件:
frontend/.github/workflows/deploy-frontend.yml
YAML
name: Deploy Frontend to Alibaba Cloud ECSon:push:branches:- main # 或者你的主部署分支jobs:build-and-deploy-frontend:runs-on: ubuntu-lateststeps:- name: Checkout codeuses: actions/checkout@v3- name: Set up Node.jsuses: actions/setup-node@v3with:node-version: '18' # 与你 Dockerfile 中一致- name: Login to Alibaba Cloud Container Registryuses: docker/login-action@v2with:registry: ${{ secrets.ACR_REGISTRY_URL }}username: ${{ secrets.ACR_USERNAME }}password: ${{ secrets.ACR_PASSWORD }}- name: Build and push Docker image for Frontenduses: docker/build-push-action@v4with:context: ./frontend # 确保这是你前端 Dockerfile 和 nginx.conf 所在的目录file: ./frontend/Dockerfilepush: truetags: |${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/react-frontend:${{ github.sha }}${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/react-frontend:latest# YOUR_ACR_NAMESPACE 替换为你的ACR命名空间- name: Deploy to ECS via SSHuses: appleboy/ssh-action@masterwith:host: ${{ secrets.ECS_HOST }}username: ${{ secrets.ECS_USER }}key: ${{ secrets.ECS_SSH_PRIVATE_KEY }}script: |cd ${{ secrets.DOCKER_COMPOSE_PATH }}echo "Pulling latest frontend image..."docker pull ${{ secrets.ACR_REGISTRY_URL }}/YOUR_ACR_NAMESPACE/react-frontend:latestecho "Updating frontend service..."docker-compose -f docker-compose.yml up -d --no-deps frontendecho "Cleaning up old Docker images..."docker image prune -af
重要:
- 在上面两个 YAML 文件中,将
YOUR_ACR_NAMESPACE
替换为你在阿里云 ACR 中创建的实际命名空间。 context
和file
路径在docker/build-push-action
中要指向你仓库中Dockerfile
的正确位置。如果Dockerfile
就在仓库根目录,context
可以是.
,file
可以是./Dockerfile
。我假设你的Dockerfile
在各自项目的子目录 (backend/
和frontend/
) 中。如果你的项目文件和 Dockerfile 都在仓库根目录,那么context: .
和file: ./Dockerfile
。请根据你的仓库结构调整。tags
部分,我们同时推送了latest
标签和一个基于 Git commit SHA 的唯一标签。在 ECS 上的docker-compose.yml
我们目前配置的是拉取latest
。
阶段六:首次部署和后续操作
-
手动触发或首次推送:
- 确保所有 GitHub Secrets 都已正确设置。
- 确保 ECS 上的
~/my-fullstack-app/docker-compose.yml
和~/my-fullstack-app/db_init/db.sql
文件已就绪。 - 将上述
deploy-backend.yml
和deploy-frontend.yml
文件提交到各自仓库的.github/workflows/
目录下,并推送到main
分支。 - 这会触发 GitHub Actions workflow。你可以在 GitHub 仓库的 "Actions" 标签页查看执行过程。
-
首次在 ECS 上运行
docker-compose up -d
:- 在第一个 Action 成功将镜像推送到 ACR 后,你可能需要手动在 ECS 上首次完整启动所有服务,特别是数据库服务,以创建网络和数据卷: Bash
这会拉取# 在 ECS 的 ~/my-fullstack-app/ 目录下 docker-compose up -d
latest
标签的镜像 (如果 Actions 已经推送了),并启动所有服务,包括 MySQL 并执行db.sql
。
- 在第一个 Action 成功将镜像推送到 ACR 后,你可能需要手动在 ECS 上首次完整启动所有服务,特别是数据库服务,以创建网络和数据卷: Bash
-
后续自动部署:
- 之后,每当你向后端或前端仓库的
main
分支推送代码,对应的 GitHub Action 就会自动触发:- 构建新的 Docker 镜像。
- 将新镜像推送到 ACR (覆盖
latest
标签并添加 commit SHA 标签)。 - SSH 到你的 ECS。
- 拉取最新的镜像 (带有
latest
标签)。 - 使用
docker-compose up -d --no-deps <service-name>
平滑地只更新发生变化的服务,而不会中断其他服务 (例如数据库)。
- 之后,每当你向后端或前端仓库的
这是一个相当复杂的流程,但一旦设置好,就能实现非常高效的自动化部署。仔细检查每个步骤中的路径、名称和凭证。如果遇到问题,GitHub Actions 的日志输出会是排查错误的关键。