Self-Hosted CI/CD
with Gitea and Woodpecker CI

GitHub Actions and GitLab CI are convenient, but they come with usage limits, opaque pricing, and your code living on someone else's servers. A self-hosted Gitea and Woodpecker stack gives you private, unlimited CI/CD on your own infrastructure - and it's surprisingly easy to set up.

Why Self-Host?

There are three main reasons to run your own CI/CD stack: cost, control, and privacy. GitHub Actions free tier gives you 2,000 minutes per month - a medium-size project with multiple developers will exhaust this quickly. More importantly, if your code contains proprietary algorithms, unreleased product features, or security-sensitive logic, you may not want it running on shared cloud infrastructure.

Gitea is a lightweight, self-hosted Git service written in Go - think GitHub, but running on a $5/month VPS. Woodpecker CI was forked from Drone CI and integrates natively with Gitea. Together they form a complete DevOps platform with a resource footprint small enough to run alongside other services on a modest machine.

Docker Compose Setup

Both services are distributed as Docker images. Create a directory for your stack and write a docker-compose.yml:

services:
  gitea:
    image: gitea/gitea:latest
    container_name: gitea
    restart: unless-stopped
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=sqlite3
      - GITEA__server__DOMAIN=git.example.com
      - GITEA__server__ROOT_URL=https://git.example.com/
      - GITEA__server__SSH_DOMAIN=git.example.com
    ports:
      - "3000:3000"
      - "2222:22"
    volumes:
      - ./gitea:/data
      - /etc/timezone:/etc/timezone:ro

  woodpecker-server:
    image: woodpeckerci/woodpecker-server:latest
    container_name: woodpecker-server
    restart: unless-stopped
    environment:
      - WOODPECKER_OPEN=false
      - WOODPECKER_HOST=https://ci.example.com
      - WOODPECKER_GITEA=true
      - WOODPECKER_GITEA_URL=https://git.example.com
      - WOODPECKER_GITEA_CLIENT=${GITEA_OAUTH_CLIENT}
      - WOODPECKER_GITEA_SECRET=${GITEA_OAUTH_SECRET}
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    ports:
      - "8000:8000"
    volumes:
      - ./woodpecker:/var/lib/woodpecker

  woodpecker-agent:
    image: woodpeckerci/woodpecker-agent:latest
    container_name: woodpecker-agent
    restart: unless-stopped
    environment:
      - WOODPECKER_SERVER=woodpecker-server:9000
      - WOODPECKER_AGENT_SECRET=${WOODPECKER_AGENT_SECRET}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      - woodpecker-server

Gitea Setup and OAuth Application

Start Gitea first (docker compose up -d gitea) and complete the installation wizard at http://YOUR_IP:3000. Create your admin account. Then, to connect Woodpecker, create an OAuth2 application in Gitea:

  1. Go to Settings → Applications → OAuth2 Applications
  2. Application Name: Woodpecker CI
  3. Redirect URI: https://ci.example.com/authorize
  4. Save the Client ID and Client Secret - these go into your .env file

Create a .env file next to your docker-compose.yml:

GITEA_OAUTH_CLIENT=your_client_id_here
GITEA_OAUTH_SECRET=your_client_secret_here
WOODPECKER_AGENT_SECRET=a_long_random_secret_here

Generate the agent secret with: openssl rand -hex 32

Woodpecker Activation

Start the full stack with docker compose up -d. Navigate to https://ci.example.com and log in via Gitea OAuth. To activate CI on a repository, go to Repositories → Add Repository in the Woodpecker UI and enable it. Woodpecker automatically creates a webhook in Gitea.

Your First Pipeline

Woodpecker pipelines are defined in a .woodpecker.yml file at the root of your repository. Here's a practical example for a Node.js application:

# .woodpecker.yml
when:
  - event: [push, pull_request]

steps:
  - name: lint
    image: node:20-alpine
    commands:
      - npm ci
      - npm run lint

  - name: test
    image: node:20-alpine
    commands:
      - npm ci
      - npm test

  - name: build-image
    image: plugins/docker
    settings:
      repo: registry.example.com/myapp
      tags: latest,${CI_COMMIT_SHA:0:8}
      username:
        from_secret: registry_user
      password:
        from_secret: registry_password
    when:
      - branch: main
        event: push

  - name: deploy
    image: alpine/ssh
    commands:
      - ssh [email protected] "docker compose pull && docker compose up -d --no-deps app"
    when:
      - branch: main
        event: push

Managing Secrets

Secrets like registry credentials and SSH keys should never be hardcoded in your pipeline file. Add them in the Woodpecker UI under Repository → Settings → Secrets, then reference them with from_secret as shown above. Secrets are encrypted at rest and only injected into pipeline steps that explicitly request them.

💡
Add a deploy key for SSH deployments. Generate a dedicated SSH key pair for Woodpecker: ssh-keygen -t ed25519 -f woodpecker_deploy. Add the public key to your production server's ~/.ssh/authorized_keys with a command= restriction, and store the private key as a Woodpecker secret.
Services Technologies Process Blog Get in Touch