Building FastAPI Microservices That Run Anywhere: Lambda, Docker, or Localhost
Hook
Most FastAPI templates lock you into one deployment strategy. This one lets you develop locally with uvicorn, containerize for Kubernetes, and deploy the same code to AWS Lambda—without changing a single line.
Context
FastAPI has become the go-to framework for building Python APIs, but production deployment remains fragmented. You either build for traditional containerized hosting (uvicorn/gunicorn on EC2/ECS/Kubernetes) or serverless Lambda functions, rarely both. This creates painful decisions early in projects: choose serverless for cost efficiency and auto-scaling, and you're locked into AWS Lambda with its cold starts and 15-minute execution limits. Choose containers for flexibility, and you're managing infrastructure and paying for idle capacity.
The tfpgh/fastapi-microservice-template emerged from this deployment dilemma. It's an opinionated starter that treats deployment targets as implementation details rather than architectural constraints. By combining FastAPI with Mangum (an ASGI-to-Lambda adapter), Serverless Framework for infrastructure-as-code, and Docker for containerization, the template provides escape hatches at every level. Start serverless, migrate to containers later. Develop locally without mocking AWS services. Deploy to Lambda today, ECS tomorrow, without refactoring your route handlers.
Technical Insight
The architectural trick enabling tri-modal deployment is FastAPI's ASGI foundation combined with the Mangum adapter. FastAPI applications are ASGI apps—they expect HTTP events conforming to the ASGI specification. Uvicorn speaks ASGI natively during local development. Docker containers run uvicorn (or gunicorn wrapping uvicorn workers) in production. But AWS Lambda doesn't speak ASGI—it sends events as JSON payloads. Mangum bridges this gap by translating Lambda's API Gateway events into ASGI format and FastAPI responses back to Lambda-compatible JSON.
Here's what the main application entry point looks like:
from fastapi import FastAPI
from mangum import Mangum
from app.routers import example_router
from app.middleware import setup_middleware
app = FastAPI(
title="My Microservice",
description="Production-ready API template",
version="1.0.0"
)
# Add routers
app.include_router(example_router.router, prefix="/api/v1")
# Setup middleware (CORS, logging, etc.)
setup_middleware(app)
# Lambda handler - only used in serverless deployment
handler = Mangum(app)
# For local/container deployment, uvicorn runs 'app' directly
# For Lambda deployment, handler is the entry point
Notice the dual nature: the app object is a standard FastAPI application that uvicorn can serve. The handler wraps that same app for Lambda. Your route handlers remain deployment-agnostic:
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from app.stores.example_store import ExampleStore
router = APIRouter()
class Item(BaseModel):
name: str
description: str | None = None
price: float
@router.post("/items", response_model=Item)
async def create_item(
item: Item,
store: ExampleStore = Depends()
) -> Item:
# Business logic doesn't know or care about deployment target
created_item = await store.save(item)
return created_item
The template enforces clean separation through repository-pattern stores (abstracting data access), dependency injection via FastAPI's Depends(), and Pydantic models for validation. This isn't just good architecture—it's essential for portability. When you move from Lambda to containers, your database connections, authentication middleware, and business logic remain identical.
Environment configuration showcases another thoughtful decision: .env files for local development, environment variables for containers, and AWS Systems Manager Parameter Store (or similar) for Lambda. The template uses python-dotenv to load .env locally, but environment variables override everything, giving you consistent configuration patterns:
from pydantic import BaseSettings
class Settings(BaseSettings):
app_name: str = "FastAPI Microservice"
database_url: str
api_key: str
class Config:
env_file = ".env" # Used locally, ignored in Lambda/containers
case_sensitive = False
settings = Settings()
The Serverless Framework configuration (serverless.yml) handles Lambda-specific concerns—function memory allocation, API Gateway routing, IAM permissions, and the critical serverless-python-requirements plugin. This plugin compiles Python dependencies inside a Docker container matching Lambda's execution environment (Amazon Linux 2), ensuring binary compatibility for packages with C extensions:
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
layer: true # Puts dependencies in Lambda layer for faster deploys
Developer tooling integration sets this template apart from minimal examples. Pre-commit hooks enforce Black formatting, isort import sorting, mypy type checking, and flake8 linting before every commit. The template includes mypy configuration for strict type checking, catching type errors that would otherwise surface in production. This toolchain isn't optional decoration—it's infrastructure for maintaining code quality as teams grow and the microservice evolves.
Gotcha
The template's Python 3.9 lockfile is its most glaring limitation. Python 3.9 reached end-of-life for security updates in October 2025, and you're missing structural pattern matching (3.10), improved error messages (3.11), and significant performance gains (3.11+). Upgrading isn't trivial because AWS Lambda's Python 3.9 runtime must match your local development environment, and changing the Serverless Framework configuration to Python 3.11+ requires verifying all dependencies work with newer runtimes. For new projects, fork the template and update runtime versions immediately.
AWS Lambda cold starts remain unavoidable. The template can't fix FastAPI's import time (loading the framework, parsing routes, initializing dependencies), which adds 1-3 seconds to cold start latency. If your API sits behind user-facing web requests requiring sub-200ms response times, this deployment strategy fails. Lambda cold starts trigger after periods of inactivity—exactly when users first hit your service. Provisioned concurrency mitigates this but eliminates serverless cost savings. The template works best for internal APIs, webhook handlers, scheduled jobs, and backend services where occasional cold starts are acceptable trade-offs for auto-scaling and pay-per-use pricing.
The Docker-based dependency packaging via serverless-python-requirements introduces deployment friction. Each deployment requires spinning up a Docker container, installing all requirements, and zipping the package. With large dependency trees (data science libraries, image processing, etc.), this process takes minutes. The layer caching helps on subsequent deploys, but initial setup and major dependency updates remain slow. Teams accustomed to instant deploys with compiled languages or smaller frameworks will find this frustrating.
Verdict
Use if: You're building API microservices where deployment flexibility matters more than absolute performance—internal tools, webhook processors, CRUD services, or projects where you want serverless economics with the escape hatch to containers if Lambda constraints become painful. Also ideal if your team values strong typing, comprehensive linting, and battle-tested FastAPI patterns over bleeding-edge Python features. Skip if: You need consistent sub-200ms latency (Lambda cold starts kill this), require Python 3.10+ features like pattern matching, prefer GCP/Azure over AWS, or you're building a monolithic API where the microservice structure adds unnecessary complexity. Also skip if your dependencies include heavy compiled libraries—Lambda's 250MB unzipped package limit and slow Docker-based packaging make this painful.