Skip to content

How to deploy a uv project to Google Cloud Run

uv

Cloud Run runs a container and scales it to zero when idle. Getting a uv project there comes down to one decision: let Google build the image from your source with its buildpack, or hand Cloud Run a Dockerfile you control. The same gcloud run deploy --source . command drives both. Two things decide whether the result runs: the lockfile that the build installs from, and listening on the port Cloud Run assigns.

Deploy from source with buildpacks

The fast path needs no Dockerfile. Commit pyproject.toml and uv.lock, leave requirements.txt out, and Cloud Run’s Python buildpack installs with uv on its own. The buildpack activates uv whenever it finds a uv.lock next to a pyproject.toml, on every supported Python version (Build a Python application).

Add a web framework and an entrypoint the buildpack can start. A Procfile is the explicit choice, and its web: command must bind the port Cloud Run injects:

uv add fastapi "uvicorn[standard]"
Procfile
web: uvicorn main:app --host 0.0.0.0 --port ${PORT}

Lock the project, commit, and deploy. The first deploy in a project prompts to enable the Cloud Build and Cloud Run APIs:

uv lock
git add pyproject.toml uv.lock .python-version main.py Procfile
git commit -m "Add Cloud Run entrypoint"

gcloud run deploy myapp --source . --region us-central1 --allow-unauthenticated

Cloud Run uploads the source, the buildpack reads uv.lock and installs the exact locked versions with uv, and the service comes up at the URL the command prints.

The buildpack treats requires-python as a floor, not a pin. With requires-python = ">=3.13" it installs the latest available runtime, which is Python 3.14 today. To pin an exact version, commit a .python-version file with uv python pin 3.13; the buildpack reads it and builds on that version.

Warning

A requirements.txt in the project root takes precedence over pyproject.toml, and on Python 3.13 and earlier the buildpack reads it with pip, not uv. A stale requirements.txt left beside the lockfile silently bypasses uv. Delete it, or regenerate it from the lockfile with uv export --frozen --no-dev --no-emit-project -o requirements.txt so the two agree.

Build a container with a Dockerfile

Reach for a Dockerfile when the buildpack’s choices don’t fit: a pinned base image, or a system library that a wheel links against. When a Dockerfile is present in the source root, gcloud run deploy --source . builds from it instead of the buildpack (Deploy services from source code).

Install uv, sync dependencies in two steps so the dependency layer caches separately from the source, and bind 0.0.0.0:$PORT in the CMD:

Dockerfile
FROM python:3.13-slim

COPY --from=ghcr.io/astral-sh/uv:0.11.24 /uv /uvx /bin/

ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-install-project --no-dev
COPY . .
RUN uv sync --frozen --no-dev

ENV PATH="/app/.venv/bin:$PATH"

CMD exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}

The --no-dev flag drops pytest, Ruff, and other dev tools from the image, and ${PORT:-8080} reads Cloud Run’s port while still running locally.

Warning

The source build runs Cloud Build’s classic Docker builder, which does not enable BuildKit. A Dockerfile that uses RUN --mount=type=cache or --mount=type=bind fails here with “the –mount option requires BuildKit.” Use the plain COPY form above. To run a BuildKit Dockerfile (cache mounts, a multi-stage build that copies only .venv), build and push the image yourself, then deploy it with gcloud run deploy myapp --image .... The multi-stage pattern and cache mounts are covered in How to use uv in a Dockerfile.

Deploy the same way:

gcloud run deploy myapp --source . --region us-central1 --allow-unauthenticated

Choose between buildpacks and a Dockerfile

Default to the buildpack. It removes the Dockerfile from what you maintain, and Google patches the base image and rebuilds it without your involvement. Switch to a Dockerfile when the application needs something the buildpack can’t express.

Concern Buildpack (--source, no Dockerfile) Dockerfile
Maintenance Google owns the base image and patches it You own the base image and updates
Base image Fixed by the buildpack Any image you choose
System libraries Only what the buildpack provides Install any apt package
Build steps Dependency install only Arbitrary RUN steps
Local parity Build runs only in Cloud Build Same image builds and runs anywhere

Make the container listen on Cloud Run’s port

Cloud Run sets a PORT environment variable, defaults it to 8080, and routes requests to it. The app must listen on 0.0.0.0:$PORT (Container runtime contract). A server bound to 127.0.0.1 accepts only connections from inside the container, so Cloud Run’s health check never reaches it and the deploy fails with a “container failed to start” error.

Both paths above read $PORT in the start command rather than hardcoding 8080, which is why each survives a port change. Web frameworks that default to localhost are the usual cause of a container that runs locally but fails on Cloud Run.

Pass secrets with Secret Manager

Keep API keys and database passwords out of the image and the lockfile. Store each value in Secret Manager and mount it at deploy time. Create the secret, then grant the service’s runtime service account permission to read it:

echo -n "s3cr3t-value" | gcloud secrets create API_KEY --data-file=-

gcloud secrets add-iam-policy-binding API_KEY \
  --member="serviceAccount:$(gcloud projects describe "$(gcloud config get-value project)" \
    --format='value(projectNumber)')-compute@developer.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

Mount the secret as an environment variable on deploy. --set-secrets maps ENV_VAR=SECRET:VERSION:

gcloud run deploy myapp --source . --region us-central1 \
  --set-secrets=API_KEY=API_KEY:latest

The application reads it like any other environment variable with os.environ["API_KEY"]. The value never enters the build, so it stays out of image layers and the source upload.

Speed up cold starts in serverless containers

Cloud Run scales to zero, so a request after idle time pays a cold start: pull the image, start the container, import the app. Two uv settings shrink that cost, both already in the Dockerfile above.

UV_COMPILE_BYTECODE=1 compiles .pyc files at build time instead of on first import. A scaled-from-zero container skips the bytecode-compile step, which matters most for apps with heavy import graphs. uv sync --no-dev keeps test and lint dependencies out of the runtime image, so there is less to pull and import.

For a buildpack deploy, the same image-size win comes from keeping requirements.txt absent so the build installs only the locked runtime dependencies from uv.lock, not a dev-inclusive export.

Learn more

Last updated on