Deploy a FastAPI App With HTTPS — and Why It 502s if You Bind to localhost

·5 min read·Updated
fastapipythonapihttpsdeploymenthow-to

Deploying a FastAPI app to a container takes one HTTPS port binding and gets you a real certificate on a subdomain automatically. The part that catches people isn't the deploy — it's that a misconfigured server still deploys successfully and then returns 502 at the URL. Three things decide whether it answers: your server must bind to 0.0.0.0 rather than localhost, the port it listens on must be the same port you bound, and your app has to be the thing actually holding that port. Get those right and there's nothing else to configure — no reverse proxy, no certificate, no renewal.

Why a broken app still deploys "successfully"

There's no HTTP health check between the edge router and your container. The platform routes the subdomain to a container port you nominate and takes your word for it that something is listening there.

That's usually convenient — it means slow-starting apps aren't killed for failing a probe, and non-HTTP services work the same way as web ones. The cost is that a deploy succeeding tells you the container started, not that your app is reachable. If nothing is listening on the port you named, the request reaches the router, finds no backend, and you get a 502.

So when a fresh deploy 502s, don't look at the build. Look at these three things.

1. Bind to 0.0.0.0, not localhost

Inside a container, 127.0.0.1 means this container and nothing else. A server bound there is reachable only from processes inside the same container — which is nothing. It's the single most common cause of a container that runs happily and answers no requests.

Pass the host explicitly rather than relying on a default:

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

If you use FastAPI's own CLI, fastapi run is designed for this case — the framework's container docs use it directly:

CMD ["fastapi", "run", "app/main.py", "--port", "8080"]

Either is fine. What isn't fine is a dev-server invocation copied from your laptop, where binding to localhost is the correct and safe default.

2. Match the port to the binding

Two numbers have to agree: the port your server listens on, and the container port in your HTTPS binding. If uvicorn listens on 8080 and you bound 8000, the router looks for a backend on 8000, finds nothing, and returns 502 — with no error anywhere in your app's logs, because your app never saw the request.

You don't need to expose 443 or 80 yourself. Bind your app's actual port as HTTPS and the platform terminates TLS at the edge on 443 and forwards to that port inside the container. The certificate is issued and renewed for you; HTTPS is included free on every app.

A useful habit is to read the port from the environment and bind whatever you're given, so there's only one number to get wrong:

import os
port = int(os.environ.get("PORT", 8080))

Then set PORT as an environment variable to the same value as the binding.

3. Make sure your app is what's holding the port

Less common but genuinely confusing: the container is up, the port is right, and it still doesn't answer — because the process listening isn't the one you think. Usually that's a dev server started by a wrapper script, an entrypoint that launches something else, or an app that crashed after binding.

Check from the app's shell, which is included free:

curl -sv http://127.0.0.1:8080/health

From inside the container, 127.0.0.1 is the right address to test. If curl answers there but the public URL 502s, your binding number is wrong. If curl doesn't answer either, your app isn't listening — go back to steps 1 and 2, and check the logs for a crash after startup.

Read your own URL from the environment

Every app with an HTTPS port is given a DF_URLS environment variable containing its public URLs, semicolon-separated. That's handy for anything that needs to know where it lives — OAuth redirect URIs, absolute links in responses, a webhook you register at startup:

import os
base = os.environ["DF_URLS"].split(";")[0]   # https://<your-app-host>

Hard-coding the host is how a staging API ends up telling clients to call production.

A minimal working Dockerfile

FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Push it with git and the platform builds and runs it — the git deploy path uses the Dockerfile at your repository root. Then add an HTTPS binding for container port 8080, and the URL is live.

No Dockerfile at all? Upload the project as a zip and the build agent writes one, including the port. You'll still want to check it bound 0.0.0.0.

Sizing a small API

A FastAPI service is a good fit for a small container — it's mostly idle between requests, like the bots and workers this kind of platform suits best. Two things worth setting deliberately:

  • Workers. Each uvicorn/gunicorn worker is a separate Python process with its own copy of your imports. On a small memory quota, one worker is usually right; adding four is the fastest way to meet an out-of-memory kill.
  • Startup work. Loading a large model or dataset at import time multiplies by worker count and can push you over the limit before the first request arrives.

If the app restarts for no obvious reason, that's the first thing to check — an OOM kill looks like an unexplained restart, not an error.

FAQ

Why does my FastAPI app return 502 after deploying?

Almost always the server is bound to 127.0.0.1 instead of 0.0.0.0, or it's listening on a different port than the one in the HTTPS binding. The deploy succeeds either way because there's no HTTP health check.

What host should uvicorn bind to in a container?

0.0.0.0. Pass --host 0.0.0.0 explicitly rather than relying on a default, since binding to localhost is correct for local development and wrong inside a container.

Do I need to expose port 443?

No. Bind your app's own port as HTTPS. TLS is terminated at the edge on 443 and forwarded to your container port.

Do I need to buy or configure an SSL certificate?

No. An HTTPS binding gets a certificate issued and renewed automatically on the app's subdomain.

How do I find my app's public URL from code?

Read the DF_URLS environment variable — it holds the app's HTTPS URLs, semicolon-separated.

How many uvicorn workers should I run?

On a small memory quota, one. Each worker is a full process, and workers multiply memory use rather than sharing it.

Wrapping up

The deploy is the easy half — a Dockerfile, a port, and HTTPS you don't have to think about. The half that costs an afternoon is that nothing checks whether your app is actually reachable, so a wrong bind address or a mismatched port produces a green deploy and a 502. Two numbers and one address: bind 0.0.0.0, listen on the port you bound, and confirm with curl from inside the container before you go looking anywhere else.