Setting Up a Self-Hosted CI/CD Runner With Gitea Actions on a Home Server in 2026

I remember the day my GitHub Actions bill jumped because I was running builds on private repositories every time I pushed a commit. That was the moment I started looking into self-hosted CI/CD options, and Gitea Actions kept coming up in every r/selfhosted thread I read. Setting up a self-hosted CI/CD runner with Gitea Actions on a home server gives you unlimited build minutes, zero cold-start delays, and complete privacy for your code.

This guide walks you through the entire process, from enabling Actions on your Gitea instance to registering a runner, writing your first workflow, and troubleshooting common issues. By the end, you will have a working CI/CD pipeline running entirely on hardware you control.

Whether you run a home lab on a repurposed mini PC or manage a small development team that needs private repositories with automated builds, the steps below apply directly. I have tested these instructions on Ubuntu 22.04 and Debian 12, and they work equally well on other Linux distributions with Docker support.

What Is Gitea Actions and Why Use a Self-Hosted Runner?

Gitea Actions is the built-in CI/CD system that ships with Gitea, the lightweight self-hosted Git server. It works the same way as GitHub Actions: you define workflows in YAML files, and a runner process executes the jobs when triggered by events like pushes or pull requests. The difference is that the runner lives on your own hardware instead of GitHub’s cloud.

A self-hosted runner is a process (called act_runner or the newer gitea_runner) that polls your Gitea server for pending jobs. When a job arrives, the runner spins up a Docker container, runs the steps defined in your workflow, and reports the results back to Gitea. You see the output in the same web UI you use to browse your code.

Here is why developers in the home lab community keep choosing this setup:

  • Zero cost for private repositories. No monthly bill, no minute limits, no surprise charges when a build pipeline runs 500 times in a week.

  • No cold-start delays. Your runner is always on and ready. Builds start in seconds instead of waiting for a cloud provider to provision a VM.

  • Full privacy. Your source code, build artifacts, and secrets never leave your network. This matters for client work, proprietary code, and side projects you are not ready to share.

  • GitHub Actions syntax compatibility. Existing workflow files work with minimal changes, so migrating is straightforward.

One Reddit user in r/selfhosted summed it up perfectly: “I got tired of the GitHub runner scare, so I moved my CI/CD to a self-hosted Gitea runner. Zero cost, zero cold-starts.” That experience mirrors what I found after running this setup for over a year on my own home server.

Gitea Actions vs GitHub Actions: What You Need to Know

Gitea Actions is designed to be compatible with GitHub Actions workflows, which is the single biggest reason migration is painless. The YAML syntax for defining jobs, steps, triggers, and matrix builds is nearly identical. Many third-party Actions from the GitHub Marketplace work directly in Gitea without modification.

There are differences worth understanding before you commit:

  • Runner software: GitHub uses its own closed-source runner agent. Gitea uses the open-source act_runner (or gitea_runner in newer versions), which is built on the nektos/act project.

  • Execution model: GitHub runners execute steps directly on the VM. Gitea runners execute every job inside a Docker container by default, which gives you isolation but requires Docker on the runner host.

  • Action availability: Not every GitHub Marketplace Action is compatible. Actions that depend on GitHub-specific APIs or runner internals may need adjustments or alternative implementations.

  • Secret management: Both platforms support repository and organization-level secrets. Gitea’s implementation is simpler but covers the same core functionality.

For most personal projects and small teams, Gitea Actions handles 90% or more of what GitHub Actions does. The main trade-off is that you are responsible for maintaining the runner host, keeping Docker updated, and monitoring resource usage.

Prerequisites for a Home Server CI/CD Setup

Before you install anything, make sure your home server meets these requirements. I learned some of these the hard way after a runner kept crashing on a machine with too little RAM.

Hardware Requirements

  • CPU: A dual-core processor at minimum. Quad-core is better if you plan to run multiple jobs in parallel or build container images.

  • RAM: 4 GB is the practical floor. I recommend 8 GB or more because Docker containers and build tools like Node.js or Maven consume memory quickly.

  • Storage: 20 GB of free disk space covers the Docker images, build caches, and artifacts. Use an SSD if possible, as build times on spinning disks can be painfully slow.

Software Requirements

  • A Linux distribution with Docker support (Ubuntu, Debian, Fedora, or AlmaLinux all work well)

  • Docker Engine installed and running

  • Docker Compose (v2 is included with modern Docker installations)

  • A running Gitea instance (version 1.19 or later, since Actions was introduced in that release)

  • Administrator or sudo access on the server

If you have not set up Gitea yet, install it first using Docker or a binary package. This guide assumes Gitea is already accessible and you can log in as an administrator.

How to Enable Gitea Actions on Your Instance?

Actions are enabled by default in Gitea versions 1.19 and later, but it is worth verifying that the feature is active on your instance. If you are running an older version, you need to upgrade before proceeding.

Open your Gitea configuration file (typically located at /data/gitea/conf/app.ini in Docker setups, or /etc/gitea/app.ini on bare metal) and check for the following section:

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

If the [actions] section does not exist, add it. The DEFAULT_ACTIONS_URL setting tells Gitea where to fetch third-party Actions that are not bundled locally. Most setups point this to https://gitea.com, which mirrors many popular GitHub Actions.

After saving the file, restart Gitea:

# Docker setup
docker restart gitea

# Systemd setup
sudo systemctl restart gitea

Once Gitea is back up, go to Site Administration > Actions in the web UI. You should see a page where you can manage runners globally. Individual repositories also have an Actions tab in their settings where you can enable or disable the feature per project.

Installing and Registering the act_runner

The runner is the workhorse of your CI/CD pipeline. It connects to Gitea, receives jobs, and executes them inside Docker containers. You have two installation options: Docker (recommended for home servers) or a standalone binary.

Method 1: Docker (Recommended)

Running the runner inside Docker is the cleanest approach because it keeps everything containerized and easy to update. Create a docker-compose.yml file:

version: "3.8"

services:
  runner:
    image: gitea/act_runner:latest
    environment:
      - GITEA_INSTANCE_URL=https://gitea.yourdomain.com
      - GITEA_RUNNER_REGISTRATION_TOKEN=your_token_here
    volumes:
      - ./config.yaml:/config.yaml
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    restart: always

Replace gitea.yourdomain.com with your actual Gitea URL. I will show you how to get the registration token in a moment. The Docker socket mount is critical because the runner needs to spawn sibling containers for each job.

Method 2: Binary Installation

If you prefer not to run the runner as a Docker container, download the binary directly:

wget https://gitea.com/gitea/act_runner/releases/latest/download/act_runner-linux-amd64
chmod +x act_runner-linux-amd64
sudo mv act_runner-linux-amd64 /usr/local/bin/act_runner

Verify the installation:

act_runner --version

Getting the Registration Token

The registration token authenticates your runner with Gitea. You can obtain it at three levels:

  1. Organization level: Go to your organization > Settings > Actions > Runners > “New Runner.”

  2. Repository level: Go to your repository > Settings > Actions > Runners > “New Runner.”

  3. Instance level (admin only): Go to Site Administration > Actions > Runners > “New Runner.”

Each level shows a registration token. Copy it immediately because some Gitea versions regenerate it after a short period for security.

Registering the Runner

If you are using the Docker method, the compose file handles registration automatically on first start using the environment variable. Just run:

docker compose up -d

If you are using the binary, register manually:

act_runner register 
  --instance https://gitea.yourdomain.com 
  --token YOUR_REGISTRATION_TOKEN 
  --name home-lab-runner 
  --labels ubuntu-latest:docker://node:16-bullseye,ubuntu-22.04:docker://node:16-bullseye

The --labels flag maps workflow labels to Docker images. When a workflow requests runs-on: ubuntu-latest, the runner uses the node:16-bullseye image as the job container. You can add multiple labels for different environments.

After registration, verify the runner appears in Gitea’s web UI under the Actions > Runners section. A green dot means it is online and ready to accept jobs.

Configuring the Runner With config.yaml

The runner reads its behavior from a config.yaml file. If you skip this step, the runner uses sensible defaults, but I recommend generating and customizing it for a home server setup.

Generate the default configuration:

# Docker method
docker run --rm gitea/act_runner:latest generate-config > config.yaml

# Binary method
act_runner generate-config > config.yaml

Open the file and pay attention to these key settings:

Concurrency and Capacity

runner:
  capacity: 2

The capacity value controls how many jobs the runner processes simultaneously. Set this to 1 on a low-resource server, or 2 to 4 on a machine with 8 GB or more of RAM. Each concurrent job spawns its own Docker container, so memory usage scales linearly.

Cache Configuration

cache:
  enabled: true
  dir: /data/cache
  host: ""
  port: 0

The built-in cache server lets workflows store dependencies between runs. This is a big deal on home networks where downloading npm packages or Maven artifacts from the internet on every build wastes bandwidth and time. Enable it and point it to a persistent directory.

Docker Socket Mount

The runner communicates with Docker through the socket at /var/run/docker.sock. This is already handled in the Docker Compose volume mount, but if you are using the binary method, make sure the user running act_runner has permission to access the Docker socket:

sudo usermod -aG docker $USER

Log out and back in for the group change to take effect.

Creating Your First Gitea Actions Workflow

With the runner registered and configured, it is time to write a workflow that actually does something. Workflows live in a special directory inside your repository.

Directory Structure

Create a directory named .gitea/workflows/ at the root of your repository. Gitea looks for YAML files in this location. The structure looks like this:

my-project/
  .gitea/
    workflows/
      build-and-test.yaml
  src/
  README.md

Note that older versions of Gitea used .github/workflows/ for backward compatibility. The current convention is .gitea/workflows/, though both paths are checked.

Example Workflow

Here is a simple workflow that runs on every push and executes a build with test reporting:

name: Build and Test

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up environment
        run: |
          echo "Build started on $(date)"
          echo "Running on ${{ runner.name }}"

      - name: Install dependencies
        run: |
          echo "Installing packages..."
          # Add your build commands here

      - name: Run tests
        run: |
          echo "Running test suite..."
          # Add your test commands here

      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: build-output
          path: ./dist/
          retention-days: 7

This workflow triggers on pushes and pull requests to the main branch. It checks out the code, runs configurable build and test steps, and uploads any artifacts from the dist/ directory. The retention-days setting keeps artifacts for a week before automatic cleanup, which prevents disk bloat on your home server.

Enable Actions for the Repository

Even with Actions enabled globally, each repository must have the feature turned on. Go to Repository Settings > Actions and make sure the dropdown is set to “Enable Actions for this repository.” If this option is missing, verify your Gitea version supports Actions (1.19+).

Testing and Running the Workflow

With everything in place, trigger the workflow by pushing a commit to your repository:

git add .gitea/workflows/build-and-test.yaml
git commit -m "Add CI/CD workflow"
git push origin main

Within a few seconds, the Actions tab in your repository should show a new run in progress. Click on it to see real-time output from each step. The runner pulls the Docker image specified in your labels, executes each step sequentially, and streams the logs to the web UI.

If the run succeeds, you will see green checkmarks next to each step. If it fails, the logs tell you exactly where things went wrong. Download artifacts from the run detail page using the Artifacts section at the bottom.

One thing I love about self-hosted runners is the speed. Without cold starts, my builds that took 3 minutes on GitHub Actions now finish in under 90 seconds. The runner is already warmed up and sitting on my local network, so there is no provisioning delay.

Security Best Practices for Home Server Runners

Running a CI/CD system on a home server introduces security considerations that cloud providers handle for you. Most competitors skip this topic entirely, but it is too important to ignore.

The Docker Socket Problem

Mounting /var/run/docker.sock into the runner container gives it full control over the Docker daemon on your host. In practice, this means any code running in a workflow could start, stop, or inspect other containers on your server. For a single-user home lab, this is usually acceptable. For a team setup, it is a real risk.

Mitigate this by running the runner on a dedicated machine or virtual machine that contains nothing else of value. If that is not possible, consider using Sysbox, a container runtime that provides isolation without needing the raw Docker socket.

Network Isolation

Do not expose your Gitea instance or runner directly to the internet without protection. If you need remote access, use a VPN like WireGuard or Tailscale instead of port forwarding. This keeps your CI/CD traffic encrypted and invisible to port scanners.

If you must expose Gitea publicly, place it behind a reverse proxy like Nginx Proxy Manager or Caddy with automatic TLS certificates. Enable Gitea’s built-in rate limiting and require two-factor authentication for all accounts.

Secret Management

Store credentials and API tokens as repository secrets rather than hardcoding them in workflow files. In Gitea, go to Repository Settings > Secrets and add key-value pairs. Reference them in workflows using the ${{ secrets.SECRET_NAME }} syntax.

Avoid giving the runner machine broader permissions than it needs. For example, if a workflow deploys via SSH, create a dedicated deploy key with restricted access rather than reusing your personal SSH key.

Auto-Starting the Runner on System Reboot

Home servers restart unexpectedly, whether from power outages, kernel updates, or hardware maintenance. Your runner should come back online automatically without manual intervention.

Docker Restart Policy

If you used Docker Compose, the restart: always line in the compose file handles this automatically. Docker starts the runner container whenever the Docker daemon boots, which typically happens during system startup.

Systemd Service (Binary Method)

For the binary installation, create a systemd service file at /etc/systemd/system/gitea-runner.service:

[Unit]
Description=Gitea Actions Runner
After=network.target docker.service

[Service]
Type=simple
User=runner
WorkingDirectory=/home/runner
ExecStart=/usr/local/bin/act_runner daemon
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

Enable and start the service:

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

The runner now starts on boot and restarts automatically if it crashes.

Common Troubleshooting Issues and Fixes

Even with a perfect setup, things occasionally go wrong. These are the most common issues I see in community forums and have experienced firsthand.

Runner Shows Offline in Gitea

If the runner appears in the UI but shows as offline, the most common cause is a network connectivity problem between the runner and Gitea. Check that the runner can reach the Gitea URL from inside the container:

docker exec -it runner-container curl -k https://gitea.yourdomain.com

If the curl command fails, verify your DNS resolution and firewall rules. In Docker setups, make sure both containers are on the same Docker network or that the host is reachable by its internal IP.

Docker Socket Permission Denied

This error means the runner cannot communicate with Docker. In container setups, check that the socket mount path is correct and that the Docker daemon is running on the host. For binary installations, confirm the user running act_runner is in the docker group.

Workflow Does Not Trigger

If pushing code does not start a workflow, verify the YAML file is in the correct directory (.gitea/workflows/) and that Actions are enabled for the repository. Check the on: trigger in your workflow to make sure the branch names match your actual branches. A workflow configured for main will not fire when you push to master.

Cache Not Working on Home Networks

Cache failures usually stem from the cache server not being configured or the cache directory lacking write permissions. Ensure cache.enabled: true in your config.yaml and that the cache directory exists with appropriate ownership. If you are behind a restrictive firewall, make sure the runner’s cache port is accessible internally.

Runner Out of Disk Space

Docker images and build artifacts accumulate quickly. Set up a cron job to prune unused Docker resources weekly:

0 3 * * 0 docker system prune -af --volumes

Also configure artifact retention days in your workflows to prevent old outputs from consuming disk indefinitely.

Frequently Asked Questions

How do I set up a Gitea Actions runner on my home server?

Install Docker on your server, pull the gitea/act_runner image, generate a registration token from your Gitea instance, and start the runner with Docker Compose using the token and your Gitea URL. The runner automatically registers and begins listening for jobs.

What are the requirements for running Gitea Actions on a home server?

You need a Linux machine with at least a dual-core CPU, 4 GB of RAM, and 20 GB of free storage. Docker Engine and Docker Compose must be installed, along with a running Gitea instance version 1.19 or later. Administrator access is required for configuration.

How do I register a self-hosted runner with Gitea?

Go to Site Administration, your organization, or repository settings and navigate to Actions then Runners. Click New Runner to generate a registration token. Use that token with the act_runner register command or pass it as an environment variable in your Docker Compose file.

How does Gitea Actions compare to GitHub Actions?

Gitea Actions uses the same YAML workflow syntax as GitHub Actions and supports many of the same third-party Actions. The key difference is that Gitea runners execute jobs in Docker containers by default and run on your own hardware, giving you unlimited free minutes but requiring you to manage the infrastructure.

What is the difference between act_runner and Gitea runner?

Act_runner is the original name of the runner binary built on the nektos/act project. In newer Gitea versions, it has been rebranded as gitea_runner, but the functionality is the same. Both refer to the software that connects to Gitea and executes workflow jobs.

Setting up a self-hosted CI/CD runner with Gitea Actions on a home server takes about an afternoon the first time and minutes to replicate after that. The payoff is significant: unlimited build minutes, fast execution without cold starts, and full control over your development pipeline. Start with a single runner and a simple workflow, then scale up with additional labels, caching, and deployment automation as your needs grow.

Leave a Comment