2025, Dec 29 17:00

FastAPI behind Nginx at a subpath: why StaticFiles 404 and how to fix it with Uvicorn --root-path

Static assets 404 when serving FastAPI behind Nginx at /my_app? Learn how root_path double-prefixes StaticFiles mounts and fix it via Uvicorn --root-path.

Serving a FastAPI app behind Nginx at a subpath often looks trivial until static assets suddenly vanish. A common setup works locally and inside Docker, yet stylesheets and scripts 404 when proxied through Nginx. The root cause in this case sits in how the application-level root_path interacts with mounted routes like StaticFiles.

Problem setup

The app is mounted under /my_app, uses StaticFiles for assets, and renders Jinja2 templates. Locally and in Docker, everything renders. Through Nginx, the HTML loads but styles.css is missing.

from fastapi import Request, FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import os.path as p
BASE_DIR = p.abspath(p.join(__file__, "../"))
web = FastAPI(title="my_app", root_path="/my_app")
web.mount("/static_stuff", StaticFiles(directory=f"/{BASE_DIR}/static_stuff"), name="static")
views = Jinja2Templates(directory=f"/{BASE_DIR}/templates")
@web.get("/items/{item_id}", response_class=HTMLResponse, include_in_schema=False)
async def get_item(req: Request, item_id: str):
    return views.TemplateResponse(
        request=req, name="item.html", context={"id": item_id}
    )

Nginx forwards /my_app to the backend.

location /my_app {
    proxy_pass        http://my_host:6543;
    include           proxy_params;
}

Direct requests to http://my_host/my_app/items/5 load the page, but the stylesheet at /my_app/static_stuff/styles.css is not found.

What actually happens

Using root_path on the FastAPI application causes mounted routes to be prefixed twice. The StaticFiles mount ends up effectively available at /my_app/my_app/static_stuff instead of /my_app/static_stuff. That is why the browser can’t fetch /my_app/static_stuff/styles.css, while the file is actually reachable at /my_app/my_app/static_stuff/styles.css. This behavior stems from Starlette’s handling of root_path. A discussion is tracked here: https://github.com/encode/starlette/discussions/2931.

Fix

Don’t pass root_path to FastAPI().__p>

Provide the root path to the ASGI server instead, so the application’s mounted paths don’t get double-prefixed. With Uvicorn, that means using the --root-path argument.

from fastapi import Request, FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import os.path as p
BASE_DIR = p.abspath(p.join(__file__, "../"))
web = FastAPI(title="my_app")
web.mount("/static_stuff", StaticFiles(directory=f"/{BASE_DIR}/static_stuff"), name="static")
views = Jinja2Templates(directory=f"/{BASE_DIR}/templates")
@web.get("/items/{item_id}", response_class=HTMLResponse, include_in_schema=False)
async def get_item(req: Request, item_id: str):
    return views.TemplateResponse(
        request=req, name="item.html", context={"id": item_id}
    )

Run the server with a root path:

uvicorn main:web --root-path my_app

This makes the effective URLs line up with what Nginx proxies: /my_app/items/... for routes and /my_app/static_stuff/... for assets. No extra Nginx location for static is necessary. Note that Uvicorn accepts --root-path; Gunicorn does not expose that flag directly. If you need more context on related upstream behavior, see also the FastAPI discussion on http/https handling: https://github.com/fastapi/fastapi/discussions/6073.

Why this matters

Applications deployed behind a reverse proxy under a subpath are common in multi-app environments. When root path handling is off at the framework layer, you’ll chase phantom Nginx issues and broken asset paths. Keeping root_path at the server layer prevents duplicate prefixes on mounted ASGI apps such as StaticFiles, avoiding brittle rewrites and special-case proxy rules.

Takeaways

If static assets 404 only when the app is served under a subpath, first check the effective URL of mounts. Avoid setting root_path in the FastAPI constructor when using mounted routes; prefer the server’s --root-path. With Nginx, a straightforward proxy of the subpath is sufficient once the server knows the correct root path. Track the upstream Starlette discussion for changes that may alter this behavior over time.