How I Deployed My FastAPI Contact Form API to Google Cloud Run

How I Deployed My FastAPI Contact Form API to Google Cloud Run

After building my small FastAPI contact form API, the next step was obvious: I needed a simple and reliable place to deploy it.

The whole point of this project was to support all of my static site projects with one reusable backend. AI was helping me create lots of beautiful static sites, but I still needed a secure way for all of those contact forms to send messages somewhere useful.

I had already built the API with FastAPI, Docker, Cloudflare Turnstile, and email provider support. If you want the full backstory of why I built it, you can read my earlier post here:

Build and deploy a simple contact form API with FastAPI and Docker

The source code for the project is public here:

GitHub Repository

In this post, I want to walk through how I deployed that API to Google Cloud Run and why it turned out to be a great fit for a small containerized backend like this.

Why I Chose Google Cloud Run

I wanted deployment to stay simple.

I did not want to manage servers. I did not want to think too much about operating systems, patching, or scaling. I just wanted to deploy a container and let the platform handle the rest.

That is exactly why Google Cloud Run felt like a good match.

Cloud Run works especially well for small APIs because:

  • It runs containers directly
  • It scales automatically
  • It can scale down when there is no traffic
  • It is easy to connect with environment variables and secrets
  • It works nicely for stateless web services like FastAPI apps

Since my contact form API was already packaged with Docker, moving it to Cloud Run felt natural.

The Project Was Already a Good Fit

One reason this deployment was straightforward is that the app was already designed in a deployment-friendly way.

The project already had:

  • A small FastAPI app
  • A Dockerfile
  • Environment-based configuration
  • A single HTTP API endpoint
  • No database dependency
  • External service integration for email delivery

That combination makes Cloud Run a very strong choice.

My API only needs to:

  1. Receive a POST request
  2. Validate the contact form payload
  3. Verify Cloudflare Turnstile
  4. Send an email through Mailjet or SendGrid
  5. Return a success or failure response

That is a perfect example of a stateless container service.

High-Level Deployment Flow

Here is the full deployment path I followed:

  1. Build the Docker image
  2. Push the image to Google Artifact Registry
  3. Deploy the container to Cloud Run
  4. Set required environment variables
  5. Allow public access to the API endpoint
  6. Point my frontend contact forms to the Cloud Run URL
  7. Test the full flow end-to-end

Once that was done, all of my static sites could send contact form requests to the same backend.

Step 1: Make Sure the App Is Cloud Run Friendly

Before deploying to Cloud Run, I made sure the FastAPI app worked the way Cloud Run expects.

The important thing here is that Cloud Run sends traffic to the port defined by the PORT environment variable. My Docker setup already used port 8080, which matches the common Cloud Run pattern.

A typical FastAPI container start command looks like this:

uvicorn main:app --host 0.0.0.0 --port 8080

That is important because Cloud Run needs the app to listen on all interfaces, not just localhost.

Step 2: Build the Docker Image

Since the app already had a Dockerfile, building the image was easy.

Example:

docker build -t fastapi-contactform .

This step packages everything needed to run the app into one container image.

I like this approach because it gives me consistency:

  • The same image can run locally
  • The same image can run in Cloud Run
  • Deployment becomes predictable

Step 3: Tag the Image for Google Artifact Registry

Before deploying to Cloud Run, I needed a place to store the container image.

On Google Cloud, that usually means Artifact Registry.

After creating a project and enabling the right services, I tagged the image like this:

docker tag fastapi-contactform REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/fastapi-contactform:latest

Replace these values with your own:

  • REGION β†’ your Google Cloud region
  • PROJECT_ID β†’ your Google Cloud project ID
  • REPOSITORY β†’ your Artifact Registry repository name

For example:

docker tag fastapi-contactform australia-southeast1-docker.pkg.dev/my-project/my-repo/fastapi-contactform:latest

Step 4: Push the Image to Artifact Registry

After tagging the image, I pushed it to Google Cloud:

docker push REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/fastapi-contactform:latest

Once the image is in Artifact Registry, Cloud Run can deploy it.

Step 5: Deploy to Cloud Run

This is the part where everything comes together.

I deployed the container with a command like this:

gcloud run deploy fastapi-contactform \
  --image REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/fastapi-contactform:latest \
  --platform managed \
  --region REGION \
  --allow-unauthenticated

Let’s break that down:

  • fastapi-contactform is the Cloud Run service name
  • --image points to the pushed container image
  • --platform managed tells Google to use fully managed Cloud Run
  • --region selects the deployment region
  • --allow-unauthenticated makes the API publicly reachable

Since this API is meant to receive requests from browser-based static sites, public access is usually necessary.

Step 6: Configure Environment Variables

My app depends on environment variables for configuration, so I needed to add those during deployment.

For example:

gcloud run deploy fastapi-contactform \
  --image REGION-docker.pkg.dev/PROJECT_ID/REPOSITORY/fastapi-contactform:latest \
  --platform managed \
  --region REGION \
  --allow-unauthenticated \
  --set-env-vars MAIL_PROVIDER=mailjet,TO_EMAIL=you@example.com,FROM_EMAIL=you@example.com,LOG_LEVEL=INFO \
  --set-env-vars CORS_ALLOWED_ORIGINS=https://mysite.com,https://myothersite.com \
  --set-env-vars TURNSTILE_SECRET=your_turnstile_secret \
  --set-env-vars MAILJET_API_KEY=your_mailjet_api_key \
  --set-env-vars MAILJET_API_SECRET=your_mailjet_api_secret

In a real deployment, I would strongly recommend using a more secure secret management approach instead of putting sensitive values directly in shell history.

But from a learning and first-deployment point of view, this is the easiest way to understand the flow.

Step 7: Use Secret Manager for Sensitive Values

For production, secrets should be handled more carefully.

Good candidates for secret storage include:

  • TURNSTILE_SECRET
  • MAILJET_API_KEY
  • MAILJET_API_SECRET
  • SENDGRID_API_KEY

The reason is simple: these are sensitive credentials and should not live in code.

A better production-style setup is:

  1. Store secrets in Google Secret Manager
  2. Grant Cloud Run access to those secrets
  3. Inject them into the service at deploy time

That keeps the deployment cleaner and safer.

Step 8: Update the Frontend to Use the Cloud Run URL

After deployment, Cloud Run gives you a service URL.

It usually looks something like this:

https://fastapi-contactform-xxxxx-uc.a.run.app

Once I had that URL, I updated my static site contact forms to send requests there.

Example frontend request:

const response = await fetch("https://your-cloud-run-url/contact", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    name: "Gopal",
    email: "gopal@example.com",
    subject: "Hello",
    message: "Testing the form",
    turnstileToken: "token-from-turnstile"
  })
});

That was the final connection point between the static frontend and the deployed backend.

Step 9: Test the Full Contact Form Flow

After deployment, I tested everything from end to end.

My checklist looked like this:

  • Open the static site
  • Fill in the contact form
  • Complete the Turnstile challenge
  • Submit the form
  • Confirm the API returns success
  • Confirm the email arrives in the inbox
  • Check Cloud Run logs if anything fails

This step matters a lot because deployment success does not always mean feature success.

Sometimes the container runs fine, but:

  • CORS is wrong
  • Turnstile validation fails
  • Email provider credentials are missing
  • Sender email is not configured correctly

End-to-end testing catches those problems quickly.

Why Cloud Run Worked Well for This Project

After deploying it, I felt Cloud Run was a strong fit for several reasons.

It matches the project size

This is a very small API. It does not need a full server management setup.

It works naturally with Docker

Since the app was already containerized, the deployment path stayed clean.

It is reusable across many sites

One Cloud Run service can support many static frontend projects.

It removes infrastructure overhead

I can focus on the API and frontend integration instead of server maintenance.

It scales with usage

Some of my static sites may get very little traffic, while others may get more. Cloud Run makes that easier to handle.

Example Deployment Script

Here is a simple example script that shows the overall deployment flow:

PROJECT_ID="your-project-id"
REGION="australia-southeast1"
REPOSITORY="containers"
IMAGE="fastapi-contactform"
SERVICE="fastapi-contactform"

docker build -t $IMAGE .
docker tag $IMAGE $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:latest
docker push $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:latest

gcloud run deploy $SERVICE \
  --image $REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/$IMAGE:latest \
  --platform managed \
  --region $REGION \
  --allow-unauthenticated \
  --set-env-vars MAIL_PROVIDER=mailjet \
  --set-env-vars TO_EMAIL=you@example.com \
  --set-env-vars FROM_EMAIL=you@example.com \
  --set-env-vars LOG_LEVEL=INFO \
  --set-env-vars CORS_ALLOWED_ORIGINS=https://mysite.com \
  --set-env-vars TURNSTILE_SECRET=your_turnstile_secret \
  --set-env-vars MAILJET_API_KEY=your_mailjet_api_key \
  --set-env-vars MAILJET_API_SECRET=your_mailjet_api_secret

This script is not the only way to do it, but it shows the full idea in one place.

Key Considerations

Before deploying a project like this to Google Cloud Run, I would pay attention to a few things:

  • Secrets management: Use Secret Manager for sensitive credentials
  • CORS settings: Only allow trusted frontend domains
  • Email provider setup: Make sure your sender is configured correctly
  • Logging: Check Cloud Run logs when debugging failed submissions
  • Cold starts: For small APIs, occasional cold starts may be acceptable
  • Region choice: Pick a region close to your users or your own infrastructure
  • Custom domain setup: You may want a cleaner API domain later

Recommendations

If you are building a similar reusable backend for static sites, here is what I recommend:

  1. Keep the service small and focused
  2. Containerize it early with Docker
  3. Use environment variables for all configuration
  4. Start with Cloud Run before thinking about more complex infrastructure
  5. Add Secret Manager for production deployments
  6. Test the full contact form flow after every deploy
  7. Reuse the same service across multiple static sites where it makes sense

I also recommend documenting the deployment steps right inside the repo so future updates stay easy.

What This Deployment Changed for Me

Deploying this API to Google Cloud Run made the whole project feel complete.

Before deployment, I had a useful little FastAPI service on my machine. After deployment, I had a real backend that my static sites could actually use.

That changed the workflow a lot.

Now I can:

  • Build a new static site quickly
  • Reuse the same contact form backend
  • Configure allowed origins
  • Connect email delivery
  • Deploy without building a brand new backend every time

That is the real value of this project.

It is not just about one API. It is about creating a small reusable system that supports many static websites in a secure and practical way.

Conclusion

Google Cloud Run turned out to be a very good home for this project.

My FastAPI contact form API was already lightweight, containerized, and stateless, which made the deployment process much smoother. Once the container was pushed and the environment variables were configured, the service was ready to receive form submissions from all of my static sites.

If you want to see the code behind this project, check out the repository here:

GitHub Repository

And if you want the background story of why I built this API in the first place, read the earlier post here:

Build and deploy a simple contact form API with FastAPI and Docker

This was a small deployment, but it solved a real problem for me.

Sometimes that is exactly the kind of project worth building.