Cómo desplegar un servidor MCP en Docker y Dokploy paso a paso
Esta es la guía que armé después de poner mi servidor MCP de tramites.gub.uy en producción. Cubre lo que hace falta para pasar de "anda en mi máquina" a un servicio HTTPS público que se puede conectar desde Claude Desktop, Claude Code o ChatGPT — sin que se rompa al primer tráfico real.
Si todavía no sabés qué es MCP o por qué importa, leé primero Qué es Model Context Protocol: guía técnica. Esta guía asume que ya tenés un servidor MCP funcionando localmente con transporte stdio o HTTP, y que querés deployarlo.
Pre-requisitos
- Un servidor MCP que ya funciona localmente (Python, TypeScript, Go, Rust — el lenguaje no importa).
- Docker Engine 24+ y Docker Compose v2 en tu entorno de desarrollo.
- Un VPS, servidor cloud o instancia donde corra Dokploy. Hetzner, DigitalOcean, AWS Lightsail, Linode — cualquiera con 1-2 GB de RAM alcanza para empezar.
- Un dominio con acceso DNS (puede ser un subdominio:
mcp.tudominio.com).
Paso 1: HTTP Streamable, no stdio
Para deploy multi-usuario el transporte tiene que ser HTTP Streamable. Stdio sirve solo cuando el cliente lanza el servidor como subproceso local. La mayoría de los SDKs MCP soportan ambos transportes — chequeá la documentación de tu SDK para activar HTTP.
Tu servidor debería terminar exponiendo un endpoint /mcp sobre HTTP. La ruta es convención de la mayoría de los clientes — respetala salvo que tengas razón fuerte para cambiarla.
Paso 2: Dockerfile multi-stage
Un Dockerfile multi-stage te da imágenes chicas, builds reproducibles y separación entre lo que necesitás para compilar y lo que necesitás para ejecutar. Patrón general (ejemplo en Go, adaptable a cualquier lenguaje):
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o /app/server ./cmd/server
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server /app/server
COPY --from=builder /src/migrations /app/migrations
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:8080/health || exit 1
CMD ["/app/server"]Detalles que importan:
- CGO_ENABLED=0 en Go evita dependencia de libc del host. Para SQLite, usá un driver puro Go como
modernc.org/sqlite. - HEALTHCHECK: Docker (y Dokploy) usan esto para saber si tu servicio está listo. Sin healthcheck, el orquestador no sabe cuándo enrutar tráfico.
- tzdata: si tu app maneja timestamps localizados, necesitás los tzdata del sistema.
Paso 3: docker-compose.yml para producción
Un docker-compose simple pero correcto:
services:
mcp:
image: ghcr.io/tu-usuario/tu-mcp-server:latest
restart: unless-stopped
environment:
- APP_ENV=production
- LOG_LEVEL=info
- PORT=8080
- DATABASE_URL=sqlite:///data/app.db
volumes:
- mcp-data:/data
expose:
- "8080"
labels:
- "traefik.enable=true"
- "traefik.http.routers.mcp.rule=Host(`mcp.tudominio.com`)"
- "traefik.http.routers.mcp.entrypoints=websecure"
- "traefik.http.routers.mcp.tls.certresolver=letsencrypt"
volumes:
mcp-data:Tres decisiones a notar:
- Volumen persistente para datos que tienen que sobrevivir al restart del contenedor (base SQLite, índices, configuración runtime).
- expose en lugar de ports: el puerto solo se publica al network interno de Docker, no al host. Traefik (que viene con Dokploy) hace el reverse proxy.
- Labels de Traefik: configuración declarativa del routing y certificados HTTPS automáticos vía Let's Encrypt.
Paso 4: variables de entorno para 12-factor
Toda configuración que cambia entre dev/staging/prod va en variables de entorno. Lista típica para un servidor MCP:
APP_ENV:production|staging|development.LOG_LEVEL:info,debug,warn.PORT: 8080 por defecto.DATABASE_URL: conexión a la base, formato URI.API_KEYuOAUTH_*: secrets para autenticación.RATE_LIMIT_RPM: requests por minuto por IP.- Variables específicas a tu integración (API keys de OpenAI, GitHub, Slack, etc.).
Nunca hardcodees secrets en el Dockerfile o el compose. Usá el sistema de variables de Dokploy o un secret manager externo.
Paso 5: deploy en Dokploy
Dokploy es una alternativa open source y self-hosted a Vercel/Railway/Heroku. Corre en tu VPS y te da UI para deploys, dominios, secrets, monitoreo. Si todavía no lo tenés instalado, una sola línea:
curl -sSL https://dokploy.com/install.sh | shUna vez con Dokploy corriendo en tu servidor:
- Crear un proyecto nuevo en la UI de Dokploy.
- Agregar una "Application" tipo Docker Compose. Apuntala a tu repo de Git (GitHub/GitLab/Bitbucket) o pegale el compose directo.
- Configurar variables de entorno en la pestaña Environment. Las que pongas acá se inyectan al contenedor en runtime.
- Configurar el dominio en la pestaña Domains. Dokploy usa Traefik internamente y resuelve los certificados Let's Encrypt automáticamente — solo necesitás que el DNS apunte a la IP de tu VPS.
- Deploy. La primera vez tarda 1-3 minutos; después solo el delta.
Paso 6: validar el endpoint /mcp
Una vez deployado, probá manualmente:
curl -sS -X POST https://mcp.tudominio.com/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Deberías ver un JSON con la lista de tools que tu servidor expone. Si recibís 502/504, casi siempre es que el contenedor no está pasando el healthcheck — revisar logs en Dokploy.
Paso 7: conectar desde Claude Desktop o Claude Code
Claude Desktop / Claude.ai
Para custom connectors remotos, el endpoint tiene que ser HTTPS público:
- Abrir Claude Desktop o claude.ai.
- Settings → Customize → Connectors → Add custom connector.
- Pegar la URL:
https://mcp.tudominio.com/mcp. - Si tu servidor requiere autenticación, configurar headers o tokens.
- Probar invocando una tool desde el chat.
Claude Code (CLI local de developer)
claude mcp add --transport http my-server https://mcp.tudominio.com/mcp Después listá tus servidores configurados con claude mcp list para confirmar.
Hardening mínimo para producción
Cosas que sí o sí necesitás antes de exponer el servidor a usuarios reales:
1. Rate limiting
In-memory por IP es suficiente para empezar (60 req/min por IP es un default razonable). Si esperás tráfico distribuido, considerar Redis para rate limit compartido entre instancias.
2. Logs estructurados
JSON estructurado, no texto libre. Campos clave para un servidor MCP: timestamp, level, method, tool, duration_ms, status, request_id. Útil después cuando necesités debuggear con grep o mandar a una plataforma como Loki o Datadog.
3. Healthcheck HTTP
Endpoint /health que devuelva 200 OK si el servidor está sano (DB conectada, dependencias respondiendo). Si tu DB cae, /health debería devolver 503 — eso permite a Dokploy/Traefik dejar de mandar tráfico.
4. Autenticación
Si tu servidor expone tools que tocan datos sensibles o ejecutan acciones, necesitás autenticación. Opciones de menor a mayor complejidad:
- API key vía header:
Authorization: Bearer XXX. Fácil de implementar, suficiente para uso interno o entre máquinas. - OAuth 2.0: cuando tenés varios usuarios humanos. MCP soporta el flujo desde 2025.
- mTLS: cuando el cliente es otra máquina y querés autenticación bidireccional fuerte.
5. Validación de inputs
Cada tool debería validar sus inputs antes de ejecutar lógica. Si el modelo te pasa un parámetro mal-formado (pasa más seguido de lo que parece), tu servidor no debería crashear.
6. Observabilidad
Métricas Prometheus básicas (request count, latency p50/p95/p99 por tool) más logs JSON te alcanzan para empezar. Sin esto, cuando algo falla en producción, vas a estar adivinando.
Patrón útil: BOOTSTRAP_INGEST para servidores con datos
Si tu servidor MCP necesita cargar un dataset al inicio (lo típico cuando consultás datos abiertos como hace mi MCP de trámites), un patrón que funciona bien:
# Variable de entorno
BOOTSTRAP_INGEST=true
# Entrypoint del contenedor
#!/bin/sh
if [ "$BOOTSTRAP_INGEST" = "true" ]; then
/app/ingest --if-empty
fi
exec /app/server El flag --if-empty hace que el ingest solo corra si la base está vacía. En el primer deploy carga datos; en los siguientes salta y arranca rápido. Para refrescos periódicos, agregá un cron job o un endpoint admin protegido que dispare el reingest.
Troubleshooting común
"502 Bad Gateway" después del deploy
El contenedor no pasa el healthcheck. Revisar los logs en Dokploy. Causas comunes: variable de entorno faltante, puerto wrong, dependencia que no levanta (Postgres, Redis), permisos del volumen.
"Connection refused" desde Claude Desktop
Casi siempre HTTPS. Claude Desktop no acepta endpoints HTTP planos para custom connectors remotos. Asegurate de que tu certificado Let's Encrypt esté emitido (verificable con curl -v al dominio).
"Tool not found" cuando el cliente lista tools
El cliente está cacheando la lista de tools vieja. En Claude Desktop, desconectar y reconectar el connector. En Claude Code, reiniciar el proceso.
Latencia alta en la primera request del día
Cold start del contenedor. Si Dokploy escaló a cero por inactividad, la primera request paga el costo de bootstrap. Para servidores con tráfico bajo y predecible, configurar el contenedor para que no escale a cero.
El servidor crashea con OOM bajo carga
Tu memoria está mal calibrada o tenés un leak. Configurá mem_limit en docker-compose y monitoreá con métricas Prometheus. Para SDKs Python con embeddings en memoria, considerar mover a base vectorial dedicada si crecés.
Scaling cuando el tráfico crece
Para tráfico modesto (< 10K req/día), una sola instancia con SQLite alcanza. Cuando crece:
- Vertical primero: subir RAM/CPU del VPS. Es lo más barato y simple.
- Cache de respuestas frecuentes: Redis o memoria. Las queries repetidas son comunes en agentes que iteran sobre la misma data.
- Migrar de SQLite a Postgres: cuando necesitás escribir desde múltiples instancias o el dataset supera unos GB.
- Múltiples instancias detrás de Traefik: una vez que tu servidor es stateless o usa storage compartido, escalás horizontal con replicas en docker-compose.
Preguntas frecuentes
¿Puedo deployar en otra plataforma sin Dokploy?
Sí. El mismo Dockerfile y docker-compose corren en Coolify, CapRover, Railway, Fly.io, AWS ECS, Google Cloud Run o Kubernetes. Dokploy es solo lo más simple para arrancar con VPS propio. Si ya estás en una nube específica, usá los servicios nativos.
¿Cuánto cuesta correr un servidor MCP en producción?
Para tráfico bajo, USD 5-10/mes (VPS Hetzner CX22 o equivalente). Para producción seria con observabilidad, backups y staging, USD 20-50/mes. Las API keys del LLM (Anthropic, OpenAI) son costo aparte y no dependen del deploy del servidor MCP en sí.
¿Necesito CI/CD desde el primer día?
No. Para empezar, deploy manual desde Dokploy alcanza. Cuando estás iterando rápido o tenés equipo, GitHub Actions con build y push a registry + webhook a Dokploy es lo siguiente.
¿Cómo manejo migraciones de schema cuando deployo nuevas versiones?
Migraciones forward-only que el servidor corre al startup, idempotentes. Para SQLite, herramientas como golang-migrate o alembic (Python) funcionan bien. Nunca dropear columnas en la misma versión que dejás de usarlas — dos versiones después está bien.
¿Dónde puedo ver un ejemplo real de servidor MCP con todo esto aplicado?
En github.com/fabdelgado/mcp-tramites-gub-uy. Está escrito en Go, expone /mcp, tiene Dockerfile multi-stage, healthcheck, rate limiting, logs JSON, BOOTSTRAP_INGEST, y está corriendo en Dokploy. La historia del proyecto en el blog.
¿Necesitás ayuda para deployar un servidor MCP en producción? Construyo MCPs a medida con esta arquitectura: chico, mantenible, desplegable. Ver servicios · Conversemos un proyecto.
Lectura previa: Qué es Model Context Protocol: guía técnica. Caso de uso real: Cómo construí un MCP para tramites.gub.uy.
