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.
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
| Feature | Gitea Actions | GitHub Actions |
|---|---|---|
| Hosting | Self-hosted | Cloud (GitHub) |
| Privacy | Complete control | Code on GitHub servers |
| Resource Requirements | 512MB RAM+ | N/A (GitHub provides) |
| Workflow Syntax | GitHub Actions compatible | Native |
| Action Marketplace | Use GitHub Actions | Yes |
| Runner Hardware | Your infrastructure | GitHub-hosted or self-hosted |
| Cost | Free (your hardware) | Free tier + paid minutes |
| Custom Runners | Yes | Yes (self-hosted) |
| Artifact Storage | Your storage | GitHub 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
Method 1: Docker Compose (Recommended)
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:
- Database: SQLite (default, simplest for homelabs)
- Site Title: Your Homelab Git
- Server Domain: git.homelab.local
- SSH Port: 2222
- Admin account: Create your admin user
- 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:
- Log into Gitea web UI
- Go to Site Administration (gear icon)
- Check Configuration
- Look for
[actions]section withENABLED = 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:
- Log into Gitea as admin
- Go to Site Administration → Actions → Runners
- Click Create new Runner
- Copy the registration token
- Save to
.envfile:
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:
- Gitea UI → Site Admin → Actions → Runners
- You should see your runner listed
- 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
- Log into Gitea
- Click + → New Repository
- Configure:
- Owner: Your user
- Repository Name:
hello-gitea-actions - Visibility: Private
- Initialize with README: Checked
- 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:
- Go to repository in Gitea web UI
- Click Actions tab
- You should see workflow running
- Click on workflow run to see details
- 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:
- Go to repository → Settings
- Click Secrets
- Click Add Secret
- Enter:
- Name:
REGISTRY_PASSWORD - Value:
your-secret-value
- Name:
- Click Add Secret
Organization secrets:
- Go to Organization → Settings
- Click Secrets
- 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
| Feature | Gitea Actions | GitLab CI |
|---|---|---|
| RAM Requirements | 512MB+ | 4-8GB minimum |
| Storage | ~1GB | ~10GB+ |
| Setup Complexity | Low | Medium-High |
| Workflow Syntax | GitHub Actions | GitLab CI YAML |
| Runner Management | Simple registration | More complex |
| Container Registry | Requires separate setup | Built-in |
| Package Registry | Limited | Comprehensive |
| Best For | Homelabs, small teams | Enterprises, 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
Recommended Next Steps
- Create your first repository and workflow today
- Migrate GitHub Actions workflows to Gitea
- Set up Docker build pipeline for your apps
- Deploy to Kubernetes with automated workflows
- Explore GitHub Action marketplace for reusable actions
Related Guides
- Woodpecker CI - Alternative lightweight CI/CD
- OpenTofu - Automate infrastructure deployment
- Kubernetes with K3s - Deploy to K8s clusters
- Docker Registry - Host your container images
Resources
- Official Documentation: docs.gitea.com/usage/actions
- GitHub Actions Docs: docs.github.com/actions
- Community Forum: discourse.gitea.io
- Act Runner: gitea.com/gitea/act_runner
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!