How do I deploy a FastAPI endpoint on Modal and put it behind a custom domain?
Platform as a Service (PaaS)

How do I deploy a FastAPI endpoint on Modal and put it behind a custom domain?

9 min read

Quick Answer: Deploy a FastAPI app on Modal by wrapping it with @modal.asgi_app() or @modal.fastapi_endpoint() in Python, then running modal deploy .... To put it behind a custom domain, front it with your own reverse proxy (e.g., Cloudflare, Nginx, API Gateway) that forwards traffic to the Modal endpoint URL and handles DNS, TLS, and routing.

Most teams hit the same wall: they can get a FastAPI service running on localhost in minutes, but actually deploying it on autoscaling GPU infrastructure and serving it from a clean, branded domain is where everything slows down. With Modal, the infrastructure part collapses to a few decorators and a modal deploy command. You still own your domain and DNS, but you no longer have to babysit Kubernetes or hand-roll autoscaling for spiky LLM or embedding workloads.

Key Benefits:

  • Fast from notebook to production: Turn a local FastAPI function into a horizontally scaling endpoint with @modal.asgi_app() and modal deploy instead of wiring up Docker, registries, and K8s.
  • Autoscaling without quotas: Let Modal’s AI-native runtime spin up containers only when requests hit your FastAPI endpoint and scale back to zero between spikes.
  • Keep your own domain: Expose the Modal-generated URL through your custom domain using any reverse proxy or edge provider (Cloudflare, Nginx, API Gateway) so users never see an internal infrastructure hostname.

Core Concepts & Key Points

ConceptDefinitionWhy it's important
FastAPI on ModalA FastAPI app served from a Modal container using @modal.asgi_app() or @modal.fastapi_endpoint() decorators.Lets you write “just Python” while Modal handles containers, autoscaling, and deployment.
Modal App & FunctionsAn modal.App groups functions, classes, and web endpoints; each @app.function runs in a containerized environment.Gives you a single deployable unit that can expose multiple endpoints, workers, and background jobs.
Custom domain via reverse proxyA DNS entry (e.g., api.mycompany.com) pointing to a reverse proxy that forwards HTTP traffic to the Modal endpoint URL.Keeps a stable, branded URL for your API while still gaining Modal’s elasticity and speed.

How It Works (Step-by-Step)

At a high level, you:

  1. Define a FastAPI app in Python and wrap it with Modal decorators.
  2. Run modal deploy to turn it into a scalable web endpoint with a Modal URL.
  3. Put a reverse proxy (or edge gateway) in front of that URL and point your custom domain to the proxy.

Let’s go through that in code, then talk about the custom domain setup patterns that work well in production.

1. Define your FastAPI endpoint on Modal

First, install dependencies locally:

pip install "modal==1.*" "fastapi[standard]"

Now create fastapi_modal_app.py:

import modal

# Build the runtime image: Python + FastAPI
image = (
    modal.Image.debian_slim()
    .pip_install("fastapi[standard]")
)

app = modal.App("fastapi-on-modal", image=image)


@app.function()
@modal.asgi_app()
def fastapi_app():
    # Define the FastAPI app inside the function so Modal
    # can import it safely in the container.
    from fastapi import FastAPI

    web_app = FastAPI()

    @web_app.get("/health")
    async def health():
        return {"status": "ok"}

    @web_app.get("/echo")
    async def echo(msg: str = "hello"):
        return {"echo": msg}

    return web_app

What this does:

  • image = modal.Image.debian_slim().pip_install("fastapi[standard]") defines the container environment. This will be cached and reused across containers.
  • app = modal.App("fastapi-on-modal", image=image) creates a named Modal app. You’ll see this in the Modal dashboard.
  • @app.function() declares fastapi_app as a function that runs inside a Modal container.
  • @modal.asgi_app() tells Modal this function returns an ASGI app; Modal will mount it behind an HTTPS endpoint for you.

If you want a single-route endpoint instead of a full FastAPI app, you can also use @modal.fastapi_endpoint() on a function, like in the docs snippet:

@app.function(image=image)
@modal.fastapi_endpoint()
async def hello(name: str = "world"):
    return {"message": f"Hello, {name}!"}

But for anything non-trivial (multiple routes, middleware, auth), use the @modal.asgi_app() pattern.

2. Test locally on Modal’s runtime

Before you deploy, run it via the CLI from your repo root:

modal run fastapi_modal_app.py::fastapi_app

This:

  • Spins up a container with your Image.
  • Starts the ASGI app inside that container.
  • Prints a temporary URL where your FastAPI app is reachable.

You can hit the /health and /echo endpoints with curl or a browser to confirm behavior:

curl "$(modal run fastapi_modal_app.py::fastapi_app)/health"
curl "$(modal run fastapi_modal_app.py::fastapi_app)/echo?msg=hi"

If anything is off (import errors, missing deps), fix it now. Fixing it here is cheaper than debugging a deployed endpoint.

3. Deploy the FastAPI endpoint on Modal

Once you’re satisfied, deploy:

modal deploy fastapi_modal_app.py

or, if you prefer module paths:

modal deploy -m fastapi_modal_app

Deployment details:

  • Modal builds the Image (if needed), registers the app, and exposes the ASGI app as a permanent HTTPS endpoint.

  • The CLI prints the endpoint URL. It will look something like:

    https://fastapi-on-modal--fastapi-app.modal.run
    
  • You can always retrieve the app and function in other code via:

    import modal
    
    fastapi_app = modal.App.from_name("fastapi-on-modal").m.functions["fastapi_app"]
    

To tail logs for the deployed app:

modal app logs fastapi-on-modal

Use Ctrl+C to stop streaming logs.

4. Put the endpoint behind a custom domain

Modal gives you a stable HTTPS URL, but you likely want something like api.mycompany.com. Since Modal doesn’t (today) manage your DNS records, the pattern is:

  • Keep Modal as your origin server.
  • Put your own reverse proxy / gateway between your users and Modal.
  • Map your custom domain → proxy → Modal endpoint URL.

There are several ways to do this; the mechanics are the same:

  1. DNS: Create a DNS record for your custom domain pointing to your proxy.
  2. TLS: Terminate HTTPS at the proxy using a certificate for your domain.
  3. Proxy: Forward incoming traffic to the Modal origin URL.

Below are three typical setups.

Option A: Cloudflare as reverse proxy (common, simple)

  1. Point DNS to Cloudflare

    • Add your domain to Cloudflare.

    • Create a CNAME record:

      • Name: api
      • Target: fastapi-on-modal--fastapi-app.modal.run.
        (Your actual endpoint hostname from modal deploy.)
    • Keep “Proxied” enabled so Cloudflare sits in the middle.

  2. Configure HTTPS and routes

    • In Cloudflare, ensure SSL/TLS is set to “Full” or “Full (strict)” so Cloudflare talks HTTPS to Modal.
    • Create a route rule if you want path-level behavior (e.g., only proxy /v1/*).
  3. Optional: Restrict origin access

    If you don’t want anyone to hit the Modal URL directly:

    • Configure Cloudflare to send an extra header (e.g., X-Origin-Key: <secret>) to Modal.
    • In your FastAPI app, check that header in a middleware and reject requests that don’t have it.

    Example middleware:

    from fastapi import FastAPI, Request, HTTPException
    
    web_app = FastAPI()
    
    @web_app.middleware("http")
    async def verify_origin_key(request: Request, call_next):
        if request.headers.get("x-origin-key") != "my-shared-secret":
            raise HTTPException(status_code=403, detail="Forbidden")
        return await call_next(request)
    

This gives you https://api.mycompany.com/... as the public face, with Modal scaling in the background.

Option B: Nginx reverse proxy (self-hosted)

If you already run Nginx (on a VM, container, or edge), add a server block:

server {
    listen 80;
    server_name api.mycompany.com;

    # Redirect HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.mycompany.com;

    ssl_certificate     /etc/letsencrypt/live/api.mycompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.mycompany.com/privkey.pem;

    location / {
        proxy_pass https://fastapi-on-modal--fastapi-app.modal.run;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Then:

  • Point api.mycompany.com DNS to your Nginx box.
  • Use Let’s Encrypt to keep certificates updated.
  • Nginx forwards all traffic to Modal’s HTTPS origin.

Option C: API Gateway / Load Balancer

If you’re already on a cloud provider:

  • AWS API Gateway or ALB:
    • Create an HTTP API that forwards to the Modal HTTPS URL as the “origin.”
    • Attach a custom domain to the API via ACM certificates.
    • Create a DNS record (Route 53) pointing api.mycompany.com to the API Gateway domain.

Mechanically:

  • Inbound: https://api.mycompany.com/...
  • Gateway: TLS termination + routing, optional auth, rate limiting.
  • Outbound: HTTPS to https://fastapi-on-modal--fastapi-app.modal.run.

This is overkill if you don’t already have AWS infra, but useful if you want to layer auth, WAF, or per-tenant routing before your Modal app.

Common Mistakes to Avoid

  • Forgetting to define the Image explicitly:
    If you don’t pin dependencies in your modal.Image, you’ll get “works on my machine” behavior. Use modal.Image.debian_slim().pip_install("fastapi[standard]", "your-lib==1.2.3") and keep your versions tight.

  • Using the wrong decorator for your use case:
    Use @modal.asgi_app() for a full FastAPI app with multiple routes. Use @modal.fastapi_endpoint() for a single endpoint. Mixing patterns leads to confusing routing when you try to mount additional paths.

  • Terminating TLS twice incorrectly:
    When using a reverse proxy, TLS terminates at the proxy; it then usually re-encrypts to Modal. Don’t try to run plain HTTP to Modal—Modal endpoints are HTTPS. Ensure your proxy is configured as HTTPS → HTTPS with correct SNI/hostname.

  • Not checking logs after deploy:
    If your FastAPI app throws during startup, you might see 5xx from the proxy and assume the proxy is broken. Always check modal app logs <app_name> first; that’s where import errors and startup failures show up.

Real-World Example

Let’s say you’re shipping an LLM-powered text summarization API that must:

  • Handle sudden spikes from evals or batch summarization jobs.
  • Serve from api.docsummarizer.com with a simple /summarize POST.
  • Run on GPUs when you move to heavier models, but you don’t want to re-architect the plumbing.

You set up the FastAPI app on Modal:

import modal

image = (
    modal.Image.debian_slim()
    .pip_install(
        "fastapi[standard]",
        "transformers==4.39.0",
        "torch==2.2.0",
    )
)

app = modal.App("summarizer-api", image=image)


@app.function()
@modal.asgi_app()
def fastapi_app():
    from fastapi import FastAPI
    from pydantic import BaseModel
    from transformers import pipeline

    web_app = FastAPI()

    summarizer = pipeline("summarization", model="facebook/bart-large-cnn")

    class SummarizeRequest(BaseModel):
        text: str
        max_length: int = 128

    @web_app.post("/summarize")
    async def summarize(req: SummarizeRequest):
        result = summarizer(req.text, max_length=req.max_length, min_length=30, do_sample=False)
        return {"summary": result[0]["summary_text"]}

    return web_app

Then:

  1. Deploy with:

    modal deploy summarizer_app.py
    
  2. Get the Modal endpoint:

    https://summarizer-api--fastapi-app.modal.run
    
  3. Add a Cloudflare CNAME for api.docsummarizer.com pointing to that hostname.

  4. Set SSL/TLS mode to “Full (strict)” so Cloudflare talks HTTPS to Modal.

  5. (Optional) Add a middleware that validates a X-Origin-Key header from Cloudflare.

Now you have:

  • A branded, HTTPS-secured https://api.docsummarizer.com/summarize endpoint.
  • Modal handling cold starts, autoscaling, and GPU capacity when you upgrade the model.
  • No servers to patch or scale manually as usage spikes.

Pro Tip: Use a dedicated Modal app per critical API (modal.App("summarizer-api"), modal.App("embedding-api"), etc.) so you can deploy, roll back, and inspect logs independently. Combine that with infrastructure-as-code for your proxy (Cloudflare rules, Nginx config) so you can keep endpoint routing, TLS, and Modal deployment in one Git review.

Summary

Deploying a FastAPI endpoint on Modal is mostly about writing normal FastAPI code and wrapping it with Modal’s decorators and Image definitions. You define the environment in Python, run modal deploy, and Modal gives you a scalable HTTPS endpoint backed by an AI-native runtime with fast autoscaling and sub-second cold starts. To put that endpoint behind a custom domain, you front it with a reverse proxy or API gateway and point your DNS at that proxy, treating the Modal URL as a secure origin.

This split of responsibilities works well in practice: Modal handles the hard runtime problems (autoscaling, GPU capacity, container startup), while you keep full control over DNS, TLS, and routing at the edge.

Next Step

Get Started