Oct 26, 20267 min read

How to Deploy FastAPI to Google Cloud Run

Shipping a FastAPI app with Postgres and Pub/Sub to Cloud Run with Encore, gcloud run deploy from source, or a Dockerfile with Cloud Build, Artifact Registry and Cloud SQL.

FastAPI fits Cloud Run well, since Cloud Run runs any container that answers HTTP on a port and scales it to zero when idle. Building the container takes little effort, and most of the setup is Artifact Registry, Cloud SQL, service accounts and Pub/Sub permissions for each environment.

This guide deploys a notes API with Postgres and a Pub/Sub indexer to Cloud Run. It compares Encore, gcloud run deploy --source and a Dockerfile built by Cloud Build.

What you need to decide:

  • Who builds the container, and from what.
  • How the service reaches Cloud SQL and which identity it uses.
  • Where Pub/Sub topics and subscriptions are defined.
  • How local development matches what runs on Cloud Run.

FastAPI on Cloud Run options at a glance

CapabilityEncoregcloud run deploy --sourceDockerfile with Cloud Build
Container buildBuilt by Encore on pushBuildpacks or your DockerfileYour Dockerfile
Cloud SQLProvisioned and connected from the declarationCreate and attach yourselfCreate and attach yourself
Pub/SubTopics and subscriptions from the codeCreate with gcloudCreate with gcloud or Terraform
IAMA service account created per serviceDefault compute account unless you set oneYou define it
Local developmentencore run starts Postgres and Pub/Sub locallyYou run themYou run them

Encore: deploy FastAPI to Cloud Run from your code

Encore

Encore combines the infrastructure your FastAPI app declares in code with a local environment and a deployment to Cloud Run in your own GCP project, so Cloud SQL, Pub/Sub and the service account come from the same declarations you develop against. It is an infrastructure SDK you add to an existing app, and the FastAPI code stays the same.

Encore declares infrastructure in Python code, runs it locally and provisions it in your AWS or GCP account
Infrastructure declared in Python, running locally under encore run and provisioned in your own AWS or GCP account on deploy.

The notes table lives in a regular SQL migration:

CREATE TABLE notes ( id BIGSERIAL PRIMARY KEY, body TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() );

The app declares the database and a note-created topic with an indexer subscription, and writes and reads notes with the SDK's query methods:

import os from contextlib import asynccontextmanager import encore import uvicorn from encore import pubsub from encore.storage import postgres from fastapi import FastAPI, HTTPException from pydantic import BaseModel # Cloud SQL for PostgreSQL and a Pub/Sub topic on GCP, # a local Postgres database and topic under `encore run`. db = postgres.Database("notes", migrations="./migrations") class NoteCreated(BaseModel): note_id: int note_created = pubsub.Topic[NoteCreated]("note-created") async def index_note(msg: pubsub.Message[NoteCreated]) -> None: # Safe to run more than once for the same note. print(f"indexing note {msg.data.note_id}") indexer = pubsub.Subscription[NoteCreated](note_created, "index-note", handler=index_note) @asynccontextmanager async def lifespan(app: FastAPI): await encore.start() yield app = FastAPI(lifespan=lifespan) class NewNote(BaseModel): body: str @app.post("/notes", status_code=201) async def create_note(req: NewNote) -> dict: row = await db.query( "INSERT INTO notes (body) VALUES ($1) RETURNING id", req.body ).one() await note_created.publish(NoteCreated(note_id=row["id"])) return {"id": row["id"]} @app.get("/notes/{note_id}") async def get_note(note_id: int) -> dict: row = await db.query("SELECT id, body FROM notes WHERE id = $1", note_id).first() if row is None: raise HTTPException(status_code=404, detail="note not found") return {"id": row["id"], "body": row["body"]} @app.get("/healthz") async def healthz() -> dict: return {"ok": True} if __name__ == "__main__": # ENCORE_LISTEN_ADDR under `encore run`, PORT on Cloud Run, 8000 otherwise. addr = os.environ.get("ENCORE_LISTEN_ADDR") if addr: host, _, port = addr.rpartition(":") else: host, port = "0.0.0.0", os.environ.get("PORT", "8000") uvicorn.run(app, host=host or "0.0.0.0", port=int(port))

encore run starts Postgres locally, applies the migration and delivers note-created messages to the indexer, which is the same flow that will run on Cloud Run:

~/notes
$ encore run$ curl -X POST localhost:4000/notes -H 'content-type: application/json' -d '{"body":"second note"}'{"id":2}notes │ indexing note 2$ curl localhost:4000/notes/2{"id":2,"body":"second note"}

To deploy, you connect your GCP project once and push. Encore builds the container and creates the Cloud Run service, a Cloud SQL for PostgreSQL instance with the migration applied, the Pub/Sub topic and push subscription, and a service account for the service, all in your GCP project. Pushing through the GitHub integration deploys each commit, and each environment gets its own copies of these resources. Instance sizes and other settings can be adjusted per environment without changing the code.

An agent that adds the topic and indexer here can run the whole flow locally and push, and cloud access sits with Encore's service identity in your project, not the agent.

Good to know

The /healthz route is for your own checks. The PORT fallback in main.py lets the same file run under plain Uvicorn, but Encore sets the listen address itself.

Try Encore →

gcloud run deploy from source

gcloud run deploy --source uploads the directory, builds it with Cloud Build and deploys the result. With a Dockerfile in the directory it uses that, otherwise Google Cloud's buildpacks detect Python. The database and topic are created first:

~/notes
$ gcloud sql instances create notes-db --database-version=POSTGRES_17 --tier=db-custom-1-3840 --edition=ENTERPRISE --region=europe-west1$ gcloud sql databases create notes --instance=notes-db$ gcloud pubsub topics create note-created$ gcloud iam service-accounts create notes-api$ gcloud projects add-iam-policy-binding PROJECT --member=serviceAccount:notes-api@PROJECT.iam.gserviceaccount.com --role=roles/cloudsql.client$ printf '%s' "$DB_PASSWORD" | gcloud secrets create notes-db-password --data-file=-$ gcloud secrets add-iam-policy-binding notes-db-password --member=serviceAccount:notes-api@PROJECT.iam.gserviceaccount.com --role=roles/secretmanager.secretAccessor$ gcloud pubsub topics add-iam-policy-binding note-created --member=serviceAccount:notes-api@PROJECT.iam.gserviceaccount.com --role=roles/pubsub.publisher$ gcloud run deploy notes --source . --region=europe-west1 \ --add-cloudsql-instances=PROJECT:europe-west1:notes-db \ --service-account=notes-api@PROJECT.iam.gserviceaccount.com \ --set-secrets=DB_PASSWORD=notes-db-password:latest

Good to know

Cloud Run mounts the instance as a Unix socket under /cloudsql/PROJECT:REGION:INSTANCE, as described in connecting from Cloud Run, and the service account needs the Cloud SQL Client role. The indexer needs either a push subscription pointing at a route in the service, with an identity allowed to invoke it, or a separate worker. The database password goes into Secret Manager, and migrations need a step of their own, often a Cloud Run job run before each deploy.

Dockerfile with Cloud Build and Artifact Registry

Teams that want the container under their own control write the Dockerfile and build it in Cloud Build or CI, then deploy the image. The Cloud Run container contract requires listening on PORT, and the image below runs as a non-root user:

FROM python:3.13-slim ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd --create-home --uid 10001 app USER app # Shell form so ${PORT} is expanded; exec so Uvicorn receives SIGTERM. CMD exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080} --forwarded-allow-ips='*'

Cloud Build builds the image from that Dockerfile, and the deploy points Cloud Run at it:

~/notes
$ gcloud artifacts repositories create apps --repository-format=docker --location=europe-west1$ gcloud builds submit --tag europe-west1-docker.pkg.dev/PROJECT/apps/notes:v1$ gcloud run deploy notes --image europe-west1-docker.pkg.dev/PROJECT/apps/notes:v1 --region=europe-west1

Good to know

Uvicorn reads proxy headers by default but only trusts them from the addresses in --forwarded-allow-ips, which defaults to 127.0.0.1, as its deployment guide explains. Cloud Run requests arrive through Google's front end, so --forwarded-allow-ips='*' is what makes client IPs and schemes correct. Cloud Run scales by instances, so one Uvicorn process per container is the usual choice. Multiple environments mean repeating every command per project, or moving the setup into Terraform.

Running FastAPI well on Cloud Run

With the default request-based billing, Cloud Run only allocates CPU while a request is in flight, so asyncio tasks started after the response may stall until the next request, which the CPU allocation documentation covers in detail. Cold starts include importing FastAPI and every dependency, so keep heavy imports out of module level where you can, and set a minimum of one instance for latency-sensitive services. Cloud SQL connections count against the instance's limit, which grows quickly when Cloud Run scales out, so keep pools small per instance.

How to choose

  • Encore if you want Cloud SQL, Pub/Sub and the service account created in your GCP project from the code, with the same setup running locally.
  • gcloud run deploy --source for a single service that you are happy to wire up by hand.
  • Dockerfile with Cloud Build if you need full control of the image and already maintain the surrounding resources.

Python support in Encore is in beta, and you can sign up for access to try it with your FastAPI app.

Frequently asked questions

How do I deploy FastAPI to Cloud Run?

Package the app in a container that listens on the PORT environment variable, push it to Artifact Registry and create a Cloud Run service from it. gcloud run deploy --source . builds and deploys in one command. With Encore, declare postgres.Database("notes", migrations="./migrations") and pubsub.Topic[NoteCreated]("note-created") in the FastAPI code, connect your GCP project, and push, and Encore creates the Cloud Run service, Cloud SQL instance and Pub/Sub topic in that project.

What port does FastAPI need to listen on in Cloud Run?

Cloud Run sets the PORT environment variable, 8080 by default, and the container must listen on it on 0.0.0.0. Start Uvicorn with --port ${PORT:-8080} in a shell-form CMD so the variable is expanded.

How does Cloud Run connect to Cloud SQL?

Attach the Cloud SQL instance to the service with --add-cloudsql-instances, which mounts a Unix socket under /cloudsql, and give the service account the Cloud SQL Client role. The Cloud SQL Python Connector is the alternative when you want IAM database authentication from code.

Can a FastAPI app on Cloud Run run background work?

With request-based billing, CPU is only allocated while a request is being handled, so work started after the response can stall. Use instance-based billing, or move the work to Pub/Sub push subscriptions or Cloud Tasks that call back into the service.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders