Docker Swarm: развёртывание в существующем кластере

Open WebUI в готовом Docker Swarm: одна реплика, узел данных, секрет и Ollama. Разберём доступ, проверки запуска, обновление и границы восстановления.

После урока вы сможете

  • Подготовить stack для одного Open WebUI в существующем Linux Swarm
  • Связать выбранный узел, постоянные данные и стабильный секрет приложения
  • Различать доступ к странице, готовность приложения и ответ модели
  • Проверить сохранение тестового диалога после замены задачи и объяснить границы восстановления

Проверяемый результат: Вы сможете подготовить один stack для готового Swarm, объяснить размещение данных и ключа, а при выполнении на разрешённом кластере проверить вход, ответ модели и сохранение тестового диалога после замены задачи.

Допустим, в вашей инфраструктуре уже работает Linux-кластер Docker Swarm и есть готовый сервер Ollama. Нужно добавить к ним новый экземпляр Open WebUI так, чтобы после замены контейнера учётная запись и начатый разговор остались на месте. В готовом Swarm это решается привязкой одного экземпляра сервиса к выбранному узлу с отдельным каталогом данных и постоянным секретным ключом. Сам по себе перезапуск задачи в Swarm не переносит локальную базу между машинами и не проверяет связь с языковой моделью, поэтому в уроке мы последовательно свяжем узел, хранилище, ключ и два сетевых маршрута — от браузера до интерфейса и от контейнера до Ollama.

Что именно запустим в готовом Swarm

При запуске в кластере важно различать описание сервиса и место хранения файлов. В нашей задаче мы создаём новую отдельную установку: оператор передаёт кластеру описание в виде stack, Swarm поддерживает один логический service, а внутри него запускает заменяемую задачу task — конкретный контейнер с приложением. Если этот контейнер остановится или будет принудительно обновлён, планировщик создаст вместо него новую задачу. Чтобы новая задача увидела прежние чаты и не сбросила вход пользователей, она должна открывать ту же самую папку на диске и читать тот же секретный ключ.

В этой схеме четыре роли:

  • Узел управления (manager) принимает команды развёртывания, хранит метаданные секретов и следит за тем, чтобы задача работала на предписанном месте.
  • Выбранный узел данных (data-node) — конкретный Linux-сервер кластера, на котором лежит новый каталог /srv/open-webui-course/data и запускается контейнер сервиса webui.
  • Компьютер читателя — рабочее место с браузером, откуда вы подключаетесь по SSH именно к data-node, чтобы открыть веб-интерфейс.
  • Существующий сервер Ollama — уже настроенный в вашей сети сервис с загруженной моделью, к которому контейнер webui обращается напрямую со своей стороны.

Мы ограничиваем конфигурацию одной репликой (replicas: 1) и одним рабочим процессом (UVICORN_WORKERS: "1"). По умолчанию в выпуске Open WebUI v0.12.3 (опубликован 16 октября 2026 года) база данных SQLite и локальное векторное хранилище размещаются внутри каталога данных приложения. Как отмечено в руководстве Open WebUI по многорепликовому запуску, несколько экземпляров требуют внешней базы данных и координации состояния, поэтому мы не добавляем сюда ни вторую реплику поверх SQLite, ни отдельные контейнеры PostgreSQL, Redis или Chroma.

В официальной документации Open WebUI по Docker Swarm приведён трёхсервисный рецепт, где в одном файле объединены сам веб-интерфейс, ChromaDB и Ollama. Документация прямо поясняет, что это иллюстрация совместного запуска компонентов, а не обязательное требование держать все три сервиса внутри каждого стека. Мы используем ограниченную адаптацию этого подхода для ситуации, когда сервер моделей уже существует отдельно. Кроме того, в примере из документации Open WebUI в блоке публикации портов встречается запись mode: overlay. Переносить её в рабочий файл нельзя: согласно документации Docker по сети и публикации портов в Swarm, overlay — это драйвер межузловой сети для общения контейнеров, тогда как у публикуемых наружу портов есть только два режима — ingress и host.

Разница между ingress и host напрямую влияет на безопасность и маршрут подключения. По умолчанию режим ingress включает встроенную маршрутизацию (routing mesh) и открывает опубликованный порт на всех узлах кластера, даже там, где контейнер сервиса не запущен. Нам это не нужно: данные привязаны к одному серверу, поэтому мы выбираем mode: host, который обходит общую сетку маршрутизации и открывает порт только на том узле, где работает задача. Это решение для нового экземпляра без миграции старой базы и без высокой доступности (HA): если выбранный узел данных выйдет из строя, задача перейдёт в ожидание (Pending), а сервис временно остановится, при сохранённом ограничении node.id планировщик не перенесёт контейнер на соседний сервер с пустой папкой вместо вашей базы.

Если готового кластера Swarm у вас нет и нужно просто поднять интерфейс на одной машине, создавать кластер ради этой задачи не требуется — воспользуйтесь уроком «Установка Open WebUI через Docker Compose». Если кластер уже работает, проверим его узлы, сеть и секреты перед сохранением конфигурации.

Что проверить на manager и узле данных до запуска

До любых команд развёртывания должно выполняться главное сетевое условие: в вашей инфраструктуре уже действует правило межсетевого экрана или сетевой политики, которое закрывает прямой доступ к порту 18080/tcp на выбранном узле данных из нежелательных сетей и оставляет только разрешённый маршрут оператора. Согласно документации Docker по публикации сервисных портов, режим mode: host публикует порт непосредственно на сетевых интерфейсах узла и не даёт гарантии привязки только к локальному адресу 127.0.0.1. При этом обычное правило UFW на хосте само по себе не служит доказательством защиты для портов, публикуемых через Docker. Мы не меняем правила межсетевого экрана в этом уроке: если ограничение доступа к порту 18080 на узле данных ещё не подтверждено владельцем инфраструктуры, выполнять deploy нельзя. Для публичного или командного доступа в будущем потребуется отдельно согласованный обратный прокси с HTTPS и поддержкой WebSocket, а здесь мы проверим сервис через личный SSH-туннель.

Чтобы не перепутать, где именно вводится каждая команда, сверим карту ответственности по машинам и ролям:

Контекст или рольЧто проверяем и делаем здесьЧего здесь делать не нужно
Узел managerПроверяем список стеков, находим ID выбранного узла данных, проверяем метаданные внешнего секрета, рендерим и применяем stack.ymlНе создаём здесь каталог /srv/open-webui-course/data (если manager и data-node — разные машины) и не проверяем контейнеры через docker exec
Выбранный data-nodeПроверяем, свободен ли порт 18080, создаём пустой каталог /srv/open-webui-course/data, проверяем /ready и связь контейнера с OllamaНе запускаем здесь docker stack deploy и не храним управляющие файлы стека
Компьютер читателяОткрываем SSH-туннель непосредственно до data-node и заходим в интерфейс через локальный браузер по адресу http://127.0.0.1:3000Не направляем SSH-туннель на произвольный manager, где нет задачи сервиса
Оператор хранилища секретовЗаранее предоставляет в Swarm стабильный секрет openwebui_course_key_v1 из утверждённого хранилища и сохраняет отдельную резервную копию значенияНе генерирует ключ случайной командой в историю терминала и не записывает его открытым текстом в stack.yml

Начнём с проверок на узле manager. Нам нужно убедиться, что имя стека openwebui-course свободно, выбранный узел данных находится в состоянии ready и active под управлением Linux с архитектурой amd64 или arm64, а внешний секрет openwebui_course_key_v1 уже зарегистрирован в Swarm. В блоке ниже замените DATA_NODE_NAME на реальное имя вашего узла данных, а адрес с доменом .example — на реальный URL существующего сервера Ollama, который предоставил оператор инфраструктуры.

Выполните на manager (в оболочке Bash):

docker stack ls
docker node ls
export DATA_NODE_ID="$(docker node inspect --format '{{.ID}}' DATA_NODE_NAME)"
docker node inspect "$DATA_NODE_ID" --format '{{.Status.State}} {{.Spec.Availability}} {{.Description.Platform.OS}} {{.Description.Platform.Architecture}}'
docker secret inspect openwebui_course_key_v1 --format '{{.Spec.Name}}'
export OLLAMA_BASE_URL='http://ollama.internal.example:11434'

Если в выводе docker stack ls уже есть стек с именем openwebui-course, остановитесь и не применяйте конфигурацию поверх существующего окружения. Команда docker node inspect должна подтвердить статус ready active linux и одну из поддерживаемых архитектур нашего образа (amd64 или arm64). Команда docker secret inspect с форматом {{.Spec.Name}} проверяет только наличие метаданных секрета в Raft-хранилище Swarm и не выводит само секретное значение на экран.

В переменной OLLAMA_BASE_URL нельзя оставлять заглушку .example: до проверки конфигурации подставьте туда действующий адрес сервера моделей, достижимый из сети будущего контейнера. Не указывайте там localhost или 127.0.0.1 — внутри контейнера WebUI эти адреса указывают на сам контейнер интерфейса, а не на хост-машину. Специальное имя host.docker.internal в Linux Swarm также не гарантировано по умолчанию, а короткое имя ollama сработает только в том случае, если оба сервиса реально подключены к одной общей overlay-сети со встроенным обнаружением сервисов (в нашей схеме общая overlay-сеть с внешним сервером моделей не предполагается). Заранее узнайте у оператора точный тег модели, которая уже скачана на этом сервере Ollama.

Секрет openwebui_course_key_v1 по правилам безопасности заранее создаёт оператор через штатный процесс управления секретами вашей организации. Как объясняется в документации Open WebUI по WEBUI_SECRET_KEY, этот ключ подписывает пользовательские сессии и участвует в шифровании чувствительных данных. Наличие объекта в docker secret inspect подтверждает, что Swarm знает о секрете, но само по себе не заменяет резервную копию его значения: оператор должен хранить копию ключа в защищённом хранилище отдельно от кластера.

Теперь перейдём на выбранный узел данных (data-node). Команды на manager не создают папок на рабочих узлах, поэтому подготовить каталог и проверить локальный порт нужно непосредственно на целевом сервере.

Выполните на выбранном data-node (в оболочке Bash на rootful Linux без режима rootless, переназначения userns-remap и особых политик SELinux):

sudo ss -ltnp 'sport = :18080'
sudo sh -c 'test ! -e /srv/open-webui-course && install -d -m 0700 -o 0 -g 0 /srv/open-webui-course/data'

Утилита ss показывает текущие локальные процессы, слушающие порт 18080/tcp. В норме вывод должен быть пустым. ss не видит ни правил межсетевого экрана, ни портов, зарезервированных другими неактивными сервисами в планировщике. Отсутствие конфликтов в кластере оператор проверяет дополнительно. Вторая команда проверяет, что пути /srv/open-webui-course ещё не существует, и создаёт иерархию /srv/open-webui-course/data с правами 0700 и владельцем 0:0 (root:root). Если путь уже существует, проверка test ! -e завершится ошибкой: в этом случае остановитесь, не удаляйте старые файлы наугад и не ослабляйте права доступа через chmod 777. Права 0:0 и режим 0700 точно соответствуют метаданным нашего закреплённого образа v0.12.3, процесс которого работает от пользователя 0:0. Если на ваших серверах используется userns-remap, rootless Docker или строгие профили SELinux, правила владения для bind-монтирования потребуют отдельного согласования с администратором безопасности.

Когда узел, каталог, внешний секрет, защищённый порт и адрес сервера модели подтверждены, можно собрать эти параметры в единый файл стека.

Как связать данные, ключ и версию в stack.yml

Чтобы после замены контейнера вы попадали в тот же самый экземпляр Open WebUI, файл конфигурации должен жёстко связать четыре вещи: неизменяемый образ приложения, конкретный узел с папкой данных, внешний секретный ключ и безопасный порядок перезапуска.

На узле manager в рабочем каталоге оператора сохраните файл stack.yml со следующим содержимым:

version: "3.8"
services:
  webui:
    image: ghcr.io/open-webui/open-webui@sha256:a537bbe1e6b428820f52ed4a029d7fff97062e24d5cf32c365fba36fd0542def
    environment:
      WEBUI_AUTH: "True"
      WEBUI_SECRET_KEY_FILE: /run/secrets/openwebui_secret_key
      UVICORN_WORKERS: "1"
      OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:?Set approved existing Ollama URL}"
    secrets:
      - source: webui_secret_key
        target: openwebui_secret_key
    volumes:
      - type: bind
        source: /srv/open-webui-course/data
        target: /app/backend/data
    ports:
      - target: 8080
        published: 18080
        protocol: tcp
        mode: host
    deploy:
      replicas: 1
      placement:
        constraints:
          - "node.id == ${DATA_NODE_ID:?Set existing data node ID}"
      update_config:
        parallelism: 1
        order: stop-first
        failure_action: pause
      rollback_config:
        parallelism: 1
        order: stop-first
secrets:
  webui_secret_key:
    external: true
    name: openwebui_course_key_v1

Каждая связка в этом файле защищает от конкретных ошибок:

  • Формат version: "3.8" и закреплённый digest образа. Согласно документации Docker по docker stack deploy, развёртывание стеков в Swarm опирается на спецификацию Compose v3 (опции современного локального Compose вроде build или restart здесь не работают). В поле image вместо плавающего тега :main или :latest указан точный индексный digest выпуска v0.12.3: ghcr.io/open-webui/open-webui@sha256:a537bbe1e6b428820f52ed4a029d7fff97062e24d5cf32c365fba36fd0542def. Digest закрепляет один индекс образа; при пересоздании задачи на той же архитектуре узел использует ту же платформенную сборку (проверка опирается на метаданные реестра GHCR и исходный код релиза v0.12.3).
  • Жёсткая привязка node.id и локальный каталог данных. В разделе volumes используется type: bind, который монтирует абсолютный путь /srv/open-webui-course/data выбранного узла в контейнерный каталог /app/backend/data. Как предупреждает документация Docker по томам в Swarm, локальные тома и bind-монтирования принадлежат конкретной машине, а не всему кластеру. Поэтому в deploy.placement.constraints задано жёсткое условие node.id == ${DATA_NODE_ID:?...}. Конструкция :? не позволит сгенерировать файл, если переменная DATA_NODE_ID пуста, а само ограничение запретит планировщику переносить задачу на другие серверы.
  • Связка внешнего секрета и WEBUI_SECRET_KEY_FILE. По умолчанию стартовый скрипт контейнера backend/start.sh создаёт файл .webui_secret_key в рабочей папке /app/backend, которая находится вне примонтированного тома /app/backend/data. Если бы мы сохранили только папку data, при первой же замене контейнера ключ внутри слоя образа сменился бы и существующие сессии перестали бы работать. Согласно документации Docker Secrets, Swarm монтирует внешний секрет openwebui_course_key_v1 в память контейнера по пути /run/secrets/openwebui_secret_key. Мы указываем этот путь в переменной WEBUI_SECRET_KEY_FILE и сохраняем штатную команду запуска образа (bash start.sh). В коде backend/start.sh релиза v0.12.3 переменная WEBUI_SECRET_KEY_FILE читается только тогда, когда не заданы прямые переменные WEBUI_SECRET_KEY и устаревшая WEBUI_JWT_SECRET_KEY. Поэтому не добавляйте эти две переменные в environment и не переносите суффикс _FILE на произвольные ключи API — для них такая логика в стартовом скрипте не предусмотрена.
  • Публикация порта 18080 -> 8080 в режиме host и включённая аутентификация. Переменная WEBUI_AUTH: "True" включает обязательный вход по учётной записи. Блок ports связывает внутренний порт приложения 8080 с портом 18080 на узле данных в режиме mode: host. Внутренние порты других сервисов (такие как 11434 для Ollama или 8000 для Chroma) мы здесь не публикуем.
  • Одна реплика и порядок обновления stop-first. Параметры replicas: 1 и UVICORN_WORKERS: "1" ограничивают запуск одним экземпляром приложения с одним рабочим процессом. А настройки order: stop-first, parallelism: 1 и failure_action: pause в блоках update_config и rollback_config (см. справочник спецификации Deploy) требуют от Swarm сначала полностью остановить старую задачу, освободить порт 18080 и закрыть базу SQLite, и лишь затем запускать новый контейнер. Если Swarm признает обновление неуспешным по его условиям контроля, failure_action: pause приостановит дальнейшее обновление. Этот параметр не отменяет политику перезапуска самой задачи. Во время замены контейнера интерфейс на короткое время станет недоступен.

Теперь проверим, как Docker подставит наши переменные в этот шаблон, и передадим проверенный файл планировщику.

Как применить stack и проверить задачу Swarm

Перед отправкой описания в кластер разделим два шага: сначала локально проверить структуру YAML и подстановку переменных через docker stack config, сохранить результат в отдельный файл stack.rendered.yml и только после этого выполнить docker stack deploy.

Убедитесь, что в вашей текущей сессии терминала на manager по-прежнему заданы непустые переменные DATA_NODE_ID и OLLAMA_BASE_URL из второго раздела, а в текущей папке лежит stack.yml.

Выполните на manager (в оболочке Bash из рабочего каталога со stack.yml):

docker stack config --compose-file stack.yml > stack.rendered.yml
docker stack deploy --compose-file stack.rendered.yml openwebui-course
docker stack services openwebui-course
docker service ps --no-trunc openwebui-course_webui
docker service inspect openwebui-course_webui --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'

Команда docker stack config выполняет локальную интерполяцию переменных окружения и проверяет синтаксис файла без обращения к демону Swarm. Если DATA_NODE_ID или OLLAMA_BASE_URL не заданы, команда завершится с ошибкой. Перенаправление > при этом уже может создать или обнулить stack.rendered.yml: остановитесь и не выполняйте deploy, пока docker stack config не завершится успешно. Внутри stack.rendered.yml остаётся только ссылка на имя внешнего секрета openwebui_course_key_v1, а само секретное значение туда не попадает.

У этой предварительной проверки есть границы. При подготовке урока мы провели офлайн-валидацию нашего stack.yml с помощью docker stack config (Docker CLI 29.6.1), проверили синтаксис всех Bash-команд и 12 структурных инвариантов отрендеренного манифеста, но не запускали реальный кластер Swarm. Показательная деталь из этой офлайн-проверки: парсер docker stack config проверяет схему YAML, но не проверяет все семантические правила движка Swarm — в контрольном отрицательном тесте он без ошибки пропустил даже заведомо невалидный режим порта mode: overlay. Кроме того, docker stack config не знает, существует ли в кластере указанный узел, создана ли папка на диске, свободен ли порт 18080 и доступен ли секрет.

Именно поэтому сразу после docker stack deploy мы проверяем фактическое состояние сервиса в планировщике тремя последними командами:

  1. В выводе docker stack services openwebui-course у сервиса openwebui-course_webui в колонке REPLICAS должно появиться значение 1/1.
  2. В выводе docker service ps --no-trunc openwebui-course_webui текущее состояние (CURRENT STATE) должно перейти в Running, колонка NODE должна указывать точно на ваш выбранный узел данных, а поле ERROR должно оставаться пустым. Флаг --no-trunc здесь обязателен: без него Docker обрезает длинные сообщения об ошибках и полные идентификаторы.
  3. Команда docker service inspect должна вернуть точную строку нашего образа с неизменённым индексным хешем @sha256:a537bbe1e6b428820f52ed4a029d7fff97062e24d5cf32c365fba36fd0542def.

Если задача застряла в состоянии Pending, Rejected или Failed, посмотрите на текст в колонке ERROR вывода docker service ps --no-trunc и сопоставьте причину с местом проверки:

  • Ошибки no suitable node (не выполнено ограничение constraint или availability) — проверьте на manager, правильный ли DATA_NODE_ID попал в stack.rendered.yml, а также находится ли целевой узел в состоянии ready и active. Никогда не удаляйте ограничение node.id из файла ради того, чтобы сервис позеленел на другом узле: там не окажется вашей папки данных.
  • Ошибки монтирования (mount или bind source path does not exist) — проверьте на data-node, создан ли точный путь /srv/open-webui-course/data и соответствуют ли его права условиям вашего окружения.
  • Конфликт порта (port уже занят или зарезервирован) — проверьте слушателей порта 18080 на data-node через ss и убедитесь на manager, что этот порт не закреплён за другим сервисом.
  • Ошибка доступа к секрету (secret not found) — проверьте на manager точное имя openwebui_course_key_v1 через docker secret inspect.
  • Ошибка загрузки образа (image, доступ к реестру или несовпадение платформы) — проверьте доступ узла data-node к ghcr.io, сохранность digest-строки и архитектуру процессора (amd64 или arm64).

Если задача перешла в Running, но ведёт себя нестабильно, на узле manager можно просмотреть последние 50 строк журнала сервиса:

docker service logs --tail 50 openwebui-course_webui

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

Даже когда в docker stack services горит 1/1, а задача находится в статусе Running, это означает лишь то, что планировщик Swarm создал контейнер на нужном сервере. Чтобы убедиться, что приложение внутри контейнера завершило инициализацию базы данных и действительно видит сервер Ollama, перейдём к проверкам на узле данных.

Как проверить готовность приложения и связь с Ollama

Почему нельзя сразу открывать браузер, ориентируясь только на статус Running и встроенную проверку здоровья образа? Если заглянуть в Dockerfile и исходный код backend/open_webui/main.py выпуска v0.12.3, окажется, что встроенный HEALTHCHECK контейнера опрашивает маршрут /health, а обработчик этого маршрута просто возвращает константу {'status': True}, как только запустился веб-сервер. В том же файле main.py есть более строгий маршрут /ready: он возвращает успешный ответ только после завершения стартовых процедур приложения и проверки подключения к базе данных (а также к Redis, если бы он был настроен).

Перейдём в терминал выбранного узла данных (data-node), проверим готовность приложения через /ready и найдём идентификатор запущенного контейнера.

Выполните на выбранном data-node (в оболочке Bash):

curl -fsS http://127.0.0.1:18080/ready
docker ps --filter label=com.docker.swarm.service.name=openwebui-course_webui --filter status=running --format '{{.ID}}'

Первая команда обращается к локальному порту 18080 на самом узле данных и проверяет, что приложение ответило со статусом HTTP 200 без ошибок. Вторая команда выводит короткий идентификатор (CONTAINER_ID) единственного работающего контейнера нашей задачи. Если docker ps не вернул ни одного идентификатора или вывел сразу несколько строк, не угадывайте нужный контейнер — сначала вернитесь на manager и разберитесь с состоянием задач через docker service ps.

Когда у вас есть единственный идентификатор текущего контейнера, проверим вторую сетевую связь: способен ли сам контейнер webui достучаться до сервера Ollama по адресу из переменной OLLAMA_BASE_URL. Проверять это командой curl с узла manager или напрямую с хоста data-node недостаточно, поскольку у контейнера своё сетевое пространство имён и свои маршруты. Кроме того, мы не устанавливаем внутрь образа сторонние утилиты, а используем штатный Python, который уже входит в контейнер Open WebUI.

На выбранном data-node подставьте полученный идентификатор вместо CONTAINER_ID и выполните:

docker exec CONTAINER_ID python -c 'import os, urllib.request; response = urllib.request.urlopen(os.environ["OLLAMA_BASE_URL"].rstrip("/") + "/api/tags", timeout=10); print(response.status)'

Однострочник берёт значение OLLAMA_BASE_URL непосредственно из окружения работающего контейнера и запрашивает у сервера моделей служебный маршрут /api/tags, возвращающий список установленных моделей. Код ответа 200 в выводе подтверждает, что сетевой маршрут от задачи Swarm до API Ollama работает.

У этой проверки два ограничения. Во-первых, HTTP-код 200 от /api/tags подтверждает доступность API, но сам по себе не гарантирует, что список моделей не пуст и что сервер способен выполнить генерацию ответа; наличие конкретной загруженной модели мы проверим на следующем шаге в интерфейсе. Во-вторых, как отмечено в справочнике конфигурации Open WebUI по OLLAMA_BASE_URL, настройки подключений относятся к параметрам PersistentConfig, которые после изменения через панель администратора могут сохраняться в базе данных приложения. Поскольку мы развернули чистый экземпляр с новой базой в /srv/open-webui-course/data, при первом старте приложение берёт адрес из переменной окружения OLLAMA_BASE_URL. Но если позже вы поменяете этот URL вручную в веб-интерфейсе, повторное развёртывание стека с новым значением переменной в stack.yml не обязана автоматически перезаписать сохранённую в базе настройку — при диагностике всегда сверяйте переменную окружения с тем, что фактически указано в разделе администратора.

Убедившись, что на стороне сервера готовы и база данных приложения, и сетевой путь до Ollama, проверим работу чата глазами пользователя.

Как открыть интерфейс и получить первый ответ

Теперь пройдём путь от вашего рабочего компьютера до веб-интерфейса. Поскольку порт 18080 опубликован в режиме mode: host только на выбранном узле данных, пробрасывать SSH-туннель нужно именно до data-node, а не до произвольного узла manager (если в вашем кластере это разные серверы). Убедитесь, что локальный порт 3000 на вашем компьютере свободен.

На компьютере читателя замените DATA_NODE_SSH на разрешённое имя или адрес SSH-подключения к вашему узлу данных и выполните в терминале:

ssh -N -L 127.0.0.1:3000:127.0.0.1:18080 DATA_NODE_SSH

Флаг -N указывает SSH не запускать удалённую командную оболочку, а только удерживать перенаправление портов: обращения к 127.0.0.1:3000 на вашем компьютере теперь передаются по шифрованному соединению на адрес 127.0.0.1:18080 выбранного узла данных. Не закрывайте это окно терминала на время работы в браузере. Сам по себе SSH-туннель — это удобный личный канал связи, а не сетевой экран; он не закрывает порт 18080 на внешних интерфейсах сервера, поэтому мы и проверяли правила фильтрации трафика до развёртывания.

Оставив туннель открытым, перейдите в браузере на своём компьютере по адресу http://127.0.0.1:3000 и последовательно проверьте четыре слоя пользовательского сценария:

  1. Загрузка страницы и создание первого администратора. На новой установке Open WebUI предложит создать первую учётную запись, которая автоматически получит права администратора. После входа сразу отключите свободную самостоятельную регистрацию новых пользователей до того, как решите кого-либо приглашать; подробно эти настройки разобраны в уроке «Первый вход и базовые настройки доступа».
  2. Проверка подключения к Ollama в панели администратора. Перейдите в настройки администратора (Settings → Admin Settings → Connections) и убедитесь, что в поле адреса Ollama отображается ваш рабочий URL, а проверка соединения проходит успешно (подробности настройки интерфейса описаны в материале «Подключаем локальную модель через Ollama»). В этом сценарии с внутренним сервером Ollama мы не добавляем никаких внешних API-ключей.
  3. Выбор модели и получение первого ответа. Вернитесь на главный экран чата, выберите в выпадающем списке тот модельный тег, который заранее подготовил оператор вашего сервера Ollama, и отправьте короткий проверочный вопрос. Дождитесь осмысленного ответа в окне диалога. Для этой проверки скачивать новые модели через интерфейс не нужно.
  4. Сохранение тестового диалога. Убедитесь, что созданный диалог появился в боковой панели истории чатов под вашей учётной записью.

Загрузившаяся страница входа, успешная авторизация, видимый в списке тег модели и реально сгенерированный ответ — это четыре разные проверки, которые читатель наблюдает в своей разрешённой среде. Теперь, когда в базе данных /srv/open-webui-course/data есть ваша учётная запись и сохранённый диалог, мы можем проверить главный вопрос урока: переживёт ли этот чат принудительную замену контейнера в Swarm.

Что сохранится после замены задачи и где границы восстановления

В работающем кластере задача сервиса может быть пересоздана планировщиком или перезапущена оператором. Проверим на нашем новом тестовом экземпляре, как поведёт себя Open WebUI, если принудительно заменить его задачу тем же самым образом без изменения файла конфигурации, узла, каталога и секрета. Поскольку в stack.yml задано правило order: stop-first и одна реплика, учтите, что на время перезапуска контейнера веб-интерфейс кратковременно перестанет отвечать.

На узле manager выполните команду принудительного обновления сервиса:

docker service update --force --detach=false openwebui-course_webui

Флаг --force заставляет Swarm остановить текущую задачу сервиса openwebui-course_webui и запустить взамен новый контейнер с теми же параметрами, а --detach=false оставляет команду ждать сходимости сервиса; при проблемах нужно смотреть состояние задач. Мы не меняем здесь ни digest образа, ни секрет openwebui_course_key_v1, ни путь /srv/open-webui-course/data, ни ограничение node.id.

Когда команда завершится, повторите уже знакомые проверки в том же порядке:

  • На manager проверьте через docker service ps --no-trunc openwebui-course_webui и docker service inspect, что предыдущая задача перешла в состояние Shutdown, а на том же самом узле данных (data-node) запустилась новая задача Running с тем же индексным digest образа v0.12.3.
  • На data-node дождитесь успешного ответа от curl -fsS http://127.0.0.1:18080/ready, получите через docker ps новый CONTAINER_ID и при необходимости убедитесь тем же docker exec ... urllib.request, что новый контейнер видит /api/tags сервера Ollama.
  • На компьютере читателя (при работающем SSH-туннеле) обновите страницу http://127.0.0.1:3000 в браузере, проверьте, что вход в учётную запись работает, ранее сохранённый тестовый диалог открывается в истории, а модель отвечает на следующее сообщение в том же чате.

Если после замены контейнера тестовый разговор сохранился, это означает, что новая задача прочитала прежние данные. Ведь сама задача (task) в Swarm — это временный процесс, а состояние вашего экземпляра определяется тремя постоянными привязками за пределами контейнера: жёстким правилом размещения node.id, папкой /srv/open-webui-course/data на диске этого узла и неизменным секретом openwebui_course_key_v1, подключённым через WEBUI_SECRET_KEY_FILE.

Замену задачи и восстановление после сбоев или обновлений нужно различать:

  1. Замена задачи того же образа (--force) проверяет только то, что новый контейнер корректно подхватывает существующую папку данных и ключ на прежнем узле. Это не проверка резервного копирования и не доказательство отказоустойчивости кластера: если сам сервер data-node выключится или станет недоступен, жёсткое ограничение node.id оставит задачу в состоянии Pending, и сервис будет простаивать до возвращения узла в строй.
  2. Обновление версии образа (upgrade) выполняется иначе: в исходном файле stack.yml заменяют индексный digest на новый проверенный выпуск, после чего снова прогоняют docker stack config и docker stack deploy. Перед таким обновлением необходимо остановить приложение (чтобы прекратить запись в SQLite), сделать полную консистентную копию всего каталога /srv/open-webui-course/data, а также убедиться, что у вас отдельно сохранены значение стабильного ключа openwebui_course_key_v1, файл stack.yml и прежний digest образа. Копировать файлы активно работающей базы SQLite «на лету» в расчёте на гарантированный бэкап нельзя.
  3. Откат сервиса в Swarm (rollback) не откатывает базу данных. Согласно документации Docker по откату сервисов и руководству Open WebUI по обновлению, откат в Swarm возвращает лишь прежнюю конфигурацию сервиса и старый образ контейнера, но не отменяет изменения на диске. Миграции базы данных в Open WebUI односторонние (Database migrations are one-way): если новая версия образа уже изменила схему SQLite, старый контейнер после автоматического отката может не запуститься поверх обновлённой базы.
  4. Полноценное восстановление (recovery) — это согласованная процедура оператора, при которой на остановленном сервисе возвращают совместимую версию образа, восстанавливают из проверенной копии каталог данных и подключают тот же самый секретный ключ.

Сохраните свой рабочий stack.yml, отрендеренный stack.rendered.yml и записи о выбранном узле и секрете в документации вашей инфраструктуры. А прежде чем планировать переход на следующую версию образа или настраивать регламент резервного копирования, изучите соседний урок «Обновления, резервная копия и безопасная эксплуатация».

Материалы урока

Отметить прохождение

Отметьте урок после того, как получили проверяемый результат из практической части.

and>