DEV Community

Cover image for Self-hosted Gitea CI
Diogo
Diogo

Posted on

Self-hosted Gitea CI

The goal of this guide is to turn your Gitea from a simple code repository into a full automation pipeline. We’ll use Gitea Actions, which is compatible with GitHub Actions syntax, making the process straightforward if you already know GitHub CI/CD.


1. The Concept: Gitea Actions and Runners

Unlike GitHub, where the machines that execute workflows (Runners) are provided by Microsoft, in a self-hosted environment you must run your own Runner.

Since your Gitea is already running in Docker, the most natural approach is to create a sibling container that acts as the Runner.


2. Enabling Actions in Gitea

First, you need to explicitly enable Actions in Gitea.

Edit the Gitea configuration file (usually located at gitea/conf/app.ini).

Add or update the following section:

ini
[actions]
ENABLED = true
Enter fullscreen mode Exit fullscreen mode

Restart the Gitea container to apply the changes.

3. Creating the Gitea Runner (The Worker)

Now you need a service that continuously listens to Gitea and waits for jobs.
We’ll use the official act_runner.

Create a docker-compose.yml file for the runner (it can be in the same compose file as Gitea or a separate one):

services:
  runner:
    image: gitea/act_runner:latest
    container_name: gitea-runner
    environment:
      - CONFIG_FILE=/config.yaml
      - GITEA_INSTANCE_URL=http://192.168.x.x:3000
      - GITEA_RUNNER_REGISTRATION_TOKEN=<TOKEN_HERE>
      - GITEA_RUNNER_NAME=thinkpad-runner
    volumes:
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    restart: always
Enter fullscreen mode Exit fullscreen mode

Getting the Runner Registration Token

Go to Site Administration → Actions → Runners → Create new Runner.
Copy the generated token and paste it into the GITEA_RUNNER_REGISTRATION_TOKEN field.

4. The Workflow: From Push to Deployment

Inside your repository (for example, a Next.js or Python project), create a workflow that defines what happens when code is pushed.

Create the following file:

.gitea/workflows/deploy.yaml
Enter fullscreen mode Exit fullscreen mode

Below is a simple end-to-end example.

Scenario:
You push new code to main, a Docker image is built, pushed to a registry, and the production container is updated automatically.

name: Deploy to Production
run-name: ${{ gitea.actor }} is deploying 🚀

on:
  push:
    branches:
      - main

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Login to Docker Hub (optional)
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: your-user/your-app:latest

      - name: Update production container
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.HOST_IP }}
          username: ${{ secrets.HOST_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker pull your-user/your-app:latest
            docker stop my-app-prod || true
            docker rm my-app-prod || true
            docker run -d \
              --name my-app-prod \
              -p 8080:80 \
              your-user/your-app:latest
Enter fullscreen mode Exit fullscreen mode

Why a Separate production Branch Is Usually a Bad Idea

You might consider creating a production branch and having Gitea automatically merge main into it.
In practice, this often introduces unnecessary complexity.

Modern CI/CD follows Trunk-Based Development principles:

  • main is the single source of truth

  • Code merged into main must always be production-ready

  • CI/CD builds and deploys directly from main

This eliminates manual merges, reduces errors, and keeps the pipeline simple.

Optional: Mirroring the Repository to GitHub

Even when using Gitea as your primary Git platform, mirroring your repository to GitHub can be a strategic advantage rather than a contradiction.

Why Mirror to GitHub?

  • Visibility & Portfolio

GitHub is still the most widely used platform by recruiters and open-source communities. Mirroring ensures your work is publicly visible without changing your main workflow.

  • Backup & Redundancy

Your self-hosted Gitea remains the source of truth, while GitHub acts as a read-only backup.

  • Ecosystem Compatibility

Some tools, integrations, and third-party services still only support GitHub natively.

  • Zero Workflow Impact

You continue pushing to Gitea, while GitHub stays automatically in sync.


How Gitea Repository Mirroring Works

Gitea supports one-way and two-way mirroring.

For most setups, one-way push mirroring is the safest option:

  • Gitea → GitHub
  • Main branch remains authoritative
  • No accidental changes coming back from GitHub

Setting Up Push Mirroring (Gitea → GitHub)

  1. Create an empty repository on GitHub
  2. In Gitea, open your repository settings
  3. Navigate to Settings → Repository → Mirroring
  4. Add a new push mirror
  5. Use the GitHub repository URL (SSH is recommended)
  6. Add your GitHub SSH key or token (example: git@github.com:your-name/your-repo.git)
  7. Enable Push Mirror
  8. Save the configuration

From this point on, every push to Gitea is automatically mirrored to GitHub.

Recommended Best Practices

  • Disable GitHub Actions on the mirrored repository Gitea remains responsible for CI/CD.
  • Protect the GitHub repository Treat it as read-only to avoid divergence.
  • Mirror only main (optional) Keeps the public repo clean and focused. Mental Model
  • Gitea = Source of Truth

  • GitHub = Public Mirror / Backup

  • CI/CD = Gitea Actions

This setup gives you full ownership and control, while still benefiting from GitHub’s network effects.

Summary: Key Benefits
Automation

  • No more SSH access and manual git pull on the server.

Observability

  • Each build and deployment is logged and visible directly in the Gitea UI.

Safety

  • If the build fails (syntax errors, failing tests, broken TypeScript or Python code), the deployment is automatically blocked, keeping production stable.

Top comments (0)