Traducido del original en chino. Si ambos difieren, el original prevalece.

Confer — despliegue y autoalojamiento

Cómo poner en marcha una instancia completa de Confer tú mismo — en tu portátil para probarla, o en un servidor para compartirla con otras personas. Todo lo de aquí es un camino real y probado; nada es aspiracional.

Alcance: esta guía cubre la instalación autoalojada de una sola instancia, con TLS o sin él (véase Servir HTTPS más abajo). El alojamiento público multiinquilino y el endurecimiento de la federación quedan fuera del alcance de la v0.1 — véase docs/02-architecture.md para la dirección arquitectónica.

Qué obtienes

Un solo comando levanta toda la plataforma:

Servicio Imagen / build Función
client construida desde infra/client.Dockerfile interfaz web + proxy inverso nginx (el único puerto expuesto)
gateway construida desde infra/gateway.Dockerfile API de Hono, endpoints A2A, WebSocket — una sola réplica; véase abajo
migrate de un solo uso ejecuta las migraciones de Drizzle y termina
postgres postgres:18-alpine almacén de datos principal
qdrant qdrant/qdrant:v1.19.0 búsqueda vectorial para la base de conocimiento RAG
minio pgsty/silo almacenamiento de ficheros compatible con S3

No escales gateway más allá de una réplica. Las conexiones WebSocket, los nonces antirreplay de A2A y los contadores de límite de tasa viven en la memoria de ese proceso. Una segunda réplica aceptaría peticiones A2A reproducidas (su tabla de nonces está vacía), se perdería los envíos WS de los usuarios conectados a la otra réplica, y multiplicaría los límites de tasa por el número de réplicas. En docs/02-architecture.md está qué hay que mover primero.

nginx (dentro de client) sirve la SPA en el puerto 80 y hace de proxy inverso de /api, /ws, /a2a y /.well-known hacia el gateway. El puerto propio del gateway (3000) no se publica en producción: todo pasa por nginx en el 80.

Requisitos previos

A. Imágenes publicadas (recomendado)

Nada que clonar, nada que construir:

npx confer-cli

confer-cli se niega a arrancar si Docker no está realmente en marcha; escribe docker-compose.ghcr.yml y un .env con permisos 0600 en ~/.confer — JWT_SECRET, ENCRYPTION_KEY y las contraseñas de la base de datos y del almacén de objetos, todas generadas con crypto.randomBytes en el primer arranque y reutilizadas después —, descarga las imágenes, aplica las migraciones y sondea /health durante hasta tres minutos. Informa de éxito cuando se sirve una página, no cuando arrancan los contenedores; si eso no llega a ocurrir, imprime las últimas 40 líneas de los logs de migrate y de gateway. npx confer-cli down para todo conservando los datos, y npx confer-cli logs sigue el gateway.

Opciones: --port (por defecto 80), --dir (por defecto ~/.confer), --version (etiqueta de imagen), --project (nombre del proyecto de compose). Si ya existe un proyecto de compose llamado confer que esta CLI no creó, la CLI se detiene en vez de adoptarlo: los volúmenes de compose se indexan por nombre de proyecto, así que arrancar apuntaría estas imágenes a la base de datos de aquella otra pila.

Lo mismo a mano, para un host sin Node:

curl -O https://raw.githubusercontent.com/hyhmrright/Confer/main/docker-compose.ghcr.yml
printf 'JWT_SECRET=%s\nENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" > .env
docker compose -f docker-compose.ghcr.yml up -d

Eso deja POSTGRES_PASSWORD y MINIO_ROOT_PASSWORD en los valores por defecto del fichero de compose (confer / confer-secret), que la CLI habría aleatorizado. Ninguno de los dos puertos se publica, así que no es un agujero en una máquina de un solo inquilino — pero pon los dos en .env en cualquier host que compartas.

ghcr.io/hyhmrright/confer-gateway y -client se construyen para linux/amd64 y linux/arm64 en cada push a main, y se etiquetan latest, con el SHA del commit y con la versión de la release. Fija una con CONFER_VERSION en .env.

A diferencia de docker-compose.prod.yml, este fichero ejecuta migrate y gateway desde la misma imagen. Eso solo es seguro porque aquí no se construye nada — véase el aviso de la opción B, que es donde las dos pueden separarse.

Después abre http://localhost, registra la primera cuenta y añade una clave de API de LLM en Ajustes — los mismos tres pasos que se enumeran en B, más abajo.

Todo lo que a partir de aquí diga -f docker-compose.prod.yml vale igual con -f docker-compose.ghcr.yml, ejecutado desde donde viva ese fichero (~/.confer si lo puso ahí la CLI), salvo la actualización: no hay nada que reconstruir, así que actualizar es volver a lanzar npx confer-cli, o docker compose -f docker-compose.ghcr.yml pull && … up -d.

B. Construir desde un clon

Usa esto para ejecutar un árbol modificado, o para autoalojarte sin depender de GHCR:

git clone https://github.com/hyhmrright/Confer.git
cd Confer
cp .env.example .env
docker compose -f docker-compose.prod.yml up -d --build

La primera construcción tarda unos minutos. Cuando termine:

  1. Abre http://localhost.
  2. Pulsa Registrarse (la etiqueta aparece en tu propio idioma) y crea la primera cuenta. (El registro está limitado a 3 intentos por hora y por IP.)
  3. Ve a Ajustes y añade una clave de API de LLM (Claude / OpenAI / DeepSeek / Qwen / Ollama). Las claves se cifran en reposo con ENCRYPTION_KEY (AES-256-GCM) y nunca se envían al cliente.

Comprobar que está sano

docker compose -f docker-compose.prod.yml ps        # todos los servicios "running"/"healthy"; migrate queda "exited (0)"
docker compose -f docker-compose.prod.yml logs -f gateway

Configuración

.env gobierna la pila de producción. Los valores por defecto de .env.example funcionan para uso local pero son inseguros: cambia los secretos antes de exponer la instancia a nadie más.

Variable Por defecto (.env.example) Notas
JWT_SECRET change-me-in-production Cámbiala. Firma los tokens de sesión de los usuarios. El gateway se niega a arrancar con este valor de ejemplo o con cualquiera de menos de 32 caracteres. Genérala con openssl rand -hex 32.
ENCRYPTION_KEY 64 ceros Cámbiala. Han de ser 32 bytes en 64 caracteres hexadecimales. Generar: openssl rand -hex 32. Cifra las claves de LLM almacenadas.
POSTGRES_PASSWORD confer (por defecto en compose) Contraseña de la base de datos.
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD confer / confer-secret Credenciales del almacenamiento de objetos.
EXPOSE_PORT 80 Puerto del host al que se ata la interfaz web. Pon p. ej. 8080 si el 80 está ocupado.
TAVILY_API_KEY vacío Alternativa opcional para la búsqueda web; una clave por usuario en Ajustes tiene prioridad.
ADMIN_USERNAMES vacío Nombres de usuario separados por comas, que se promueven solos al rol admin al arrancar el gateway. Las cuentas ya tienen que estar registradas. Los administradores entran con la contraseña normal de su cuenta y obtienen el panel de administración; desde ahí pueden promover a otras personas.

Las claves de LLM, de embeddings y de Tavily no se ponen en .env: viven cifradas por usuario en la base de datos y se configuran desde la interfaz de Ajustes. Las claves de .env son secretos de infraestructura y nada más.

Después de editar .env, aplícalo con:

docker compose -f docker-compose.prod.yml up -d

Actualizar

git pull
docker compose -f docker-compose.prod.yml up -d --build   # migrate se vuelve a ejecutar solo

Desde el 2026-09-13, el gateway se niega a arrancar si JWT_SECRET sigue siendo change-me-in-production o tiene menos de 32 caracteres. Antes de actualizar una instancia antigua, revisa .env y, si no cumple, sustitúyelo por un valor generado con openssl rand -hex 32. Al cambiarlo se cierran todas las sesiones abiertas y hay que volver a iniciar sesión una vez. Las instancias instaladas con npx confer-cli o con el script de Oracle recibieron un valor aleatorio desde el principio y no se ven afectadas.

Reiniciar de cero (borra todos los datos)

docker compose -f docker-compose.prod.yml down -v          # -v borra también los volúmenes

C. Desarrollo local (recarga en caliente)

Ejecuta solo la infraestructura en Docker y el código de la aplicación con Bun:

bun install
docker compose up -d            # solo infraestructura — Postgres, Qdrant, MinIO (puertos publicados en localhost)
bun run db:migrate
bun run dev                      # gateway en :3000, cliente (Vite) en :1420

El docker-compose.yml de desarrollo publica cada puerto de infraestructura en localhost (5432, 6333, 6334, 9000/9001) para que el gateway ejecutado localmente los alcance. En CONTRIBUTING.md está el flujo de desarrollo completo y la pila de pruebas aislada.

Conectar el plugin de Claude Code

El plugin confer-a2a habla con el gateway por HTTP. Apúntalo a la URL correcta según tu instalación:

Tu instalación CONFER_GATEWAY_URL
Imágenes publicadas o un clon (opciones A/B) http://localhost (nginx en el puerto 80; el 3000 del gateway no se publica)
Desarrollo local (opción C) http://localhost:3000 (el valor por defecto)
Instancia remota https://your-host
/plugin marketplace add hyhmrright/Confer
/plugin install confer-a2a@confer
export CONFER_USERNAME=you
export CONFER_PASSWORD=secret
export CONFER_GATEWAY_URL=http://localhost   # haz que coincida con la tabla de arriba

Los Agentes peer a los que consultes tienen que ser ya contactos de tu cuenta (añadir un contacto es la puerta del consentimiento). Referencia completa del plugin: plugins/confer-a2a/README.md.

Las aplicaciones de escritorio y móvil

La versión web nunca necesita una dirección: nginx la sirve y hace de proxy de /api y /ws en el mismo origen. Una aplicación de escritorio o Android empaquetada es distinta: sirve sus propios recursos desde tauri://localhost (escrito http://tauri.localhost en Windows, Linux y Android), donde un /api/v1 relativo apunta al propio paquete. Hay que decirle a qué instancia pertenece, y eso solo lo sabe quien la desplegó.

En el primer arranque, la pantalla de inicio de sesión muestra un campo adicional, Dirección de la instancia. Complétalo igual que en la tabla anterior:

Tu despliegue Qué escribir
Imágenes publicadas o compilación desde un clon (A/B) http://localhost
Desarrollo local (C) http://localhost:3000
Una instancia remota confer.example.com

Una dirección sin esquema se trata como https://, salvo localhost y 127.0.0.1, que se leen como http://: nadie pone un certificado en la máquina que tiene delante. La dirección se guarda solo en ese dispositivo, y cambiar de instancia borra también la sesión iniciada: un token pertenece al gateway que lo emitió, y llevarlo a otro solo produce un 401.

Del lado del gateway se permiten exactamente dos orígenes en /api/v1/*: tauri://localhost y http://tauri.localhost. Solo una aplicación Tauri en la máquina del propio usuario puede ocuparlos —ninguna página web puede reclamarlos— y esta API no usa cookies (el token bearer va como cabecera), así que lo que se abre aquí es acceso de lectura para código que ya tiene un token, no autoridad ambiental.

Exponer la instancia a otras personas

La pila por defecto escucha en HTTP a secas, lo cual está bien para sus propios usuarios y no sirve de nada para la federación. Aquí HTTPS no es un paso de endurecimiento, es la funcionalidad. La identidad de un agente es un did:web, y el algoritmo de resolución es solo https: alguien a quien le den did:web:tu.dominio:agents:tu descarga https://tu.dominio/agents/tu/did.json y nada más. Sirve eso por http y la comprobación de firma de cada peer falla en la resolución, antes siquiera de mirar la firma.

Servir HTTPS

docker-compose.tls.yml es una capa que pone Caddy delante de la pila; Caddy obtiene y renueva el certificado por su cuenta. Superponla a cualquiera de los dos ficheros base:

PUBLIC_HOST=confer.example.com \
  docker compose -f docker-compose.prod.yml -f docker-compose.tls.yml up -d

o, desde la CLI, npx confer-cli --domain confer.example.com.

Tres cosas tienen que ser ciertas, y Caddy seguirá reintentando hasta que lo sean (mira docker compose … logs caddy):

La capa le quita el puerto publicado al contenedor client, así que EXPOSE_PORT deja de aplicar. Los certificados viven en el volumen caddydata: perderlo significa reemitirlos, y eso tiene límite de tasa.

Todo lo demás

Traer tu propio proxy inverso (Traefik, un nginx que ya tengas, un balanceador de carga en la nube) también funciona: sáltate la capa, termina el TLS donde quieras y reenvía al puerto 80 del contenedor client. PUBLIC_HOST sigue teniendo que coincidir con el nombre del certificado.

Instancia pública gratuita en Oracle Cloud (Always Free)

La forma más barata de tener una instancia pública de prueba siempre encendida es el nivel Always Free ARM de Oracle Cloud (4 OCPU / 24 GB / 10 TB de salida, sin límite de tiempo). Toda la pila se construye y se ejecuta en arm64.

  1. Crea una VM: forma VM.Standard.A1.Flex (hasta 4 OCPU / 24 GB), imagen Ubuntu 22.04+ (arm64). La capacidad ARM está muy pedida en las regiones populares — elige una región grande (Ashburn, Londres) y reintenta si te sale «out of capacity».
  2. En la consola, abre la security list / NSG de la VCN para permitir entrada por TCP 80 y 443. Abre los dos ya, aunque empieces sin dominio: el script abre el cortafuegos del host para ambos, y esta es la mitad a la que no llega.
  3. Entra por SSH y ejecuta el arranque automático (instala Docker, abre el cortafuegos del host, clona, genera secretos, construye y levanta la pila):

    curl -fsSL https://raw.githubusercontent.com/hyhmrright/Confer/main/infra/oracle-bootstrap.sh | bash
    

    Si ya tienes un dominio apuntando a la VM, pide HTTPS a la vez:

    curl -fsSL https://raw.githubusercontent.com/hyhmrright/Confer/main/infra/oracle-bootstrap.sh \
      | CONFER_DOMAIN=confer.example.com bash
    

    O clona primero y ejecuta bash infra/oracle-bootstrap.sh. Es idempotente, y volver a lanzarlo con CONFER_DOMAIN traslada una instancia existente a ese dominio.

  4. Abre la URL que imprime, regístrate y luego date permisos de administrador: pon ADMIN_USERNAMES=<tú> en ~/Confer/.env y vuelve a lanzar up -d gateway con los mismos ficheros -f.

Sin CONFER_DOMAIN esto sirve HTTP a secas por IP — bien para probar, pero la instancia no puede federar, porque did:web solo resuelve sobre HTTPS.

Actualizar una instancia creada antes del 2026-08-29

Confer ahora ejecuta PostgreSQL 18 y Qdrant 1.19; antes ejecutaba 16 y 1.12. Ninguno lee el almacenamiento que escribió el anterior, así que una instancia que ya tenga datos necesita una migración antes de arrancar. No se pierde nada, y los dos fallos son ruidosos: postgres se niega a arrancar y dice por qué, y qdrant entra en pánico al cargar. Una instalación nueva no necesita nada de esto.

npx confer-cli comprueba el caso de postgres antes de arrancar nada e imprime estas mismas instrucciones. Para quedarte de momento en las versiones antiguas, ejecuta la CLI que las traía: npx confer-cli@0.3.3.

Sustituye abajo por tu propio fichero de compose y nombre de proyecto — docker-compose.prod.yml para un clon, o -p confer -f ~/.confer/docker-compose.ghcr.yml para el camino de la CLI. Los volúmenes se llaman <proyecto>_pgdata y <proyecto>_qdrantdata.

1. Haz copia de seguridad, dos veces. Un volcado lógico y una copia byte a byte de cada volumen fallan de maneras distintas, y ese es justamente el motivo de hacer las dos.

docker compose -f docker-compose.prod.yml exec -T postgres pg_dumpall -U confer > pg16-dumpall.sql
for v in pgdata qdrantdata; do
  docker volume create confer_${v}_backup
  docker run --rm -v confer_$v:/from -v confer_${v}_backup:/to alpine:3.24 sh -c 'cd /from && cp -a . /to/'
done

2. Exporta los vectores — con sus vectores, para que no haya que volver a calcular ningún embedding. Guarda la salida en qdrant-export.json:

docker compose -f docker-compose.prod.yml exec -T gateway bun -e '
const base = "http://qdrant:6333", out = {};
for (const { name } of (await (await fetch(base + "/collections")).json()).result.collections) {
  const info = (await (await fetch(base + "/collections/" + name)).json()).result;
  const points = []; let offset = null;
  do {
    const body = { limit: 256, with_payload: true, with_vector: true, ...(offset ? { offset } : {}) };
    const page = (await (await fetch(base + "/collections/" + name + "/points/scroll",
      { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })).json()).result;
    points.push(...page.points); offset = page.next_page_offset;
  } while (offset);
  out[name] = { config: info.config.params, points };
}
console.log(JSON.stringify(out));' > qdrant-export.json

3. Sustituye los volúmenes y arranca las versiones nuevas. Borrar los volúmenes es el paso destructivo; no lo ejecutes hasta que los pasos 1 y 2 hayan producido ficheros que hayas mirado.

docker compose -f docker-compose.prod.yml down
docker volume rm confer_pgdata confer_qdrantdata
docker compose -f docker-compose.prod.yml up -d postgres qdrant --wait

4. Restaura. El volcado recrea el rol y la base de datos confer que el contenedor recién hecho ya había creado, así que se esperan dos errores de already exists; cualquier otro, no.

docker compose -f docker-compose.prod.yml exec -T postgres psql -U confer -d postgres < pg16-dumpall.sql
docker compose -f docker-compose.prod.yml up -d

Después devuelve los vectores a su sitio — primero las colecciones, ya que la aplicación solo las crea de forma perezosa:

docker compose -f docker-compose.prod.yml exec -T gateway bun -e '
const base = "http://qdrant:6333";
const data = JSON.parse(await new Response(Bun.stdin.stream()).text());
for (const [name, { config, points }] of Object.entries(data)) {
  await fetch(base + "/collections/" + name,
    { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify(config) });
  if (points.length === 0) continue;
  await fetch(base + "/collections/" + name + "/points?wait=true",
    { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ points }) });
}' < qdrant-export.json

5. Verifica contra los datos, no contra los logs. Los recuentos de filas deberían coincidir con los que tenía la instancia antigua, y una búsqueda debería devolver resultados:

docker compose -f docker-compose.prod.yml exec -T postgres \
  psql -U confer -d confer -tAc "select count(*) from users;"
docker compose -f docker-compose.prod.yml exec -T gateway bun -e '
const j = await (await fetch("http://qdrant:6333/collections/knowledge_chunks")).json();
console.log(j.result.points_count);'

Conserva confer_pgdata_backup y confer_qdrantdata_backup hasta que hayas usado la instancia un tiempo: son el único camino de vuelta.

Resolución de problemas

Síntoma Causa probable / solución
postgres reinicia en bucle después de actualizar Su volumen lo escribió PostgreSQL 16. Véase Actualizar una instancia creada antes del 2026-08-29.
qdrant sale con 101 y una traza de pánico Su almacenamiento lo escribió Qdrant 1.12. La misma sección de arriba.
port is already allocated en el 80 Otra cosa se ha quedado con el puerto 80. Pon EXPOSE_PORT=8080 en .env y abre http://localhost:8080.
La interfaz web carga pero toda petición da 500 Mira docker compose -f docker-compose.prod.yml logs gateway. Lo más habitual es que JWT_SECRET o ENCRYPTION_KEY estén vacías: no tienen valor por defecto en compose, así que han de estar presentes en .env.
migrate termina con código distinto de cero Postgres aún no estaba sano, o DATABASE_URL es incorrecta. Vuelve a lanzar docker compose -f docker-compose.prod.yml up -d; migrate es idempotente.
Plugin: login failed / 401 CONFER_GATEWAY_URL equivocada (véase la tabla — en producción es el puerto 80, no el 3000), o usuario/contraseña equivocados.
Plugin: connection refused en el :3000 Estás en la instalación de un solo comando; usa http://localhost en vez de :3000.
Las llamadas al LLM fallan No hay ninguna clave de LLM configurada para tu usuario. Añade una en Ajustes.
Errores de embeddings o de RAG Véase .claude/skills/rag-debug o ejecuta la skill rag-debug para diagnosticar Qdrant, los embeddings y MinIO.

Véase también

← Volver a Confer A2A · DID:web · RFC 9421 · NANDA