依赖Github Action的Docker CICD
2025年4年16日 · 1873 字
这段时间一直在沉迷于建站SEO,看看能不能赚点咖啡钱,而像持续内容输出的网站不可避免地要持续更新、推送、部署。 之前一直人工去部署到VPS上,实在繁琐,就开始使用CICD,下面就是我使用的一些分享心得。
什么是 CI/CD?
CI/CD 是现代开发流程里很重要的一部分。它主要是两个东西:
- CI(Continuous Integration)叫“持续集成”,意思是我们一有代码更新,就自动跑测试、打包、构建,确保代码没问题。
- CD(Continuous Deployment / Delivery)叫“持续部署”或“持续交付”,意思是代码通过了测试后,可以自动部署到服务器上,真正上线。
简单说,就是:
写完代码 → 推送代码 → 自动测试构建 → 自动部署上线
这样能大大减少出错的机会,也让我们可以更快上线新功能,是团队开发中非常推荐使用的一种方式。
自动构建并部署 Docker 应用的 GitHub Actions 脚本
这是一个完整的 CI/CD 流程。当你推送代码到 main 分支时,自动构建 Docker 镜像,并部署到服务器。然后这个配置对于普通的nextjs项目是比较通用的,大部分情况你只需要修改环境变量就可以了,然后要注意Github Action中的一些组件版本可能需要手动调整。
大致的流程图如下:
完整的YAML文件如下:
name: Build and Deploy Docker Application
on:
push:
branches: [ main ]
workflow_dispatch:
env:
DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE_NAME || 'app' }}
DOCKER_CONTAINER_NAME: ${{ secrets.DOCKER_CONTAINER_NAME || 'app' }}
DOCKER_PORT_MAPPING: ${{ secrets.DOCKER_PORT_MAPPING || '3000:3000' }}
DOCKER_VOLUME_MAPPING: ${{ secrets.DOCKER_VOLUME_MAPPING || '' }}
DOCKER_EXTRA_ARGS: ${{ secrets.DOCKER_EXTRA_ARGS || '' }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.set_version.outputs.new_version }}
old_version: ${{ steps.set_version.outputs.old_version }}
steps:
- name: Check out the code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Read & increment version
id: set_version
run: |
OLD_VERSION=$(cat .version)
IFS='.' read -r major minor patch <<< "$OLD_VERSION"
patch=$((patch + 1))
NEW_VERSION="$major.$minor.$patch"
echo "$NEW_VERSION" > .version
echo "OLD_VERSION=$OLD_VERSION" >> $GITHUB_ENV
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
echo "::set-output name=old_version::$OLD_VERSION"
echo "::set-output name=new_version::$NEW_VERSION"
- name: Pull latest changes (to avoid non-fast-forward push)
run: |
git config --global user.email "github-actions@github.com"
git config --global user.name "github-actions"
git stash push -m "temp"
git pull origin main --rebase
git stash pop || true
- name: Commit and push version update
uses: EndBug/add-and-commit@v9
with:
add: '.version'
message: 'Bump version to ${{ env.NEW_VERSION }}'
author_name: 'github-actions'
author_email: 'github-actions@github.com'
- name: Build Docker image
run: |
docker build -t ${{ env.DOCKER_IMAGE_NAME }}:${{ env.NEW_VERSION }} .
- name: Save Docker image to tar file
run: |
docker save ${{ env.DOCKER_IMAGE_NAME }}:${{ env.NEW_VERSION }} -o ${{ env.DOCKER_IMAGE_NAME }}-v${{ env.NEW_VERSION }}.tar
- name: Upload Docker image tar file as artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: ${{ env.DOCKER_IMAGE_NAME }}-v${{ env.NEW_VERSION }}.tar
retention-days: 1
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Docker image tar file
uses: actions/download-artifact@v4
with:
name: docker-image
path: .
- name: Debug NEW_VERSION in deploy job
run: echo "Deploying version ${{ needs.build.outputs.new_version }}"
- name: Install sshpass
run: sudo apt-get install -y openssh-client sshpass
- name: Add SSH host key
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
- name: SCP Docker image tar file to server
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SERVER_USER: ${{ secrets.SERVER_USER }}
SERVER_IP: ${{ secrets.SERVER_IP }}
DEST_PATH: ${{ secrets.SERVER_PATH }}
run: |
sshpass -p $SSH_PRIVATE_KEY scp -o StrictHostKeyChecking=no ${{ env.DOCKER_IMAGE_NAME }}-v${{ needs.build.outputs.new_version }}.tar $SERVER_USER@$SERVER_IP:$DEST_PATH/${{ env.DOCKER_IMAGE_NAME }}-v${{ needs.build.outputs.new_version }}.tar
- name: SSH into server and deploy container
env:
SERVER_USER: ${{ secrets.SERVER_USER }}
SERVER_IP: ${{ secrets.SERVER_IP }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DEST_PATH: ${{ secrets.SERVER_PATH }}
IMG_VERSION: ${{ needs.build.outputs.new_version }}
OLD_VERSION: ${{ needs.build.outputs.old_version }}
ENV_VARS: ${{ secrets.ENV_VARS || '' }}
run: |
sshpass -p $SSH_PRIVATE_KEY ssh $SERVER_USER@$SERVER_IP << EOF
echo "Login server successfully"
cd $DEST_PATH
docker load -i ${{ env.DOCKER_IMAGE_NAME }}-v$IMG_VERSION.tar
echo "Image loaded successfully"
docker stop ${{ env.DOCKER_CONTAINER_NAME }} || true
echo "Old container stopped"
docker rm ${{ env.DOCKER_CONTAINER_NAME }} || true
echo "Old container removed"
docker image rm ${{ env.DOCKER_IMAGE_NAME }}:$OLD_VERSION || true
echo "Old image removed successfully"
DOCKER_CMD="docker run -d --name ${{ env.DOCKER_CONTAINER_NAME }} -p ${{ env.DOCKER_PORT_MAPPING }}"
if [ ! -z "${{ env.DOCKER_VOLUME_MAPPING }}" ]; then
DOCKER_CMD="\$DOCKER_CMD -v ${{ env.DOCKER_VOLUME_MAPPING }}"
fi
if [ ! -z "$ENV_VARS" ]; then
for VAR in \$(echo \$ENV_VARS | tr ',' '\n'); do
DOCKER_CMD="\$DOCKER_CMD -e \$VAR"
done
fi
if [ ! -z "${{ env.DOCKER_EXTRA_ARGS }}" ]; then
DOCKER_CMD="\$DOCKER_CMD ${{ env.DOCKER_EXTRA_ARGS }}"
fi
DOCKER_CMD="\$DOCKER_CMD ${{ env.DOCKER_IMAGE_NAME }}:$IMG_VERSION"
echo "Running command: \$DOCKER_CMD"
eval \$DOCKER_CMD
echo "New container deployed successfully"
rm ${{ env.DOCKER_IMAGE_NAME }}-v$IMG_VERSION.tar
echo "Cleanup completed"
EOF
对整个YAML文件的解释
一、触发条件
on:
push:
branches: [ main ] # 每次推送到 main 分支就会触发
workflow_dispatch: # 也可以手动触发
二、全局变量(环境变量)
env:
DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE_NAME || 'app' }}
DOCKER_CONTAINER_NAME: ${{ secrets.DOCKER_CONTAINER_NAME || 'app' }}
DOCKER_PORT_MAPPING: ${{ secrets.DOCKER_PORT_MAPPING || '3000:3000' }}
DOCKER_VOLUME_MAPPING: ${{ secrets.DOCKER_VOLUME_MAPPING || '' }}
DOCKER_EXTRA_ARGS: ${{ secrets.DOCKER_EXTRA_ARGS || '' }}
这些值可以从 secrets 设置。没有设置的话,就用默认值。
三、构建步骤(build job)
jobs:
build:
runs-on: ubuntu-latest # 使用 GitHub 提供的 Ubuntu 环境
1. 拉取代码
- name: Check out the code
usesactions/checkout@v4
2. 设置 Docker 构建工具
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
3. 自动更新版本号
- name: Read & increment version
id: set_version
run: |
OLD_VERSION=$(cat .version)
IFS='.' read -r major minor patch <<< "$OLD_VERSION"
patch=$((patch + 1))
NEW_VERSION="$major.$minor.$patch"
echo "$NEW_VERSION" > .version
echo "OLD_VERSION=$OLD_VERSION" >> $GITHUB_ENV
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
echo "::set-output name=old_version::$OLD_VERSION"
echo "::set-output name=new_version::$NEW_VERSION"
读取 .version 文件,版本号 +1。然后保存回文件。
4. 拉最新代码,避免冲突
- name: Pull latest changes ...
run: |
git config --global user.email "github-actions@github.com"
git config --global user.name "github-actions"
git stash push -m "temp"
git pull origin main --rebase
git stash pop || true
先把当前修改存起来,再拉取远程代码,然后把存的内容还原。
5. 提交版本更新
- name: Commit and push version update
uses: EndBug/add-and-commit@
with:
add: '.version'
message: 'Bump version to ${{ env.NEW_VERSION }}'
author_name: 'github-actions'
author_email: 'github-actions@github.com'
把更新后的 .version 文件提交并推送。
6. 构建 Docker 镜像
- name: Build Docker image
run: |
docker build -t ${{ env.DOCKER_IMAGE_NAME }}:${{ env.NEW_VERSION }} .
7. 保存镜像成 .tar 文件
- name: Save Docker image to tar file
run: |
docker save ${{ env.DOCKER_IMAGE_NAME }}:${{ env.NEW_VERSION }} -o ${{ env.DOCKER_IMAGE_NAME }}-v${{ env.NEW_VERSION }}.tar
把镜像保存成文件,方便传输到服务器。
8. 上传构建产物
- name: Upload Docker image tar file as artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: ${{ env.DOCKER_IMAGE_NAME }}-v${{ env.NEW_VERSION }}.tar
retention-days: 1
把镜像文件上传为 GitHub Artifact,方便下一个步骤下载。
四、部署步骤(deploy job)
deploy:
needs: build # 需要 build job 完成后才能执行
runs-on: ubuntu-latest
1. 下载镜像文件
- name: Download Docker image tar file
uses: actions/download-artifact@v4
with:
name: docker-image
path:
2. 打印版本号(用于调试)
- name: Debug NEW_VERSION in deploy job
run: echo "Deploying version ${{ needs.build.outputs.new_version }}
3. 安装 sshpass 工具
- name: Install sshpass
run: sudo apt-get install -y openssh-client sshpass
用来远程连接服务器用的。
4. 添加服务器 SSH key
- name: Add SSH host key
run: |
mkdir -p ~/.ssh
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
避免第一次连接时报“未知主机”的错误。
5. 把镜像传到服务器
- name: SCP Docker image tar file to server
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SERVER_USER: ${{ secrets.SERVER_USER }}
SERVER_IP: ${{ secrets.SERVER_IP }}
DEST_PATH: ${{ secrets.SERVER_PATH }}
run: |
sshpass -p $SSH_PRIVATE_KEY scp -o StrictHostKeyChecking=no ${{ env.DOCKER_IMAGE_NAME }}-v${{ needs.build.outputs.new_version }}.tar $SERVER_USER@$SERVER_IP:$DEST_PATH/${{ env.DOCKER_IMAGE_NAME }}-v${{ needs.build.outputs.new_version }}.ta
使用 scp 命令把镜像上传到服务器目录。
6. 登录服务器并部署
- name: SSH into server and deploy container
env:
SERVER_USER: ${{ secrets.SERVER_USER }}
SERVER_IP: ${{ secrets.SERVER_IP }}
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DEST_PATH: ${{ secrets.SERVER_PATH }}
IMG_VERSION: ${{ needs.build.outputs.new_version }}
OLD_VERSION: ${{ needs.build.outputs.old_version }}
ENV_VARS: ${{ secrets.ENV_VARS || '' }}
run: |
sshpass -p $SSH_PRIVATE_KEY ssh $SERVER_USER@$SERVER_IP << EOF
echo "Login server successfully"
cd $DEST_PATH
docker load -i ${{ env.DOCKER_IMAGE_NAME }}-v$IMG_VERSION.tar
echo "Image loaded successfully"
docker stop ${{ env.DOCKER_CONTAINER_NAME }} || true
echo "Old container stopped"
docker rm ${{ env.DOCKER_CONTAINER_NAME }} || true
echo "Old container removed"
docker image rm ${{ env.DOCKER_IMAGE_NAME }}:$OLD_VERSION || true
echo "Old image removed successfully"
DOCKER_CMD="docker run -d --name ${{ env.DOCKER_CONTAINER_NAME }} -p ${{ env.DOCKER_PORT_MAPPING }}"
if [ ! -z "${{ env.DOCKER_VOLUME_MAPPING }}" ]; then
DOCKER_CMD="\$DOCKER_CMD -v ${{ env.DOCKER_VOLUME_MAPPING }}"
fi
if [ ! -z "$ENV_VARS" ]; then
for VAR in \$(echo \$ENV_VARS | tr ',' '\n'); do
DOCKER_CMD="\$DOCKER_CMD -e \$VAR"
done
fi
if [ ! -z "${{ env.DOCKER_EXTRA_ARGS }}" ]; then
DOCKER_CMD="\$DOCKER_CMD ${{ env.DOCKER_EXTRA_ARGS }}"
fi
DOCKER_CMD="\$DOCKER_CMD ${{ env.DOCKER_IMAGE_NAME }}:$IMG_VERSION"
echo "Running command: \$DOCKER_CMD"
eval \$DOCKER_CMD
echo "New container deployed successfully"
rm ${{ env.DOCKER_IMAGE_NAME }}-v$IMG_VERSION.tar
echo "Cleanup completed"
EOF
- 首先通过 SSH 登录服务器。
- 进入指定的部署目录
- 把刚上传的镜像文件加载进 Docker。这样服务器上就有了这个新镜像。
- 停掉并删除旧容器,顺带清掉旧版本镜像。
|| true
是为了防止失败后中断。- 根据有没有传 volume、环境变量、额外参数,动态追加拼接 docker run 命令
- 最后把镜像名和版本拼上去,执行拼出来的 run 命令,启动新容器!