1. 引言
近期,我正在进行个人项目的开发。在开发过程中,我希望能够将项目直接部署到线上,以便观察开发环境与生产环境之间的差异。这一做法有助于尽早发现潜在问题,降低解决的成本。为此,我采用了 GitHub Actions 和 Portainer 实现CI/CD。本文将详细阐述从代码提交到服务部署全流程自动化的实现方案。
2. 核心工具介绍
GitHub Actions
GitHub Actions 是 GitHub 提供的一项功能,允许开发者自动化软件开发工作流程。它使得用户能够在代码库中定义 CI/CD(持续集成和持续交付)流程,以便在特定事件发生时(如代码提交、拉取请求等)自动执行一系列操作。
主要特点
- 工作流定义:用户可以通过 YAML 文件定义工作流,指定触发条件、执行步骤和所需的环境。
- 事件驱动:支持多种事件触发,包括代码推送、拉取请求、定时任务等。
- 自定义操作:用户可以创建自定义操作,或者使用社区提供的现成操作,来扩展工作流的功能。
- 集成与部署:可以与其他服务和工具(如 Docker、AWS、Azure 等)无缝集成,支持自动构建、测试和部署应用程序。
- 并行执行:支持并行执行多个任务,提高工作流的效率。
使用场景
- 自动化测试:在每次代码提交后自动运行测试。
- 自动构建:在代码合并后自动构建应用程序。
- 部署:在代码推送后自动将应用程序部署到生产环境或测试环境。
Portainer
Portainer 是一个轻量级的管理界面,用于简化 Docker 容器、镜像、网络和卷的管理。它提供了一个用户友好的 Web 界面,使得用户能够更方便地管理 Docker 环境。
主要特点
- 直观的用户界面:提供易于使用的图形界面,用户可以通过浏览器轻松管理 Docker 容器。
- 多种管理功能:支持创建、启动、停止、删除容器,管理镜像、网络和卷等。
- 支持多种环境:可以管理本地 Docker 环境、远程 Docker 主机以及 Docker Swarm 集群。
- 用户管理:支持用户角色和权限管理,适合团队协作。
- API 支持:提供 REST API,方便与其他工具集成。
使用场景
- 容器管理:适合开发人员和运维人员快速管理和监控 Docker 容器。
- 团队协作:在团队中共享和管理 Docker 环境,简化操作流程。
- 监控和日志:提供容器的实时监控和日志查看功能,帮助排查问题。
3. GitHub Actions
核心概念
-
Workflow(工作流)
- 一个自动化流程的完整定义,由 YAML 文件描述。
- 存储在仓库的
.github/workflows
目录下,支持多工作流并行。
-
Job(任务)
- 工作流中的独立执行单元,例如构建、测试、部署。
- 多个 Job 默认并行执行,可通过
needs
关键字定义依赖关系。
-
Step(步骤)
- Job 中的具体操作步骤,如安装依赖、运行脚本。
- 支持使用预定义 Action(如
actions/checkout
)或自定义命令。
-
Event(触发事件)
- 触发工作流执行的条件,例如
push
、pull_request
、定时任务等。
- 触发工作流执行的条件,例如
配置流程
1. 创建工作流文件
- 在仓库根目录创建
.github/workflows
文件夹。 - 新建 YAML 文件(如
ci-cd.yml
),定义工作流逻辑。
2. 配置变量
- 涉及到敏感信息的,比如 DockerHub 密码、API 密钥等信息的,可以使用
serect
,serect
一旦创建便不可见。 - 其他非敏感信息可以使用
variable
,创建后仍可以查看其值。
3. 实现工作流
yaml
# .github/workflows/docker-build.yml
name: CI/CD Pipeline # 工作流名称
on: # 触发条件
push:
branches: [ "main" ] # main分支提交时触发
pull_request:
branches: [ "main" ]
jobs: # 定义任务
build-and-test: # 任务ID
runs-on: ubuntu-latest # 运行环境
steps:
# 步骤1:检出代码
- name: Checkout repository
uses: actions/checkout@v4
# 步骤2:登录容器注册中心
- name: Log in to ${{ env.REGISTRY }}
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# 步骤3:设置构建元数据
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
# 步骤4:构建并推送镜像(支持缓存)
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile # 指定Dockerfile路径
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# 步骤5:触发外部部署(如Portainer)
- name: Trigger Portainer Webhook
if: success()
run: |
curl -X POST \
-H "Content-Type: application/json" \
"${{ secrets.PORTAINER_WEBHOOK_URL }}?trigger_force_update=true"
4. Portainer自动化部署
1. 安装 Portainer
首先,需要在服务器上安装 Portainer。可以使用以下命令通过 Docker 运行 Portainer。
bash
docker pull portainer/portainer
docker run -d -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer/portainer
2. 访问 Portainer
在浏览器中访问 Portainer 的 Web 界面,输入以下 URL:
arduino
http://<your-server-ip>:9000
3. 创建应用程序的 Docker 镜像
在自动化部署之前,用户需要先将应用程序打包成 Docker 镜像。用户可以通过 Dockerfile 来构建镜像。以下是一个构建 Node 应用程序的例子。
dockerfile
# 使用官方 Node.js 镜像
FROM node:14
# 设置工作目录
WORKDIR /usr/src/app
# 复制 package.json 和 package-lock.json
COPY package*.json ./
# 安装依赖
RUN npm install
# 复制应用程序代码
COPY . .
# 暴露应用程序端口
EXPOSE 3000
# 启动应用程序
CMD ["node", "app.js"]
使用以下命令构建镜像:
bash
docker build -t my-node-app .
4. 在 Portainer 中部署应用程序
-
登录 Portainer:使用您设置的管理员账户登录 Portainer。
-
创建新容器:
- 在 Portainer 的左侧菜单中,选择"Containers"。
- 点击"Add container"按钮。
-
配置容器:
- 在"Name"字段中输入容器名称。
- 在"Image"字段中输入您刚刚构建的镜像名称(例如
my-node-app
)。 - 配置端口映射、环境变量等其他设置。
-
启动容器:完成配置后,点击"Deploy the container"按钮,Portainer 将自动启动您的应用程序。
5. 完整流程
1. 创建项目
bash
//创建next项目
npx create-next-app@latest
//启动项目
npm run dev
修改Next
项目的输出模式
typescript
//next.config.ts
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
};
2. 添加Dockerfile文件
在项目根目录下添加Dockerfile
文件,并添加如下代码
dockerfile
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
# 首先只复制 package.json 和 lock 文件以利用缓存
COPY package.json package-lock.json ./
RUN npm install
# 然后复制其他源文件
COPY . .
RUN npm run build
# 运行阶段
FROM node:18-alpine AS runner
WORKDIR /app
# 设置环境变量
ENV NODE_ENV=production
# 只复制必要的文件
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
3. 添加workflow文件
在根目录下新建version
文件,并输入当前的版本号
version
v0.1.0
在.github/workflows
目录下新建docker-build.yml
文件,并添加如下代码
yaml
name: Docker Build and Push
on:
push:
branches: ['master']
pull_request:
branches: ['master']
env:
# 腾讯云镜像仓库地址
REGISTRY: ccr.ccs.tencentyun.com
# 镜像仓库命名空间
NAMESPACE: naf9nahz
# 镜像名称
IMAGE_NAME: my-next-app
# 如果有其他环境变量,也可以在这里添加
# OTHER_ENV_VAR: ${{ secrets.OTHER_ENV_VAR }}
jobs:
build:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: 1
BUILDKIT_INLINE_CACHE: 1
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Get Version
id: get_version
run: echo "VERSION=$(cat version)" >> $GITHUB_OUTPUT
- name: Login to TencentCloud CCR
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.TENCENT_CLOUD_ACCOUNT_ID }}
password: ${{ secrets.TENCENT_CLOUD_API_KEY }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}
build-args: |
# 如果有其他环境变量,也可以在这里添加
# OTHER_ENV_VAR=${{ secrets.OTHER_ENV_VAR }}
4. 部署Portainer
在服务器上分别运行这两条指令
bash
docker pull portainer/portainer
docker run -d -p 9000:9000 --restart=always -v /var/run/docker.sock:/var/run/docker.sock --name portainer portainer/portainer
输入服务器的IP
地址访问Portainer的页面
arduino
http://<your-server-ip>:9000
5. 构建镜像
向master
分支提交代码或发起合并请求,以触发workflow
6. 发布项目
- 点击Portainer页面左侧的"Stacks"
- 点击右上角的"Add Stack"
- 输入
Stack
的名字并将Docker Compose
粘贴到Web editor
中 - 点击
Deploy the stack
yaml
version: '3.8'
services:
app:
image: ccr.ccs.tencentyun.com/naf9nahz/my-next-app:v0.1.0
container_name: my-next-app
restart: always
ports:
- "3001:3000"
networks:
- app-network
networks:
app-network:
driver: bridge
我的服务器的3000
端口已经被其他项目占用,因此改为3001
端口
输入服务器的IP
地址访问项目
arduino
http://<your-server-ip>:3001
6. 其他问题
Q:Portainer页面无法访问
- 检查服务器防火墙是否放行端口。
Q:Github Action如何自动触发 Portainer 的更新
- 可以在
workflow
执行完构建镜像流程后通过执行curl
指令触发 Portainer 中容器的更新。