Files
gitea-mcp-remote/DEPLOYMENT.md
lmiranda 2dbb66deae docs: Create CLAUDE.md and update deployment documentation (#27)
- Create CLAUDE.md with comprehensive project guidance for Claude Code
- Update README.md with correct architecture (direct import, not subprocess)
- Update project structure to reflect tests/ at repo root and docker/ directory
- Update default port from 8000 to 8080
- Update repository links to Gitea
- Update DEPLOYMENT.md with two-service Docker architecture (app + Caddy)
- Fix Claude Desktop config example to use /mcp endpoint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 21:32:39 -05:00

16 KiB

Deployment Guide

This guide covers production deployment of Gitea MCP Remote in various environments.

Table of Contents

  1. Prerequisites
  2. Docker Deployment
  3. Security Best Practices
  4. Monitoring and Health Checks
  5. Reverse Proxy Configuration
  6. Cloud Deployment
  7. Kubernetes Deployment
  8. Troubleshooting

Prerequisites

Required

  • Docker and Docker Compose (for Docker deployment)
  • Gitea instance with API access
  • Gitea API token with appropriate permissions
  • Network connectivity between wrapper and Gitea instance
  • HTTPS-capable reverse proxy (Nginx, Caddy, Traefik)
  • Secrets management solution (not .env files in production)
  • Monitoring and logging infrastructure
  • Firewall or VPN for network security

Docker Deployment

Quick Start

  1. Clone the repository:
git clone https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote.git
cd gitea-mcp-remote
  1. Create configuration:
cp docker/.env.example docker/.env
nano docker/.env  # Edit with your values

Required configuration:

GITEA_URL=https://gitea.example.com
GITEA_TOKEN=your_gitea_api_token
GITEA_OWNER=your_username_or_org
GITEA_REPO=your_default_repo  # Optional
AUTH_TOKEN=your_bearer_token  # Recommended
  1. Start the services (app + Caddy):
docker-compose -f docker/docker-compose.yml up -d
  1. Verify deployment:
curl http://localhost/health      # Via Caddy
curl http://localhost:8080/health # Direct to app

Production Configuration

The default docker/docker-compose.yml includes both app and Caddy reverse proxy services. For customization:

services:
  # Python MCP Server
  app:
    build:
      context: ..
      dockerfile: docker/Dockerfile
    image: gitea-mcp-remote:latest
    container_name: gitea-mcp-remote-app
    restart: always
    expose:
      - "8080"
    environment:
      - GITEA_URL=${GITEA_URL}
      - GITEA_TOKEN=${GITEA_TOKEN}
      - GITEA_OWNER=${GITEA_OWNER}
      - GITEA_REPO=${GITEA_REPO:-}
      - HTTP_HOST=0.0.0.0
      - HTTP_PORT=8080
      - AUTH_TOKEN=${AUTH_TOKEN:-}
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 5s
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - gitea-mcp-network

  # Caddy Reverse Proxy (HTTPS termination)
  caddy:
    image: caddy:2-alpine
    container_name: gitea-mcp-remote-caddy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      app:
        condition: service_healthy
    networks:
      - gitea-mcp-network

networks:
  gitea-mcp-network:
    driver: bridge

volumes:
  caddy_data:
  caddy_config:

Docker Build Options

Build the image:

docker build -f docker/Dockerfile -t gitea-mcp-remote:latest .

Build with specific Python version:

docker build -f docker/Dockerfile --build-arg PYTHON_VERSION=3.11 -t gitea-mcp-remote:latest .

Tag for registry:

docker tag gitea-mcp-remote:latest registry.example.com/gitea-mcp-remote:latest
docker push registry.example.com/gitea-mcp-remote:latest

Security Best Practices

1. Use Authentication

Always set AUTH_TOKEN in production:

# Generate a secure token
openssl rand -base64 32

# Add to .env
AUTH_TOKEN=<generated_token>

2. Use HTTPS

Never expose the wrapper directly to the internet without HTTPS. Use a reverse proxy (see below).

3. Network Isolation

  • Bind to localhost only (127.0.0.1) if using a reverse proxy
  • Use Docker networks to isolate services
  • Consider VPN or private networking for access

4. Secrets Management

Don't use .env files in production. Use Docker secrets, Kubernetes secrets, or a secrets manager:

Docker Secrets Example:

services:
  gitea-mcp-wrapper:
    secrets:
      - gitea_token
      - auth_token
    environment:
      - GITEA_TOKEN_FILE=/run/secrets/gitea_token
      - AUTH_TOKEN_FILE=/run/secrets/auth_token

secrets:
  gitea_token:
    external: true
  auth_token:
    external: true

5. Regular Updates

  • Rotate Gitea API token regularly
  • Rotate AUTH_TOKEN regularly
  • Keep Docker base image updated
  • Update dependencies: pip install --upgrade -r requirements.txt

6. Minimal Permissions

Grant the Gitea API token only the minimum required permissions:

  • Repository read/write
  • Issue management
  • Label management
  • Milestone management

Avoid granting admin or organization-level permissions.

Monitoring and Health Checks

Health Check Endpoints

The wrapper provides three health check endpoints:

GET /health
GET /healthz
GET /ping

All return {"status": "healthy"} with HTTP 200 when the server is operational.

Docker Health Checks

Docker automatically monitors the health check and can restart if unhealthy:

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
  interval: 30s
  timeout: 3s
  retries: 3
  start_period: 5s

Monitoring Integration

Prometheus metrics: (Not yet implemented, but can be added)

Log monitoring:

# View logs
docker-compose logs -f gitea-mcp-wrapper

# JSON structured logs
docker logs gitea-mcp-wrapper --tail 100

Uptime monitoring:

Use tools like UptimeRobot, Pingdom, or Datadog to monitor /health endpoint.

Reverse Proxy Configuration

Nginx

server {
    listen 443 ssl http2;
    server_name mcp.example.com;

    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;

        # Pass through Authorization header
        proxy_set_header Authorization $http_authorization;
        proxy_pass_header Authorization;

        # WebSocket support (if needed in future)
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Health check endpoint (optional, can bypass auth)
    location /health {
        proxy_pass http://127.0.0.1:8000/health;
        access_log off;
    }
}

Caddy

mcp.example.com {
    reverse_proxy localhost:8000 {
        # Pass through Authorization header
        header_up Authorization {>Authorization}
    }

    # Optional: Rate limiting
    rate_limit {
        zone mcp_zone
        rate 100r/m
    }
}

Traefik

# docker-compose.yml
services:
  gitea-mcp-wrapper:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.mcp.rule=Host(`mcp.example.com`)"
      - "traefik.http.routers.mcp.entrypoints=websecure"
      - "traefik.http.routers.mcp.tls.certresolver=letsencrypt"
      - "traefik.http.services.mcp.loadbalancer.server.port=8000"

Cloud Deployment

AWS EC2

  1. Launch EC2 instance:

    • Amazon Linux 2 or Ubuntu 22.04
    • t3.micro or larger
    • Security group: Allow port 443 (HTTPS)
  2. Install Docker:

sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -aG docker ec2-user
  1. Deploy wrapper:
git clone https://github.com/lmiranda/gitea-mcp-remote.git
cd gitea-mcp-remote
cp .env.docker.example .env
nano .env  # Configure
docker-compose up -d
  1. Configure Nginx or ALB for HTTPS

AWS ECS (Fargate)

  1. Create task definition:
{
  "family": "gitea-mcp-wrapper",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "gitea-mcp-wrapper",
      "image": "your-ecr-repo/gitea-mcp-wrapper:latest",
      "portMappings": [
        {
          "containerPort": 8000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {"name": "GITEA_URL", "value": "https://gitea.example.com"},
        {"name": "HTTP_HOST", "value": "0.0.0.0"},
        {"name": "HTTP_PORT", "value": "8000"}
      ],
      "secrets": [
        {
          "name": "GITEA_TOKEN",
          "valueFrom": "arn:aws:secretsmanager:region:account:secret:gitea-token"
        },
        {
          "name": "AUTH_TOKEN",
          "valueFrom": "arn:aws:secretsmanager:region:account:secret:auth-token"
        }
      ],
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3,
        "startPeriod": 10
      }
    }
  ]
}
  1. Create ECS service with ALB

Google Cloud Run

  1. Build and push image:
gcloud builds submit --tag gcr.io/PROJECT_ID/gitea-mcp-wrapper
  1. Deploy:
gcloud run deploy gitea-mcp-wrapper \
  --image gcr.io/PROJECT_ID/gitea-mcp-wrapper \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars GITEA_URL=https://gitea.example.com \
  --set-secrets GITEA_TOKEN=gitea-token:latest,AUTH_TOKEN=auth-token:latest \
  --port 8000

Azure Container Instances

az container create \
  --resource-group myResourceGroup \
  --name gitea-mcp-wrapper \
  --image your-registry/gitea-mcp-wrapper:latest \
  --ports 8000 \
  --dns-name-label gitea-mcp \
  --environment-variables \
    GITEA_URL=https://gitea.example.com \
    HTTP_HOST=0.0.0.0 \
    HTTP_PORT=8000 \
  --secure-environment-variables \
    GITEA_TOKEN=your_token \
    AUTH_TOKEN=your_auth_token

Kubernetes Deployment

Deployment Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitea-mcp-wrapper
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: gitea-mcp-wrapper
  template:
    metadata:
      labels:
        app: gitea-mcp-wrapper
    spec:
      containers:
      - name: gitea-mcp-wrapper
        image: your-registry/gitea-mcp-wrapper:latest
        ports:
        - containerPort: 8000
        env:
        - name: GITEA_URL
          value: "https://gitea.example.com"
        - name: HTTP_HOST
          value: "0.0.0.0"
        - name: HTTP_PORT
          value: "8000"
        - name: GITEA_TOKEN
          valueFrom:
            secretKeyRef:
              name: gitea-secrets
              key: token
        - name: AUTH_TOKEN
          valueFrom:
            secretKeyRef:
              name: gitea-secrets
              key: auth-token
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: gitea-mcp-wrapper
  namespace: default
spec:
  selector:
    app: gitea-mcp-wrapper
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8000
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gitea-mcp-wrapper
  namespace: default
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - mcp.example.com
    secretName: mcp-tls
  rules:
  - host: mcp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: gitea-mcp-wrapper
            port:
              number: 80

Secrets Management

# Create secret
kubectl create secret generic gitea-secrets \
  --from-literal=token=your_gitea_token \
  --from-literal=auth-token=your_auth_token \
  --namespace=default

Troubleshooting

Container Won't Start

# Check logs
docker-compose logs gitea-mcp-wrapper

# Check container status
docker-compose ps

# Rebuild image
docker-compose build --no-cache
docker-compose up -d

Health Check Failing

# Test health endpoint directly
docker exec gitea-mcp-wrapper curl http://localhost:8000/health

# Check if server is listening
docker exec gitea-mcp-wrapper netstat -tlnp

Cannot Reach Gitea from Container

# Test connectivity
docker exec gitea-mcp-wrapper curl -v https://gitea.example.com

# Check DNS resolution
docker exec gitea-mcp-wrapper nslookup gitea.example.com

# For docker-compose, ensure network allows egress

High Memory Usage

# Check container stats
docker stats gitea-mcp-wrapper

# Adjust resource limits in docker-compose.yml
deploy:
  resources:
    limits:
      memory: 256M

Authentication Failures

# Verify AUTH_TOKEN is set
docker exec gitea-mcp-wrapper printenv AUTH_TOKEN

# Test with curl
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/tools/list

# Check logs for auth failures
docker-compose logs gitea-mcp-wrapper | grep -i auth

Scaling Considerations

Horizontal Scaling

The wrapper is stateless and can be scaled horizontally:

# docker-compose.yml
services:
  gitea-mcp-wrapper:
    deploy:
      replicas: 3

Or in Kubernetes:

kubectl scale deployment gitea-mcp-wrapper --replicas=5

Load Balancing

Use a load balancer to distribute traffic:

  • Docker Swarm: Built-in load balancing
  • Kubernetes: Service with multiple pods
  • Cloud: AWS ALB, GCP Load Balancer, Azure Load Balancer

Caching

Consider caching responses to reduce Gitea API load:

  • Add Redis or Memcached
  • Cache tool list responses
  • Cache frequently accessed issues/labels

Rate Limiting

Implement rate limiting at reverse proxy level to prevent API abuse:

Nginx:

limit_req_zone $binary_remote_addr zone=mcp:10m rate=10r/s;
limit_req zone=mcp burst=20 nodelay;

Caddy:

rate_limit {
    rate 100r/m
}

Backup and Disaster Recovery

Configuration Backup

# Backup .env file
cp .env .env.backup.$(date +%Y%m%d)

# Backup docker-compose.yml
cp docker-compose.yml docker-compose.yml.backup.$(date +%Y%m%d)

Image Backup

# Save Docker image
docker save gitea-mcp-wrapper:latest | gzip > gitea-mcp-wrapper-backup.tar.gz

# Load Docker image
docker load < gitea-mcp-wrapper-backup.tar.gz

Recovery Plan

  1. Restore configuration files
  2. Rebuild or load Docker image
  3. Start services: docker-compose up -d
  4. Verify health: curl http://localhost:8000/health
  5. Test authentication and tool access

Production Checklist

  • HTTPS configured via reverse proxy
  • AUTH_TOKEN set and secure
  • Secrets stored in secrets manager (not .env)
  • Health checks configured
  • Monitoring and alerting set up
  • Logs aggregated and retained
  • Firewall rules configured
  • Rate limiting enabled
  • Resource limits set
  • Backup strategy in place
  • Disaster recovery plan documented
  • Security updates scheduled
  • Token rotation process defined

For questions or issues, please open an issue on the GitHub repository.