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:
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.

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:
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.
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.
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:
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.
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:
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.
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.
gcloud run deploy --source for a single service that you are happy to wire up by hand.Python support in Encore is in beta, and you can sign up for access to try it with your FastAPI app.
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.
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.
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.
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.