How to deploy a FastAPI app with uv
Deploy an existing FastAPI app managed with uv 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 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 lockfileA file that records the exact version of every installed package, so everyone working on the project gets identical installs.
, 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 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:
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.
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.
.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.
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.
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:
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-appExpect 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:
docker stop fastapi-appExpect 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.
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.