actions-template/DEPLOYMENT.md

44 KiB
Raw Blame History

🚀 Gitea Runner Docker 部署完整教程

📋 目录


💡 简介

Gitea Runner 是 Gitea 的 CI/CD 执行器,类似于 GitLab Runner 或 GitHub Actions Runner用于执行 Gitea Actions 工作流。


🎯 版本选择

📦 版本 1标准版推荐

适合场景:

  • 大多数日常使用场景
  • 只需在 arm64 架构上运行应用
  • 简单的 CI/CD 流程
  • 不需要构建多架构容器镜像

特点:

  • 🪶 轻量级,镜像体积约 400MB
  • 配置简单启动快速5-10 秒)
  • 🔒 无需特权模式,更安全
  • 🎯 易于维护

🚀 版本 2Buildx 多架构版

适合场景:

  • 需要构建 arm64 + amd64 双架构镜像
  • 发布容器镜像到公共仓库
  • 跨平台应用开发和测试
  • 高级 CI/CD 需求

特点:

  • 🌐 支持 Docker Buildx 多架构构建
  • 🔧 内置 QEMU 跨架构模拟
  • 🤖 自动配置 Buildx builder
  • ⚠️ 需要特权模式和 Docker socket

📦 版本 1标准版推荐

📂 目录结构

gitea-runner/
├── Dockerfile             # 容器构建文件
├── docker-compose.yml     # Docker Compose 配置
├── entrypoint.sh          # 容器启动脚本
├── setup.sh               # Runner 安装脚本
├── register.sh            # Runner 注册脚本
├── manage.sh              # Runner 管理脚本
└── runner-data/           # 数据持久化目录(自动创建)
    └── runners/           # 多个 runner 存储目录
        ├── runner-1/
        │   ├── .runner
        │   ├── config.yaml
        │   └── cache/
        └── ...

📝 文件配置

1 Dockerfile

创建 Dockerfile 文件:

FROM ubuntu:22.04

# 设置环境变量避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive

# 更新系统并安装必要软件
RUN apt-get update && apt-get install -y \
    curl \
    git \
    python3 \
    python3-yaml \
    supervisor \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# 创建必要目录
RUN mkdir -p /data /etc/supervisor/conf.d /var/log/supervisor

# 设置工作目录
WORKDIR /data

# 使用自定义入口点
ENTRYPOINT ["/data/entrypoint.sh"]

2 docker-compose.yml

创建 docker-compose.yml 文件:

services:
  gitea-runner:
    build: .
    container_name: gitea-runner
    restart: unless-stopped
    volumes:
      - ./runner-data:/data
      - ./setup.sh:/data/setup.sh:ro
      - ./register.sh:/data/register.sh:ro
      - ./manage.sh:/data/manage.sh:ro
      - ./entrypoint.sh:/data/entrypoint.sh:ro

      # 如果需要在容器内运行 Docker取消下面的注释
      # - /var/run/docker.sock:/var/run/docker.sock

    environment:
      - TZ=Asia/Shanghai

      # 如果需要使用代理,取消下面的注释并修改端口
      # 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP
      # - http_proxy=http://host.docker.internal:20122
      # - https_proxy=http://host.docker.internal:20122
      # - HTTP_PROXY=http://host.docker.internal:20122
      # - HTTPS_PROXY=http://host.docker.internal:20122
      # - no_proxy=localhost,127.0.0.1

    # Linux 系统需要取消下面的注释以支持 host.docker.internal
    # extra_hosts:
    #   - "host.docker.internal:host-gateway"

3 entrypoint.sh

创建 entrypoint.sh 文件:

#!/bin/bash
set -e

echo "==================================="
echo "Gitea Runner Container Starting..."
echo "==================================="

# 定义路径
PERSISTENT_BIN="/data/bin"
RUNNER_PATH="$PERSISTENT_BIN/act_runner"
SYSTEM_LINK="/usr/local/bin/act_runner"

# 创建必要目录
mkdir -p /data/runners
mkdir -p "$PERSISTENT_BIN"
mkdir -p /var/log/supervisor
mkdir -p /var/run

# 创建主 supervisor 配置文件
cat > /etc/supervisor/supervisord.conf <<EOF
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[include]
files = /etc/supervisor/conf.d/*.conf
EOF

# 检查持久化目录中的 act_runner
if [ -f "$RUNNER_PATH" ]; then
    echo "✓ Found act_runner in persistent storage"
    echo "  Location: $RUNNER_PATH"

    # 创建软链接到系统路径(如果不存在)
    if [ ! -L "$SYSTEM_LINK" ] || [ ! -e "$SYSTEM_LINK" ]; then
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "  Created system link: $SYSTEM_LINK -> $RUNNER_PATH"
    fi

    # 验证版本
    RUNNER_VERSION=$("$SYSTEM_LINK" --version 2>/dev/null || echo "unknown")
    echo "  Version: $RUNNER_VERSION"
elif [ -f "$SYSTEM_LINK" ]; then
    # 旧版本可能在系统路径,迁移到持久化目录
    echo "⚠ Found act_runner in system path, migrating to persistent storage..."
    cp "$SYSTEM_LINK" "$RUNNER_PATH"
    chmod +x "$RUNNER_PATH"
    ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
    echo "  ✓ Migrated to: $RUNNER_PATH"
else
    # 没有找到 act_runner
    echo "⚠ act_runner not installed yet!"
    echo ""
    echo "Please run the setup script first:"
    echo "  docker-compose exec gitea-runner /data/setup.sh"
    echo ""
    echo "Container is waiting..."

    # 等待 act_runner 安装
    while [ ! -f "$RUNNER_PATH" ] && [ ! -f "$SYSTEM_LINK" ]; do
        sleep 10
    done

    # 再次检查并创建链接
    if [ -f "$RUNNER_PATH" ]; then
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "✓ act_runner detected and linked!"
    elif [ -f "$SYSTEM_LINK" ]; then
        cp "$SYSTEM_LINK" "$RUNNER_PATH"
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "✓ act_runner detected and migrated!"
    fi
fi

# 为每个已注册的 runner 创建 supervisor 配置
echo ""
echo "Scanning for registered runners..."
RUNNER_COUNT=0

if [ -d "/data/runners" ]; then
    for runner_dir in /data/runners/*/; do
        if [ -d "$runner_dir" ]; then
            runner_name=$(basename "$runner_dir")

            if [ -f "$runner_dir/.runner" ] && [ -f "$runner_dir/config.yaml" ]; then
                echo "Found runner: $runner_name"

                # 创建该 runner 的 supervisor 配置
                cat > "/etc/supervisor/conf.d/runner-${runner_name}.conf" <<EOF
[program:runner-${runner_name}]
command=/usr/local/bin/act_runner daemon --config ${runner_dir}/config.yaml
directory=${runner_dir}
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/runner-${runner_name}.err.log
stdout_logfile=/var/log/supervisor/runner-${runner_name}.out.log
user=root
environment=HOME="/root"
EOF
                RUNNER_COUNT=$((RUNNER_COUNT + 1))
            fi
        fi
    done
fi

if [ $RUNNER_COUNT -eq 0 ]; then
    echo "⚠ No runners registered yet!"
    echo ""
    echo "Please run the register script to add a runner:"
    echo "  docker-compose exec gitea-runner /data/register.sh"
    echo ""
fi

echo "Total runners configured: $RUNNER_COUNT"
echo ""
echo "==================================="
echo "Starting Supervisor..."
echo "==================================="

# 启动 supervisord使用主配置文件
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

4 setup.sh

创建 setup.sh 文件:

#!/bin/bash

echo "=========================================="
echo "  Gitea Runner Installation Script       "
echo "=========================================="
echo ""

# 持久化安装路径
INSTALL_PATH="/data/bin/act_runner"
SYSTEM_PATH="/usr/local/bin/act_runner"

# 创建目录
mkdir -p /data/bin

# 检查是否已安装
if [ -f "$INSTALL_PATH" ]; then
    CURRENT_VERSION=$($INSTALL_PATH --version 2>/dev/null | grep -oP 'version \K[0-9.]+' || echo "unknown")
    echo "⚠ act_runner already installed (version: $CURRENT_VERSION)"
    echo "  Location: $INSTALL_PATH"
    echo ""
    read -p "Do you want to reinstall/upgrade? (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Installation cancelled."
        exit 0
    fi
    rm -f "$INSTALL_PATH" "$SYSTEM_PATH"
fi

# 获取要安装的版本
echo "Available versions: https://dl.gitea.com/act_runner/"
echo ""
read -p "Enter version to install (default: 0.2.13): " RUNNER_VERSION
RUNNER_VERSION=${RUNNER_VERSION:-0.2.13}

ARCH=$(uname -m)
case "$ARCH" in
    x86_64)
        RUNNER_ARCH="amd64"
        ;;
    aarch64|arm64)
        RUNNER_ARCH="arm64"
        ;;
    armv7l)
        RUNNER_ARCH="arm-7"
        ;;
    *)
        echo "⚠ Unknown architecture: $ARCH"
        RUNNER_ARCH="arm64"
        ;;
esac

# 确认架构
echo ""
echo "Detected architecture: $ARCH -> $RUNNER_ARCH"
read -p "Is this correct? (Y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Nn]$ ]]; then
    echo ""
    echo "Available architectures:"
    echo "  1. amd64 (x86_64)"
    echo "  2. arm64 (aarch64)"
    echo "  3. arm-7 (armv7l)"
    read -p "Select architecture (1-3): " ARCH_CHOICE
    case "$ARCH_CHOICE" in
        1) RUNNER_ARCH="amd64" ;;
        2) RUNNER_ARCH="arm64" ;;
        3) RUNNER_ARCH="arm-7" ;;
        *)
            echo "Invalid choice. Exiting."
            exit 1
            ;;
    esac
fi

DOWNLOAD_URL="https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-${RUNNER_ARCH}"

echo ""
echo "Download Configuration:"
echo "  Version:          $RUNNER_VERSION"
echo "  Architecture:     $RUNNER_ARCH"
echo "  URL:              $DOWNLOAD_URL"
echo "  Install Location: $INSTALL_PATH (persistent)"
echo ""
read -p "Proceed with download? (Y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Nn]$ ]]; then
    echo "Installation cancelled."
    exit 0
fi

echo ""
echo "Downloading act_runner..."
echo ""

# 下载到持久化目录
if curl -L "$DOWNLOAD_URL" -o "$INSTALL_PATH"; then
    chmod +x "$INSTALL_PATH"

    # 同时创建软链接到系统路径
    ln -sf "$INSTALL_PATH" "$SYSTEM_PATH"

    # 验证安装
    if $INSTALL_PATH --version; then
        echo ""
        echo "=========================================="
        echo "✓ act_runner installed successfully!"
        echo "=========================================="
        echo ""
        echo "Version: $($INSTALL_PATH --version)"
        echo "Location: $INSTALL_PATH (persistent storage)"
        echo ""
        echo "Next steps:"
        echo "1. Register the runner:"
        echo "   docker-compose exec gitea-runner /data/register.sh"
        echo ""
        echo "2. Restart the container:"
        echo "   docker-compose restart"
        echo ""
        echo "Note: act_runner is saved in persistent storage"
        echo "      and will survive container restarts."
        echo ""
    else
        echo ""
        echo "✗ Installation verification failed!"
        rm -f "$INSTALL_PATH" "$SYSTEM_PATH"
        exit 1
    fi
else
    echo ""
    echo "✗ Download failed!"
    echo "Please check:"
    echo "  - Internet connection"
    echo "  - Version number is correct: $RUNNER_VERSION"
    echo "  - Architecture is correct: $RUNNER_ARCH"
    echo "  - URL is accessible: $DOWNLOAD_URL"
    echo ""
    echo "You can check available versions at:"
    echo "  https://dl.gitea.com/act_runner/"
    exit 1
fi

5 register.sh

创建 register.sh 文件:

#!/bin/bash
set -e

echo "=========================================="
echo "   Gitea Runner Registration Script      "
echo "=========================================="
echo ""

# 检查 act_runner 是否安装
if ! command -v act_runner &> /dev/null; then
    echo "✗ act_runner is not installed!"
    echo ""
    echo "Please run the setup script first:"
    echo "  docker-compose exec gitea-runner /data/setup.sh"
    exit 1
fi

echo "✓ act_runner found: $(act_runner --version)"
echo ""

# 获取注册信息并验证
while true; do
    read -p "Enter Gitea instance URL (e.g., https://gitea.example.com): " GITEA_INSTANCE

    # 验证 URL 格式
    if [[ ! "$GITEA_INSTANCE" =~ ^https?:// ]]; then
        echo "✗ Error: URL must start with http:// or https://"
        echo ""
        continue
    fi

    # 移除末尾的斜杠
    GITEA_INSTANCE="${GITEA_INSTANCE%/}"
    echo "✓ URL validated: $GITEA_INSTANCE"
    break
done

read -p "Enter registration token: " GITEA_TOKEN

if [ -z "$GITEA_TOKEN" ]; then
    echo "✗ Error: Token cannot be empty!"
    exit 1
fi

read -p "Enter runner name (default: docker-runner): " RUNNER_NAME
RUNNER_NAME=${RUNNER_NAME:-docker-runner}

# 多个 label逗号分隔无空格
# ubuntu-22.04:host://ubuntu:22.04,ubuntu-20.04:host://ubuntu:20.04,node:docker://node:18
read -p "Enter runner labels (default: ubuntu-22.04:docker://ubuntu:22.04): " RUNNER_LABELS
RUNNER_LABELS=${RUNNER_LABELS:-ubuntu-22.04:host://ubuntu:22.04}

# 创建 runner 目录
RUNNER_DIR="/data/runners/${RUNNER_NAME}"
mkdir -p "$RUNNER_DIR"
cd "$RUNNER_DIR"

echo ""
echo "Registration Information:"
echo "  Instance:  $GITEA_INSTANCE"
echo "  Name:      $RUNNER_NAME"
echo "  Labels:    $RUNNER_LABELS"
echo "  Directory: $RUNNER_DIR"
echo ""

# 检查是否已经注册
if [ -f ".runner" ] || [ -f "config.yaml" ]; then
    echo "⚠ Runner already exists in this directory!"
    read -p "Do you want to re-register? This will overwrite existing configuration. (y/N): " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Registration cancelled."
        exit 0
    fi

    echo "Cleaning up existing runner..."

    # 停止现有 runner
    echo "Stopping existing runner..."
    supervisorctl stop "runner-${RUNNER_NAME}" 2>/dev/null || true

    # 删除 supervisor 配置
    rm -f "/etc/supervisor/conf.d/runner-${RUNNER_NAME}.conf"

    # 重新加载 supervisor
    supervisorctl reread 2>/dev/null || true
    supervisorctl update 2>/dev/null || true

    # 删除日志
    rm -f "/var/log/supervisor/runner-${RUNNER_NAME}".*.log*

    # 删除旧配置和缓存
    rm -f .runner config.yaml
    rm -rf cache
fi

# 执行注册
echo ""
echo "Registering runner..."
act_runner register \
    --instance "$GITEA_INSTANCE" \
    --token "$GITEA_TOKEN" \
    --name "$RUNNER_NAME" \
    --labels "$RUNNER_LABELS" \
    --no-interactive

if [ ! -f ".runner" ]; then
    echo ""
    echo "✗ Registration failed! .runner file not created."
    exit 1
fi

echo "✓ Registration successful!"

# 生成配置文件
echo ""
echo "Generating config.yaml..."
act_runner generate-config > config.yaml

echo "✓ Configuration file generated!"

# 创建缓存目录
mkdir -p cache

# 使用 Python 修改配置(最可靠的方法)
echo ""
echo "Configuring runner settings..."

if command -v python3 &> /dev/null; then
    python3 << PYEOF
import yaml
import sys

try:
    # 读取生成的配置
    with open('config.yaml', 'r') as f:
        config = yaml.safe_load(f)

    # 读取 .runner 获取实际注册的 labels
    import json
    with open('.runner', 'r') as f:
        runner_data = json.load(f)

    # 使用 .runner 中的 labels这是实际注册的
    registered_labels = runner_data.get('labels', [])

    # 修改配置
    if 'runner' not in config:
        config['runner'] = {}

    # 使用实际注册的 labels
    config['runner']['labels'] = registered_labels
    config['runner']['capacity'] = 2

    # 启用缓存
    if 'cache' not in config:
        config['cache'] = {}
    config['cache']['enabled'] = True
    config['cache']['dir'] = './cache'

    # 保存配置
    with open('config.yaml', 'w') as f:
        yaml.dump(config, f, default_flow_style=False, sort_keys=False)

    print("✓ Configuration updated using Python")
    print(f"  - Labels: {registered_labels}")
    print(f"  - Capacity: 2")
    print(f"  - Cache enabled: ./cache")
    sys.exit(0)

except Exception as e:
    print(f"✗ Python configuration failed: {e}", file=sys.stderr)
    sys.exit(1)
PYEOF

    PYTHON_EXIT=$?

    if [ $PYTHON_EXIT -ne 0 ]; then
        echo ""
        echo "⚠ Python configuration failed, using basic sed..."

        # 基本的 sed 修改(只修改简单的值,不动 labels
        sed -i 's/capacity: 1/capacity: 2/g' config.yaml || true
        sed -i 's/enabled: false/enabled: true/g' config.yaml || true
        sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true

        echo "✓ Basic configuration applied"
        echo "  Note: Please manually verify labels in config.yaml match .runner"
    fi
else
    echo "⚠ Python3 not found, applying basic configuration..."

    # 基本的 sed 修改
    sed -i 's/capacity: 1/capacity: 2/g' config.yaml || true
    sed -i 's/enabled: false/enabled: true/g' config.yaml || true
    sed -i 's|dir: ""|dir: ./cache|g' config.yaml || true

    echo "✓ Basic configuration applied"
    echo "  Note: Labels will use act_runner defaults"
fi

# 验证配置文件
echo ""
echo "Validating configuration..."

# 检查 YAML 语法
if command -v python3 &> /dev/null; then
    python3 << PYEOF
import yaml
import sys
try:
    with open('config.yaml', 'r') as f:
        yaml.safe_load(f)
    print("✓ config.yaml syntax is valid")
    sys.exit(0)
except Exception as e:
    print(f"✗ config.yaml syntax error: {e}", file=sys.stderr)
    sys.exit(1)
PYEOF

    if [ $? -ne 0 ]; then
        echo ""
        echo "✗ Configuration file has syntax errors!"
        echo "  Backup available at: config.yaml.bak"
        exit 1
    fi
fi

# 显示配置摘要
echo ""
echo "Configuration Summary:"
echo "-------------------------------------------"
echo ".runner labels:"
cat .runner | grep -A 10 '"labels"' | head -15
echo ""
echo "config.yaml labels:"
grep -A 5 "^  labels:" config.yaml | head -10

# 创建 supervisor 配置
echo ""
echo "Creating supervisor configuration..."
cat > "/etc/supervisor/conf.d/runner-${RUNNER_NAME}.conf" <<EOF
[program:runner-${RUNNER_NAME}]
command=/usr/local/bin/act_runner daemon --config ${RUNNER_DIR}/config.yaml
directory=${RUNNER_DIR}
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/runner-${RUNNER_NAME}.err.log
stdout_logfile=/var/log/supervisor/runner-${RUNNER_NAME}.out.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10
user=root
environment=HOME="/root"
EOF

echo "✓ Supervisor configuration created"

# 重新加载 supervisor
echo ""
echo "Reloading supervisor..."
supervisorctl reread
supervisorctl update

echo "Starting runner..."
sleep 2
# 启动 runner
supervisorctl restart "runner-${RUNNER_NAME}" 2>/dev/null || \
    supervisorctl start "runner-${RUNNER_NAME}"

# 等待启动
sleep 3

# 显示状态
RUNNER_STATUS=$(supervisorctl status "runner-${RUNNER_NAME}" 2>/dev/null || echo "UNKNOWN")

echo ""
echo "=========================================="
echo "✓ Runner registered and started!"
echo "=========================================="
echo ""
echo "Runner Information:"
echo "  Name:      $RUNNER_NAME"
echo "  Directory: $RUNNER_DIR"
echo "  Status:    $RUNNER_STATUS"
echo ""
echo "Configuration files:"
echo "  .runner:      $(ls -lh .runner 2>/dev/null | awk '{print $5}' || echo 'N/A')"
echo "  config.yaml:  $(ls -lh config.yaml 2>/dev/null | awk '{print $5}' || echo 'N/A')"
echo ""
echo "Useful commands:"
echo "  View logs:     docker-compose exec gitea-runner /data/manage.sh logs ${RUNNER_NAME}"
echo "  Follow logs:   docker-compose exec gitea-runner /data/manage.sh follow ${RUNNER_NAME}"
echo "  Check status:  docker-compose exec gitea-runner /data/manage.sh status"
echo "  Restart:       docker-compose exec gitea-runner /data/manage.sh restart ${RUNNER_NAME}"
echo ""

# 显示最近的日志
if [ -f "/var/log/supervisor/runner-${RUNNER_NAME}.out.log" ]; then
    echo "Recent logs:"
    echo "-------------------------------------------"
    tail -n 20 "/var/log/supervisor/runner-${RUNNER_NAME}.out.log" 2>/dev/null || echo "No logs yet"
    echo ""
fi

# 检查是否有错误
if [ -f "/var/log/supervisor/runner-${RUNNER_NAME}.err.log" ]; then
    # 只查找 error/fatal/panic 级别的日志
    ERR_CONTENT=$(grep -E 'level=(error|fatal|panic)' \
                  "/var/log/supervisor/runner-${RUNNER_NAME}.err.log" | tail -n 5 2>/dev/null)

    if [ -n "$ERR_CONTENT" ]; then
        echo "❌ Recent errors detected:"
        echo "-------------------------------------------"
        echo "$ERR_CONTENT"
        echo ""
        echo "Check full error log with:"
        echo "  docker-compose exec gitea-runner cat /var/log/supervisor/runner-${RUNNER_NAME}.err.log"
    fi
fi

6 manage.sh

创建 manage.sh 文件:

#!/bin/bash

echo "=========================================="
echo "      Gitea Runner Management Tool       "
echo "=========================================="
echo ""

# 函数:列出所有 runners
list_runners() {
    echo "Registered Runners:"
    echo "-------------------------------------------"

    if [ ! -d "/data/runners" ] || [ -z "$(ls -A /data/runners 2>/dev/null)" ]; then
        echo "No runners registered yet."
        return
    fi

    printf "%-20s %-15s %-30s\n" "Name" "Status" "Log File"
    echo "-------------------------------------------"

    for runner_dir in /data/runners/*/; do
        if [ -d "$runner_dir" ]; then
            runner_name=$(basename "$runner_dir")

            if [ -f "$runner_dir/.runner" ]; then
                # 获取状态
                status=$(supervisorctl status "runner-${runner_name}" 2>/dev/null | awk '{print $2}')
                [ -z "$status" ] && status="NOT_LOADED"

                log_file="/var/log/supervisor/runner-${runner_name}.out.log"

                printf "%-20s %-15s %-30s\n" "$runner_name" "$status" "$log_file"
            fi
        fi
    done
    echo ""
}

# 函数:查看 runner 详细信息
show_runner() {
    local runner_name=$1
    local runner_dir="/data/runners/${runner_name}"

    if [ ! -d "$runner_dir" ]; then
        echo "✗ Runner '$runner_name' not found!"
        return 1
    fi

    echo "Runner Details: $runner_name"
    echo "-------------------------------------------"
    echo "Directory: $runner_dir"

    if [ -f "$runner_dir/config.yaml" ]; then
        echo ""
        echo "Configuration:"
        cat "$runner_dir/config.yaml"
    fi

    echo ""
    echo "Status:"
    supervisorctl status "runner-${runner_name}" 2>/dev/null | awk '{print $2}'
    echo ""
}

# 函数:删除 runner
delete_runner() {
    local runner_name=$1
    local runner_dir="/data/runners/${runner_name}"

    if [ ! -d "$runner_dir" ]; then
        echo "✗ Runner '$runner_name' not found!"
        return 1
    fi

    echo "⚠ Warning: This will permanently delete runner '$runner_name'"
    read -p "Are you sure? (y/N): " -n 1 -r
    echo

    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "Deletion cancelled."
        return 0
    fi

    echo "Stopping runner..."
    supervisorctl stop "runner-${runner_name}" 2>/dev/null || true

    echo "Removing configuration..."
    rm -f "/etc/supervisor/conf.d/runner-${runner_name}.conf"

    echo "Deleting runner directory..."
    rm -rf "$runner_dir"

    echo "Updating supervisor..."
    supervisorctl reread
    supervisorctl update

    echo ""
    echo "✓ Runner '$runner_name' deleted successfully!"
}

# 函数:查看 runner 日志
logs_runner() {
    local runner_name=$1
    local lines=${2:-50}

    local log_file="/var/log/supervisor/runner-${runner_name}.out.log"

    if [ ! -f "$log_file" ]; then
        echo "✗ Log file not found for runner '$runner_name'"
        return 1
    fi

    echo "Showing last $lines lines of '$runner_name' logs:"
    echo "-------------------------------------------"
    tail -n "$lines" "$log_file"
}

# 函数:实时查看日志
follow_logs() {
    local runner_name=$1
    local log_file="/var/log/supervisor/runner-${runner_name}.out.log"

    if [ ! -f "$log_file" ]; then
        echo "✗ Log file not found for runner '$runner_name'"
        return 1
    fi

    echo "Following logs for '$runner_name' (Press Ctrl+C to exit):"
    echo "-------------------------------------------"
    tail -f "$log_file"
}

# 函数:启动/停止/重启 runner
control_runner() {
    local action=$1
    local runner_name=$2

    case $action in
        start|stop|restart)
            echo "${action^}ing runner '$runner_name'..."
            supervisorctl "$action" "runner-${runner_name}"
            ;;
        *)
            echo "✗ Invalid action: $action"
            return 1
            ;;
    esac
}

# 函数:显示所有 runner 状态
status_all() {
    echo "All Runners Status:"
    echo "-------------------------------------------"
    supervisorctl status | grep "^runner-" || echo "No runners running."
    echo ""
}

# 主菜单
show_menu() {
    echo "Choose an action:"
    echo "  1) List all runners"
    echo "  2) Show runner details"
    echo "  3) Add new runner"
    echo "  4) Delete runner"
    echo "  5) Start runner"
    echo "  6) Stop runner"
    echo "  7) Restart runner"
    echo "  8) View runner logs"
    echo "  9) Follow runner logs (real-time)"
    echo " 10) Show all runners status"
    echo "  0) Exit"
    echo ""
}

# 主程序
if [ $# -eq 0 ]; then
    # 交互模式
    while true; do
        show_menu
        read -p "Enter your choice: " choice
        echo ""

        case $choice in
            1)
                list_runners
                ;;
            2)
                read -p "Enter runner name: " runner_name
                show_runner "$runner_name"
                ;;
            3)
                echo "Starting registration process..."
                /data/register.sh
                ;;
            4)
                read -p "Enter runner name to delete: " runner_name
                delete_runner "$runner_name"
                ;;
            5)
                read -p "Enter runner name to start: " runner_name
                control_runner start "$runner_name"
                ;;
            6)
                read -p "Enter runner name to stop: " runner_name
                control_runner stop "$runner_name"
                ;;
            7)
                read -p "Enter runner name to restart: " runner_name
                control_runner restart "$runner_name"
                ;;
            8)
                read -p "Enter runner name: " runner_name
                read -p "Number of lines (default 50): " lines
                logs_runner "$runner_name" "${lines:-50}"
                ;;
            9)
                read -p "Enter runner name: " runner_name
                follow_logs "$runner_name"
                ;;
            10)
                status_all
                ;;
            0)
                echo "Goodbye!"
                exit 0
                ;;
            *)
                echo "Invalid choice!"
                ;;
        esac

        echo ""
        read -p "Press Enter to continue..."
        clear
    done
else
    # 命令行模式
    case $1 in
        list|ls)
            list_runners
            ;;
        show|info)
            show_runner "$2"
            ;;
        add|register)
            /data/register.sh
            ;;
        delete|rm|remove)
            delete_runner "$2"
            ;;
        start)
            control_runner start "$2"
            ;;
        stop)
            control_runner stop "$2"
            ;;
        restart)
            control_runner restart "$2"
            ;;
        logs)
            logs_runner "$2" "${3:-50}"
            ;;
        follow)
            follow_logs "$2"
            ;;
        status)
            status_all
            ;;
        *)
            echo "Usage: $0 [command] [runner_name]"
            echo ""
            echo "Commands:"
            echo "  list                  - List all runners"
            echo "  show <name>           - Show runner details"
            echo "  add                   - Add new runner"
            echo "  delete <name>         - Delete a runner"
            echo "  start <name>          - Start a runner"
            echo "  stop <name>           - Stop a runner"
            echo "  restart <name>        - Restart a runner"
            echo "  logs <name> [lines]   - View runner logs"
            echo "  follow <name>         - Follow runner logs (real-time)"
            echo "  status                - Show all runners status"
            echo ""
            echo "Or run without arguments for interactive mode."
            exit 1
            ;;
    esac
fi

🚀 部署步骤

1. 设置脚本权限

chmod +x entrypoint.sh setup.sh register.sh manage.sh

2. 构建并启动容器

docker-compose build
docker-compose up -d

3. 安装 Runner

docker-compose exec gitea-runner /data/setup.sh

按照提示选择版本和架构。

4. 注册 Runner

docker-compose exec gitea-runner /data/register.sh

输入你的 Gitea 实例 URL 和注册令牌。

5. 重启容器启动 Runner

docker-compose restart

6. 验证运行状态

# 查看日志
docker-compose logs -f

# 查看 runner 状态
docker-compose exec gitea-runner /data/manage.sh list
docker-compose exec gitea-runner /data/manage.sh status

🚀 版本 2Buildx 多架构版

📝 文件配置差异

Buildx 版本与标准版的主要区别在于以下文件:

1 DockerfileBuildx 版)

FROM ubuntu:22.04

# 设置环境变量避免交互式安装
ENV DEBIAN_FRONTEND=noninteractive

# 更新系统并安装必要软件
RUN apt-get update && apt-get install -y \
    curl \
    git \
    python3 \
    python3-yaml \
    supervisor \
    ca-certificates \
    gnupg \
    lsb-release \
    qemu-user-static \
    binfmt-support \
    && rm -rf /var/lib/apt/lists/*

# 安装 Docker包含 Buildx 插件)
RUN curl -fsSL https://get.docker.com -o get-docker.sh && \
    sh get-docker.sh && \
    rm get-docker.sh

# 验证安装
RUN docker --version && \
    qemu-aarch64-static --version && \
    qemu-x86_64-static --version

# 创建必要目录
RUN mkdir -p /data /etc/supervisor/conf.d /var/log/supervisor

# 设置工作目录
WORKDIR /data

# 使用自定义入口点
ENTRYPOINT ["/data/entrypoint.sh"]

2 docker-compose.ymlBuildx 版)

services:
  gitea-runner:
    build: .
    container_name: gitea-runner
    restart: unless-stopped
    privileged: true
    volumes:
      - ./runner-data:/data
      - ./setup.sh:/data/setup.sh:ro
      - ./register.sh:/data/register.sh:ro
      - ./manage.sh:/data/manage.sh:ro
      - ./entrypoint.sh:/data/entrypoint.sh:ro
      - /var/run/docker.sock:/var/run/docker.sock

    environment:
      - TZ=Asia/Shanghai

      # 如果需要使用代理,取消下面的注释并修改为你的代理地址
      # 注意:容器内访问宿主机需要使用 host.docker.internal 或宿主机IP
      - http_proxy=http://host.docker.internal:20122
      - https_proxy=http://host.docker.internal:20122
      - HTTP_PROXY=http://host.docker.internal:20122
      - HTTPS_PROXY=http://host.docker.internal:20122
      # - no_proxy=localhost,127.0.0.1

    # Linux 系统需要取消下面的注释以支持 host.docker.internal
    # extra_hosts:
    #   - "host.docker.internal:host-gateway"

3 entrypoint.shBuildx 版)

#!/bin/bash
set -e

echo "==================================="
echo "Gitea Runner Container Starting..."
echo "==================================="

# 定义路径
PERSISTENT_BIN="/data/bin"
RUNNER_PATH="$PERSISTENT_BIN/act_runner"
SYSTEM_LINK="/usr/local/bin/act_runner"

# 创建必要目录
mkdir -p /data/runners
mkdir -p "$PERSISTENT_BIN"
mkdir -p /data/buildx
mkdir -p /var/log/supervisor
mkdir -p /var/run

# ============================================
# 初始化 Docker Buildx 支持
# ============================================
echo ""
echo "Initializing Docker Buildx..."

# 启动 Docker 守护进程(如果使用主机 socket 则跳过)
if [ -S /var/run/docker.sock ]; then
    echo "✓ Using host Docker socket"
else
    echo "Starting Docker daemon..."
    dockerd > /var/log/dockerd.log 2>&1 &
    sleep 5
fi

# 等待 Docker 就绪
echo "Waiting for Docker daemon..."
for i in {1..30}; do
    if docker info > /dev/null 2>&1; then
        echo "✓ Docker daemon is ready"
        break
    fi
    if [ $i -eq 30 ]; then
        echo "✗ Docker daemon failed to start"
        [ -f /var/log/dockerd.log ] && cat /var/log/dockerd.log
        exit 1
    fi
    sleep 1
done

# 注册 QEMU binfmt
echo ""
echo "Registering QEMU binary formats..."
update-binfmts --enable 2>/dev/null || {
    echo "⚠ binfmt_misc not available, trying to mount..."
    mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc 2>/dev/null || true
    update-binfmts --enable
}

# 验证多架构支持
echo "Verifying multi-arch support..."
if docker run --rm arm64v8/alpine uname -m > /dev/null 2>&1; then
    echo "  ✓ arm64 support verified"
else
    echo "  ⚠ arm64 verification failed"
fi

if docker run --rm amd64/alpine uname -m > /dev/null 2>&1; then
    echo "  ✓ amd64 support verified"
else
    echo "  ⚠ amd64 verification failed"
fi

# 配置 Buildx
if [ ! -f "/data/buildx/.configured" ]; then
    echo ""
    echo "Setting up Buildx for the first time..."

    # 创建 BuildKit 配置
    cat > /data/buildx/buildkitd.toml <<EOF
[worker.oci]
  max-parallelism = 4
EOF

    # 创建 Buildx builder
    docker buildx create \
        --name gitea-multiarch \
        --driver docker-container \
        --bootstrap \
        --use \
        --config /data/buildx/buildkitd.toml 2>/dev/null || \
    docker buildx use gitea-multiarch 2>/dev/null

    # 验证
    echo "Verifying Buildx..."
    docker buildx inspect --bootstrap > /dev/null 2>&1

    # 标记为已配置
    touch /data/buildx/.configured

    echo "✓ Buildx configured successfully!"
    docker buildx inspect | grep "Platforms:" | head -1
else
    echo "✓ Buildx already configured"

    # 确保 builder 可用
    docker buildx use gitea-multiarch 2>/dev/null || {
        echo "⚠ Recreating Buildx builder..."
        docker buildx rm gitea-multiarch 2>/dev/null || true
        docker buildx create \
            --name gitea-multiarch \
            --driver docker-container \
            --bootstrap \
            --use 2>/dev/null
    }
fi

echo ""

# ============================================
# 检查 act_runner 安装
# ============================================
# 创建主 supervisor 配置文件
cat > /etc/supervisor/supervisord.conf <<EOF
[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[unix_http_server]
file=/var/run/supervisor.sock
chmod=0700

[supervisorctl]
serverurl=unix:///var/run/supervisor.sock

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[include]
files = /etc/supervisor/conf.d/*.conf
EOF

# 检查持久化目录中的 act_runner
if [ -f "$RUNNER_PATH" ]; then
    echo "✓ Found act_runner in persistent storage"
    echo "  Location: $RUNNER_PATH"

    if [ ! -L "$SYSTEM_LINK" ] || [ ! -e "$SYSTEM_LINK" ]; then
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "  Created system link: $SYSTEM_LINK -> $RUNNER_PATH"
    fi

    RUNNER_VERSION=$("$SYSTEM_LINK" --version 2>/dev/null || echo "unknown")
    echo "  Version: $RUNNER_VERSION"
elif [ -f "$SYSTEM_LINK" ]; then
    echo "⚠ Found act_runner in system path, migrating to persistent storage..."
    cp "$SYSTEM_LINK" "$RUNNER_PATH"
    chmod +x "$RUNNER_PATH"
    ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
    echo "  ✓ Migrated to: $RUNNER_PATH"
else
    echo "⚠ act_runner not installed yet!"
    echo ""
    echo "Please run the setup script first:"
    echo "  docker-compose exec gitea-runner /data/setup.sh"
    echo ""
    echo "Container is waiting..."

    while [ ! -f "$RUNNER_PATH" ] && [ ! -f "$SYSTEM_LINK" ]; do
        sleep 10
    done

    if [ -f "$RUNNER_PATH" ]; then
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "✓ act_runner detected and linked!"
    elif [ -f "$SYSTEM_LINK" ]; then
        cp "$SYSTEM_LINK" "$RUNNER_PATH"
        ln -sf "$RUNNER_PATH" "$SYSTEM_LINK"
        echo "✓ act_runner detected and migrated!"
    fi
fi

# ============================================
# 配置已注册的 Runners
# ============================================
echo ""
echo "Scanning for registered runners..."
RUNNER_COUNT=0

if [ -d "/data/runners" ]; then
    for runner_dir in /data/runners/*/; do
        if [ -d "$runner_dir" ]; then
            runner_name=$(basename "$runner_dir")

            if [ -f "$runner_dir/.runner" ] && [ -f "$runner_dir/config.yaml" ]; then
                echo "Found runner: $runner_name"

                cat > "/etc/supervisor/conf.d/runner-${runner_name}.conf" <<EOF
[program:runner-${runner_name}]
command=/usr/local/bin/act_runner daemon --config ${runner_dir}/config.yaml
directory=${runner_dir}
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/runner-${runner_name}.err.log
stdout_logfile=/var/log/supervisor/runner-${runner_name}.out.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
stderr_logfile_maxbytes=50MB
stderr_logfile_backups=10
user=root
environment=HOME="/root",PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
EOF
                RUNNER_COUNT=$((RUNNER_COUNT + 1))
            fi
        fi
    done
fi

if [ $RUNNER_COUNT -eq 0 ]; then
    echo "⚠ No runners registered yet!"
    echo ""
    echo "Please run the register script to add a runner:"
    echo "  docker-compose exec gitea-runner /data/register.sh"
    echo ""
fi

echo "Total runners configured: $RUNNER_COUNT"
echo ""
echo "==================================="
echo "Starting Supervisor..."
echo "==================================="

# 启动 supervisord
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf

4 setup.sh, register.sh, manage.sh

这三个文件与标准版完全相同

🚀 部署步骤Buildx 版)

1. 设置权限

chmod +x entrypoint.sh setup.sh register.sh manage.sh

2. 构建并启动

docker-compose build
docker-compose up -d

3. 验证 Buildx

# 查看日志
docker-compose logs -f

# 应该看到:
# ✓ Docker daemon is ready
# ✓ arm64 support verified
# ✓ amd64 support verified
# ✓ Buildx configured successfully!

# 进入容器验证
docker-compose exec gitea-runner docker buildx ls
# 应该看到 gitea-multiarch builder

4. 安装和注册

docker-compose exec gitea-runner /data/setup.sh
docker-compose exec gitea-runner /data/register.sh
docker-compose restart

🔧 Buildx 多架构构建示例

示例 1基础构建

在 Gitea Actions workflow 中使用:

name: Multi-Arch Build

on: [push]

jobs:
  build:
    runs-on: ubuntu-22.04
    steps:
      - uses: actions/checkout@v3

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v2

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build and Push
        uses: docker/build-push-action@v4
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          tags: myapp:latest
          push: true

示例 2Go 应用优化构建

使用交叉编译可以大幅提升构建速度:

FROM --platform=$BUILDPLATFORM golang:1.21 AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app

FROM alpine
COPY --from=builder /app/app /app
CMD ["/app"]

⚖️ 版本对比

功能对比表

功能 标准版 Buildx 版
基础 Runner 功能
arm64 原生构建
amd64 模拟构建
多架构镜像
Buildx 支持
QEMU 内置
特权模式要求 必需
Docker socket 可选 必需
镜像大小 ~400MB ~850MB
启动时间 5-10 秒 15-20 秒
复杂度 简单 中等

性能对比Buildx 版)

在 ARM64 主机上使用 Buildx 构建不同架构的性能:

项目类型 ARM64 (原生) AMD64 (QEMU) 双架构并行
Alpine 镜像 5s 15s 18s
Node.js 应用 2min 8min 9min
Go 应用 1.5min 6min 7min
Go交叉编译 1min 1.5min 2min

关键发现:

  • 🏆 ARM64 原生构建最快
  • 🐌 AMD64 模拟慢 3-4 倍QEMU
  • 交叉编译比纯 QEMU 快 4 倍
  • 💾 使用缓存可提速 80-90%

🎯 选择建议

选择标准版

  • 只在 arm64 上运行 arm64 应用
  • 不需要发布多架构镜像
  • 追求简单和轻量
  • 不需要 Docker-in-Docker

选择 Buildx 版

  • 需要构建 amd64 + arm64 镜像
  • 发布到 Docker Hub 等公共仓库
  • 跨平台应用开发
  • CI/CD 需要多架构支持

常见问题

Q1: 如何更新 Runner 版本?

docker-compose exec gitea-runner /data/setup.sh
# 脚本会询问是否升级

Q2: 如何添加多个 Runners

# 多次运行注册脚本
docker-compose exec gitea-runner /data/register.sh
# 每次使用不同名称

# 查看所有 runners
docker-compose exec gitea-runner /data/manage.sh list

Q3: 容器启动失败 "exec format error"

原因: Windows 换行符问题

解决:

# 转换换行符
sed -i 's/\r$//' *.sh
chmod +x *.sh

# 重新构建
docker-compose down
docker-compose build --no-cache
docker-compose up -d

Q4: 如何完全重置?

docker-compose down -v
rm -rf runner-data
docker-compose build --no-cache
docker-compose up -d

Q5: 如何清理 Docker 资源?

🧹 清理未使用的镜像

# 清理悬空镜像dangling images
docker image prune

# 清理所有未使用的镜像
docker image prune -a

# 查看镜像占用空间
docker system df

🗑️ 清理 Buildx 缓存(仅 Buildx 版)

# 清理构建缓存(保留 builder
docker buildx prune

# 清理所有构建缓存
docker buildx prune -a -f

# 删除 builder会自动清理相关容器和镜像
docker buildx rm gitea-multiarch

🔄 完整清理流程

# 1. 停止所有容器
docker-compose down

# 2. 清理 Buildx如果使用 Buildx 版)
docker buildx rm gitea-multiarch

# 3. 清理未使用的镜像
docker image prune -a

# 4. 清理系统(慎用!会清理所有未使用资源)
docker system prune -a --volumes

# 5. 查看清理效果
docker system df

⚠️ 注意事项

  • docker system prune -a 会删除所有未使用的镜像,包括其他项目的
  • 如果只想清理 Gitea Runner 相关资源,建议单独删除:
  docker rmi buildx-gitea-runner:latest
  docker rmi arm64v8/alpine:latest
  docker rmi moby/buildkit:buildx-stable-1

Q6: 如何查看日志?

# 容器日志
docker-compose logs -f

# Runner 日志
docker-compose exec gitea-runner /data/manage.sh logs runner-name

# 实时跟踪日志
docker-compose exec gitea-runner /data/manage.sh follow runner-name

Q7: Buildx 版 - 构建速度慢?

优化方法:

1. 启用缓存

- name: Build with cache
  uses: docker/build-push-action@v4
  with:
    cache-from: type=registry,ref=myimage:buildcache
    cache-to: type=registry,ref=myimage:buildcache,mode=max
    platforms: linux/amd64,linux/arm64

2. 使用交叉编译

FROM --platform=$BUILDPLATFORM golang:1.21 AS builder
ARG TARGETOS TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app

3. 按需构建

# 开发分支只构建 arm64生产分支构建双架构
platforms: ${{ github.ref == 'refs/heads/main' && 'linux/amd64,linux/arm64' || 'linux/arm64' }}

Q8: Buildx 版 - 如何验证 Buildx

# 进入容器
docker-compose exec gitea-runner bash

# 检查 builder
docker buildx ls

# 测试构建
echo 'FROM alpine' | docker buildx build --platform linux/amd64,linux/arm64 -

📚 参考资源


💬 反馈与支持

如果你在使用过程中遇到问题,欢迎:

  • 📝 提交 Issue
  • 💡 分享你的使用经验
  • 🌟 给项目点个 Star