Building a Scalable Greeting Service with Temporal, FastAPI, Docker, and Traefik
In this post, we’ll walk through building a scalable greeting service using Temporal, a powerful orchestration framework, and FastAPI, a modern web framework for building APIs with Python. We’ll also containerize our application using Docker and set up a reverse proxy with Traefik for secure routing and load balancing. We’ll cover the code structure, key components, containerization, and how they work together to provide a robust and efficient service.
Project Structure
Here’s the structure of our project:
root
├── internal
│ ├── activity
│ │ └── name.py
│ ├── worker
│ │ ├── name.py
│ │ └── run.py
├── main.py
├── Dockerfile
└── docker-compose.yaml
Activities in Temporal
An activity in Temporal is a unit of work that can be executed independently. In our project, we define an activity to say hello in internal/activity/name.py.
from temporalio import activity
@activity.defn
async def say_hello(name: str) -> str:
return f"Hello {name}!"
Workflows in Temporal
A workflow in Temporal orchestrates the execution of activities. We define a workflow to use our say_hello activity in internal/worker/name.py.
from temporalio import workflow
from datetime import timedelta
from internal.activity.name import say_hello
@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=120)
)
Worker to Execute Workflows
Temporal workers are responsible for polling the Temporal server for tasks and executing workflows and activities. We set up a worker in internal/worker/run.py.
import asyncio
import concurrent.futures
from internal.activity.name import say_hello
from internal.worker.name import GreetingWorkflow
from temporalio.client import Client
from temporalio.worker import Worker
async def main() :
client = await Client.connect('localhost:7233')
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
worker = Worker(client, task_queue='name-task-queue', workflows=[GreetingWorkflow], activities=[say_hello], activity_executor=executor)
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
FastAPI Application
We use FastAPI to expose our greeting service via HTTP endpoints. The main application is defined in main.py.
import logging
from contextlib import asynccontextmanager
from pydantic import BaseModel
from fastapi import FastAPI
from temporalio.client import Client
from internal.worker.name import GreetingWorkflow
import uvicorn
log = logging.getLogger(__name__)
class NameRequest(BaseModel):
name: str
@asynccontextmanager
async def lifespan(app: FastAPI):
logging.info("Setting up temporal client")
app.state.temporal_client = await Client.connect('localhost:7233')
yield
app = FastAPI(lifespan=lifespan)
@app.get('/', status_code=200, response_model=dict)
def root():
return {"hello": "world"}
@app.post('/name', status_code=201, response_model=dict)
async def say_hello(request: NameRequest):
result = await app.state.temporal_client.execute_workflow(
GreetingWorkflow.run, request.name, id=f"name-workflow-{request.name}", task_queue='name-task-queue'
)
return {
"result": result
}
if __name__ == "__main__":
uvicorn.run("main:app", reload=True, port=8000)
Containerization with Docker
We’ll use Docker to containerize our application. The Dockerfile defines the build process for both the FastAPI application and the Temporal worker.
# Use an official Python runtime as a parent image
FROM python:3.11-slim as base
# Set the working directory in the container
WORKDIR /app
# Copy the current directory contents into the container at /app
COPY . .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Multi-stage build to separate FastAPI and Temporal worker
# FastAPI stage
FROM base as fastapi
# Expose the port that the FastAPI app runs on
EXPOSE 8000
# Command to run FastAPI application
CMD ["python", "main.py"]
# Temporal worker stage
FROM base as worker
# Command to run the Temporal worker
CMD ["python", "internal/worker/run.py"]
Docker Compose for Orchestration
We use Docker Compose to define and run multi-container Docker applications. Our docker-compose.yaml file sets up the FastAPI app, the Temporal worker, and the Traefik reverse proxy.
version: "3.8"
services:
reverse-proxy:
image: traefik:v3.0.2
container_name: "traefik"
command:
- --api.insecure=true
- --providers.docker=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.myresolver.acme.tlschallenge=true
- --certificatesresolvers.myresolver.acme.email=cimomof752@cnurbano.com
- --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- ./letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- traefik-net
fastapi:
build:
context: .
dockerfile: Dockerfile
target: fastapi
ports:
- "8000:8000"
volumes:
- .:/app
labels:
- "traefik.enable=true"
- "traefik.http.routers.fastapi.rule=Host(`fastapi.localhost.com`)"
- "traefik.http.routers.fastapi.entrypoints=websecure"
- "traefik.http.routers.fastapi.tls.certresolver=myresolver"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.redirs.entrypoints=web"
- "traefik.http.routers.redirs.middlewares=redirect-to-https"
networks:
- traefik-net
networks:
traefik-net:
external: true
Running the Application with Traefik
- Start the Temporal Server: Ensure that your Temporal server is running on
localhost:7233. - Build and Run the Containers: Use Docker Compose to build and start the containers.
docker-compose -f docker-compose.yaml up -d
Running the Application in local
Temporal Worker with FastAPI
create virtual environment
python -m venv .venv
source .venv/bin/activate
Install requirements
pip install -r requirements.txt
NOTE: TEMPORAL SHOULD BE INSTALLED IN THE VIRTUAL ENVIRONMENT
Run the temporal server in development mode
temporal server start-dev
Set Python Path and Run the Application
When running your scripts, make sure to set the PYTHONPATH so that Python can locate the internal module:
export PYTHONPATH=$(pwd)
Run the Temporal worker:
python internal/worker/run.py
Run the FastAPI application:
python main.py
Test the API
Test the API using curl or any HTTP client like Postman:
curl -X POST "http://127.0.0.1:8000/name" -H "Content-Type: application/json" -d '{"name": "Suresh"}'
You should receive a response like:
{
"result": "Hello Suresh!"
}
Check the workflows in the Temporal UI
http://localhost:8233/namespaces/default/workflows
Source Code: https://github.com/azar-writes-code/fastapi-traefik-temporal-poc
Conclusion
In this post, we’ve built a greeting service using Temporal for workflow orchestration, FastAPI for exposing our service via HTTP endpoints, Docker for containerization, and Traefik for reverse proxy and load balancing. This setup provides a scalable, secure, and reliable way to handle complex workflows in a microservices architecture. The combination of these technologies makes it a powerful solution for building modern web services.