Deployment guide

Self-hosted AI stack: Gitea, runners and Ollama on a VPS

Deploy on a VPS Cloud →

Artificial Intelligence9 min read

Self-hosted AI stack: Gitea, runners and Ollama on a VPS

GitHub Actions bills compute time, cloud LLM APIs send your code to the provider's servers. For a developer handling confidential client code, both external dependencies are non-starters. This guide shows how to assemble, on a single VPS, a Gitea forge with its actions engine, a Docker-based runner and an Ollama inference server — a complete pipeline where neither a line of code nor a prompt leaves your infrastructure. This approach gathered 119 points on Hacker News on August 21, 2026 (discussion: https://news.ycombinator.com/item?id=49390463), a clear signal that appetite for sovereign AI pipelines is real.

Why assemble this stack on a VPS

GitHub Actions and cloud LLM APIs share the same flaw: they offload processing. In the first case, your source code runs on shared runners; in the second, the context of your requests is sent to a third party. For an agency or freelancer managing client code under NDA, fixing one without fixing the other is not enough.

Gitea has a built-in actions engine compatible with GitHub Actions since version 1.19. Ollama exposes a local REST API on the Docker internal network. An act_runner container reads your .yml workflows exactly as GitHub would — and calls Ollama instead of a cloud API. The entire stack runs with docker compose up -d and generates no outbound traffic to major model APIs.

Concrete benefits of this architecture

  • Total code confidentiality: runners execute on your VPS, cloned code never leaves your Docker internal network.
  • Zero tokens sent externally: Ollama serves inference locally; no request reaches openai.com or anthropic.com.
  • Fixed, predictable cost: no per-workflow-run billing, no per-token billing — one monthly budget line, regardless of load.
  • Reusable GitHub Actions workflows: Gitea Actions is compatible with .github/workflows/ syntax; your pipelines migrate without rewriting.
  • Freely swappable models: Qwen2.5-Coder, DeepSeek-Coder, Llama 3.1 or Mistral — one ollama pull command to change model, without touching the pipeline.
  • Complete audit and traceability: runner logs, model load history and nginx journals stay on your infrastructure and belong to you.

Hardware and software prerequisites

The limiting constraint is RAM: the model must fit entirely in memory for inference to remain responsive. A 7B model quantized in Q4_K_M format requires around 5 to 6 GB of RAM; adding Gitea (less than 100 MB at rest) and the runner, count 8 GB minimum for a 7B model and 16 GB recommended for a 13B model.

For CPU, two vCPUs are enough for Gitea and runners; CPU-only inference on a 7B model takes a few seconds per response, which is acceptable for automated code review. A dedicated GPU reduces this to under one second, but is not required for CI pipeline use.

Required software on the VPS: Docker Engine and Docker Compose v2, a domain name pointing to your server (for Gitea TLS certificates), and ports 3000 (Gitea) and 11434 (Ollama, internal network only) available.

Recommended models by available RAM

  • Qwen2.5-Coder:7B (Q4_K_M format, ~5 GB RAM) — solid quality/resource ratio for code reviews in 2026; extended context understanding and style convention awareness.
  • DeepSeek-Coder:6.7B (Q4 format, ~4.5 GB RAM) — compact alternative when RAM is tight; precise on Python, JavaScript and diffs under 200 lines.
  • Llama 3.1:8B (Q4_K_M format, ~5.5 GB RAM) — multilingual generalist, useful when projects mix code and documentation in multiple languages.
  • Mistral:7B (Q4_K_M format, ~4.5 GB RAM) — short, direct responses, ideal for a diff summary rather than a detailed analysis.
  • Upgrading to a 13B model — on a VPS with 16 GB or more, codellama:13b or qwen2.5-coder:14b improve reviews on large diffs; CPU inference time goes from ~5 s to ~15 s per call.

Deploy the Gitea + Ollama + runner stack

01

Create the file structure

Create a project directory and a docker-compose.yml file that declares three services: gitea, ollama and runner. Place all volumes in a data/ subfolder to simplify backups.

mkdir -p ~/gitea-stack/data/{gitea,ollama,runner}
cd ~/gitea-stack
02

Write the docker-compose.yml

The file declares an internal ai-net network on which all three services communicate. Ollama is not exposed on the host: only the runner can reach it via the Docker network.

cat > docker-compose.yml << 'EOF'
version: "3.8"

networks:
  ai-net:
    driver: bridge

volumes:
  gitea-data:
  ollama-data:
  runner-data:

services:
  gitea:
    image: gitea/gitea:latest
    restart: unless-stopped
    networks: [ai-net]
    ports:
      - "3000:3000"
      - "2222:22"
    volumes:
      - gitea-data:/data
    environment:
      - GITEA__server__DOMAIN=git.yourdomain.com
      - GITEA__server__ROOT_URL=https://git.yourdomain.com
      - GITEA__actions__ENABLED=true

  ollama:
    image: ollama/ollama:latest
    restart: unless-stopped
    networks: [ai-net]
    volumes:
      - ollama-data:/root/.ollama

  runner:
    image: gitea/act_runner:latest
    restart: unless-stopped
    networks: [ai-net]
    volumes:
      - runner-data:/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - GITEA_INSTANCE_URL=http://gitea:3000
      - GITEA_RUNNER_REGISTRATION_TOKEN=${RUNNER_TOKEN}
      - GITEA_RUNNER_NAME=local-runner
      - OLLAMA_URL=http://ollama:11434
EOF
03

Start Gitea and retrieve the runner token

Start only Gitea first to complete the initial setup and generate the runner registration token.

docker compose up -d gitea

Open http://<vps-ip>:3000 in your browser, complete the setup wizard, then go to Site Administration → Actions → Runners to create a registration token. Note this token — you will need it in the next step.

04

Start Ollama and download a model

Start Ollama and download your model. For a code review pipeline, qwen2.5-coder:7b or deepseek-coder:6.7b offer a good quality/resource ratio.

docker compose up -d ollama
# download a code model
docker exec gitea-stack-ollama-1 ollama pull qwen2.5-coder:7b
# verify the API responds on the internal network
docker run --rm --network gitea-stack_ai-net curlimages/curl \
  http://ollama:11434/api/tags

The JSON response lists available models — confirmation that the Ollama API is reachable from within the Docker internal network.

05

Register the runner and start the full stack

Create a .env file with the token retrieved in step 3, then start the runner.

echo "RUNNER_TOKEN=your_token_here" > .env
docker compose up -d runner
# verify the runner registered successfully
docker compose logs runner | tail -20

Check in Site Administration → Actions → Runners that your runner appears with Active status.

06

Create a workflow that calls Ollama

In a Gitea repository, create .gitea/workflows/review.yml. The workflow clones the code, calls Ollama's /api/chat endpoint via curl and posts the result as a pull request comment.

cat > .gitea/workflows/review.yml << 'EOF'
name: AI Code Review
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Code review via Ollama
        run: |
          DIFF=$(git diff HEAD~1 --unified=5 | head -200)
          curl -s ${OLLAMA_URL:-http://ollama:11434}/api/chat \
            -H 'Content-Type: application/json' \
            -d "{\"model\": \"qwen2.5-coder:7b\", \"stream\": false,
                 \"messages\": [{\"role\": \"user\",
                 \"content\": \"Review this Git diff: $DIFF\"}]}" \
            | jq -r '.message.content'
EOF

Push this file to your Gitea forge — the runner detects it and runs the job on the internal network, without any Internet access.

07

Put Gitea behind an HTTPS reverse proxy

Place Gitea behind nginx or Caddy with a Let's Encrypt certificate to expose the forge on git.yourdomain.com. Port 3000 should no longer be directly accessible from outside.

# minimal nginx example
cat > /etc/nginx/sites-available/gitea.conf << 'EOF'
server {
    listen 443 ssl;
    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;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
EOF
nginx -t && systemctl reload nginx

Ollama remains exclusively on the Docker internal network and is never exposed on the Internet.

Verify no token leaves your server

To confirm that LLM calls stay on your VPS, capture outbound network traffic during a workflow run: tcpdump -i eth0 -n 'dst port 443 and dst host openai.com'. Zero packets captured validates the isolation. You can also inspect Ollama logs (docker compose logs ollama): each inference request is logged with its source address — it should always be a Docker internal network IP, never an external one.

Cost comparison: self-hosted VPS vs cloud API

A 7B model processes around 30,000 to 50,000 tokens per minute on two vCPUs. A 150-line diff review consumes about 800 tokens (prompt + response). Over 1,000 monthly reviews — a realistic volume for a team of five developers — that's 800,000 tokens.

On the cloud side, gpt-4o-mini charges $0.15 per million input tokens and $0.60 per million output tokens (OpenAI pricing, September 2026). For 800,000 tokens: roughly $0.70 per month. Claude Haiku is in the same range. The financial argument is therefore weak at moderate volumes.

The argument that holds is confidentiality: client code diffs, API keys appearing in error messages, business variable names — all of this leaves your network with every external API call. On a ServOrbit VPS with 8 GB of RAM (around €15 per month), local inference adds no extra cost and no token ever leaves your server.

Going further: observing and enriching the pipeline

Once the base stack is running, two extensions are natural. The first is adding Langfuse as a fifth service: it traces every LLM call (model, duration, tokens consumed, result), allowing you to measure review quality, detect model regressions and compare results on your real corpus before switching versions. The second is parallelizing runners: a second act_runner with a different label (for example gpu) can target a node with GPU access for heavy inference jobs, while lightweight jobs (lint, unit tests) continue on the base CPU runner.

For backups, the gitea-data and ollama-data volumes contain repositories and downloaded models respectively. A daily snapshot of these two volumes is enough to restore the full stack in under ten minutes. Since Ollama models can be re-downloaded on demand from the public registry, only gitea-data is truly critical for service continuity and preserving Git history.

Your VPS for this AI stack

A ServOrbit VPS with root access, pre-installed Docker and a dedicated IPv4 is the foundation on which Gitea, runners and Ollama run without shared resources or forced outbound traffic.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab