MAATRIX / Блог / Temporal в Docker Compose: готовый файл

Temporal в Docker Compose: готовый файл

MAATRIX

Микросервисная архитектура рано или поздно упирается в один и тот же вопрос: как гарантированно довести до конца процесс, растянутый на часы или дни и проходящий через десяток сервисов — с ретраями, таймаутами и падениями по дороге. Cron-скрипты и очереди с ручной логикой повторов такое не тянут — состояние теряется, а отладка превращается в чтение логов пяти сервисов одновременно. Temporal решает именно эту задачу: хранит полное состояние workflow и переживает падение любого компонента. Ниже — рабочий docker-compose.yml, чтобы поднять кластер на своём сервере за десять минут и без сюрпризов.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое Temporal и зачем ему отдельный сервер

Temporal — платформа для durable execution: вы пишете обычный код на Go, Python, Java, TypeScript или .NET, а Temporal берёт на себя сохранение состояния, ретраи, таймауты и восстановление после сбоев. Workflow может выполняться неделями — если воркер упадёт посреди выполнения, при перезапуске он продолжит с того места, где остановился, а не с начала.

Технически Temporal — это отдельный кластер сервисов (frontend, history, matching, worker), которые опираются на СУБД для хранения состояния workflow и истории событий. В проде это Cassandra, MySQL или PostgreSQL, опционально плюс Elasticsearch для расширенного поиска. Для локальной разработки и небольших продакшн-нагрузок вариант с PostgreSQL — самый практичный: одна база, минимум движущихся частей, простое резервное копирование.

Важный момент: Temporal-сервер живёт постоянно и держит открытые gRPC-соединения с воркерами. Гонять его на хостинге с холодным стартом бессмысленно — нужен обычный VPS с постоянно работающими контейнерами и статическим IP.

Готовый docker-compose.yml

Ниже — минимальный, но полностью рабочий стек: PostgreSQL для хранения состояния, сам Temporal-сервер в режиме auto-setup (сам накатывает схему БД и создаёт namespace default при первом запуске), контейнер admin-tools для CLI-команд и веб-интерфейс Temporal UI.

version: "3.8"

networks:
  temporal-network:
    driver: bridge

volumes:
  temporal-postgres-data:

services:
  postgresql:
    image: postgres:15
    container_name: temporal-postgresql
    restart: unless-stopped
    environment:
      POSTGRES_USER: temporal
      POSTGRES_PASSWORD: change_me_strong_password
    volumes:
      - temporal-postgres-data:/var/lib/postgresql/data
    networks:
      - temporal-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U temporal"]
      interval: 5s
      timeout: 5s
      retries: 10

  temporal:
    image: temporalio/auto-setup:latest
    container_name: temporal
    restart: unless-stopped
    depends_on:
      postgresql:
        condition: service_healthy
    environment:
      - DB=postgresql
      - DB_PORT=5432
      - POSTGRES_USER=temporal
      - POSTGRES_PWD=change_me_strong_password
      - POSTGRES_SEEDS=postgresql
      - DYNAMIC_CONFIG_FILE_PATH=config/dynamicconfig/development-sql.yaml
    ports:
      - "7233:7233"
    networks:
      - temporal-network

  temporal-admin-tools:
    image: temporalio/admin-tools:latest
    container_name: temporal-admin-tools
    depends_on:
      - temporal
    environment:
      - TEMPORAL_ADDRESS=temporal:7233
      - TEMPORAL_CLI_ADDRESS=temporal:7233
    networks:
      - temporal-network
    stdin_open: true
    tty: true

  temporal-ui:
    image: temporalio/ui:latest
    container_name: temporal-ui
    restart: unless-stopped
    depends_on:
      - temporal
    environment:
      - TEMPORAL_ADDRESS=temporal:7233
      - TEMPORAL_CORS_ORIGINS=http://localhost:3000
    ports:
      - "8080:8080"
    networks:
      - temporal-network

Пара вещей на заметку:

  • Пароль от PostgreSQL замените на реальный до первого запуска — менять его после инициализации базы неудобно.
  • Тег latest подходит для знакомства с системой, но перед продакшном зафиксируйте конкретные версии temporalio/auto-setup, temporalio/admin-tools и temporalio/ui — актуальные теги смотрите на Docker Hub, версии обновляются часто и не всегда совместимы между собой.
  • DYNAMIC_CONFIG_FILE_PATH со значением development-sql.yaml — конфигурация для разработки с ослабленными лимитами. Для прода её стоит проработать отдельно, ориентируясь на официальный docker-compose репозиторий Temporal.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Запуск и проверка кластера

Поднимаем стек:

docker compose up -d
docker compose logs -f temporal

Первый запуск auto-setup-образа занимает 30–60 секунд — он ждёт готовности PostgreSQL, накатывает SQL-схему для всех namespace'ов Temporal (default, visibility) и создаёт namespace default. В логах должна появиться строка о старте frontend-сервиса без ошибок подключения к БД.

Проверить, что кластер живой, можно через admin-tools — там уже установлен tctl/temporal CLI:

docker compose exec temporal-admin-tools tctl namespace list

Если в выводе есть default — сервер поднят и готов принимать workflow. UI открывается по адресу http://<ip-сервера>:8080 — список workflow, статус, история событий и стек вызовов при ошибках. Порт 8080 стоит закрыть от интернета (firewall или туннель), а не выставлять напрямую — UI не рассчитан на прямую публикацию без дополнительной аутентификации. Если всё же нужен HTTPS перед UI, проще всего поставить nginx или Caddy реверс-прокси и получить сертификат — общая механика разобрана в статье про проблемы с Let's Encrypt.

Первый воркер и workflow

Temporal-сервер сам по себе ничего не выполняет — он только хранит состояние и раздаёт задачи воркерам. Воркер — это ваш процесс, который подключается к серверу, забирает задачи из task queue и исполняет код workflow и activity. Пример на Python SDK (pip install temporalio):

import asyncio
from datetime import timedelta

from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.worker import Worker


@activity.defn
async def say_hello(name: str) -> str:
    return f"Hello, {name}!"


@workflow.defn
class GreetingWorkflow:
    @workflow.run
    async def run(self, name: str) -> str:
        return await workflow.execute_activity(
            say_hello,
            name,
            start_to_close_timeout=timedelta(seconds=10),
        )


async def main():
    client = await Client.connect("localhost:7233", namespace="default")
    worker = Worker(
        client,
        task_queue="hello-task-queue",
        workflows=[GreetingWorkflow],
        activities=[say_hello],
    )
    await worker.run()


if __name__ == "__main__":
    asyncio.run(main())

Запускаете этот скрипт на том же сервере (или на любой машине, у которой есть сеть до порта 7233), и он начинает слушать hello-task-queue. Запустить сам workflow можно через CLI из admin-tools:

docker compose exec temporal-admin-tools \
  tctl workflow start \
  --taskqueue hello-task-queue \
  --workflow_type GreetingWorkflow \
  --input '"World"'

Результат и полная история выполнения появятся в UI на вкладке этого workflow — по клику видно каждый шаг: когда стартовала activity, сколько заняла, был ли ретрай. Это и есть главное преимущество Temporal перед самописной очередью с ретраями — вся история операций видна без раскопок в логах.

Если workflow должен реагировать на внешние события через HTTP (вебхуки от платёжных систем, колбэки от внешних API), логику приёма и валидации таких запросов удобно вынести в отдельный сервис перед воркером — типовые грабли с CORS, таймаутами и порядком запуска сервисов для похожего сценария разобраны в статье про n8n и обработку вебхуков.

Ресурсы сервера и хранилище

Compose-стек Temporal — это четыре постоянно работающих контейнера плюс СУБД. Ориентировочно для разработки и небольшой продакшн-нагрузки хватает VPS с 2 vCPU и 4 ГБ RAM — это именно ориентир, реальное потребление зависит от количества активных workflow и длины истории событий. На больших объёмах узким местом становится PostgreSQL.

Что стоит держать в голове с самого начала:

КомпонентНа что влияетРекомендация
PostgreSQLХранение состояния и истории workflowОтдельный диск/volume, регулярный бэкап, pg_isready healthcheck обязателен
Retention periodСколько закрытые workflow видны в UIЗадаётся в namespace, по умолчанию несколько дней — увеличивать с оглядкой на размер БД
Elasticsearch (опционально)Расширенный поиск по атрибутам workflowНужен только при сложных запросах в UI/API; для простого списка workflow достаточно visibility-таблиц в Postgres
Task queueБалансировка задач между воркерамиОдин task queue — один пул воркеров; разносите по queue независимые типы работы

Если стек Temporal — часть большего продакшн-окружения на том же сервере, общие принципы устойчивого Docker Compose — лимиты памяти, healthcheck'и, restart-политики — разобраны в статье про production-конфигурацию Docker Compose. А если параллельно крутятся dev- и staging-копии Temporal, чтобы не плодить дублирующиеся compose-файлы, посмотрите на профили Docker Compose.

Для мониторинга кластера — метрики frontend/history/matching, задержки истории, состояние очередей — Temporal отдаёт Prometheus-метрики из коробки. Логи контейнеров удобно завести в общую систему по схеме из статьи про Grafana Loki в Docker Compose.

Типичные проблемы при развёртывании

rpc error: code = Unavailable desc = connection error при первом запуске воркера или CLI-команды — сервер ещё не поднялся или PostgreSQL не готов. Убедитесь, что depends_on настроен именно на condition: service_healthy — обычный depends_on без healthcheck ждёт только запуска процесса базы, а не её готовности.

UI открывается, но список workflow пуст, хотя воркер их точно запускал. Чаще всего дело в namespace — воркер и клиент подключены к default, а в UI выбран другой, или наоборот. Проверьте namespace явно в коде клиента и в выпадающем списке UI.

После обновления temporalio/auto-setup сервер не стартует с ошибкой про версию схемы. Auto-setup катит миграции только вперёд и только при совместимых версиях. Если перепрыгнули через несколько мажорных релизов, накатите промежуточные или запустите миграцию вручную через temporal-sql-tool из admin-tools. Перед любым обновлением — снапшот volume с PostgreSQL.

Порт 7233 или 8080 уже занят на сервере. Поменяйте маппинг в секции ports (например, 7233:723317233:7233) и укажите новый адрес в переменной подключения клиента.

База PostgreSQL растёт быстрее, чем ожидали. Temporal хранит полную историю событий каждого workflow до истечения retention period namespace'а. Уменьшите retention там, где длинная история не нужна:

docker compose exec temporal-admin-tools \
  tctl namespace update default --retention 3

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Чем Temporal отличается от очереди задач вроде Kafka или RabbitMQ?

Очередь доставляет сообщение один раз и забывает о нём — логика повторов и состояния остаётся на вашей стороне. Temporal хранит полное состояние workflow и сам управляет ретраями и восстановлением после падений воркеров. Это не взаимоисключающие инструменты: очередь часто остаётся транспортом, а Temporal — слоем оркестрации поверх него. Разницу в архитектуре удобно посмотреть в статье про Kafka в Docker Compose.

Можно ли обойтись без Elasticsearch?

Да, для базового сценария — списка workflow, поиска по ID, статусу и времени запуска — хватает visibility-таблиц в PostgreSQL. Elasticsearch нужен, только если требуется поиск по произвольным пользовательским атрибутам (search attributes) со сложными фильтрами.

Нужен ли отдельный сервер под Temporal или можно на одной машине с остальным бэкендом?

Для разработки и небольшой нагрузки — можно на одной машине. Для продакшна с заметной нагрузкой стоит выносить PostgreSQL под Temporal на отдельный диск, чтобы история workflow не конкурировала за I/O с основной бизнес-базой.

Что будет, если контейнер с воркером упадёт посреди выполнения workflow?

Ничего страшного — состояние хранится на сервере Temporal, а не в памяти воркера. При перезапуске workflow продолжится с последней сохранённой точки, а незавершённая activity будет повторена согласно политике ретраев.

Как обновлять Temporal без даунтайма?

Обновляйте образы по одному, начиная с admin-tools и ui (без состояния, рестарт безопасен), затем auto-setup-контейнер сервера — он сам применит миграции схемы. PostgreSQL обновляйте отдельно, штатной процедурой major-upgrade, с обязательным бэкапом перед началом.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →