Skip to content

Deployment

Running the admin behind a reverse proxy changes two things you can otherwise ignore in local development: the secret key must be stable across worker processes, and generated URLs must reflect HTTPS even though your app only ever sees plain HTTP from the proxy.

Framework-specific deployment guides

This page covers only what's specific to Admin. For the underlying application and ASGI server, see:

import os

from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette_admin.contrib.sqla import Admin, ModelView

from myapp.models import Post

engine = create_engine(os.environ["DATABASE_URL"])
app = Starlette()

admin = Admin(
    engine,
    title="My Admin",
    base_url="/admin",
    secret_key=os.environ["ADMIN_SECRET_KEY"],
)
admin.add_view(ModelView(Post))
admin.mount_to(app)
Running behind a reverse proxy
uvicorn myapp.main:app --forwarded-allow-ips='*' --proxy-headers

Secret key

Admin generates a random secret_key at startup when you don't pass one. That's fine for a single local process, but every worker process generates its own key independently, so a CSRF token signed by one worker won't validate on another. Set secret_key from an environment variable before you run more than one process. See Security for the full multi-worker failure mode and how the key is used.

Reverse proxy and HTTPS

Admin builds every internal link (list pages, edit forms, exports, the /static mount, uploaded files served through /_files/...) by calling request.url_for(...), which derives its scheme from the incoming request. When a proxy such as Nginx, Caddy, or Traefik terminates TLS and forwards plain HTTP to your app, Starlette can't tell that the original request was HTTPS unless the proxy sends X-Forwarded-Proto and your ASGI server trusts it. Leave this unconfigured and generated links downgrade to http://, which browsers block or rewrite when the page itself loaded over HTTPS.

Fix this in two places:

  1. The proxy forwards the header:

    nginx
    location /admin/ {
        proxy_pass http://127.0.0.1:8000/admin/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
  2. Uvicorn trusts it, via --proxy-headers plus --forwarded-allow-ips naming the proxy's IP (or '*' if the proxy is only reachable from inside your network):

    uvicorn myapp.main:app --forwarded-allow-ips='*' --proxy-headers
    

Gunicorn's uvicorn.workers.UvicornWorker reads the same two settings from --forwarded-allow-ips. See Uvicorn's deployment docs for the full set of process-manager options.

Warning

--forwarded-allow-ips='*' trusts forwarded headers from any source. Only use it when the app is unreachable except through your proxy, for example when it's bound to a private network or a Unix socket. If the app is directly reachable, restrict the setting to the proxy's actual IP. Otherwise a client can spoof X-Forwarded-Proto and X-Forwarded-For directly.

Static assets

The admin's CSS and JS ship inside the starlette_admin package, and Admin serves them itself through a /static mount under base_url rather than from a separate static host. static_dir only lets you override individual files (see Templates), it doesn't move asset serving off your app process. There's no built-in option to serve these from a CDN. If you need one, add a cache-friendly Cache-Control rule for {base_url}/static/* at the proxy layer instead.

Uploaded files are different: LocalStorage serves them through the app too (/_files/{storage}/{path}, so auth middleware still applies), but S3Storage and other remote backends can serve directly from the provider. See File Storage.


What's next

  • Security: The secret_key multi-worker footgun in full, plus everything else the built-in protections do and don't cover.
  • Authentication: Gate access to the admin before it's reachable in production.
  • File Storage: Configuring S3Storage and other remote backends.