Оригинал: https://timofei-ermolaev.ru/courses/open-webui/drugie-sposoby/docker-swarm-v-sushchestvuyushchem-klastere/

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

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

[![Тимофей Ермолаев](https://timofei-ermolaev.ru/_astro/timofei-ermolaev-portrait.s0mCFw-J_Z1x8STQ.webp)](<https://timofei-ermolaev.ru/ob-avtore/>)

[Тимофей Ермолаев](<https://timofei-ermolaev.ru/ob-avtore/>) Директор по информационным технологиям (CIO)

**Опубликовано**

19 октября 2026 г.

**Обновлено**

19 октября 2026 г.

**На урок**

35 мин

**Тема**

[Другие способы развёртывания](<https://timofei-ermolaev.ru/courses/open-webui/drugie-sposoby/>)

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

- Подготовить 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 по многорепликовому запуску](<https://docs.openwebui.com/troubleshooting/multi-replica/>) , несколько экземпляров требуют внешней базы данных и координации состояния, поэтому мы не добавляем сюда ни вторую реплику поверх SQLite, ни отдельные контейнеры PostgreSQL, Redis или Chroma.

В [официальной документации Open WebUI по Docker Swarm](<https://docs.openwebui.com/deployment/docker-swarm/>) приведён трёхсервисный рецепт, где в одном файле объединены сам веб-интерфейс, ChromaDB и Ollama. Документация прямо поясняет, что это иллюстрация совместного запуска компонентов, а не обязательное требование держать все три сервиса внутри каждого стека. Мы используем ограниченную адаптацию этого подхода для ситуации, когда сервер моделей уже существует отдельно. Кроме того, в примере из документации Open WebUI в блоке публикации портов встречается запись `mode: overlay`. Переносить её в рабочий файл нельзя: согласно [документации Docker по сети и публикации портов в Swarm](<https://docs.docker.com/engine/swarm/ingress/>) , `overlay` — это драйвер межузловой сети для общения контейнеров, тогда как у публикуемых наружу портов есть только два режима — `ingress` и `host`.

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

Если готового кластера Swarm у вас нет и нужно просто поднять интерфейс на одной машине, создавать кластер ради этой задачи не требуется — воспользуйтесь уроком [«Установка Open WebUI через Docker Compose»](<https://timofei-ermolaev.ru/courses/open-webui/install/docker-compose/>) . Если кластер уже работает, проверим его узлы, сеть и секреты перед сохранением конфигурации.

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

До любых команд развёртывания должно выполняться главное сетевое условие: в вашей инфраструктуре уже действует правило межсетевого экрана или сетевой политики, которое закрывает прямой доступ к порту `18080/tcp` на выбранном узле данных из нежелательных сетей и оставляет только разрешённый маршрут оператора. Согласно [документации Docker по публикации сервисных портов](<https://docs.docker.com/engine/swarm/services/>) , режим `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`](<https://docs.openwebui.com/reference/env-configuration/#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`](<https://docs.docker.com/engine/swarm/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](<https://docs.docker.com/engine/swarm/services/>) , локальные тома и `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](<https://docs.docker.com/engine/swarm/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](<https://docs.docker.com/reference/compose-file/deploy/>) ) требуют от Swarm сначала полностью остановить старую задачу, освободить порт `18080` и закрыть базу SQLite, и лишь затем запускать новый контейнер. Если Swarm признает обновление неуспешным по его условиям контроля, `failure_action: pause` приостановит дальнейшее обновление. Этот параметр не отменяет политику перезапуска самой задачи. Во время замены контейнера интерфейс на короткое время станет недоступен.

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

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

Перед отправкой описания в кластер разделим два шага: сначала локально проверить структуру YAML и подстановку переменных через [`docker stack config`](<https://docs.docker.com/reference/cli/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`](<https://docs.openwebui.com/reference/env-configuration/#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 предложит создать первую учётную запись, которая автоматически получит права администратора. После входа сразу отключите свободную самостоятельную регистрацию новых пользователей до того, как решите кого-либо приглашать; подробно эти настройки разобраны в уроке [«Первый вход и базовые настройки доступа»](<https://timofei-ermolaev.ru/courses/open-webui/configure/first-admin/>) .
2. **Проверка подключения к Ollama в панели администратора.** Перейдите в настройки администратора (**Settings** → **Admin Settings** → **Connections**) и убедитесь, что в поле адреса Ollama отображается ваш рабочий URL, а проверка соединения проходит успешно (подробности настройки интерфейса описаны в материале [«Подключаем локальную модель через Ollama»](<https://timofei-ermolaev.ru/courses/open-webui/configure/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 по откату сервисов](<https://docs.docker.com/engine/swarm/services/>) и [руководству Open WebUI по обновлению](<https://docs.openwebui.com/getting-started/updating/#rolling-back>) , откат в Swarm возвращает лишь прежнюю конфигурацию сервиса и старый образ контейнера, но не отменяет изменения на диске. Миграции базы данных в Open WebUI односторонние (`Database migrations are one-way`): если новая версия образа уже изменила схему SQLite, старый контейнер после автоматического отката может не запуститься поверх обновлённой базы.
4. **Полноценное восстановление (recovery)** — это согласованная процедура оператора, при которой на остановленном сервисе возвращают совместимую версию образа, восстанавливают из проверенной копии каталог данных и подключают тот же самый секретный ключ.

Сохраните свой рабочий `stack.yml`, отрендеренный `stack.rendered.yml` и записи о выбранном узле и секрете в документации вашей инфраструктуры. А прежде чем планировать переход на следующую версию образа или настраивать регламент резервного копирования, изучите соседний урок [«Обновления, резервная копия и безопасная эксплуатация»](<https://timofei-ermolaev.ru/courses/open-webui/operate/update-backup/>) .

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

- [Первый вход и базовые настройки доступа timofei-ermolaev.ru](<https://timofei-ermolaev.ru/courses/open-webui/configure/first-admin/>)
- [Подключаем локальную модель через Ollama timofei-ermolaev.ru](<https://timofei-ermolaev.ru/courses/open-webui/configure/ollama/>)
- [Обновления, резервная копия и безопасная эксплуатация timofei-ermolaev.ru](<https://timofei-ermolaev.ru/courses/open-webui/operate/update-backup/>)
- [Установка Open WebUI через Docker Compose timofei-ermolaev.ru](<https://timofei-ermolaev.ru/courses/open-webui/install/docker-compose/>)

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

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

Урок пройден

![Тимофей Ермолаев](https://timofei-ermolaev.ru/_astro/timofei-ermolaev-portrait.s0mCFw-J_2r7Nkv.webp)

## Об авторе

### Тимофей Ермолаев

Директор по информационным технологиям (CIO)

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

[Подробнее об авторе](<https://timofei-ermolaev.ru/ob-avtore/>)
