Gitea Actions on a VPS: Self-Hosted Git CI/CD Without GitHub or GitLab

Gitea Actions on a VPS: Self-Hosted Git CI/CD Without GitHub or GitLab

Gitea is a lightweight, self-hosted Git service with a GitHub-like interface. Gitea Actions (added in Gitea 1.19) runs CI/CD workflows using the same YAML syntax as GitHub Actions — meaning your existing .github/workflows/ files work in Gitea with minimal changes. Self-hosting Gitea + Actions on a VPS gives your team a complete Git and CI/CD platform with no per-seat pricing, no usage limits, and your code never leaving your infrastructure.

Why Self-Host Instead of GitHub/GitLab

  • Data sovereignty: Code, secrets, and artifacts stay on your infrastructure
  • No usage limits: Unlimited repositories, CI minutes, and storage
  • Cost: VPS cost replaces GitHub Teams ($4/user/month) or GitLab Premium ($19/user/month)
  • Performance: Local runner — no queue, full VPS CPU for builds
  • Gitea Actions compatibility: GitHub Actions YAML syntax — migrate workflows with minimal changes

Step 1: Docker Compose Setup

<code">mkdir -p /opt/gitea && cd /opt/gitea
nano docker-compose.yml
<code">version: '3.8'

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: always
    ports:
      - "127.0.0.1:3000:3000"   # Web UI
      - "2222:22"                # SSH for git push (non-standard port)
    environment:
      USER_UID: 1000
      USER_GID: 1000
      GITEA__database__DB_TYPE: postgres
      GITEA__database__HOST: gitea-db:5432
      GITEA__database__NAME: gitea
      GITEA__database__USER: gitea
      GITEA__database__PASSWD: ${POSTGRES_PASSWORD}
      GITEA__server__DOMAIN: git.yourdomain.com
      GITEA__server__ROOT_URL: https://git.yourdomain.com/
      GITEA__server__SSH_DOMAIN: git.yourdomain.com
      GITEA__server__SSH_PORT: 2222
      GITEA__actions__ENABLED: "true"
      GITEA__mailer__ENABLED: "true"
      GITEA__mailer__SMTP_ADDR: smtp.mailgun.org
      GITEA__mailer__SMTP_PORT: 587
      GITEA__mailer__USER: postmaster@mg.yourdomain.com
      GITEA__mailer__PASSWD: ${SMTP_PASSWORD}
      GITEA__mailer__FROM: gitea@yourdomain.com
    volumes:
      - gitea_data:/data
    depends_on:
      gitea-db:
        condition: service_healthy

  gitea-db:
    image: postgres:16-alpine
    restart: always
    environment:
      POSTGRES_DB: gitea
      POSTGRES_USER: gitea
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - gitea_db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U gitea"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  gitea_data:
  gitea_db:
<code">cat > .env << 'EOF'
POSTGRES_PASSWORD=StrongGiteaDbPassword!
SMTP_PASSWORD=your_mailgun_smtp_password
EOF
chmod 600 .env
docker compose up -d

Step 2: Nginx Reverse Proxy

<code">sudo nano /etc/nginx/sites-available/gitea
<code">server {
    listen 80;
    server_name git.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name git.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/git.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/git.yourdomain.com/privkey.pem;

    client_max_body_size 200M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 300s;
    }
}
<code">sudo ln -s /etc/nginx/sites-available/gitea /etc/nginx/sites-enabled/
sudo certbot --nginx -d git.yourdomain.com
sudo systemctl reload nginx

Step 3: Initial Setup

  1. Visit https://git.yourdomain.com
  2. Complete the install wizard — database settings are pre-configured via environment variables
  3. Create the admin account
  4. Site Administration → Settings → Actions → Enable Actions globally

Step 4: Install act_runner (Actions Executor)

<code"># Download act_runner (runs Gitea Actions jobs)
wget -O /usr/local/bin/act_runner \
    https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64
chmod +x /usr/local/bin/act_runner

# Create runner directory
mkdir -p /opt/act_runner && cd /opt/act_runner

# Generate config
act_runner generate-config > config.yaml

# Register runner with Gitea
# First: get registration token from Gitea Admin → Actions → Runners
act_runner register \
    --instance https://git.yourdomain.com \
    --token YOUR_REGISTRATION_TOKEN \
    --name "vps-runner" \
    --labels "ubuntu-latest:docker://node:20,ubuntu-22.04:docker://ubuntu:22.04" \
    --no-interactive
<code">sudo nano /etc/systemd/system/act_runner.service
<code">[Unit]
Description=Gitea act_runner
After=docker.service network.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/act_runner
ExecStart=/usr/local/bin/act_runner daemon --config config.yaml
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
<code">sudo systemctl daemon-reload
sudo systemctl enable act_runner
sudo systemctl start act_runner

Step 5: Write Gitea Actions Workflows

<code"># .gitea/workflows/build.yml
# (Same syntax as .github/workflows/ — just different folder name)
name: Build and Test

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

jobs:
  test:
    runs-on: ubuntu-latest   # Matches runner label
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

      - name: Build
        run: npm run build

  docker-build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: |
          docker build -t myapp:${{ gitea.sha }} .
          docker tag myapp:${{ gitea.sha }} myapp:latest

      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        run: |
          docker compose -f /opt/myapp/docker-compose.yml up -d --no-deps myapp
          echo "Deployed ${{ gitea.sha }}"

Step 6: Secrets Management

<code"># Store secrets in Gitea UI:
# Repository → Settings → Secrets and Variables → Actions → New secret

# Reference in workflow:
steps:
  - name: Deploy with SSH
    run: |
      echo "${{ secrets.SSH_PRIVATE_KEY }}" > /tmp/deploy_key
      chmod 600 /tmp/deploy_key
      ssh -i /tmp/deploy_key deploy@production.yourdomain.com \
        "cd /opt/myapp && docker compose pull && docker compose up -d"
      rm /tmp/deploy_key

Step 7: Migrate from GitHub

<code"># Migrate a GitHub repository to Gitea:
# Gitea → + → Migrate External Repository → GitHub
# Enter: GitHub username/repo, Personal Access Token
# Options: migrate wiki, issues, labels, milestones, releases

# For .github/workflows/ → rename to .gitea/workflows/
# Most actions work identically; some GitHub-specific actions need alternatives:
# actions/upload-artifact → works in Gitea
# actions/cache → works in Gitea
# GitHub Packages → use Gitea's built-in container registry

Gitea Container Registry

<code"># Gitea includes a container registry (packages)
# Enable in admin: Site Administration → Configuration → Packages

# Push Docker images:
docker login git.yourdomain.com -u username -p password_or_token
docker tag myapp:latest git.yourdomain.com/username/myapp:latest
docker push git.yourdomain.com/username/myapp:latest

# In workflows:
- name: Push to Gitea Registry
  run: |
    docker login git.yourdomain.com -u ${{ gitea.actor }} -p ${{ secrets.GITEA_TOKEN }}
    docker push git.yourdomain.com/${{ gitea.repository }}:${{ gitea.sha }}

Getting Started

Gitea with PostgreSQL uses 200–400 MB RAM. With act_runner (which runs each job in a Docker container), allocate 1–2 GB additional RAM for concurrent builds. A 4 GB Ubuntu VPS at VPS.DO runs Gitea, the database, and handles 2–3 concurrent CI builds comfortably. NVMe storage speeds up Docker layer caching significantly, making subsequent builds much faster.

Conclusion

Gitea with Actions provides a fully self-hosted Git and CI/CD platform with GitHub Actions-compatible YAML syntax — most existing workflows migrate with only folder path changes. For teams prioritizing data sovereignty, cost reduction, or private network CI access, Gitea on a VPS eliminates GitHub/GitLab per-seat fees while maintaining a familiar developer experience. The built-in container registry, issue tracker, and code review complete the development platform.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!