Главная / Статьи / Ollama на WSL2 отвечает «connection refused»: чиним и подключаем Dify и vLLM

Ollama на WSL2 отвечает «connection refused»: чиним и подключаем Dify и vLLM

Ollama работает в WSL2, а Dify получает connection refused? Проверяем адрес привязки, задаём OLLAMA_HOST, добавляем portproxy и обходим ловушку localhost в Docker при подключении vLLM.

1543 слов

Ollama спокойно работала на машине с Windows 11. Можно было скачать модель и общаться с ней прямо из терминала WSL2. Но как только к ней пытался обратиться другой сервис, соединение отклонялось. Дело было не в моделях, а в сети.

Ниже — путь от сломанной изолированной конфигурации к той, что переживает перезагрузки и раздаёт локальные модели через несколько виртуальных границ.

Цель и где всё сломалось

Задача была собрать локальный стек для ИИ-разработки для тех, кто хочет работать с LLM, не отправляя приватные данные облачному провайдеру. Ollama брала на себя быстрые эксперименты и локальные RAG-сценарии. vLLM отвечал за более тяжёлый инференс с высокой пропускной способностью. Оба затем подключались к Dify, open-source конструктору LLM-приложений, работающему в Docker.

Первые шаги прошли гладко. Ollama установили внутри дистрибутива Ubuntu в WSL2, скачали модель llama3, CLI работал, а тестовый запрос к локальному API из того же Linux-шелла возвращался мгновенно.

Потом Dify, запущенный в Docker-контейнере в сети хоста, сообщил об ошибке подключения. Первым подозреваемым стала конфигурация Dify, и это подозрение оказалось ошибочным. Настоящая причина — непонимание того, как сервис привязывается к сетевому интерфейсу, усугублённое отдельными сетевыми пространствами имён, которые создают WSL2 и Docker.

Представьте схему как вложенные комнаты. Снаружи — хост с Windows 11. Docker Engine образует одно пространство имён, внутри которого контейнеры Dify API и Dify worker. Служебная виртуальная машина WSL2 образует другое, в нём Ollama на порту 11434 и vLLM на порту 8000. Важны две границы: между Windows и ВМ WSL2 и между сетью Docker и хостом. Именно у этих стен и живёт печально известная путаница с localhost.

Запущен — не значит доступен

Здоровый сервис не обязательно доступный. Ollama была жива, но это означало лишь, что её процесс поднят и привязан к одному конкретному интерфейсу глубоко в ядре Linux.

Сервер не слушает «сеть» вообще. Он привязывается к конкретному IP-адресу и порту, и клиент достучится, только отправляя пакеты на маршрутизируемый адрес, совпадающий с этой привязкой. Многие безопасные настройки по умолчанию привязываются к 127.0.0.1, петлевому адресу IPv4, что запирает сервис в том самом пространстве имён, где он запущен. Привязка к 0.0.0.0 делает его доступным на всех интерфейсах машины, и это обязательно, если WSL2 нужно связать с Windows или Docker.

Ищем отклонённое соединение

Когда что-то не может достучаться до Ollama, сначала выясните, какой уровень падает: DNS, TCP или приложение. Здесь симптомом была ошибка подключения в Dify. Гипотеза — не проходит TCP-рукопожатие через границу ВМ. Проверка — голый TCP-тест со стороны Windows, которая находится в другом пространстве имён, чем WSL2, и минует всю логику приложения. Из PowerShell:

Test-NetConnection -ComputerName localhost -Port 11434

Результат — TcpTestSucceeded : False, значит, рукопожатие не проходило и Dify ни при чём. Дальше посмотрите на слушающий сокет изнутри Linux, в терминале WSL2:

ss -tlnp | grep 11434

Вывод всё решил:

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

Судя по выводу ss, Ollama слушала только 127.0.0.1:11434. Всё, что приходило через виртуальный Ethernet-интерфейс WSL2, а именно через него Windows общается с ВМ, ядро отклоняло сразу.

Три термина, в которых стоит разобраться

localhost — это просто имя хоста, которое разрешается через файл hosts. На какую машину оно на самом деле указывает, зависит от того, в каком контейнере или ВМ вы его набираете.

127.0.0.1 — петлевой адрес IPv4. Это виртуальный интерфейс внутри одного ядра ОС, и отправленные на него пакеты никогда не доходят до физической или виртуальной сетевой карты.

И есть подстановочный адрес 0.0.0.0 (INADDR_ANY). Когда сервер привязывается к нему, ОС принимает входящие TCP-соединения на всех своих интерфейсах IPv4.

Документация Microsoft по сети в WSL на https://learn.microsoft.com/en-us/windows/wsl/networking объясняет, что гипервизор запускает лёгкую служебную ВМ со своим сетевым адаптером и NAT-адресом, так что Windows приходится проходить через этот NAT, чтобы попасть на сторону Linux.

Одно изменение, которое всё чинит

Заставьте Ollama слушать все интерфейсы, задав переменную окружения OLLAMA_HOST. Если процессом управляет systemd, не трогайте основной unit-файл: обновления пакета перезапишут вашу правку. Вместо этого создайте drop-in override. Внутри WSL2 сначала создайте каталог:

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

Затем запишите файл 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

После перезапуска снова проверьте сокет. Так выглядит успех:

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

Звёздочка означает, что сервис слушает все интерфейсы, и повторный сетевой тест из Windows теперь дал True.

Почему вставлять IP WSL2 — ловушка

Сначала Dify направили на Ollama так: выполнили ip addr show eth0 в WSL2, скопировали адрес и вставили его в настройки Dify, что-то вроде http://172.20.5.42:11434. Какое-то время это работало, но конструкция хрупкая. Виртуальный коммутатор Hyper-V выдаёт этот адрес динамически, поэтому он меняется после каждой перезагрузки или перезапуска службы и тихо ломает интеграцию.

Надёжнее позволить хосту Windows детерминированно пересылать трафик через netsh portproxy. Чтобы это переживало перезагрузки без ручной работы, используйте PowerShell-скрипт, запущенный на хосте от имени администратора. Первая часть определяет текущий NAT-адрес экземпляра 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}''')

Вторая часть удаляет устаревшие правила маршрутизации и добавляет новые, указывающие на текущий адрес:

# 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

В итоге запросы к localhost в Windows попадают на тот IP, который сейчас у экземпляра Linux. Сохраните скрипт, поставьте его в Планировщик заданий Windows на запуск при старте, и ручной учёт IP-адресов уходит в прошлое.

Подключаем vLLM

Когда Ollama заработала стабильно, настала очередь vLLM. Его архитектура PagedAttention даёт большой выигрыш в пропускной способности, когда параллельно идёт много генераций. Сервер запустили внутри WSL2 с флагом host, открытым портом и осторожным выделением памяти GPU, чтобы избежать падений из-за нехватки памяти:

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

Прежде чем открывать интерфейс Dify, проверьте маршрут API отдельно. Если путать ошибки приложения с сетевыми, можно потерять часы. JSON-полезной нагрузке нужны двойные кавычки, а с ними неудобно писать быстрые однострочники в bash, поэтому задачу решает короткий скрипт на Python. Он начинается с импорта:

import requests

и продолжается самим запросом:

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())

HTTP 200 означает, что vLLM работает как OpenAI-совместимый сервер. Если бы этот вызов упал, было бы ясно, что проблема целиком внутри ВМ Linux.

Ловушка localhost в Docker

Последнее препятствие: внутри контейнера localhost указывает на сам контейнер. Если ввести в Dify http://localhost:8000/v1, контейнер Dify API станет искать модель в собственном пространстве имён и сразу упадёт.

Раз portproxy в Windows уже открывает ИИ-сервисы на хосте, контейнеру нужен лишь выход на хост. Docker Desktop даёт для этого специальное DNS-имя, оно описано на https://docs.docker.com/desktop/networking/. В Dify откройте Model Provider, выберите OpenAI-API-compatible и заполните:

  • Endpoint: http://host.docker.internal:8000/v1
  • Model: то, что раздаёт vLLM, здесь mistralai/Mistral-7B-Instruct-v0.3
  • Key: буквально слово EMPTY, потому что локальные экземпляры его не проверяют

Конфигурация, которую стоит оставить

Итоговый поток выглядит так. Запрос уходит из контейнера Dify на host.docker.internal:11434. Хост Windows принимает его, и срабатывает правило netsh portproxy. Правило пересылает его на адрес 172.x.x.x виртуального NAT WSL2, где демон Ollama, привязанный к 0.0.0.0, принимает полезную нагрузку.

Такая конфигурация избавляет от ручной возни с IP, а мост между ОС хоста, гипервизором и движком контейнеров теперь каждый день ведёт себя одинаково. Понимание того, как стыкуются эти пространства имён, нужно не только для починки капризного соединения: это базовый навык для надёжной локальной ИИ-инфраструктуры.