- 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>
16 KiB
Deployment Guide
This guide covers production deployment of Gitea MCP Remote in various environments.
Table of Contents
- Prerequisites
- Docker Deployment
- Security Best Practices
- Monitoring and Health Checks
- Reverse Proxy Configuration
- Cloud Deployment
- Kubernetes Deployment
- 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
Recommended
- HTTPS-capable reverse proxy (Nginx, Caddy, Traefik)
- Secrets management solution (not
.envfiles in production) - Monitoring and logging infrastructure
- Firewall or VPN for network security
Docker Deployment
Quick Start
- Clone the repository:
git clone https://gitea.hotserv.cloud/personal-projects/gitea-mcp-remote.git
cd gitea-mcp-remote
- 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
- Start the services (app + Caddy):
docker-compose -f docker/docker-compose.yml up -d
- 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
-
Launch EC2 instance:
- Amazon Linux 2 or Ubuntu 22.04
- t3.micro or larger
- Security group: Allow port 443 (HTTPS)
-
Install Docker:
sudo yum update -y
sudo yum install -y docker
sudo service docker start
sudo usermod -aG docker ec2-user
- 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
- Configure Nginx or ALB for HTTPS
AWS ECS (Fargate)
- 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
}
}
]
}
- Create ECS service with ALB
Google Cloud Run
- Build and push image:
gcloud builds submit --tag gcr.io/PROJECT_ID/gitea-mcp-wrapper
- 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
- Restore configuration files
- Rebuild or load Docker image
- Start services:
docker-compose up -d - Verify health:
curl http://localhost:8000/health - Test authentication and tool access
Production Checklist
- HTTPS configured via reverse proxy
AUTH_TOKENset 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.