Ditch the Cloud: Self-Hosting a Lean DevOps Stack with Gitea and Actions

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Why I Stopped Overpaying for Cloud Git Hosting

Standard cloud providers for Git hosting and CI/CD are convenient, but they often come with baggage: unpredictable pricing, vendor lock-in, and privacy trade-offs. While GitLab is a powerhouse, its memory requirements are steep, often demanding at least 4GB of RAM just to stay stable. In contrast, Gitea is a single binary written in Go that can run comfortably on a $5/month VPS or even a Raspberry Pi with as little as 250MB of RAM.

For any DevOps engineer, the ability to deploy a private, air-gapped CI/CD pipeline is a massive advantage. Whether you are working in a high-security environment or just want to avoid the “GitHub is down” productivity slump, Gitea provides total autonomy. With the recent maturity of Gitea Actions, you no longer need to bridge multiple tools like Jenkins or Drone. You can now run your workflows using the familiar GitHub Actions syntax directly within your own infrastructure.

The Power of Gitea Actions

Gitea handles the repository management, while Gitea Actions manages the heavy lifting of automation. This system relies on the act_runner, a tool based on the nektos/act project. Because it shares the same logic as GitHub Actions, you can often port your existing .github/workflows files to Gitea with zero configuration changes. It is the closest you can get to the GitHub experience without the monthly subscription fees.

Deployment: Launching Gitea with Docker Compose

Docker Compose is the most straightforward way to manage this stack. It keeps your application logic separate from your data, making upgrades as simple as changing a version tag. While Gitea supports SQLite, I recommend PostgreSQL for any team environment to ensure better data integrity and handling of concurrent users.

Create a project folder and save this configuration as docker-compose.yml:

version: "3"

networks:
  gitea:
    external: false

services:
  server:
    image: gitea/gitea:1.21.7
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=gitea_password
    restart: always
    networks:
      - gitea
    volumes:
      - ./gitea-data:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "3000:3000"
      - "2222:22"
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=gitea_password
      - POSTGRES_DB=gitea
    networks:
      - gitea
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

Fire up the stack with a single command:

docker-compose up -d

Navigate to http://localhost:3000 to finish the setup. Remember to set the SSH Port to 2222 and the Gitea Base URL to your actual server IP. If you leave these as defaults, your Git clone URLs will be broken from the start.

Activating the CI/CD Engine

Gitea Actions is turned off by default to save resources. To wake it up, you need to edit the app.ini file found in your volume at ./gitea-data/gitea/conf/app.ini. Add these lines to the bottom of the file:

[actions]
ENABLED = true

Restart the container to let the changes take effect:

docker-compose restart server

Provisioning the Act Runner

The Git server doesn’t execute code; it delegates that work to a runner. For better security and performance, I usually put the runner on a separate host or at least a separate container. This prevents a heavy build job from crashing your entire Git web interface. Add the following service to your docker-compose.yml:

  runner:
    image: gitea/act_runner:latest
    environment:
      - CONFIG_FILE=/config.yaml
      - GITEA_INSTANCE_URL=http://server:3000
      - GITEA_RUNNER_REGISTRATION_TOKEN=<YOUR_TOKEN>
      - GITEA_RUNNER_NAME=prod-runner-01
    volumes:
      - ./runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - gitea
    depends_on:
      - server

To find your GITEA_RUNNER_REGISTRATION_TOKEN, log in as an admin and go to Site Administration > Actions > Runners. Click Create New Runner and copy the string provided.

Be aware that mapping /var/run/docker.sock gives the runner significant permissions over your host. It allows the runner to pull images and start containers for your CI steps. For production environments, consider using a rootless Docker setup to minimize security risks.

Testing Your New Pipeline

Let’s verify everything works. Create a new repository and make sure Actions are enabled in the repository settings. Then, create a file at .gitea/workflows/test.yaml with this content:

name: Gitea Actions Test
on: [push]
jobs:
  check-environment:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - run: echo "Pipeline is running on Gitea!"
      - run: npx --version

Once you push this, check the Actions tab. You should see the job move from “Waiting” to “Success” within seconds. If it hangs, check your runner logs using docker logs -f gitea-runner to see if there are connection issues.

Maintenance and Health Checks

Self-hosting means you are the SRE. Gitea includes a built-in metrics endpoint at /metrics, which you can enable in app.ini to feed data into Prometheus. This is vital for tracking database connection pools and repository sizes.

One common pitfall is the Docker cache. Over time, the act_runner will accumulate dangling images and build layers that can easily eat up 20GB or 30GB of disk space. I recommend a simple weekly cron job that runs docker system prune -f on the runner host. This keeps your CI/CD environment lean and prevents “Disk Full” errors from killing your builds.

Moving to Gitea gives you full ownership of your data and your automation logic. While it takes a bit more effort to set up than a SaaS account, the speed and cost-efficiency are hard to beat.

Share: