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
Three moving parts:
- Gitea sends a webhook when a PR is opened or updated
- review-bot (a small Python Flask app) receives the webhook, fetches the diff, and sends it to Ollama
-
Ollama runs a code-capable model (we'll use
codellama:13b) and returns the review - 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 usecodellama:7bfor 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:
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)
And review-bot/Dockerfile:
FROM python:3.12-slim
WORKDIR /app
RUN pip install flask requests
COPY app.py .
CMD ["python", "app.py"]
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
This downloads ~7 GB. Grab some coffee.
Step 4: Configure the Gitea Webhook
- Go to your Gitea repo → Settings → Webhooks → Add Webhook → Gitea
- Target URL:
http://review-bot:5000/webhook - Trigger on: Pull Request Events
- Content Type:
application/json - 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
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)
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)