Getting a Flask app onto Cloud Run with Cloud SQL Postgres, a secret key in Secret Manager and a non-root container, using Encore, the gcloud CLI or Terraform.
Flask apps fit Cloud Run well, since a container that answers HTTP on one port is all Cloud Run needs. The surrounding setup takes longer: a Cloud SQL instance, a secret for SECRET_KEY, a service account with the right roles and a way to run the same database locally.
This guide deploys a Flask books API to Cloud Run with Encore, the gcloud CLI and Terraform. Each version uses Cloud SQL Postgres and loads the Flask secret key from Secret Manager.
What you need to decide:
Encore combines the database and secrets your Flask code declares with a local environment and a deploy to Cloud Run in your own GCP project, so Cloud SQL and Secret Manager come from the same code that runs on your laptop. It is an infrastructure SDK added to an existing app, and the routes stay ordinary Flask.

The database points at a directory of SQL migrations, and the secret key is a shared secret:
from encore import secrets
from encore.storage import postgres
from flask import Flask
# Cloud SQL on GCP, RDS on AWS, and `encore run` starts Postgres locally.
db = postgres.Database("books", migrations="./migrations")
# Set per environment with `encore secret set`, stored in Secret Manager on GCP.
flask_secret = secrets.SharedSecret[str]("FlaskSecretKey")
app = Flask(__name__)
app.config["SECRET_KEY"] = flask_secret.value
The first migration creates the table:
CREATE TABLE books (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL
);
Routes query with parameters, and a health route gives Cloud Run something cheap to probe:
from flask import abort, request
@app.post("/books")
async def create_book():
data = request.get_json()
row = await db.query(
"INSERT INTO books (title, author) VALUES ($1, $2) RETURNING id",
data["title"],
data["author"],
).one()
return {"id": row["id"]}, 201
@app.get("/books/<int:book_id>")
async def get_book(book_id: int):
row = await db.query("SELECT id, title, author FROM books WHERE id = $1", book_id).first()
if row is None:
abort(404)
return {"id": row["id"], "title": row["title"], "author": row["author"]}
@app.get("/healthz")
def healthz():
return {"ok": True}
Waitress serves the app on the address Encore assigns, locally and in the cloud:
import os
from waitress import serve
if __name__ == "__main__":
host, _, port = os.environ.get("ENCORE_LISTEN_ADDR", "0.0.0.0:8000").rpartition(":")
serve(app, host=host or "0.0.0.0", port=int(port))
Locally, the key comes from .secrets.user.toml and encore run starts Postgres and applies the migration. For Cloud Run, set the secret once per environment and push:
On deploy, Encore creates the Cloud Run service, a Cloud SQL instance with the migration applied and the secret in Secret Manager in your GCP project, and provisions a service account for the service. Each environment gets its own database and secret values.
An agent can add a table or a secret, run it locally, and the Cloud Run side follows on deploy from the same commit.
Cloud Run scales to zero by default, so the first request after idle includes a cold start, and a minimum instance count avoids it. Async views need flask[async] in the dependencies.
With gcloud, you write the container. Add a .dockerignore next to the Dockerfile so local secret files stay out of the image:
.secrets.user.toml .env .git
This Dockerfile runs as a non-root user with Gunicorn:
FROM python:3.13-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home app
USER app
CMD exec gunicorn --bind 0.0.0.0:${PORT:-8080} --workers 2 main:app
Then you create every resource and grant before the deploy, following Google's Python quickstart:
PROJECT=$(gcloud config get-value project)
SA=books-run@$PROJECT.iam.gserviceaccount.com
gcloud sql instances create books-db --database-version=POSTGRES_17 \
--region=europe-west1 --tier=db-g1-small
gcloud sql databases create books --instance=books-db
gcloud sql users create books --instance=books-db --password="$DB_PASSWORD"
gcloud iam service-accounts create books-run
gcloud projects add-iam-policy-binding "$PROJECT" \
--member="serviceAccount:$SA" --role=roles/cloudsql.client
python -c "import secrets; print(secrets.token_hex(32))" | \
gcloud secrets create flask-secret-key --data-file=-
printf %s "$DB_PASSWORD" | gcloud secrets create books-db-password --data-file=-
for s in flask-secret-key books-db-password; do
gcloud secrets add-iam-policy-binding "$s" \
--member="serviceAccount:$SA" --role=roles/secretmanager.secretAccessor
done
gcloud run deploy books --source . --region=europe-west1 \
--service-account="$SA" \
--add-cloudsql-instances="$PROJECT:europe-west1:books-db" \
--set-secrets=SECRET_KEY=flask-secret-key:latest,DB_PASSWORD=books-db-password:latest
The app reads SECRET_KEY and DB_PASSWORD from the environment and connects through /cloudsql/.... Migrations need their own step, such as a Cloud Run job running Alembic, and local development uses a Postgres you run yourself.
Terraform declares the same pieces so changes go through review:
resource "google_cloud_run_v2_service" "books" {
name = "books"
location = "europe-west1"
template {
service_account = google_service_account.books.email
containers {
image = var.image
env {
name = "SECRET_KEY"
value_source {
secret_key_ref {
secret = google_secret_manager_secret.flask_key.secret_id
version = "latest"
}
}
}
}
}
}
The full configuration also declares the service account, both IAM bindings, the Cloud SQL instance, database and user, the secret and its version, an Artifact Registry repository and the state backend. Building and pushing the image is a separate CI step.
Python support in Encore is in beta, and you can sign up for access to deploy your Flask app to Cloud Run.
Serve Flask with a production WSGI server on the port Cloud Run provides, connect to Cloud SQL and load secrets from Secret Manager. With Encore, declare postgres.Database("books", migrations="./migrations") and secrets.SharedSecret[str]("FlaskSecretKey"), set the secret with encore secret set and deploy. Encore provisions Cloud Run, Cloud SQL and the secret in your GCP project.
A production WSGI server such as Gunicorn or Waitress, never the Flask development server. It must listen on 0.0.0.0 and the port in the PORT environment variable, which defaults to 8080.
Mount the instance with --add-cloudsql-instances, which exposes a Unix socket under /cloudsql/PROJECT:REGION:INSTANCE, and point the connection at that path. The service account needs the Cloud SQL Client role.
No. gcloud run deploy --source can build a Python app with Google Cloud's buildpacks. A Dockerfile gives you control over the base image and lets you run as a non-root user.
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.