我有一个自建的Gitea跑在一个配置还不错的服务器上,在上次https://srcblog.ffeng123.win:23443/archives/ccd8829c-f256-4392-8841-e00a5820b2c3,给它装了Docker。

看到联调时后端出各种不太聪明的Bug意识到单元测试的重要性、与服务器打交道多了后厌倦了手动部署···

于是我决定,尝试自动化测试和部署。

说是自动化测试和部署,主要还是解决自动化部署的问题。

Gitea的Action

Gitea可以在提交时、打标签时等时候,触发一个自动执行的程序,这个自动程序叫Action,Github也有,而且大同小异。

Gitea ActionGithub Action

项目的 .gitea/workflows 下的所有yml文件会被扫描作为Action,Action执行的时机和内容都在文件里面写着。

开始之前

Gitea开箱即用,但Gitea的Action不是,自建Gitea需要事先部署Gitea Runner。

参考Gitea的文档

Runner可以是可执行文件部署或者Docker部署,但无论哪种部署方式,机器上都必须有Docker。

无论Runner跑在哪里,Runner在执行任务的时候都会为任务临时启动一个Docker容器,且容器内能够操作Docker

Action模板

部署Hugo静态网站

Action用的SH脚本:build.sh

这个Action会在v开头的标签推送到服务器时触发,自动准备Hugo环境、构建项目、向生产服务器上传构建的内容。

需要在Gitea的项目配置里面配置这些密钥:

  • PROD_PATH:生产服务器上传最终内容的目录

  • PROD_SERVER:SSH IP

  • PROD_USER: SSH 用户名

  • PROD_PASSWORD:SSH密码

name: 部署开发服务器

on:
  push:
    tags:
      - "v*"

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: 签出代码
        uses: actions/checkout@v3

      - name: 构建项目
        run: |
          chmod +x ./build.sh
          ./build.sh      # 使用密码认证的部署方式

      - name: 部署到生产服务器
        run: |
          sudo apt-get update && sudo apt-get install -y sshpass openssh-client rsync
          echo "准备部署到生产服务器..."
          # 配置 SSH 以跳过主机密钥验证
          mkdir -p ~/.ssh
          echo "Host *" >> ~/.ssh/config
          echo "    StrictHostKeyChecking no" >> ~/.ssh/config
          echo "    UserKnownHostsFile=/dev/null" >> ~/.ssh/config
          chmod 600 ~/.ssh/config
          # 执行部署
          sshpass -p "${{ secrets.PROD_PASSWORD }}" rsync -avz --delete -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" ./public/ ${{ secrets.PROD_USER }}@${{ secrets.PROD_SERVER }}:${{ secrets.PROD_PATH }}

对于其他技术栈的纯前端项目,可以修改Action中的./public/ (构建结果目录)和构建项目时执行的./build.sh (例如换成npm run build,官方的ubuntu-latest上有npm)

部署开发用Golang后端

这个Action会在每次推送时触发,构建Docker并在Runner所在的机器上部署Docker。

name: 部署开发服务器
run-name: 正在部署开发服务器 🚀
on: [push]
branches:
  - master

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: 📥 检出代码
        uses: actions/checkout@v4

      - name: 🐳 构建并部署
        run: |
          chmod +x scripts/deploy.sh
          scripts/deploy.sh

deploy.sh:

#!/bin/bash

# Docker Compose 部署脚本配置
COMPOSE_FILE="docker-compose.yml"
SERVICE_NAME="my-server"
PORT="8080"

echo "🚀 开始使用 Docker Compose 部署 ${SERVICE_NAME}..."

# 检查 docker-compose.yml 文件是否存在
if [ ! -f "${COMPOSE_FILE}" ]; then
    echo "❌ ${COMPOSE_FILE} 文件不存在"
    exit 1
fi

# 停止并删除旧容器
echo "🗑️ 停止旧服务..."
docker compose down || true

# 强制重新构建镜像(清理构建缓存)
echo "🧹 清理旧镜像和构建缓存..."
docker compose down --rmi local || true
# docker builder prune -f || true

# 构建并启动服务
echo "📦 重新构建并启动服务..."
docker compose up -d --build --force-recreate || {
    echo "❌ 服务启动失败"
    exit 1
}

# 等待服务启动
echo "⏳ 等待服务启动..."
sleep 10

# 检查服务状态
for i in {1..10}; do
    echo "🔍 第 $i 次检查服务状态..."
    if [ "$(docker compose ps -q ${SERVICE_NAME})" ]; then
        echo "✅ 服务已成功启动"
        break
    else
        if [ $i -eq 10 ]; then
            echo "❌ 服务启动失败"
            docker compose logs ${SERVICE_NAME}
            exit 1
        else
            echo "⏳ 服务尚未启动,稍后重试..."
            sleep 5
        fi
    fi
done

echo "🎉 部署完成!"
echo "📊 部署信息:"
echo "   服务名称: ${SERVICE_NAME}"
echo "   服务端口: ${PORT}"
echo "   服务地址: http://localhost:${PORT}"
echo "📋 服务状态:"
docker compose ps

Dockerfile:

FROM golang:1.24-alpine AS builder

WORKDIR /app

RUN apk add --no-cache git ca-certificates tzdata make ffmpeg
COPY go.mod go.sum ./
RUN go mod download
COPY . .

RUN make test
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-w -s' -o main cmd/api/main.go


FROM alpine:latest

RUN apk --no-cache add ca-certificates curl tzdata ffmpeg && \
    addgroup -g 1000 appgroup && \
    adduser -u 1000 -G appgroup -s /bin/sh -D appuser

WORKDIR /app

COPY --from=builder /app/main .
#COPY --from=builder /app/.env* ./

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

CMD ["./main"]

docker-compose.yml:

services:
    my-server:
        build: .
        ports:
            - "8080:8080"
        environment: {}
        env_file:
            - .env
        restart: unless-stopped
        healthcheck:
            test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ]
            interval: 30s
            timeout: 10s
            retries: 3
            start_period: 40s
        networks:
            - main
networks:
    main:
        driver: bridge

deploy.sh和docker-compose.yml中的名称 my-server 应该是相同的,用于脚本检测容器是否启动成功。

我能想到的,最大的成功就是无愧于自己的心。