DEV Community

SIGNAL
SIGNAL

Posted on

Build a Self-Hosted AI Code Review Bot With Ollama and Gitea Webhooks

Every pull request deserves a second pair of eyes. But what if those eyes belong to a local LLM running on your own hardware — no API keys, no data leaving your network?

In this guide, we'll build a self-hosted AI code review bot that listens for Gitea pull request webhooks, sends diffs to Ollama, and posts review comments back — all running in Docker on your homelab.

Why Self-Hosted Code Review?

Cloud-based AI review tools are great, but they come with trade-offs:

  • Privacy: Your proprietary code goes to someone else's servers
  • Cost: API calls add up fast on large teams
  • Control: Rate limits, model changes, and outages are out of your hands

With Ollama + Gitea, you own the entire pipeline. Your code stays on your metal.

Architecture Overview

Gitea PR Event → Webhook → review-bot (Flask) → Ollama API → Gitea Comment API
Enter fullscreen mode Exit fullscreen mode

Three moving parts:

  1. Gitea sends a webhook when a PR is opened or updated
  2. review-bot (a small Python Flask app) receives the webhook, fetches the diff, and sends it to Ollama
  3. Ollama runs a code-capable model (we'll use codellama:13b) and returns the review
  4. The bot posts the review as a PR comment via Gitea's API

Prerequisites

  • Docker and Docker Compose
  • A running Gitea instance (or spin one up — we'll include it in the compose file)
  • ~16 GB RAM for codellama:13b (or use codellama:7b for lighter setups)

Step 1: Docker Compose Stack

Create docker-compose.yml:

version: '3.8'

services:
  gitea:
    image: gitea/gitea:latest
    ports:
      - '3000:3000'
    volumes:
      - gitea_data:/data
    environment:
      - GITEA__server__ROOT_URL=http://localhost:3000

  ollama:
    image: ollama/ollama:latest
    ports:
      - '11434:11434'
    volumes:
      - ollama_data:/root/.ollama
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]

  review-bot:
    build: ./review-bot
    ports:
      - '5000:5000'
    environment:
      - OLLAMA_URL=http://ollama:11434
      - GITEA_URL=http://gitea:3000
      - GITEA_TOKEN=${GITEA_TOKEN}
      - MODEL=codellama:13b
    depends_on:
      - ollama
      - gitea

volumes:
  gitea_data:
  ollama_data:
Enter fullscreen mode Exit fullscreen mode

Step 2: The Review Bot

Create review-bot/app.py:

import os
import requests
from flask import Flask, request, jsonify

app = Flask(__name__)

OLLAMA_URL = os.environ.get('OLLAMA_URL', 'http://localhost:11434')
GITEA_URL = os.environ.get('GITEA_URL', 'http://localhost:3000')
GITEA_TOKEN = os.environ['GITEA_TOKEN']
MODEL = os.environ.get('MODEL', 'codellama:13b')

SYSTEM_PROMPT = """You are a senior code reviewer. Analyze the following git diff and provide:
1. A brief summary of the changes
2. Potential bugs or issues
3. Suggestions for improvement
4. Security concerns if any

Be concise. Focus on what matters. Skip obvious stuff."""


def fetch_pr_diff(owner, repo, pr_number):
    url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/pulls/{pr_number}"
    headers = {
        'Authorization': f'token {GITEA_TOKEN}',
        'Accept': 'application/diff'
    }
    resp = requests.get(url, headers=headers)
    resp.raise_for_status()
    return resp.text


def review_with_ollama(diff_text):
    if len(diff_text) > 8000:
        diff_text = diff_text[:8000] + '\n\n... (diff truncated)'

    resp = requests.post(f"{OLLAMA_URL}/api/generate", json={
        'model': MODEL,
        'system': SYSTEM_PROMPT,
        'prompt': f"Review this diff:\n\n```
{% endraw %}
diff\n{diff_text}\n
{% raw %}
```",
        'stream': False,
        'options': {'temperature': 0.3, 'num_predict': 1024}
    })
    resp.raise_for_status()
    return resp.json()['response']


def post_comment(owner, repo, pr_number, body):
    url = f"{GITEA_URL}/api/v1/repos/{owner}/{repo}/issues/{pr_number}/comments"
    headers = {'Authorization': f'token {GITEA_TOKEN}'}
    resp = requests.post(url, headers=headers, json={
        'body': f"🤖 **AI Code Review**\n\n{body}"
    })
    resp.raise_for_status()
    return resp.json()


@app.route('/webhook', methods=['POST'])
def handle_webhook():
    payload = request.json
    action = payload.get('action')
    if action not in ('opened', 'synchronized'):
        return jsonify({'status': 'skipped'}), 200

    pr = payload['pull_request']
    repo = payload['repository']
    owner = repo['owner']['login']
    repo_name = repo['name']
    pr_number = pr['number']

    print(f"Reviewing PR #{pr_number} in {owner}/{repo_name}...")

    try:
        diff = fetch_pr_diff(owner, repo_name, pr_number)
        review = review_with_ollama(diff)
        post_comment(owner, repo_name, pr_number, review)
        return jsonify({'status': 'reviewed'}), 200
    except Exception as e:
        print(f"Error: {e}")
        return jsonify({'status': 'error', 'message': str(e)}), 500


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
Enter fullscreen mode Exit fullscreen mode

And review-bot/Dockerfile:

FROM python:3.12-slim
WORKDIR /app
RUN pip install flask requests
COPY app.py .
CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Step 3: Pull the Model

Before the bot can review anything, Ollama needs the model:

docker compose up -d ollama
docker compose exec ollama ollama pull codellama:13b
Enter fullscreen mode Exit fullscreen mode

This downloads ~7 GB. Grab some coffee.

Step 4: Configure the Gitea Webhook

  1. Go to your Gitea repo → SettingsWebhooksAdd WebhookGitea
  2. Target URL: http://review-bot:5000/webhook
  3. Trigger on: Pull Request Events
  4. Content Type: application/json
  5. Save

Since all services are on the same Docker network, Gitea can reach the bot by service name.

Step 5: Launch and Test

export GITEA_TOKEN=your_token_here
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Open a PR in your Gitea repo. Within seconds, you'll see a comment from the bot with a structured code review.

Tuning Tips

Model selection matters. codellama:13b gives solid reviews. For faster but less detailed feedback, drop to codellama:7b. If you have 32+ GB RAM, codellama:34b is noticeably better at catching subtle bugs.

Temperature at 0.3 keeps reviews focused and deterministic. Bump to 0.5 for more creative suggestions.

Diff truncation at 8000 chars is conservative. If your model has a 16k context window, push higher — but watch for quality degradation on very long diffs.

Add file filtering to skip reviews on generated files:

SKIP_PATTERNS = ['package-lock.json', '.min.js', '.generated.']

def should_review(filename):
    return not any(p in filename for p in SKIP_PATTERNS)
Enter fullscreen mode Exit fullscreen mode

What's Next?

This is a foundation. Ideas to build on:

  • Line-level comments using Gitea's review API instead of issue comments
  • Multiple models — route security-sensitive files to a specialized model
  • Review history — store past reviews in SQLite to track recurring issues
  • Slack/Discord notifications when a review finds critical issues

The Bottom Line

You don't need a $20/month SaaS subscription for AI code review. A spare machine with 16 GB of RAM, Docker, and about 100 lines of Python gives you a private, customizable review bot that runs on your terms.

The best part? Every review makes your code better, and none of your code leaves your network.


SIGNAL is a weekly series about building practical tools with AI — no hype, just stuff that works. Follow for more self-hosted builds.

Top comments (0)