Self-hosting Guide
This guide covers running the Spekta MCP server locally using Docker Compose. Self-hosting is useful for development, air-gapped environments, or if you want full control over your data.
Note: The Next.js frontend (dashboard, provider management) is a separate application. This guide covers the MCP server only.
Prerequisites
- Docker ≥ 24
- Docker Compose v2 (bundled with Docker Desktop)
- Git
Quick start
# 1. Clone the repo
git clone https://github.com/nanocorp-hq/spekta.git
cd spekta/spekta-mcp
# 2. Copy the example env file
cp .env.example .env
# 3. Edit .env — at minimum, set a strong SECRET_KEY
nano .env
# 4. Start all services (app + postgres + redis)
docker compose up -d
# 5. Wait for Postgres to be ready, then apply migrations
docker compose exec app python migrations/seed.py # optional: load demo data
# 6. Verify health
curl http://localhost:8080/health
# → {"status":"ok","db":"ok","version":"1.0.0"}
The MCP SSE endpoint is now live at http://localhost:8080/mcp/sse.
Docker Compose services
The spekta-mcp/docker-compose.yml defines three services:
| Service | Image | Default port | Role |
|---|---|---|---|
app | Built from ./Dockerfile | 8080 | FastAPI MCP server |
postgres | postgres:16 | 5432 | Primary database |
redis | redis:7-alpine | 6379 | Rate-limit counters (optional) |
Starting and stopping
# Start (detached)
docker compose up -d
# Watch logs
docker compose logs -f app
# Stop (keep volumes)
docker compose stop
# Stop and remove containers + volumes
docker compose down -v
Environment variables reference
Set these in spekta-mcp/.env (or pass as environment variables in your orchestration platform).
Required
| Variable | Example | Description |
|---|---|---|
DATABASE_URL | postgresql://spekta:spekta@postgres:5432/spekta | PostgreSQL connection string. Use the service name postgres when running inside Compose. |
SECRET_KEY | change-me-in-production-32chars+ | Used for session token signing. Generate with openssl rand -hex 32. |
Optional — application behaviour
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | INFO | Logging verbosity: DEBUG, INFO, WARNING, ERROR |
PORT | 8080 | Port the Uvicorn server listens on |
WORKERS | 1 | Uvicorn worker count. For production, set to (2 × CPU cores) + 1. |
REDIS_URL | (none) | Redis connection string for distributed rate limiting. If unset, rate limiting falls back to in-memory (not suitable for multi-worker deployments). |
RATE_LIMIT_PER_MINUTE | 100 | Global default requests/minute per API key (overridden by tenant plan). |
Optional — observability
| Variable | Default | Description |
|---|---|---|
SENTRY_DSN | (none) | Sentry DSN for error tracking. Leave empty to disable Sentry. |
SENTRY_ENVIRONMENT | production | Sentry environment tag |
SENTRY_TRACES_SAMPLE_RATE | 0.1 | Fraction of requests to trace (0.0–1.0) |
Optional — CORS
| Variable | Default | Description |
|---|---|---|
CORS_ORIGINS | * | Comma-separated list of allowed origins. Set to your dashboard URL in production: https://app.your-domain.com. |
Database setup
Schema migrations
All migrations/*.sql files are automatically applied by Postgres on first start (via docker-entrypoint-initdb.d). If you are connecting to an external Postgres instance, apply them in filename order:
for migration in spekta-mcp/migrations/*.sql; do
psql "$DATABASE_URL" < "$migration"
done
Seeding demo data
The seed script creates three demo tenants with events, ticket sales, and API keys:
# Inside Docker Compose
docker compose exec app python migrations/seed.py
# Against an external DB
DATABASE_URL="postgresql://..." python spekta-mcp/migrations/seed.py
After seeding, three demo API keys are printed:
tix_live_festival_hub_gmbh_0 → tenant: Festival Hub GmbH (plan: business)
tix_live_cityevents_ltd_1 → tenant: City Events Ltd (plan: pro)
tix_live_indie_shows_co_2 → tenant: Indie Shows Co (plan: free)
Connecting to the database
# Via Docker Compose
docker compose exec postgres psql -U spekta spekta
# Directly (requires psql installed locally)
psql postgresql://spekta:spekta@localhost:5432/spekta
Connecting Claude Desktop to the local server
Update ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) to point at localhost:
{
"mcpServers": {
"spekta-local": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:8080/mcp/sse"],
"env": {
"X-Spekta-API-Key": "tix_live_festival_hub_gmbh_0"
}
}
}
}
Restart Claude Desktop. You should see the Spekta tools available in the toolbar.
Production considerations
TLS / HTTPS
The Docker Compose setup does not terminate TLS. In production, place a reverse proxy (nginx, Caddy, Traefik) in front of the app service. Example with Caddy:
spekta.your-domain.com {
reverse_proxy app:8080
}
Scaling workers
Increase WORKERS in .env or use --scale with Compose. For more than 2 workers, set REDIS_URL to ensure rate limits are shared across processes.
Credential encryption
The Next.js sync engine encrypts provider credentials with ENCRYPTION_KEY. The MCP server does not handle provider credentials directly — it reads already-synced data from the database. No additional encryption config is needed for the MCP server.
Backup
Back up the postgres_data Docker volume regularly. The volume contains all synced event data. The volume name is spekta-mcp_postgres_data by default (prefixed with the Compose project name).
# One-time snapshot
docker run --rm \
-v spekta-mcp_postgres_data:/data \
-v $(pwd)/backups:/backup \
postgres:16 \
pg_dump -U spekta -h localhost spekta > /backup/spekta-$(date +%Y%m%d).sql
Updating
git pull origin main
docker compose build app
docker compose up -d --no-deps app
Check the CHANGELOG.md before updating — breaking schema changes require running new migration files.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
{"status":"ok","db":"error"} | Postgres not ready or wrong DATABASE_URL | Check docker compose logs postgres |
401 Unauthorized | Invalid or revoked API key | Re-run seed or generate a key via the dashboard |
429 Too Many Requests | Rate limit hit | Wait 60 s or increase RATE_LIMIT_PER_MINUTE |
| MCP tools not visible in Claude Desktop | Config path wrong or JSON syntax error | Validate JSON at jsonlint.com; restart Claude Desktop |
ModuleNotFoundError on startup | Python dependencies missing | Rebuild: docker compose build --no-cache app |