Продолжаем серию переводов статьей о контейнеризации OpenClaw и деплое в Kubernetes, и неочевидных проблемах переноса OpenClaw на рельсы автомасштабирования. В материале подробно разбирается каждый шаг пути от Dockerfile до продакшн-кластера, с рабочими конфигами, инструкциями и примерами сборок. Предложенные автором решения не всегда оптимальны, и мы добавили в материал комментарии нашего эксперта @Philipp451 там, где можно что-то улучшить или применить альтернативный подход.

Запущенный на сервере OpenClaw решает большинство задач, которые пользователи ставят перед агентами. Для личного использования, параллельных запусков и несложной автоматизации его возможностей хватит с запасом. Одного VPS перестает хватать, когда приходят они: пиковые нагрузки.

В продакшене пиковые нагрузки у OpenClaw появляются раньше, чем можно ожидать. Параллельные сессии, тяжелые повторяющиеся cron-задачи, тяжелые операции с памятью и входящие сообщения от нескольких коннекторов одновременно… Gateway начинает трещать по швам, упирается в лимиты CPU и RAM, а отказоустойчивость стремительно начинает падать

Когда это случается, варианта остается два: подбросить в печь больше вычислительных мощностей или пересмотреть архитектуру. Если второй вариант вам ближе, то эта статья для вас. Сегодня мы разберем контейнеризацию в Docker, отказоустойчивый деплой через Kubernetes, а также управление stateful-хранилищем, без которого стабильный запуск нескольких инстансов невозможен.

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

Когда масштабирование — не оверхед?

Большинство персональных и небольших командных конфигураций прекрасно работают на одном сервере, а добавлять сложность оркестрации без реальной причины — просто множить объем обслуживания.

Как понять, что масштабирование назрело, и стоит прекратить пытаться нагружать единственный инстанс?

  • Нагрузка на CPU выше 80% при параллельных сессиях. Если шлюз упирается в процессор, когда активны две-три сессии одновременно, больше нет смысла мучать единственный инстанс.

  • Появляются OOM-киллы в системных логах. Сложные tool chains, индексация памяти, мультиагентный роутинг — все это может быстро исчерпать RAM. Если ОС убивает процесс шлюза, вы либо выросли из текущих ресурсов, либо пора распределять нагрузку.

  • Больше 10 одновременных сессий или много крон-задач. Шлюз изолирует сессии через внутренний механизм lane, но есть предел того, сколько сессий один процесс может тянуть без роста времени отклика

  • Требования к zero-downtime. Обновление systemd на одном инстансе вызывает кратковременный простой. Поэтому если в вашем сценарии перерывы недопустимы, нужны реплики и rolling-деплой.

  • Высокий поток входящих webhook-событий. Обработка сотни входящих событий в час быстро превращают одинокий gateway в бутылочное горлышко для трафика.

Контейнеризация OpenClaw с Docker

Пишем свой Dockerfile

OpenClaw поддерживает Docker «из коробки» и для него всегда доступны официальные Docker-образы openclaw/openclaw:latest. Но если хочется больше контроля, Dockerfile придется создавать под себя. В нашем случае, это позволит добавлять apt-пакеты, назначать пользователи с низкими привилегиями и добавлять кастомные шаги сборки.

Рабочая точка отсчета — Node 22 slim:

FROM node:22-bookworm-slim

RUN corepack enable && \
    curl -fsSL https://bun.sh/install | bash && \
    mv /root/.bun/bin/bun /usr/local/bin/

WORKDIR /app

# Кэшируем зависимости до копирования исходников (ускоряет пересборку)
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./
COPY ui/package.json ./ui/
RUN pnpm install --frozen-lockfile

COPY . .
RUN pnpm build && pnpm ui:build

ENV NODE_ENV=production
EXPOSE 18789 18793

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD node dist/index.js health || exit 1

CMD ["node", "dist/index.js"]

Примечане: Хелсчек в Dockerfile. CMD node dist/index.js health || exit 1 поднимает новый Node.js-процесс при каждой проверке. Это дорого, медленно и создает ощутимую нагрузку на рантайм — особенно если интервал проверки короткий. Лучше использовать легковесный HTTP-запрос к уже запущенному процессу, что, кстати, и сделано в манифесте Kubernetes через httpGet.

Что вы можете захотеть изменить под себя:

  • Если ваш сценарий включает медиа и требует установки ffmpeg и кастомных модулей, выполните RUN apt-get update && apt-get install -y ffmpeg build-essential перед COPY. Кэширование на этом слое позволит не пересобирать при каждом изменении кода.

  • Вы можете захотеть переключиться на непривилегированного пользователя. Привычка работать из root на частном сервере становится вредной, когда ваши агенты открыты для внешних сервисов. Для этого добавьте USER node перед CMD, и убедитесь, что пользователю после chown доступны все нужные папки томов.

  • Вам нужно сделать выбор между компактными Alpine-образами и Debian slim bookworm. Alpine могут конфликтовать с нативными Node-модулями, slim bookworm надежнее для продакшн-шлюза.

Три сервиса — один Compose

Для большинства деплоев нужны минимум три сервиса: gateway, управляющий контейнер для административных CLI-команд и реверс-прокси. Ниже Compose-файл для всех трех:

version: '3.8'

services:
  openclaw-gateway:
    image: openclaw/openclaw:latest
    container_name: openclaw-gateway
    restart: unless-stopped
    ports:
      - "18789:18789"
      - "18793:18793"
    volumes:
      - openclaw-config:/root/.openclaw
      - openclaw-workspace:/root/workspace
    environment:
      - NODE_ENV=production
      - OPENCLAW_STATE_DIR=/root/.openclaw
    command: openclaw gateway
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G
    healthcheck:
      test: ["CMD", "node", "dist/index.js", "health"]
      interval: 30s
      timeout: 10s
      retries: 3

  openclaw-cli:
    image: openclaw/openclaw:latest
    volumes_from: [openclaw-gateway]
    entrypoint: openclaw

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - certs:/etc/nginx/certs
    depends_on:
      - openclaw-gateway

volumes:
  openclaw-config:
  openclaw-workspace:
  certs:

Примечание: deploy.resources в Docker Compose. Секция deploy относится к Docker Swarm, в обычном Compose она не работает. Скорее всего, автор подразумевает запуск через docker compose --compatibility.

Обратите внимание на два именованных тома:

  • openclaw-config хранит все из ~/.openclaw: конфиг, учетные данные, активные сессии и расписание задач из cron/jobs.json.

  • В openclaw-workspace находятся MEMORY.md, директория memory/, инструменты и скиллы.

Оба тома должны переживать перезапуски контейнера и редеплои — эфемерное хранилище здесь не подходит.

Также мы подключаем сервис openclaw-cli. Он позволяет работать с теми же томами, которые использует шлюз, а значит — запускать административные CLI-команды через одноразовые процессы, не вмешиваясь в работу шлюза exec-запросами.

docker compose run --rm openclaw-cli channels login

docker compose run --rm openclaw-cli status --all

docker compose exec openclaw-gateway openclaw status

Для авторизации TTY добавьте:

docker compose run -it --rm openclaw-cli channels login

Изолируем агентов в песочницах

Песочницы однозначно заслуживают места в продакшн: при аномалиях tool-цепочки можно уменьшить «радиус взрыва» и локализовать ущерб. Sandbox в OpenClaw работает через изолированные субконтейнеры с инструментами и сессиями, которые координирует хост-шлюз. Настроить изоляцию можно в agents.defaults.sandbox.mode: non-main (изолировать всех агентов кроме главного) или all.

Sandbox-контейнеры монтируют директорию /workspace. По умолчанию они запускаются с network: none, с выборочными egress разрешениями для инструментов, которым необходим доступ в сеть. Неактивные песочницы удаляются каждые 24 часа, а с истекшим сроком — раз в 7 дней.

Примечание: песочницы и docker-in-docker. Автор описывает sandbox-контейнеры с сетевой изоляцией — отличный подход с точки зрения безопасности. Но такие песочницы часто запускаются через docker-in-docker (dind), а dind внутри Kubernetes — источник отдельного класса проблем: от конфликтов с cgroup до дыр в изоляции.

Деплой OpenClaw в Kubernetes

Docker файл собран и настало время выбрать способ оркестрации. Docker Compose хорош для одиночного хоста. Kubernetes — когда нужны несколько реплик, автоматический фейловер с переключением трафика, rolling-деплой и горизонтальное автомасштабирование.

Работа с Kubernetes приносит в продакшн весомую операционную сложность. Если OpenClaw живет на VPS и одного хорошо настроенного инстанса достаточно — оставайтесь там. Если у вас продакшен с SLA по аптайму — продолжаем готовиться к деплою.

Хранилище секретов и namespace

Для начала изолируем ресурсы в namespace. OpenClaw должен хранить логин-пароль и чувствительные данные в Secret, а не в ConfigMap и переменных окружения манифеста.

apiVersion: v1
kind: Namespace
metadata:
  name: openclaw

---
apiVersion: v1
kind: Secret
metadata:
  name: openclaw-secrets
  namespace: openclaw
type: Opaque
data:
  OPENCLAW_TELEGRAM_TOKEN: <base64-encoded>
  ANTHROPIC_API_KEY: <base64-encoded>
  DISCORD_BOT_TOKEN: <base64-encoded>

Base64-значения генерируются через echo -n 'your-value' | base64. В боевом кластере лучше использовать что-то вроде External Secrets Operator, чтобы тянуть секреты из нормального хранилища, а не зашивать значения прямо в манифесты.

Манифест деплоя

apiVersion: apps/v1
kind: Deployment
metadata:
  name: openclaw-gateway
  namespace: openclaw
spec:
  replicas: 3
  selector:
    matchLabels:
      app: openclaw-gateway
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: openclaw-gateway
    spec:
      containers:
      - name: gateway
        image: openclaw/openclaw:latest
        ports:
        - containerPort: 18789
        envFrom:
        - secretRef:
            name: openclaw-secrets
        volumeMounts:
        - name: config
          mountPath: /root/.openclaw
        - name: workspace
          mountPath: /root/workspace
        resources:
          limits:
            cpu: "2"
            memory: "2Gi"
          requests:
            cpu: "1"
            memory: "1Gi"
        livenessProbe:
          httpGet:
            path: /health
            port: 18789
          initialDelaySeconds: 30
          periodSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health
            port: 18789
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: config
        persistentVolumeClaim:
          claimName: openclaw-config-pvc
      - name: workspace
        persistentVolumeClaim:
          claimName: openclaw-workspace-pvc

Три запущенных реплики — разумный минимум для HA: если один под падает (на обслуживании или из-за сбоя), два других продолжают работать. Liveness-проба перезапускает контейнер, если шлюз перестал отвечать; readiness-проба убирает под из ротации балансировщика, пока тот реально не готов принимать трафик.

Персистентные тома

Вот тут многорепликовые деплои OpenClaw становятся интересными. Тома config и workspace требуют режима доступа ReadWriteMany, чтобы несколько подов могли монтировать их одновременно. Не все провайдеры хранилищ поддерживают RWX. Если в кластере нет storage class с поддержкой RWX, проще всего добавить NFS:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: openclaw-config-pvc
  namespace: openclaw
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-client
  resources:
    requests:
      storage: 10Gi

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: openclaw-workspace-pvc
  namespace: openclaw
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-client
  resources:
    requests:
      storage: 20Gi

Если вы используете продвинутые бэкенды памяти (QMD, Cognee, Mem0), каждому из этих сервисов лучше выделить собственные PVC и запускать их отдельными деплоями, а не пакетом вместе со шлюзом. Шлюзы подключаются к ним через внутренний DNS кластера: так хранилище остается разделенным, а масштабировать каждый компонент проще.

Service и Ingress

Увеличенные таймауты прокси важны: шлюз использует WebSocket для части коммуникаций между каналами, и дефолтные nginx-таймауты в 60 секунд будут обрывать эти соединения. Ставьте минимум 3600 секунд (один час).

apiVersion: v1
kind: Service
metadata:
  name: openclaw-service
  namespace: openclaw
spec:
  selector:
    app: openclaw-gateway
  ports:
  - name: gateway
    port: 18789
    targetPort: 18789
  type: ClusterIP

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: openclaw-ingress
  namespace: openclaw
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
  rules:
  - host: openclaw.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: openclaw-service
            port:
              number: 18789

Horizontal Pod Autoscaler

Когда в Deployment заданы запросы и лимиты ресурсов, HPA может масштабировать реплики вверх и вниз по загрузке CPU.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: openclaw-hpa
  namespace: openclaw
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: openclaw-gateway
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

70% загрузки CPU как порог масштабирования — консервативное значение, и это намеренно: лучше добавить мощности до того, как все упрется в потолок, а не после. Минимум в две реплики гарантирует, что при падении одного пода всегда останется запасной.

Защита от простоя: Pod Disruption Budget

При плановом обслуживании кластера Kubernetes может гасить поды. Pod Disruption Budget задает количество подов, которые всегда остаются в строю, и деплой на кластер проходит с нулевым простоем:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: openclaw-pdb
  namespace: openclaw
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: openclaw-gateway

При трех запущенных репликах и minAvailable: 2, из работы выводится не более одного пода. Сервис остается доступным во время обслуживания.

Общее состояние между репликами

Общее состояние между репликами — самая сложная часть мультиреплицированного деплоя. Gateway не является stateless — в нем хранятся сессии, cron-задачи, данные авторизации и память агентов. Три реплики, одновременно пишущие в один NFS-том — это состояние гонки трех процессов с непредсказуемым результатом.

Архитектура OpenClaw спроектирована вокруг модели с одним шлюзом. Общее хранилище через RWX PV работает достаточно хорошо для конфигов и учетных данных (их в основном читают, редко пишут) и для файлов памяти в workspace (они записываются последовательно отдельными сессиями). Место, которое требует внимания, — cron/jobs.json: при нескольких работающих шлюзах можно получить дублирование крон-задач, если все реплики следят за одним jobs-файлом и каждая независимо запускает выполнение.

Примечание: Главный редфлаг. Фраза «архитектура OpenClaw спроектирована вокруг модели с одним шлюзом» является ключевой для всей статьи — и одновременно главным редфлагом. Если система не проектировалась как мультиинстансная, попытки запустить ее в таком режиме — это архитектурно плохой паттерн. Все, что дальше предлагается — RWX-тома, shared NFS, Redis-локи — это костыли поверх фундамента, который не рассчитан на такую нагрузку.

RWX PVC + общий NFS + несколько подов — на мой взгляд, опасная комбинация. Это чревато состояниями гонки, непредсказуемыми задержками и трудновоспроизводимыми багами. Автор, к его чести, сам признает проблему с дублированием крон-задач и предлагает OPENCLAW_SKIP_CRON — но это ручное управление с плохим фейловером. Если крон-лидер упал, автоматического переключения нет.

Решений два.

  1. Назначить одну реплику крон-лидером — включить крон только на одном поде (OPENCLAW_SKIP_CRON=0), а остальным выставить пропуск (OPENCLAW_SKIP_CRON=1). Это проще, чем звучит: создайте отдельный Deployment для пода-крон-лидера с другим значением переменной окружения. Остальные реплики обрабатывают трафик каналов и сессии, а один под занимается планированием.

  2. Использовать Redis-сайдкар или внешний сервис блокировок для распределенной координации крона — сложнее, но чище при больших масштабах.

Бэкенды памяти (QMD, Cognee, Mem0) лучше запускать как выделенные сервисы внутри кластера. Каждый под шлюза подключается к одному и тому же эндпоинту сервиса памяти через внутренний DNS, поэтому результаты выборки одинаковы для всех реплик, независимо от того, какой под обрабатывает конкретную сессию.

Высокая доступность: health-чеки, rolling-обновления и фейловер

При правильной настройке деплоймента Kubernetes сам следит за здоровьем подов через liveness- и readiness-пробы, перезапускает и убирает их из ротации до того, как на них пойдет трафик. Rolling-обновления с maxUnavailable: 1 гарантируют, что минимум два пода обслуживают трафик во время деплоя. А PDB следит, чтобы операции обслуживания не погасили слишком много подов разом.

Kubernetes не поможет с фейловером на уровне каналов. Например, если у вас настроен токен Telegram-бота, все три реплики шлюза получат один и тот же токен, но Telegram доставит сообщения только на один активный вебхук-эндпоинт. Нужно убедиться, что Ingress или балансировщик настроен так, чтобы вебхук Telegram попадал на стабильный эндпоинт, маршрутизируемый на кластер, а не на IP конкретного пода. То же касается вебхуков Discord и колбэков WhatsApp.

Также есть нюансы с обновлением данных для авторизации. WhatsApp-сессии с авторизацией через QR привязаны к состоянию телефона и часто истекают. При этом нужно, чтобы данные обновлялись для всех подов, а не только того, где запущена сессия. Решение — использовать openclaw-cli для записи данных в общих том конфигурации.

Бэкапы в контейнеризованном сетапе

Именованные тома и PVC — не стратегия бэкапа. Они защищают от перезапуска контейнера, но не от повреждения хранилища, случайного удаления или катастрофы на уровне кластера. Для Docker Compose добавьте контейнер с кроном, который делает снапшоты томов в S3-совместимое хранилище или на удаленный сервер. Для Kubernetes стандартный инструмент — Velero: снапшоты PV и бэкапы состояния кластера.

Для Kubernetes стандартным инструментом для снапшотов PV и бэкапов состояния кластера является Velero. Принципы бэкапа в целом одинаковые для VPS и для K8s-кластера.

Антихрупкость. Автор описал, как запустить OpenClaw в кластере, но забыл несколько вещей, без которых продакшн-деплой будет хрупким:

  • Распределенное хранилище сессий. Вместо файлов на общем NFS — Redis. Сессии, блокировки, состояние — все это должно жить в быстром in-memory хранилище, а не на сетевой файловой системе с ее латентностью и рисками.

  • Очередь сообщений. При нескольких репликах входящие события от каналов нужно не раскидывать по подам через балансировщик, а складывать в очередь и обрабатывать упорядоченно.

  • Rate limiting. В статье ни слова о том, как защитить шлюз от перегрузки входящим трафиком — а при открытых вебхуках это вопрос времени.

  • Логирование и трейсинг. Для мультирепликового деплоя нужна централизованная система логов (ELK, Loki) и распределенный трейсинг (OpenTelemetry, Jaeger). Без них отладка инцидента на кластере из нескольких подов — гадание.

Стоит ли в это ввязываться?

Для большинства пользователей архитектура, которую мы обсуждали выше — оверкил. Грамотно настроенный VPS с systemd, мониторингом и бэкапами закроет подавляющее большинство реальных сценариев без операционной сложности Kubernetes.

Задумываться о контейнеризации/кластеризации стоит лишь когда вы упретесь в конкретные лимиты, в виде OOM-киллов, стабильно высокой нагрузки на CPU, даунтайм с реальными последствиями.