В прошлой статье серии мы разобрали архитектуру корпоративного AI-сервера на бумаге: из каких слоёв он состоит и как они взаимодействуют. Теперь переходим от схемы к реальному развёртыванию. Самый быстрый и предсказуемый способ поднять Ollama и Open WebUI вместе — собрать их в один docker-compose.yml, где обе части стартуют, обновляются и перезапускаются синхронно, а вся конфигурация лежит в одном файле под контролем версий. Разворачивать будем на сервере srv-ai01 в сети 192.168.10.0/24, домен itpages.lab.local.

Ollama берёт на себя загрузку моделей и генерацию ответов через простой REST API на порту 11434, а Open WebUI — это веб-интерфейс, который обращается к Ollama и даёт сотрудникам знакомый чат в браузере, историю переписки и базовое управление моделями без командной строки. Связка отлично подходит для старта: не требует ручной настройки inference-сервера и покрывает большинство сценариев команды до полусотни активных пользователей. Важное преимущество именно связки Docker Compose в том, что весь стек описан декларативно в одном файле — если сервер выйдет из строя, тот же docker-compose.yml разворачивается на новом железе за считанные минуты, а не восстанавливается по памяти из отдельных команд установки.

Подготовка сервера

Перед разворачиванием стека убедись, что на srv-ai01 установлен Docker Engine, плагин docker-compose-plugin и nvidia-container-toolkit, если сервер работает с GPU (настройка описана в предыдущей статье серии). Создай структуру каталогов для постоянного хранения данных вне контейнеров:

sudo mkdir -p /srv/ai/{ollama,openwebui-data}
sudo chown -R 1000:1000 /srv/ai/openwebui-data
cd /srv/ai

Вынос данных за пределы контейнеров — не формальность: без этого при обновлении образа или пересоздании контейнера пропадут и загруженные модели, и вся история чатов пользователей.

Файл docker-compose.yml

Создай файл /srv/ai/docker-compose.yml со следующим содержимым. Обрати внимание на секцию deploy.resources с резервированием GPU — без неё контейнер Ollama не увидит видеокарту, даже если nvidia-container-toolkit настроен верно:

version: "3.8"

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - /srv/ai/ollama:/root/.ollama
    environment:
      - OLLAMA_HOST=0.0.0.0
      - OLLAMA_KEEP_ALIVE=30m
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - ai-net

  openwebui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: openwebui
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - /srv/ai/openwebui-data:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - WEBUI_SECRET_KEY=change-me-to-a-long-random-string
      - WEBUI_NAME=IT-Pages AI
    depends_on:
      - ollama
    networks:
      - ai-net

networks:
  ai-net:
    driver: bridge

Значение WEBUI_SECRET_KEY обязательно замени на собственную случайную строку — она используется для подписи сессионных токенов, и оставлять значение по умолчанию нельзя даже во внутренней сети. Сгенерировать подходящую строку можно так:

openssl rand -hex 32

Если сервер работает без GPU (только на CPU, для маленьких моделей или тестового стенда), просто удали блок deploy из сервиса ollama — контейнер запустится в режиме CPU-инференса автоматически, без дополнительных флагов.

Запуск стека

Из каталога /srv/ai подними оба сервиса одной командой:

docker compose up -d
docker compose ps

Убедись, что оба контейнера в статусе running, и проверь, что Ollama видит GPU изнутри контейнера:

docker exec -it ollama nvidia-smi

Если команда вернула таблицу с картой и загрузкой памяти — GPU проброшена верно. Дальше открой в браузере http://192.168.10.50:8080 (или заранее настроенный DNS-адрес ai.itpages.lab.local, если nginx с проксированием уже настроен) — должна открыться страница первичной настройки Open WebUI с предложением создать первого администратора.

Загрузка первой модели

Модели загружаются либо через интерфейс Open WebUI (раздел настроек → Models → Pull a model), либо напрямую командой в контейнере Ollama — второй способ удобнее для автоматизации и скриптов первичной настройки сервера:

docker exec -it ollama ollama pull llama3.1:8b
docker exec -it ollama ollama pull qwen2.5:14b
docker exec -it ollama ollama list

Для старта в корпоративной среде разумно взять одну универсальную модель среднего размера (7-14 млрд параметров, квантование Q4_K_M) — она даёт приемлемое качество ответов при разумной скорости и укладывается в память одной видеокарты уровня RTX 4090. Более крупные модели можно добавить позже, когда станет понятно, какие задачи сотрудники решают чаще всего.

Проксирование через nginx и HTTPS

Открывать Open WebUI напрямую по порту 8080 без шифрования не стоит даже во внутренней сети. Поставь nginx перед контейнером и заверни трафик в TLS с помощью внутреннего центра сертификации или самоподписанного сертификата для домена ai.itpages.lab.local:

server {
    listen 443 ssl;
    server_name ai.itpages.lab.local;

    ssl_certificate     /etc/nginx/ssl/ai.itpages.lab.local.crt;
    ssl_certificate_key /etc/nginx/ssl/ai.itpages.lab.local.key;

    client_max_body_size 50M;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 300s;
    }
}

server {
    listen 80;
    server_name ai.itpages.lab.local;
    return 301 https://$host$request_uri;
}

Заголовки Upgrade и Connection обязательны — Open WebUI использует WebSocket для потоковой генерации ответов, и без корректного проксирования апгрейда соединения ответы модели будут приходить не по мере генерации, а одним куском после завершения.

Сертификат для внутреннего домена itpages.lab.local проще всего выпустить через собственный корневой центр сертификации, если он уже развёрнут в лаборатории, и раздать его корневой сертификат сотрудникам через групповую политику — тогда браузер не будет ругаться на самоподписанный сертификат при каждом обращении к ai.itpages.lab.local. Если центра сертификации пока нет, временным решением подойдёт самоподписанный сертификат openssl, но при первом заходе сотрудникам придётся один раз подтвердить исключение безопасности в браузере.

Проверка работоспособности API

Перед тем как раздавать ссылку сотрудникам, стоит проверить, что API Ollama отвечает корректно и напрямую, в обход веб-интерфейса — это удобно для диагностики, если позже что-то пойдёт не так уже на уровне Open WebUI:

curl http://192.168.10.50:11434/api/generate -d '{
  "model": "llama3.1:8b",
  "prompt": "Кратко опиши, что такое Retrieval-Augmented Generation",
  "stream": false
}'

Если запрос вернул JSON с полем response и связным текстом — модель загрузилась и inference работает штатно. Дальше остаётся зайти в веб-интерфейс, создать первую учётную запись администратора (она автоматически становится ролью admin, все последующие регистрации по умолчанию получают роль обычного пользователя) и отправить тестовый вопрос уже через браузер, чтобы убедиться, что потоковая генерация через WebSocket работает без задержек и обрывов.

Автозапуск и проверка после перезагрузки

Параметр restart: unless-stopped в docker-compose.yml гарантирует, что оба контейнера поднимутся автоматически после перезагрузки сервера или сбоя Docker-демона, если только их не остановили вручную. Дополнительно убедись, что сама служба Docker стоит в автозапуске:

sudo systemctl enable docker
sudo systemctl is-enabled docker

После первой полноценной настройки полезно сразу же перезагрузить srv-ai01 целиком и проверить, что стек поднимается без ручного вмешательства — лучше обнаружить проблему с автозапуском сейчас, в спокойной обстановке, чем во время внепланового обслуживания сервера ночью.

Обновление стека без потери данных

Образы Ollama и Open WebUI обновляются часто — команда Open WebUI регулярно выпускает новые функции и правки безопасности, и держать версии на последнем стабильном уровне стоит хотя бы раз в месяц. Поскольку все данные вынесены в /srv/ai, обновление образов не затрагивает историю чатов и загруженные модели:

cd /srv/ai
docker compose pull
docker compose up -d
docker image prune -f

Перед обновлением на продуктивном сервере полезно сначала проверить changelog новой версии Open WebUI — иногда между минорными версиями меняется формат базы данных, и после апдейта выполняется миграция схемы автоматически при первом запуске. Это единственный момент, когда стоит сделать снимок каталога /srv/ai/openwebui-data перед обновлением, чтобы было куда откатиться, если миграция пройдёт некорректно. Подробнее про регулярное резервное копирование всего AI-сервера мы поговорим в отдельной статье серии.

Ограничение ресурсов и несколько моделей одновременно

По умолчанию Ollama держит в памяти GPU только те модели, к которым недавно были запросы, и выгружает их через промежуток времени, заданный переменной OLLAMA_KEEP_ALIVE — в примере выше это 30 минут. Если на сервере одновременно нужно держать в памяти две разные модели (например, основную для чатов и отдельную, заточенную под код), убедись, что суммарный объём видеопамяти обеих моделей укладывается в объём карты, иначе Ollama начнёт выгружать одну модель при обращении к другой, и каждый переключение будет стоить лишних секунд на повторную загрузку.

Для контроля потребления оперативной памяти хостом полезно сразу задать лимиты на уровне Docker Compose, особенно если на этом же сервере крутятся другие сервисы:

  ollama:
    ...
    mem_limit: 32g
    memswap_limit: 32g

Это не обязательный шаг для выделенного AI-сервера, но полезная страховка, если srv-ai01 в будущем станет хостить что-то ещё, кроме основного стека.

Версионирование конфигурации в Git

docker-compose.yml, конфиг nginx и сопутствующие скрипты стоит с самого первого дня держать под git, а не просто складывать в каталог на сервере. Это дешёвая страховка: если через полгода понадобится поднять точную копию стека на srv-ai02 или откатить неудачное изменение конфигурации, история коммитов расскажет, что и когда менялось, и почему. Секреты вроде WEBUI_SECRET_KEY при этом в репозиторий не кладём — их стоит вынести в отдельный .env-файл и добавить в .gitignore:

cd /srv/ai
git init
printf ".env\n*.log\nollama/\nopenwebui-data/\n" > .gitignore
git add docker-compose.yml .gitignore
git commit -m "Initial Ollama + Open WebUI stack for srv-ai01"

Сам docker-compose.yml тогда стоит переписать так, чтобы секрет подтягивался из .env, а не лежал открытым текстом в файле, который может случайно попасть в общий репозиторий компании:

  openwebui:
    ...
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
      - WEBUI_NAME=IT-Pages AI

Файл .env рядом с docker-compose.yml при этом достаточно защитить правами доступа на уровне файловой системы (chmod 600) — Docker Compose подхватывает переменные из него автоматически при каждом up.

Диагностика при первом неудачном запуске

Если после docker compose up -d что-то пошло не так, полезно проверять компоненты по порядку, а не гадать наугад. Сначала — статус и последние строки логов обоих контейнеров:

docker compose ps
docker compose logs --tail=50 ollama
docker compose logs --tail=50 openwebui

Дальше — сетевую связность между контейнерами внутри сети ai-net: Open WebUI должен уметь достучаться до Ollama по внутреннему имени сервиса, а не по localhost:

docker exec -it openwebui curl -s http://ollama:11434/api/tags

Если этот запрос зависает или возвращает ошибку соединения, а сам контейнер ollama при этом запущен, чаще всего дело в том, что оба сервиса случайно оказались в разных docker-сетях — например, один поднят вручную командой docker run без указания network, а второй через compose. Держи оба сервиса строго в одном файле docker-compose.yml и одной сети ai-net, как в примере выше, — это исключает целый класс таких проблем.

Ещё одна частая причина не запускающегося Open WebUI — занятый порт 8080 другим процессом на хосте. Проверить это можно так:

sudo ss -tulpn | grep 8080

Если порт занят, проще всего либо остановить конфликтующий сервис, либо изменить проброс порта в docker-compose.yml на свободный, например «8081:8080», не забыв поправить и конфиг nginx выше.

Частые ошибки и как их избежать

  • Контейнер Ollama не видит GPU. Обычно причина в отсутствующей или неверно настроенной секции deploy.resources в docker-compose.yml, либо в том, что nvidia-container-toolkit не подключён к Docker-демону. Проверяй командой docker exec -it ollama nvidia-smi сразу после первого запуска, не откладывая.
  • Забытый WEBUI_SECRET_KEY по умолчанию. Если оставить значение из примера, все выданные сессионные токены становятся предсказуемыми, что снижает защиту от подделки сессии. Генерируй случайную строку при каждом новом развёртывании.
  • Прямой доступ к порту 8080 без nginx и TLS. Даже во внутренней сети трафик с паролями пользователей и содержимым переписки стоит шифровать, а сам порт контейнера не выставлять наружу без прокси-слоя.
  • Игнорирование логов при первом запуске. Если после docker compose up -d контейнер openwebui через пару секунд переходит в статус restarting, причина почти всегда видна в логах — например, неверный формат OLLAMA_BASE_URL или конфликт портов с уже запущенным сервисом. Смотри docker compose logs -f openwebui сразу, не дожидаясь, пока проблема всплывёт у пользователей.

Что дальше

Свой AI-сервер редко ограничивается одной настройкой — вот что обычно идёт следом: