Головна / Статті / 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, а міст між ОС хоста, гіпервізором і рушієм контейнерів тепер щодня поводиться однаково. Розуміння того, як стикуються ці простори імен, потрібне не лише для лагодження примхливого з'єднання: це базова навичка для надійної локальної ШІ-інфраструктури.