Inicio / Artículos / Ollama en WSL2 da «connection refused»: cómo arreglarlo y conectar Dify y vLLM

Ollama en WSL2 da «connection refused»: cómo arreglarlo y conectar Dify y vLLM

¿Ollama funciona en WSL2 pero Dify recibe connection refused? Revisa la dirección de enlace, define OLLAMA_HOST, añade un portproxy y evita la trampa de localhost en Docker al conectar vLLM.

1543 palabras

Ollama funcionaba sin problemas en una máquina con Windows 11. Podías descargar un modelo y chatear con él directamente desde la terminal de WSL2. Pero en cuanto otro servicio intentaba hablar con él, la conexión se rechazaba. El problema no eran los modelos. Era la red.

Lo que sigue es el camino desde esa configuración rota y aislada hasta una que sobrevive a los reinicios y sirve modelos locales a través de varias fronteras virtuales.

El objetivo y dónde se vino abajo

La idea era montar un stack local de desarrollo de IA para quienes quieren trabajar con LLM sin enviar datos privados a un proveedor en la nube. Ollama cubriría los experimentos rápidos y los flujos RAG locales. vLLM se encargaría de la inferencia más pesada y de mayor rendimiento. Después, ambos se conectarían a Dify, un constructor open source de apps con LLM que corre en Docker.

Los primeros pasos fueron bien. Se instaló Ollama dentro de una distribución Ubuntu en WSL2, se descargó el modelo llama3, la CLI funcionaba y una petición de prueba a la API local desde la misma shell de Linux volvía al instante.

Entonces Dify, que corría en un contenedor Docker en la red del host, informó de un fallo de conexión. El primer sospechoso fue la configuración de Dify, y esa sospecha era errónea. La causa real era no entender bien cómo un servicio se enlaza a una interfaz de red, algo que empeoraban los espacios de nombres de red separados que crean WSL2 y Docker.

Imagina la disposición como habitaciones anidadas. Fuera está el host con Windows 11. Docker Engine forma un espacio de nombres, con los contenedores Dify API y Dify worker dentro. La VM de utilidad de WSL2 forma otro, con Ollama en el puerto 11434 y vLLM en el puerto 8000. Importan dos fronteras: la que hay entre Windows y la VM de WSL2, y la que hay entre la red de Docker y el host. La famosa confusión con localhost vive en esas paredes.

Que funcione no significa que sea accesible

Un servicio sano no es necesariamente un servicio accesible. Ollama estaba vivo, pero eso solo significaba que su proceso estaba en marcha y enlazado a una interfaz concreta en lo más profundo del kernel de Linux.

Un servidor no escucha «la red» en abstracto. Se enlaza a una dirección IP y un puerto concretos, y un cliente solo llega si envía paquetes a una dirección enrutable que coincida con ese enlace. Muchos valores por defecto seguros se enlazan a 127.0.0.1, el loopback de IPv4, lo que confina el servicio al mismo espacio de nombres en el que arrancó. Enlazarlo a 0.0.0.0 hace que responda en todas las interfaces de la máquina, y eso es obligatorio si hay que conectar WSL2 con Windows o Docker.

Localizar la conexión rechazada

Cuando algo no llega a Ollama, empieza por averiguar qué capa falla: DNS, TCP o la aplicación. Aquí el síntoma era un error de conexión en Dify. La hipótesis, un handshake TCP fallido a través de la frontera de la VM. La prueba, una comprobación TCP en bruto desde el lado de Windows, que está en un espacio de nombres distinto al de WSL2 y se salta toda la lógica de la aplicación. Desde PowerShell:

Test-NetConnection -ComputerName localhost -Port 11434

Devolvió TcpTestSucceeded : False, así que el handshake fallaba y Dify no tenía la culpa. Después, mira el socket que escucha desde dentro de Linux, en la terminal de WSL2:

ss -tlnp | grep 11434

La salida lo dejó claro:

LISTEN 0  4096  127.0.0.1:11434  0.0.0.0:*  users:((ollama,pid=1234,fd=7))

Según el listado de ss, Ollama solo escuchaba en 127.0.0.1:11434. Todo lo que llegaba por la interfaz Ethernet virtual de WSL2, que es la que usa Windows para hablar con la VM, el kernel lo rechazaba en el acto.

Tres términos que conviene tener claros

localhost es solo un nombre de host que se resuelve a través del archivo hosts. A qué máquina apunta en realidad depende del contenedor o la VM en la que lo estés escribiendo.

127.0.0.1 es la dirección loopback de IPv4. Es una interfaz virtual que vive dentro de un único kernel del sistema operativo, y los paquetes enviados a ella nunca llegan a una tarjeta de red física o virtual.

Y luego está el comodín, 0.0.0.0 (INADDR_ANY). Cuando un servidor se enlaza a él, el sistema operativo acepta conexiones TCP entrantes en todas sus interfaces IPv4.

La documentación de red de WSL de Microsoft, en https://learn.microsoft.com/en-us/windows/wsl/networking, explica que el hipervisor ejecuta una VM de utilidad ligera con su propio adaptador de red y su dirección NAT, de modo que Windows tiene que cruzar ese NAT para llegar al lado de Linux.

El único cambio que lo arregla

Haz que Ollama se enlace a todas las interfaces definiendo la variable de entorno OLLAMA_HOST. Si systemd gestiona el proceso, no modifiques el archivo de unidad principal, porque las actualizaciones del paquete sobrescribirán tu cambio. Crea en su lugar un override drop-in. Dentro de WSL2, primero crea el directorio:

# Create the override directory
sudo mkdir -p /etc/systemd/system/ollama.service.d

Después escribe el archivo de override:

# Create and edit the configuration file safely
echo '[Service]' | sudo tee /etc/systemd/system/ollama.service.d/override.conf
echo 'Environment=OLLAMA_HOST=0.0.0.0' | sudo tee -a /etc/systemd/system/ollama.service.d/override.conf# Reload systemd and restart the daemon
sudo systemctl daemon-reload
sudo systemctl restart ollama

Tras reiniciar, vuelve a comprobar el socket. Así se ve el éxito:

LISTEN 0  4096  *:11434  *:*  users:((ollama,pid=1234,fd=7))

El asterisco indica que escucha en todas las interfaces, y repetir la prueba de red desde Windows ahora dio True.

Por qué pegar la IP de WSL2 es una trampa

Al principio, Dify se apuntó a Ollama ejecutando ip addr show eth0 en WSL2, copiando la dirección y pegándola en los ajustes de Dify, algo como http://172.20.5.42:11434. Funcionó durante un rato y es un diseño frágil. El switch virtual de Hyper-V asigna esa dirección de forma dinámica, así que cambia tras cada reinicio o reinicio del servicio y rompe la integración sin avisar.

Un enfoque más robusto deja que el host Windows reenvíe el tráfico de forma determinista con netsh portproxy. Para que sobreviva a los reinicios sin trabajo manual, usa un script de PowerShell ejecutado como administrador en el host. La primera parte busca la dirección NAT actual de la instancia de WSL2:

# Dynamically fetch the current WSL2 NAT IP
$wsl_ip = (wsl -d Ubuntu -e sh -c 'ip -4 addr show eth0 | grep -oP ''(?<=inet\s)\d+(\.\d+){3}''')

La segunda parte elimina las reglas de enrutamiento obsoletas y añade otras nuevas que apuntan a la dirección actual:

# Clear existing stale routing rules
netsh interface portproxy delete v4tov4 listenport=11434 listenaddress=0.0.0.0
netsh interface portproxy delete v4tov4 listenport=8000 listenaddress=0.0.0.0# Add the new dynamic rules for Ollama (11434) and vLLM (8000)
netsh interface portproxy add v4tov4 listenport=11434 listenaddress=0.0.0.0 connectport=11434 connectaddress=$wsl_ip
netsh interface portproxy add v4tov4 listenport=8000 listenaddress=0.0.0.0 connectport=8000 connectaddress=$wsl_ipWrite-Host 'Port forwarding established to WSL2 IP: ' $wsl_ip

El efecto es que las peticiones a localhost en Windows llegan a la IP que tenga la instancia de Linux en ese momento. Guarda el script, haz que el Programador de tareas de Windows lo ejecute al inicio y te olvidas de llevar la cuenta de las IP a mano.

Sumar vLLM

Con Ollama estable, tocaba vLLM. Su diseño PagedAttention da una gran ventaja de rendimiento cuando hay muchas generaciones en paralelo. El servidor se arrancó dentro de WSL2 con el flag de host, el puerto expuesto y una asignación conservadora de memoria de GPU para evitar caídas por falta de memoria:

python -m vllm.entrypoints.openai.api_server \
  --model mistralai/Mistral-7B-Instruct-v0.3 \
  --host 0.0.0.0 \
  --port 8000 \
  --gpu-memory-utilization 0.8

Antes de abrir la interfaz de Dify, comprueba la ruta de la API por separado. Confundir errores de la aplicación con errores de red puede costar horas. Los payloads JSON necesitan comillas dobles, lo que complica los one-liners rápidos en bash, así que un pequeño script de Python hace el trabajo. Empieza con el import:

import requests

y sigue con la petición en sí:

api_url = 'http://localhost:8000/v1/chat/completions'
headers = {'Content-Type': 'application/json'}
payload = {
    'model': 'mistralai/Mistral-7B-Instruct-v0.3',
    'messages': [{'role': 'user', 'content': 'Explain network routing briefly.'}]
}response = requests.post(api_url, headers=headers, json=payload)
print(response.status_code)
print(response.json())

Un HTTP 200 significa que vLLM funciona como servidor compatible con OpenAI. Si esa llamada hubiera fallado, sabrías que el problema está por completo dentro de la VM de Linux.

La trampa de localhost en Docker

Último obstáculo: dentro de un contenedor, localhost se refiere al propio contenedor. Si escribes http://localhost:8000/v1 en Dify, el contenedor de Dify API busca un modelo en su propio espacio de nombres y falla de inmediato.

Como el portproxy de Windows ya expone los servicios de IA en el host, el contenedor solo necesita una salida hacia el host. Docker Desktop ofrece para ello un nombre DNS especial, descrito en https://docs.docker.com/desktop/networking/. En Dify, abre Model Provider, elige OpenAI-API-compatible y rellena:

  • Endpoint: http://host.docker.internal:8000/v1
  • Model: el que sirva vLLM, aquí mistralai/Mistral-7B-Instruct-v0.3
  • Key: literalmente la palabra EMPTY, porque las instancias locales no la comprueban

La configuración que merece quedarse

El flujo final es este. Una petición sale del contenedor de Dify hacia host.docker.internal:11434. El host Windows la recibe y se activa la regla de netsh portproxy. La regla la reenvía a la dirección 172.x.x.x del NAT virtual de WSL2, donde el daemon de Ollama, enlazado a 0.0.0.0, acepta el payload.

Esta configuración acaba con la gestión manual de IP, y el puente entre el sistema operativo del host, el hipervisor y el motor de contenedores se comporta ahora igual todos los días. Entender cómo encajan estos espacios de nombres sirve para algo más que arreglar una conexión inestable: es la habilidad básica para mantener una infraestructura de IA local fiable.