Automation intermediate ⏱ 60 minutes

Gitea Actions: Self-Hosted CI/CD That's 40x Lighter Than GitLab

GitHub Actions-compatible CI/CD without the resource overhead. Complete self-hosted DevOps pipeline.

By homelabbing.io • 2025-12-16
#gitea#ci-cd#github-actions#devops#self-hosted

Overview

GitLab CI requires 4-8GB of RAM and complex setup. GitHub Actions requires trusting Microsoft with your code and runners. Gitea Actions gives you GitHub Actions compatibility on hardware that fits in your homelab—running smoothly on as little as 512MB RAM while supporting the same workflow syntax you already know.

Introduced in Gitea 1.19, Gitea Actions provides a built-in CI/CD system that’s compatible with the GitHub Actions ecosystem. You get access to thousands of existing actions from the GitHub Marketplace while keeping your code, runners, and secrets completely under your control.

What you’ll learn:

  • Installing Gitea with Actions enabled
  • Setting up and registering Gitea Act Runners
  • Writing workflow files compatible with GitHub Actions
  • Managing secrets and environment variables
  • Building Docker images in your pipelines
  • Deploying applications automatically
  • Comparing Gitea Actions to GitHub Actions and GitLab CI
  • Production deployment and security best practices

Why Gitea Actions for Homelabs?

Run your entire git infrastructure and CI/CD on a Raspberry Pi 4. Test infrastructure code (OpenTofu, Ansible) before applying to production. Build and push Docker images automatically. Deploy to Kubernetes when you push to main. All with familiar GitHub Actions syntax, but completely self-hosted and lightweight.


Prerequisites

Before starting, ensure you have:

  • Linux server with Docker installed (512MB+ RAM for Gitea, 1GB+ recommended with runners)
  • Basic Git knowledge (commits, branches, pushing/pulling)
  • Understanding of CI/CD concepts (build, test, deploy pipelines)
  • Storage: 10GB+ for Gitea and repositories
  • Network: Port 3000 (web UI) and 22 (SSH git access)

Optional but helpful:

  • Familiarity with GitHub Actions syntax
  • Docker knowledge for building images
  • Reverse proxy experience (Nginx/Caddy)

Understanding Gitea Actions Architecture

Components Overview

Gitea Server:

  • Git repository hosting
  • Web interface for code browsing
  • Actions orchestration (schedules workflows, manages queues)
  • Workflow definition storage

Act Runner:

  • Separate daemon that executes jobs
  • Polls Gitea for jobs to run
  • Supports Docker, host, and shell executors
  • Can run on same host as Gitea or distributed

Workflow Files:

  • YAML files in .gitea/workflows/ or .github/workflows/
  • Define jobs, steps, and triggers
  • GitHub Actions compatible syntax

Gitea Actions vs. GitHub Actions

FeatureGitea ActionsGitHub Actions
HostingSelf-hostedCloud (GitHub)
PrivacyComplete controlCode on GitHub servers
Resource Requirements512MB RAM+N/A (GitHub provides)
Workflow SyntaxGitHub Actions compatibleNative
Action MarketplaceUse GitHub ActionsYes
Runner HardwareYour infrastructureGitHub-hosted or self-hosted
CostFree (your hardware)Free tier + paid minutes
Custom RunnersYesYes (self-hosted)
Artifact StorageYour storageGitHub storage (limits apply)

Key difference: Gitea Actions runs on your hardware with the same syntax as GitHub Actions.

How Workflows Execute

[Git Push] → [Gitea detects push] → [Parses workflow file] → [Queues job]
                                                                    ↓
[Act Runner polls Gitea] ← [Job assigned] ← [Runner picks up job]
                                                                    ↓
[Runner executes steps] → [Reports status] → [Gitea updates UI]

Installing Gitea with Actions

Create docker-compose.yml:

version: '3.8'

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__server__DOMAIN=git.homelab.local
      - GITEA__server__ROOT_URL=http://git.homelab.local:3000
      - GITEA__server__HTTP_PORT=3000
      - GITEA__database__DB_TYPE=sqlite3
      - GITEA__actions__ENABLED=true
    ports:
      - "3000:3000"
      - "2222:22"
    volumes:
      - ./gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - gitea

networks:
  gitea:
    driver: bridge

Deploy:

# Create directory
mkdir -p ~/gitea && cd ~/gitea

# Create compose file (above)
nano docker-compose.yml

# Start Gitea
docker-compose up -d

# Check logs
docker-compose logs -f gitea

Access Gitea:

Navigate to http://localhost:3000 (or http://YOUR-SERVER-IP:3000)

Initial setup:

  1. Database: SQLite (default, simplest for homelabs)
  2. Site Title: Your Homelab Git
  3. Server Domain: git.homelab.local
  4. SSH Port: 2222
  5. Admin account: Create your admin user
  6. Click Install Gitea

Method 2: Binary Installation (Linux)

# Download latest Gitea binary
wget -O gitea https://dl.gitea.io/gitea/latest/gitea-latest-linux-amd64
chmod +x gitea

# Create git user
sudo adduser --system --shell /bin/bash --group --disabled-password --home /home/git git

# Create directories
sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R git:git /var/lib/gitea/
sudo chmod -R 750 /var/lib/gitea/

# Move binary
sudo mv gitea /usr/local/bin/gitea

Create systemd service:

# /etc/systemd/system/gitea.service
[Unit]
Description=Gitea
After=network.target

[Service]
Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable gitea
sudo systemctl start gitea
sudo systemctl status gitea

Enabling Actions in Configuration

Edit app.ini (Docker: in gitea-data/gitea/conf/app.ini):

[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://github.com

Restart Gitea:

# Docker
docker-compose restart gitea

# Systemd
sudo systemctl restart gitea

Verify Actions enabled:

  1. Log into Gitea web UI
  2. Go to Site Administration (gear icon)
  3. Check Configuration
  4. Look for [actions] section with ENABLED = true

Setting Up Act Runners

Act Runners execute your workflow jobs. You need at least one runner for Actions to work.

Installing Act Runner (Docker)

Option 1: Docker Compose (add to existing file):

# Add to existing docker-compose.yml
  act_runner:
    image: gitea/act_runner:latest
    container_name: act_runner
    restart: unless-stopped
    environment:
      - GITEA_INSTANCE_URL=http://gitea:3000
      - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
      - GITEA_RUNNER_NAME=docker-runner-01
    volumes:
      - ./act-runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - gitea
    networks:
      - gitea

Get registration token:

  1. Log into Gitea as admin
  2. Go to Site Administration → Actions → Runners
  3. Click Create new Runner
  4. Copy the registration token
  5. Save to .env file:
echo "RUNNER_TOKEN=your-token-here" > .env

Start runner:

docker-compose up -d act_runner
docker-compose logs -f act_runner

Registering Runners Manually

Alternative: Manual registration process:

# Generate registration token in Gitea UI
# Site Admin → Actions → Runners → Create

# Register runner
docker run --rm -it   -v $(pwd)/act-runner-data:/data   gitea/act_runner:latest   register   --instance http://gitea:3000   --token YOUR_REGISTRATION_TOKEN   --name docker-runner-01   --labels docker:docker://node:16,ubuntu-latest:docker://catthehacker/ubuntu:act-latest

Verify registration:

  1. Gitea UI → Site Admin → Actions → Runners
  2. You should see your runner listed
  3. Status: “Idle” (ready to accept jobs)

Runner Configuration

Edit runner config (act-runner-data/config.yaml):

log:
  level: info

runner:
  file: .runner
  capacity: 1
  timeout: 3h
  insecure: false

  labels:
    - "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
    - "ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04"
    - "node:docker://node:16"
    - "python:docker://python:3.10"

cache:
  enabled: true
  dir: /data/cache

container:
  network: bridge
  privileged: false
  options: ""

Restart runner after config changes:

docker-compose restart act_runner

Your First Gitea Workflow

Creating a Repository

  1. Log into Gitea
  2. Click + → New Repository
  3. Configure:
    • Owner: Your user
    • Repository Name: hello-gitea-actions
    • Visibility: Private
    • Initialize with README: Checked
  4. Click Create Repository

Writing a Simple Workflow

Create workflow file:

In your repository, create .gitea/workflows/hello.yml:

name: Hello Gitea Actions

on:
  push:
    branches:
      - main
  workflow_dispatch:  # Allows manual trigger

jobs:
  hello:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Say hello
        run: |
          echo "Hello from Gitea Actions!"
          echo "Running on: $(uname -a)"
          echo "Current directory: $(pwd)"
          ls -la

      - name: Show environment
        run: env | sort

Commit and push:

git clone http://git.homelab.local:3000/yourusername/hello-gitea-actions.git
cd hello-gitea-actions

mkdir -p .gitea/workflows
# Create hello.yml with content above

git add .gitea/workflows/hello.yml
git commit -m "Add first workflow"
git push origin main

Watch it run:

  1. Go to repository in Gitea web UI
  2. Click Actions tab
  3. You should see workflow running
  4. Click on workflow run to see details
  5. Click on job to see step outputs

Workflow Triggers

Common trigger types:

# Trigger on push to specific branches
on:
  push:
    branches:
      - main
      - develop

# Trigger on pull request
on:
  pull_request:
    branches:
      - main

# Trigger on schedule (cron)
on:
  schedule:
    - cron: '0 2 * * *'  # Daily at 2 AM

# Manual trigger
on:
  workflow_dispatch:

# Multiple triggers
on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

Building and Testing Code

Node.js Application Workflow

name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16, 18, 20]

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run linter
        run: npm run lint

      - name: Run tests
        run: npm test

      - name: Build application
        run: npm run build

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build-output-node-${{ matrix.node-version }}
          path: dist/
          retention-days: 7

Python Application Workflow

name: Python CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11']

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install pytest pytest-cov

      - name: Run tests with coverage
        run: |
          pytest --cov=./ --cov-report=xml

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml

Docker Build Workflow

name: Docker Build and Push

on:
  push:
    branches: [main]
    tags:
      - 'v*'

jobs:
  docker:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

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

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: registry.homelab.local
          username: ${{ secrets.REGISTRY_USERNAME }}
          password: ${{ secrets.REGISTRY_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: registry.homelab.local/myapp
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=registry,ref=registry.homelab.local/myapp:buildcache
          cache-to: type=registry,ref=registry.homelab.local/myapp:buildcache,mode=max

Managing Secrets and Variables

Adding Secrets to Gitea

Repository secrets:

  1. Go to repository → Settings
  2. Click Secrets
  3. Click Add Secret
  4. Enter:
    • Name: REGISTRY_PASSWORD
    • Value: your-secret-value
  5. Click Add Secret

Organization secrets:

  1. Go to Organization → Settings
  2. Click Secrets
  3. Add secrets (available to all org repos)

Using secrets in workflows:

steps:
  - name: Deploy to server
    env:
      SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
      API_TOKEN: ${{ secrets.API_TOKEN }}
    run: |
      echo "$SSH_PRIVATE_KEY" > key.pem
      chmod 600 key.pem
      ssh -i key.pem [email protected] "deploy.sh"

Environment Variables

Set at workflow level:

name: Deploy

env:
  ENVIRONMENT: production
  APP_NAME: myapp

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DEPLOYMENT_REGION: us-east

    steps:
      - name: Deploy
        run: |
          echo "Deploying $APP_NAME to $ENVIRONMENT in $DEPLOYMENT_REGION"

Default environment variables:

steps:
  - name: Show default variables
    run: |
      echo "Workflow: ${{ github.workflow }}"
      echo "Repository: ${{ github.repository }}"
      echo "Ref: ${{ github.ref }}"
      echo "SHA: ${{ github.sha }}"
      echo "Actor: ${{ github.actor }}"

Advanced Workflow Patterns

Conditional Execution

jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to production
        if: success()  # Only if previous steps succeeded
        run: ./deploy.sh

      - name: Rollback on failure
        if: failure()  # Only if previous steps failed
        run: ./rollback.sh

Matrix Builds

jobs:
  test:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [ubuntu-latest, ubuntu-22.04]
        node-version: [16, 18, 20]
        include:
          - os: ubuntu-latest
            node-version: 20
            experimental: true
        exclude:
          - os: ubuntu-22.04
            node-version: 16

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

Reusable Workflows

Create reusable workflow (.gitea/workflows/reusable-deploy.yml):

name: Reusable Deploy

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      version:
        required: true
        type: string
    secrets:
      deploy_token:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          TOKEN: ${{ secrets.deploy_token }}
        run: |
          echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"

Call reusable workflow:

name: Deploy Production

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy-prod:
    uses: ./.gitea/workflows/reusable-deploy.yml
    with:
      environment: production
      version: ${{ github.ref_name }}
    secrets:
      deploy_token: ${{ secrets.PROD_DEPLOY_TOKEN }}

Composite Actions

Create composite action (.gitea/actions/setup-app/action.yml):

name: Setup Application
description: Install dependencies and configure app

inputs:
  node-version:
    description: Node.js version
    required: true
    default: '18'

runs:
  using: composite
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}

    - name: Cache dependencies
      uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

    - name: Install dependencies
      shell: bash
      run: npm ci

Use composite action:

steps:
  - uses: actions/checkout@v4
  - uses: ./.gitea/actions/setup-app
    with:
      node-version: '20'
  - run: npm test

Deployment Workflows

Deploy to Kubernetes

name: Deploy to K3s

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v4

      - name: Configure kubeconfig
        env:
          KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
        run: |
          mkdir -p ~/.kube
          echo "$KUBE_CONFIG" > ~/.kube/config
          chmod 600 ~/.kube/config

      - name: Deploy to cluster
        run: |
          kubectl apply -f k8s/
          kubectl rollout status deployment/myapp -n production

Deploy to Docker Host via SSH

name: Deploy to Docker Host

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Copy files to server
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          echo "$SSH_PRIVATE_KEY" > key.pem
          chmod 600 key.pem
          scp -i key.pem -r docker-compose.yml [email protected]:/opt/app/
          scp -i key.pem -r .env [email protected]:/opt/app/

      - name: Deploy with Docker Compose
        run: |
          ssh -i key.pem [email protected] << 'EOF'
            cd /opt/app
            docker-compose pull
            docker-compose up -d
            docker-compose ps
          EOF

Comparison with Other CI/CD Tools

Gitea Actions vs. GitLab CI

FeatureGitea ActionsGitLab CI
RAM Requirements512MB+4-8GB minimum
Storage~1GB~10GB+
Setup ComplexityLowMedium-High
Workflow SyntaxGitHub ActionsGitLab CI YAML
Runner ManagementSimple registrationMore complex
Container RegistryRequires separate setupBuilt-in
Package RegistryLimitedComprehensive
Best ForHomelabs, small teamsEnterprises, large teams

When to choose Gitea:

  • Running on limited hardware (Raspberry Pi, old servers)
  • Familiar with GitHub Actions syntax
  • Want simple, lightweight solution
  • Homelab or small team environment

When to choose GitLab:

  • Need built-in container registry, package registry
  • Large team with complex needs
  • Enterprise features required
  • Have resources (4-8GB RAM minimum)

Gitea Actions vs. GitHub Actions

Advantages of Gitea:

  • Complete data control
  • No usage limits
  • No cost for private repositories
  • Run on your hardware

Advantages of GitHub:

  • GitHub-hosted runners (no infrastructure needed)
  • Larger marketplace
  • Better documentation
  • Integrated with GitHub ecosystem

Migration from GitHub: Most GitHub workflows work in Gitea with minimal changes. Simply copy .github/workflows/ to .gitea/workflows/.


Troubleshooting Common Issues

Runner Not Picking Up Jobs

Symptom: Workflows queued but never execute

Solutions:

# Check runner status
docker logs act_runner

# Verify runner registered
# Gitea UI → Site Admin → Actions → Runners
# Should show "Idle" status

# Check runner connectivity
docker exec act_runner wget -qO- http://gitea:3000/api/v1/version

# Restart runner
docker-compose restart act_runner

Workflow Syntax Errors

Symptom: Workflow fails to parse

Solutions:

# Validate YAML syntax online
# https://www.yamllint.com/

# Check Gitea logs for specific error
docker logs gitea | grep -i error

# Common issues:
# - Indentation (use 2 spaces, not tabs)
# - Missing 'on:' trigger
# - Invalid job names (must be valid YAML keys)

Permission Denied Errors

Symptom: Runner can’t access Docker socket or files

Solutions:

# Add Docker group to runner
services:
  act_runner:
    user: "1000:999"  # UID:GID where 999 is docker group
    # OR run privileged (less secure)
    privileged: true

Action Not Found

Symptom: uses: actions/checkout@v4 fails

Solutions:

# Ensure DEFAULT_ACTIONS_URL set in app.ini
[actions]
ENABLED = true
DEFAULT_ACTIONS_URL = https://github.com

# Or use full URL
steps:
  - uses: https://github.com/actions/checkout@v4

Conclusion and Next Steps

Gitea Actions brings enterprise-grade CI/CD to your homelab without the resource overhead of GitLab. You’ve learned:

  • Installing and configuring Gitea with Actions
  • Setting up Act Runners for job execution
  • Writing GitHub Actions-compatible workflows
  • Building, testing, and deploying code
  • Managing secrets and advanced patterns

Key Takeaways

  • 40x lighter than GitLab - runs on 512MB RAM
  • GitHub Actions compatible - reuse existing workflows
  • Self-hosted privacy - your code never leaves your infrastructure
  • Simple runner management - easy to scale horizontally
  1. Create your first repository and workflow today
  2. Migrate GitHub Actions workflows to Gitea
  3. Set up Docker build pipeline for your apps
  4. Deploy to Kubernetes with automated workflows
  5. Explore GitHub Action marketplace for reusable actions
  • Woodpecker CI - Alternative lightweight CI/CD
  • OpenTofu - Automate infrastructure deployment
  • Kubernetes with K3s - Deploy to K8s clusters
  • Docker Registry - Host your container images

Resources


Document Version: 1.0 Last Updated: December 16, 2025 Based on: Gitea 1.21+, Act Runner 0.2+ Estimated Completion Time: 60 minutes

Happy automating!