# How to deploy a uv project to Fly.io


[Fly.io](https://fly.io/docs/) 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](https://pydevtools.com/handbook/explanation/about-running-python-on-serverless.md) points to when scale-to-zero platforms don't fit. [uv](https://pydevtools.com/handbook/reference/uv.md) builds the image, and its [lockfile](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) 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:

```dockerfile {filename="Dockerfile"}
# 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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md).

> [!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:

```bash
curl -L https://fly.io/install.sh | sh
```
On macOS, `brew install flyctl` also works.
```powershell
pwsh -Command "iwr https://fly.io/install.ps1 -useb | iex"
```
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:

```bash
fly auth login
fly launch
```

Because 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`:

```toml {filename="fly.toml"}
[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0
```

`fly launch` deploys on completion. Ship later changes with one command:

```bash
fly deploy
```

## Keep 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`, and `min_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:

```bash
fly secrets set DATABASE_URL=postgres://user:pass@host/db API_TOKEN=abc123
```

Fly 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](https://pydevtools.com/handbook/explanation/about-running-python-on-serverless.md) 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](https://pydevtools.com/handbook/how-to/how-to-use-uv-in-a-dockerfile.md) covers the multi-stage build, cache mounts, and bytecode compilation in depth.
- [Fly Launch documentation](https://fly.io/docs/launch/) explains how `fly launch` scans a project and generates `fly.toml`.
- [Fly App configuration reference](https://fly.io/docs/reference/configuration/) documents every `fly.toml` field, including the full `[http_service]` block.
