在编写任何自动化脚本之前,我们必须先建立起安全的信任链,让我们的“自动化管家”(GitHub Actions) 能够合法地访问我们的“云端豪宅”(EC2) 和“中央仓库”(Docker Hub)。
原则: 绝不使用你的 EC2 主密钥进行自动化操作。我们要遵循“权限最小化”原则,为 CI/CD 创建一把专用的钥匙。
在本地生成新密钥对: 打开终端,运行以下命令。这把钥匙没有密码,便于自动化脚本使用。
codeBash
ssh-keygen -t rsa -b 4096 -f github_actions_deploy_key -N ""
安装“新门锁”: 将新生成的公钥 (github_actions_deploy_key.pub) 的内容,追加到你 EC2 服务器的 ~/.ssh/authorized_keys 文件中。
重要: authorized_keys 文件是一个列表,可以存放多个公钥。确保你是另起一行追加,而不是覆盖原有内容。
修复权限: 登录 EC2,运行 chmod 700 ~/.ssh 和 chmod 600 ~/.ssh/authorized_keys 来确保 SSH 服务的安全要求。
登录 Docker Hub,在“Account Settings” -> “Security”中创建一个新的 Access Token。
立即复制并保管好这个令牌,因为它只会显示一次。
将我们刚刚准备好的所有敏感信息,安全地存放到 GitHub 仓库的 "Settings" -> "Secrets and variables" -> "Actions" 中。
Secret 名称存放的值DOCKERHUB_USERNAME你的 Docker Hub 用户名DOCKERHUB_TOKEN上一步生成的 Docker Hub 访问令牌EC2_HOST你 EC2 服务器的公网 IP 地址EC2_USERNAME你登录 EC2 的用户名 (如 ubuntu 或 ec2-user)SSH_PRIVATE_KEY新生成的私钥 github_actions_deploy_key 的全部内容
在我们深入“战争故事”之前,让我们先看一下经历九九八十一难后,最终能完美运行的几个核心配置文件。
这个文件至关重要,它能防止我们将本地的 node_modules 等无关文件打包进镜像,避免冲突和镜像臃肿。
codeCode
# .dockerignore
**/node_modules
**/target
frontend/dist
.idea
.vscode
*.iml
我们选择将前后端打包进一个镜像的“单体部署”策略。这个 Dockerfile 使用多阶段构建,并解决了我们遇到的所有路径和工具问题。
codeDockerfile
# --- 阶段 1: 构建 Angular 前端 ---
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# (关键修复) 先安装 pnpm 工具
RUN npm install -g pnpm
# 复制依赖描述文件以利用缓存
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install
# 复制所有剩余源代码
COPY frontend/ ./
RUN pnpm run build
# --- 阶段 2: 构建 Spring Boot 后端 ---
FROM maven:3.8.5-openjdk-8 AS backend-builder
WORKDIR /app
# 缓存 Maven 依赖
COPY backend/pom.xml .
RUN mvn dependency:go-offline
# 复制后端源代码
COPY backend/src ./src
# (关键修复) 从前端构建产物的 browser 子目录中复制内容
COPY --from=frontend-builder /app/dist/*/browser/* ./src/main/resources/static/
# 打包后端应用,此时前端文件已在 static 目录中
RUN mvn package -DskipTests
# --- 阶段 3: 创建最终的运行镜像 ---
FROM eclipse-temurin:8-jre-alpine
WORKDIR /app
# 使用通配符复制 JAR 包
COPY --from=backend-builder /app/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这份文件只负责协调,将所有构建的脏活累活都交给了 Dockerfile。
codeYaml
name: Build, Push Docker Image, and Deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/airline-order-manage:latest
${{ secrets.DOCKERHUB_USERNAME }}/airline-order-manage:${{ github.sha }}
- name: Deploy to EC2
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ~/airline-cicd
docker-compose pull
docker-compose up -d --remove-orphans
docker image prune -f
这是服务器上的“部署蓝图”,它使用 CI/CD 推送到 Docker Hub 的镜像。
codeYaml
# version: 字段已过时,可以删除
services:
database:
image: mysql:8.0
container_name: airline-mysql-db
restart: always
environment:
# 最佳实践:使用 .env 文件管理这些敏感信息
MYSQL_ROOT_PASSWORD: YourPassword
MYSQL_DATABASE: myappdb
MYSQL_USER: myappuser
MYSQL_PASSWORD: YourPassword
volumes:
- db_data:/var/lib/mysql
networks:
- app-network
app:
# 核心:使用 CI/CD 构建的镜像
image: kroulzhang/airline-order-manage:latest
container_name: my-springboot-angular-app
restart: always
ports:
- "8080:8080"
depends_on:
- database
environment:
- SPRING_DATASOURCE_URL=jdbc:mysql://database:3306/myappdb?useSSL=false&allowPublicKeyRetrieval=true
- SPRING_DATASOURCE_USERNAME=myappuser
- SPRING_DATASOURCE_PASSWORD=YourPassword
networks:
- app-network
volumes:
db_data:
networks:
app-network:
通往自动化的道路从不平坦。以下是我们在这条路上遇到的每一个“坑”以及如何填平它们的真实记录。
问题 #1:npm ci 失败,抱怨缺少 package-lock.json
症状: 前端安装依赖步骤报错。
诊断: 我们发现本地使用 pnpm 开发,生成的是 pnpm-lock.yaml,而 CI 脚本却在调用 npm ci,两者“语言不通”。
解决: 在 CI 脚本和 Dockerfile 中,统一使用 pnpm。引入 pnpm/action-setup 来安装 pnpm,并将所有 npm 命令替换为 pnpm。
问题 #2:pom.xml 或 package.json “未找到”
症状: CI 中的构建步骤报错 No such file or directory。
诊断: 我们的代码在 backend 和 frontend 子目录中,但 CI 脚本默认在根目录运行命令。
解决: 为 mvn 和 pnpm 相关的步骤添加 working-directory 参数,精确定位到各自的子目录。
问题 #3:Docker 构建失败,pnpm: not found
症状: Dockerfile 内部执行 RUN pnpm install 时报错。
诊断: 我们意识到,FROM node:20-alpine 这个基础镜像默认只带了 npm,不包含 pnpm。
解决: 在 Dockerfile 中,pnpm install 之前,先加一行 RUN npm install -g pnpm 来安装 pnpm 工具。
问题 #4:Docker 构建失败,前端产物 not found
症状: COPY --from=frontend-builder ... 步骤报错,找不到源文件。
诊断: 我们 COPY 命令中写的源路径,与 pnpm run build 日志中实际的 Output location 不匹配。
解决: 仔细阅读构建日志,找到 Angular CLI 告知的真实输出路径,并修正 Dockerfile 中的 COPY 命令。最终我们发现,文件在 dist/*/browser 这个更深的目录里。
问题 #5:部署到 EC2 失败,ssh: handshake failed
症状: CI/CD 的最后一步 SSH 连接失败。
诊断: “钥匙”和“门锁”不匹配。最可能的原因是 GitHub Secrets 中的 SSH_PRIVATE_KEY 内容不完整、格式错误,或与 EC2 authorized_keys 中的公钥不配对。
解决: 重新、极其小心地生成一对专用的密钥,将公钥追加到服务器,将私钥的完整内容更新到 GitHub Secrets。
问题 #6:部署后,EC2 上容器名称冲突
症状: docker-compose up 报错 container name is already in use。
诊断: 服务器上存在一个由旧的、非 Compose 方式创建的同名“历史遗留”容器。
解决: 运行 docker-compose down 和/或 docker rm [container_name] 来彻底清理旧容器。如果容器卡在“移除中”的僵尸状态,使用 sudo systemctl restart docker 来重启 Docker 服务。
问题 #7:部署成功,但访问是 Whitelabel Error Page (404)
症状: CI/CD 流水线显示成功,但访问 http://...:8080/ 却看到 Spring Boot 的 404 页面。
诊断: 这是整个过程中最微妙的问题。通过在本地解剖 app.jar 文件,我们最终确认:前端文件被正确打包了,但被放在了 static/browser/ 这个子目录里。Spring Boot 默认只在 static/ 的根目录寻找 index.html,因此找不到。
解决: 再次修正 Dockerfile 中的 COPY 命令,确保它复制的是 browser 文件夹内部的内容,而不是 browser 文件夹本身,到 static 目录中。最终的命令是 COPY --from=frontend-builder /app/dist/*/browser/* ./src/main/resources/static/。
我们的 CI/CD 之旅,就像一次真实的软件开发过程的缩影:从一个清晰的目标开始,在实践中不断遇到预期之外的问题,然后通过观察日志、隔离变量、理解工具原理,一步步逼近真相,最终构建出一个健壮、可靠的系统。
Published Aug 4, 2025
Published Aug 4, 2025
Published Aug 4, 2025
Published Aug 7, 2025
Published Aug 7, 2025
Published Aug 7, 2025
Comments (0)