Temporal es un motor de workflows duraderos: guarda el historial de cada ejecución en su base de datos, de modo que si un proceso se cae a mitad de un flujo, otro lo retoma exactamente donde se quedó, con reintentos automáticos en los pasos que fallan. Tu código de negocio corre en workers que tú despliegas, y el servidor de Temporal solo coordina y persiste el estado. En este tutorial instalarás la CLI de Temporal en Ubuntu 24.04, ejecutarás su servidor integrado como servicio de systemd con persistencia en disco, crearás un namespace y escribirás y ejecutarás un workflow en Python con reintentos.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
- Un usuario no root con privilegios
sudo. - Python 3.10 o superior (Ubuntu 24.04 incluye Python 3.12).
Notael servidor que instalarás aquí es el que integra la CLI de Temporal (
temporal server start-dev). Usa SQLite, corre en un solo proceso e incluye la interfaz web, lo que lo hace ideal para desarrollo, pruebas y entornos internos pequeños. Para producción con alta disponibilidad, Temporal recomienda desplegar el servidor con su chart de Helm sobre Kubernetes y una base de datos PostgreSQL, MySQL o Cassandra, o usar Temporal Cloud. El código de los workers y los comandos de la CLI de esta guía son los mismos en ambos casos.
Paso 1: Instalar la CLI de Temporal
La CLI de Temporal es un único binario que sirve tanto para administrar un clúster como para lanzar el servidor integrado. Descarga la última versión para Linux x86_64 desde el servidor de descargas oficial:
cd /tmp
curl -fLo temporal_cli.tar.gz "https://temporal.download/cli/archive/latest?platform=linux&arch=amd64"
tar xzf temporal_cli.tar.gz temporal
sudo install -m 0755 temporal /usr/local/bin/temporal
En servidores ARM64 cambia arch=amd64 por arch=arm64. Comprueba la instalación:
temporal --version
temporal version 1.4.1
Tus números de versión pueden ser distintos.
Paso 2: Ejecutar el servidor de Temporal como servicio
Sin opciones, temporal server start-dev guarda los datos en memoria y los pierde al reiniciar. Para conservarlos, crea un usuario de sistema y un directorio donde guardar la base de datos SQLite:
sudo useradd --system --home-dir /var/lib/temporal --create-home --shell /usr/sbin/nologin temporal
Crea la unidad de systemd:
sudo nano /etc/systemd/system/temporal.service
[Unit]
Description=Temporal development server
Wants=network-online.target
After=network-online.target
[Service]
User=temporal
Group=temporal
WorkingDirectory=/var/lib/temporal
ExecStart=/usr/local/bin/temporal server start-dev --db-filename /var/lib/temporal/temporal.db --ip 127.0.0.1 --port 7233 --ui-port 8233
Restart=on-failure
[Install]
WantedBy=multi-user.target
--db-filenameactiva la persistencia en un archivo SQLite.--ip 127.0.0.1hace que el servidor (puerto 7233, gRPC) y la interfaz web (puerto 8233) solo escuchen en local. El servidor no tiene autenticación en este modo, así que no lo expongas a Internet.
Recarga systemd y arranca el servicio:
sudo systemctl daemon-reload
sudo systemctl enable --now temporal
sudo systemctl status temporal
● temporal.service - Temporal development server
Loaded: loaded (/etc/systemd/system/temporal.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-25 11:02:14 UTC; 5s ago
Comprueba que el servidor responde:
temporal operator cluster health
SERVING
Paso 3: Crear un namespace
Un namespace es la unidad de aislamiento de Temporal: cada uno tiene sus propios workflows, colas y período de retención del historial. El servidor trae un namespace default, pero es buena práctica crear uno por aplicación. Crea el namespace pedidos con una retención de 7 días para el historial de los workflows terminados:
temporal operator namespace create --namespace pedidos --retention 168h
Namespace pedidos successfully registered.
Comprueba su configuración:
temporal operator namespace describe --namespace pedidos
La salida incluye el nombre, el estado Registered y la retención de 168 horas.
Paso 4: Preparar el proyecto Python
Los workflows y las actividades se escriben con el SDK de Temporal. Instala el soporte de entornos virtuales y crea el proyecto:
sudo apt update
sudo apt install python3-venv
mkdir -p ~/pedidos-temporal
cd ~/pedidos-temporal
python3 -m venv venv
./venv/bin/pip install temporalio
Comprueba que el paquete se ha instalado:
./venv/bin/pip show temporalio | head -n 2
Name: temporalio
Version: 1.18.0
Paso 5: Escribir las actividades
Una actividad es una función que hace trabajo con efectos externos: llamar a una API, escribir en una base de datos, enviar un email. Es la parte que puede fallar y que Temporal reintenta. Crea el archivo de actividades:
nano ~/pedidos-temporal/activities.py
import random
from dataclasses import dataclass
from temporalio import activity
from temporalio.exceptions import ApplicationError
@dataclass
class Pedido:
pedido_id: str
total: float
email: str
@activity.defn
async def cobrar_pago(pedido: Pedido) -> str:
if pedido.total <= 0:
# Un importe inválido no se arregla reintentando.
raise ApplicationError("Importe inválido", non_retryable=True)
# Simula una pasarela de pagos que falla la mitad de las veces.
if random.random() < 0.5:
raise RuntimeError("La pasarela de pagos no responde")
activity.logger.info("Pago cobrado para el pedido %s", pedido.pedido_id)
return f"pago-{pedido.pedido_id}"
@activity.defn
async def enviar_confirmacion(pedido: Pedido) -> None:
activity.logger.info("Confirmación enviada a %s", pedido.email)
cobrar_pago falla de forma aleatoria para que puedas ver los reintentos en acción. ApplicationError con non_retryable=True marca los errores que no tiene sentido reintentar.
Paso 6: Escribir el workflow
El workflow orquesta las actividades. Su código debe ser determinista: no puede llamar a redes, leer la hora del sistema ni usar números aleatorios directamente, porque Temporal lo vuelve a ejecutar para reconstruir su estado a partir del historial. Todo lo que no sea determinista va en actividades.
nano ~/pedidos-temporal/workflows.py
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
with workflow.unsafe.imports_passed_through():
from activities import Pedido, cobrar_pago, enviar_confirmacion
@workflow.defn
class ProcesarPedido:
@workflow.run
async def run(self, pedido: Pedido) -> str:
pago_id = await workflow.execute_activity(
cobrar_pago,
pedido,
start_to_close_timeout=timedelta(seconds=30),
retry_policy=RetryPolicy(
initial_interval=timedelta(seconds=1),
backoff_coefficient=2.0,
maximum_interval=timedelta(seconds=30),
maximum_attempts=10,
),
)
await workflow.execute_activity(
enviar_confirmacion,
pedido,
start_to_close_timeout=timedelta(seconds=30),
)
return pago_id
start_to_close_timeoutes el tiempo máximo de cada intento de la actividad. Es obligatorio definir al menos un timeout.- La
RetryPolicyreintenta el cobro hasta 10 veces, esperando 1, 2, 4, 8... segundos entre intentos, con un máximo de 30. Sin política explícita, Temporal reintenta indefinidamente. imports_passed_through()permite importar el módulo de actividades desde el sandbox en el que se ejecutan los workflows.
Paso 7: Crear el worker y lanzar un workflow
El worker es el proceso que escucha una cola de tareas (task queue) y ejecuta el código de workflows y actividades. Créalo:
nano ~/pedidos-temporal/worker.py
import asyncio
import logging
from temporalio.client import Client
from temporalio.worker import Worker
from activities import cobrar_pago, enviar_confirmacion
from workflows import ProcesarPedido
async def main() -> None:
logging.basicConfig(level=logging.INFO)
client = await Client.connect("127.0.0.1:7233", namespace="pedidos")
worker = Worker(
client,
task_queue="pedidos-queue",
workflows=[ProcesarPedido],
activities=[cobrar_pago, enviar_confirmacion],
)
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
Crea también un script que inicie un workflow y espere su resultado:
nano ~/pedidos-temporal/starter.py
import asyncio
import sys
from temporalio.client import Client
from activities import Pedido
from workflows import ProcesarPedido
async def main(pedido_id: str) -> None:
client = await Client.connect("127.0.0.1:7233", namespace="pedidos")
resultado = await client.execute_workflow(
ProcesarPedido.run,
Pedido(pedido_id=pedido_id, total=150.0, email="[email protected]"),
id=f"pedido-{pedido_id}",
task_queue="pedidos-queue",
)
print(f"Workflow completado: {resultado}")
if __name__ == "__main__":
asyncio.run(main(sys.argv[1]))
El id del workflow identifica la ejecución de forma única: Temporal no permite dos ejecuciones abiertas con el mismo ID, lo que evita procesar dos veces el mismo pedido.
Arranca el worker en una terminal:
cd ~/pedidos-temporal
./venv/bin/python worker.py
En una segunda terminal, lanza un workflow:
cd ~/pedidos-temporal
./venv/bin/python starter.py 1001
Workflow completado: pago-1001
En la terminal del worker verás advertencias con el error La pasarela de pagos no responde cuando la actividad falla, seguidas del cobro correcto en un reintento posterior. Detén el worker con Ctrl+C cuando termines.
Paso 8: Inspeccionar los workflows
La CLI permite consultar el estado y el historial completo de cualquier ejecución. Lista los workflows del namespace:
temporal workflow list --namespace pedidos
Status WorkflowId Type StartTime
Completed pedido-1001 ProcesarPedido 1 minute ago
Muestra el historial de eventos. Cada intento fallido de la actividad queda registrado, y el campo attempt del evento ActivityTaskStarted indica cuántos intentos hicieron falta:
temporal workflow show --workflow-id pedido-1001 --namespace pedidos
La interfaz web ofrece la misma información de forma visual. Como solo escucha en local, ábrela a través de un túnel SSH desde tu ordenador:
ssh -L 8233:127.0.0.1:8233 your_user@your_server_ip
Con el túnel abierto, visita http://localhost:8233 en tu navegador, selecciona el namespace pedidos y abre pedido-1001 para ver la línea de tiempo de las actividades y sus reintentos.
Paso 9: Ejecutar el worker como servicio
En un entorno real el worker debe arrancar solo y reiniciarse si falla. Crea una unidad de systemd que lo ejecute con tu usuario:
sudo nano /etc/systemd/system/pedidos-worker.service
[Unit]
Description=Temporal worker for pedidos
After=temporal.service
Wants=temporal.service
[Service]
User=your_user
WorkingDirectory=/home/your_user/pedidos-temporal
ExecStart=/home/your_user/pedidos-temporal/venv/bin/python worker.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Sustituye your_user por tu usuario. Activa el servicio y comprueba sus logs:
sudo systemctl daemon-reload
sudo systemctl enable --now pedidos-worker
journalctl -u pedidos-worker -n 20 --no-pager
Lanza otro workflow para confirmar que el worker del servicio lo procesa:
cd ~/pedidos-temporal
./venv/bin/python starter.py 1002
Workflow completado: pago-1002
Para comprobar la durabilidad, lanza un pedido nuevo y reinicia el worker mientras la actividad está reintentando (sudo systemctl restart pedidos-worker). El workflow continuará tras el reinicio sin repetir los pasos que ya habían terminado.
Solución de problemas
Failed client connect o connection refused en el puerto 7233. El servidor no está en marcha. Revisa sudo systemctl status temporal y journalctl -u temporal -n 50.
Namespace pedidos is not found. El namespace no existe en este servidor. Créalo con temporal operator namespace create --namespace pedidos. Si acabas de crearlo, espera unos segundos a que se propague.
El workflow se queda en Running y no avanza. No hay ningún worker escuchando la cola pedidos-queue en ese namespace. Comprueba los pollers con temporal task-queue describe --task-queue pedidos-queue --namespace pedidos; si la lista está vacía, arranca el worker.
Error de no determinismo (Nondeterminism error) tras cambiar el código. Has modificado la secuencia de actividades de un workflow que ya tenía ejecuciones abiertas. Termina esas ejecuciones con temporal workflow terminate --workflow-id <id> --namespace pedidos o usa el versionado de workflows del SDK para cambios en producción.
Conclusión
Tienes un servidor de Temporal persistente como servicio de systemd, un namespace propio y un workflow en Python que sobrevive a fallos de sus actividades y a reinicios del worker. Como siguientes pasos, puedes usar señales y queries para interactuar con workflows en ejecución, programar ejecuciones periódicas con temporal schedule create y, cuando pases a producción, desplegar el servidor con el chart de Helm oficial sobre PostgreSQL manteniendo el mismo código de workers.
