DBHub — servidor MCP para TRIVASADB3, en vm-playground
Instancia self-hosted de DBHub (bytebase/dbhub), un
servidor MCP universal para bases SQL, publicada en dbhub.estebanalcocer.cloud para que
Claude (en claude.ai chat, no solo Claude Code) pueda consultar 192.168.117.200/TRIVASADB3
directamente vía un custom connector — sin pasar por dlt/Metabase/Lightdash. Stack en
~/stacks/dbhub/docker-compose.yml.
Excepción al rol de workstation de vm-playground
Desde el rediseño del 2026-08-10 (ver hosts.md), vm-playground es
workstation pura, sin servicios de producción. DBHub es la primera excepción: corre acá
en vez de vm-main porque este host ya tenía NetBird conectado a la malla de Conkafecito
(alcance directo a 192.168.117.0/24, confirmado con un test TCP a 192.168.117.207:1433
antes de levantar el stack) y conservaba el cert.pem de un túnel Cloudflare anterior ya
borrado — condiciones que vm-main no tenía sin trabajo extra. Mismo patrón de excepción
documentado ya para Home Assistant (corre en pop-os por la LAN de casa) e Infisical
(comparte host con Conkafecito pero publica en la cuenta personal).
Por qué TRIVASADB3 y no la copia de producción
Hay tres/cuatro copias de TRIVASADB con distinto rol (detalle en
Conexiones a TRIVASADB). Se eligió
192.168.117.200/TRIVASADB3 — copia viva pero no productiva — en vez de
192.168.117.207/TRIVASADB (producción) a propósito: un query mal armado o pesado desde un
cliente de IA no le pega a la base transaccional real.
Guardrails de solo lectura
DBHub soporta modo solo-lectura vía TOML (no hay flag CLI para esto), configurado en
~/stacks/dbhub/config.toml:
[[sources]]
id = "trivasadb3"
type = "sqlserver"
host = "192.168.117.200"
port = 1433
database = "TRIVASADB3"
user = "${DB_USER}"
password = "${DB_PASSWORD}"
query_timeout = 30
[[tools]]
name = "execute_sql"
source = "trivasadb3"
readonly = true
max_rows = 1000Verificado en vivo tras levantar el stack: un SELECT normal contra INFORMATION_SCHEMA.TABLES
regresa filas; un CREATE TABLE de prueba se rechaza con
{"success": false, "error": "Read-only mode is enabled...", "code": "READONLY_VIOLATION"}
antes de tocar la base.
Gotcha: env_file de Compose también escapa $$
El password de SQL Server contiene $$ (dos signos de pesos literales). Docker Compose
aplica su regla de escapado ($$ → un solo $ literal) no solo a interpolación directa en
el YAML, sino también al parsear valores de env_file — algo no evidente porque env_file
se documenta como “carga variables tal cual”. El primer intento dejó DB_PASSWORD=p4$w0rd
(un solo $) llegando al contenedor y el login a SQL Server fallaba con ELOGIN. Se
confirmó con docker inspect dbhub --format '{{range .Config.Env}}...' comparando el valor
real recibido contra el escrito en .env. Fix: escribir el doble de signos de pesos en
.env (p4$$$$w0rd) para que, tras el escapado de Compose, lleguen los dos $ reales.
Cualquier secreto con $ en este tipo de stack necesita este mismo ajuste.
Publicación: túnel Cloudflare nuevo en vm-playground
vm-playground se quedó sin túnel propio el 2026-08-10 (ver
Túneles de Cloudflare), pero conservaba el
cert.pem de la cuenta estebanalcocer.cloud en ~/.cloudflared/ de un túnel anterior ya
borrado, así que crear uno nuevo no requirió volver a autenticar la cuenta:
cloudflared tunnel create vm-playground→ idb645f9f8-2cc9-4af9-95fd-73039c58d11fcloudflared tunnel route dns vm-playground dbhub.estebanalcocer.cloud- Config propio,
/etc/cloudflared/config-estebanalcocer.yml, ingress único haciahttp://localhost:8080(el contenedor publica en127.0.0.1:8080:8080, nunca en todas las interfaces — mismo patrón que el resto de servicios detrás de túnel). - Servicio systemd dedicado
cloudflared-estebanalcocer.service(mismo nombre que el de vm-main, por convención de nombrarlo por zona/cuenta, no por host) — nunca se reusa elcloudflared.servicegenérico, porque un solo procesocloudflared tunnel runsolo atiende un túnel/config a la vez.
Gotcha: --allowed-hosts bloquea el hostname público por default
DBHub trae protección anti DNS-rebinding: por default solo acepta requests con header
Host: localhost / 127.0.0.1 / la IP interna del contenedor. El túnel de Cloudflare
reenvía el Host original de la petición (dbhub.estebanalcocer.cloud), así que sin
ajuste el primer request público devolvía 401/rechazo aunque el bearer token fuera
correcto. Fix: agregar el hostname público a la lista (se suma a los defaults, no los
reemplaza):
--allowed-hosts dbhub.estebanalcocer.cloud,localhost,127.0.0.1
Auth del endpoint
--auth-token / DBHUB_AUTH_TOKEN (bearer token) es la única barrera de auth delante del
endpoint — se consideró suficiente dado que además está limitado a solo-lectura y a una
copia no productiva de la base, mismo razonamiento que llevó a no ponerle Cloudflare Access
a Infisical (ver esa página, sección homónima). Token generado con openssl rand -hex 32
(mismo patrón que las demás credenciales de este tipo de stack) y guardado en
Infisical bajo la clave DBHUB_AUTH_TOKEN — el
.env local del stack (chmod 600, no versionado) sigue siendo la fuente que efectivamente
usa el contenedor.
Registrar el conector en claude.ai
Settings → Connectors → Add custom connector:
- URL:
https://dbhub.estebanalcocer.cloud/mcp - Advanced → Request headers:
Authorization: Bearer <DBHUB_AUTH_TOKEN>(valor en Infisical)
Véase también
- Túneles de Cloudflare — túnel
vm-playgroundnuevo y su patrón de config/systemd - Gestión de secretos — dónde vive
DBHUB_AUTH_TOKEN - Conexiones a TRIVASADB — por qué
.200/TRIVASADB3y no.207(producción) - Infisical — mismo razonamiento de “auth propio del servicio es suficiente, sin Cloudflare Access encima”
- NetBird — malla que le da a vm-playground alcance directo a
192.168.117.0/24