# How to deploy a FastAPI app with uv


Deploy an existing [FastAPI](https://fastapi.tiangolo.com/) app managed with [uv](https://pydevtools.com/handbook/reference/uv.md) as a Linux container on a single host. Use Gunicorn to manage Uvicorn workers; when a hosting platform manages container replicas, use the [standalone Uvicorn command](#use-standalone-uvicorn-for-platform-managed-replicas) instead.

## Prerequisites

Install uv and Docker with Linux container support. Start with a project that supports Python 3.13, has `pyproject.toml` and a committed {{< term "lockfile" >}}, and exposes `app` in `main.py` (`main:app`). Substitute the import path for your app, such as `myapi.app:app` for a packaged project.

The [FastAPI setup tutorial](https://pydevtools.com/handbook/tutorial/set-up-a-fastapi-project-with-uv.md) covers creating the project. Replace its in-memory task storage with persistent storage before deployment: workers do not share Python objects, and restarts discard them. Arrange HTTPS termination through a reverse proxy on the host; this recipe publishes the API only to that host's loopback interface.

## Lock the production server dependencies

Run from the project root:

```bash
uv add gunicorn uvicorn-worker "uvicorn[standard]"
```

Expect uv to resolve and install the server packages, updating `pyproject.toml` and `uv.lock`. Commit both files; these packages belong in runtime dependencies, not a development group.

## Configure Gunicorn workers

Create `gunicorn.conf.py` in the project root. The worker class comes from `uvicorn-worker`; avoid the deprecated `uvicorn.workers` module.

```python {filename="gunicorn.conf.py"}
import os

bind = "0.0.0.0:8000"
worker_class = "uvicorn_worker.UvicornWorker"
workers = int(os.environ.get("WEB_CONCURRENCY", "2"))
accesslog = "-"  # Standard output.
errorlog = "-"   # Standard error.
timeout = 60  # Restart workers that stop reporting to Gunicorn.
graceful_timeout = 30
control_socket_disable = True  # Manage the container through Docker.
forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "")
```

Start with two workers and measure memory and latency under your workload before changing `WEB_CONCURRENCY`. Each worker loads its own application and database pool. `timeout` detects unresponsive workers; it is not a per-request deadline for asynchronous handlers.

## Build a non-root runtime image

Exclude local environments and secrets from the build context. Add any other credential files used by your project to `.dockerignore` and keep secrets out of tracked application files.

```text {filename=".dockerignore"}
.venv
.git
**/__pycache__
**/*.pyc
.env*
secrets/
```

Create this `Dockerfile`. Both stages use the same Python base; select a compatible base if your project needs another Python version. For immutable releases, pin base images by digest and update those pins through your normal security update process.

```dockerfile {filename="Dockerfile"}
FROM python:3.13-slim-bookworm AS builder
COPY --from=ghcr.io/astral-sh/uv:0.12.11 /uv /bin/uv
ENV UV_PYTHON=/usr/local/bin/python UV_PYTHON_DOWNLOADS=never
WORKDIR /app
COPY . .
# Reject stale locks, omit default dependency groups, install non-editably.
RUN uv sync --locked --no-default-groups --no-editable --no-cache

FROM python:3.13-slim-bookworm
WORKDIR /app
RUN groupadd --gid 10001 app && useradd --uid 10001 --gid app --no-create-home app
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
ENV FORWARDED_ALLOW_IPS=""
USER 10001:10001
EXPOSE 8000
CMD ["gunicorn", "--config", "gunicorn.conf.py", "main:app"]
```

The runtime retains the source for flat-layout apps and starts Gunicorn directly, with no uv synchronization at startup. For dependency-layer caching, follow [How to use uv in a Dockerfile](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md).

## Start the container behind HTTPS

Create an untracked `production.env` outside the project directory with the environment variables your application reads. Pass its path to `--env-file`; for the commands here, place it in the parent directory. Keep debug mode disabled and supply database credentials at runtime, never through Dockerfile `ARG` or `ENV` instructions.

Set `FORWARDED_ALLOW_IPS` in that file to the proxy's source IP **as seen by the container**. Docker networking may translate the host address, so do not assume it is `127.0.0.1`. Have the proxy overwrite forwarded headers; trust `*` only when network isolation ensures every connection comes through that proxy. The empty default trusts no forwarded headers.

Run these commands on the deployment host. Use Docker Desktop with Linux containers to build and smoke-test the same image on macOS or Windows:

```bash
docker build --pull -t fastapi-app:prod .
docker run -d --name fastapi-app --restart unless-stopped --stop-timeout 45 --env-file ../production.env -p 127.0.0.1:8000:8000 fastapi-app:prod
docker logs fastapi-app
```

Expect a completed image build, a container ID, and Gunicorn logs containing `Listening at:` and `Application startup complete.` Point the host's HTTPS proxy at `http://127.0.0.1:8000`. From another machine, use the proxy's HTTPS hostname to reach the API.

Verify a known endpoint through that HTTPS hostname and confirm generated redirects use HTTPS. Configure your deployment monitor to probe a readiness endpoint that returns success only when the app is ready to serve traffic; Docker's restart policy restarts exited containers, not containers that merely fail health checks.

When stopping the service, allow in-flight requests to finish:

```bash
docker stop fastapi-app
```

Expect `fastapi-app` as output. Docker allows 45 seconds before killing the container, longer than Gunicorn's 30-second graceful timeout. Run database migrations once per deployment before starting workers, rather than in each worker's startup handler.

## Use standalone Uvicorn for platform-managed replicas

When the platform handles replication and restarts, replace the Dockerfile's `CMD` with one Uvicorn process per container. Continue passing `FORWARDED_ALLOW_IPS` through the container environment; Uvicorn does not read `gunicorn.conf.py`. Configure the platform's shutdown grace period to exceed 30 seconds.

```dockerfile
CMD ["uvicorn", "main:app", "--workers", "1", "--host", "0.0.0.0", "--port", "8000", "--timeout-graceful-shutdown", "30"]
```

Rebuild the image after changing `CMD`. Keep `--reload` disabled in production.
