How to deploy a uv project to Fly.io
Fly.io runs a container on Fly Machines, lightweight VMs that boot a persistent process close to the user. That makes it the stable-latency alternative to request-scoped serverless: a long-lived app rather than a per-request handler, the option that the serverless comparison points to when scale-to-zero platforms don’t fit. uv builds the image, and its lockfile keeps every deploy reproducible.
This guide deploys a uv-managed web app (a FastAPI service on uvicorn) to Fly.io. The deploy hinges on two things: a container that installs from the frozen lockfile, and a process that listens where Fly Proxy can reach it.
Write a Fly-ready Dockerfile
Fly builds and runs the image you define. A multi-stage build installs dependencies with uv in a builder stage and copies only the resulting virtual environment into a slim runtime stage:
# Build stage: resolve and install dependencies with uv
FROM python:3.13-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:0.11.25 /uv /uvx /bin/
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-install-project --no-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable
# Runtime stage: copy only the virtual environment
FROM python:3.13-slim
RUN useradd --create-home appuser
WORKDIR /app
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
USER appuser
EXPOSE 8080
CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8080"]uv sync --frozen installs the exact versions in uv.lock and fails if the lockfile is stale, so the deployed environment matches what you tested. UV_COMPILE_BYTECODE=1 writes .pyc files at build time, which shortens startup when Fly boots a stopped Machine. --no-editable installs the project as a regular package inside .venv, so the runtime stage carries only that directory. For the reasoning behind the two-step sync and the bytecode setting, see How to use uv in a Dockerfile.
Warning
The start command binds --host 0.0.0.0, not 127.0.0.1. Fly Proxy forwards external traffic to the app over the container network, so a process listening only on loopback is unreachable and the deploy health check fails. The port (8080) must match the internal_port in fly.toml; Fly does not set a PORT environment variable for the app to read.
Launch the app on Fly.io
Install flyctl, the Fly command-line tool:
curl -L https://fly.io/install.sh | shOn macOS, brew install flyctl also works.
Sign in (or fly auth signup for a new account), then launch the app from the project root, where pyproject.toml, uv.lock, and the Dockerfile live:
fly auth login
fly launchBecause a Dockerfile is present, Fly uses it instead of guessing a builder, and it writes a fly.toml with the app name, region, and an [http_service] block. Confirm internal_port matches the port in the Dockerfile’s CMD:
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 0fly launch deploys on completion. Ship later changes with one command:
fly deployKeep a Machine warm or let it scale to zero
The [http_service] block decides whether the app stays running or stops when idle. The choice comes down to one setting:
- Scale to zero:
auto_stop_machines = "stop",auto_start_machines = true, andmin_machines_running = 0. Fly Proxy stops idle Machines and starts them on the next request. Cheapest for bursty traffic, at the cost of a cold start after idle. - Always warm: set
min_machines_running = 1(or more). One Machine stays running so requests never wait on a boot, which is the reason to choose Fly over a request-scoped platform.
auto_stop_machines also accepts "suspend", which freezes Machine memory to RAM for a faster resume than a full "stop". Keep auto_stop_machines and auto_start_machines consistent: enable both or disable both, or Machines can stop and never restart.
Set secrets without baking them into the image
Pass configuration the app reads from the environment with fly secrets set rather than an ENV line in the Dockerfile, which would persist in image layers:
fly secrets set DATABASE_URL=postgres://user:pass@host/db API_TOKEN=abc123Fly encrypts each value, restarts every Machine, and injects the secrets as environment variables at boot. The app reads them with os.environ["DATABASE_URL"]. Add --stage to store secrets without an immediate restart when you want them to take effect on the next deploy.
Learn more
- Running Python on Serverless compares Fly.io’s persistent model against Lambda, Cloud Run, Vercel, Modal, and the other scale-to-zero platforms.
- How to use uv in a Dockerfile covers the multi-stage build, cache mounts, and bytecode compilation in depth.
- Fly Launch documentation explains how
fly launchscans a project and generatesfly.toml. - Fly App configuration reference documents every
fly.tomlfield, including the full[http_service]block.