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:
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:
- Receive a POST request
- Validate the contact form payload
- Verify Cloudflare Turnstile
- Send an email through Mailjet or SendGrid
- 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:
- Build the Docker image
- Push the image to Google Artifact Registry
- Deploy the container to Cloud Run
- Set required environment variables
- Allow public access to the API endpoint
- Point my frontend contact forms to the Cloud Run URL
- 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 regionPROJECT_IDβ your Google Cloud project IDREPOSITORYβ 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-contactformis the Cloud Run service name--imagepoints to the pushed container image--platform managedtells Google to use fully managed Cloud Run--regionselects the deployment region--allow-unauthenticatedmakes 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_SECRETMAILJET_API_KEYMAILJET_API_SECRETSENDGRID_API_KEY
The reason is simple: these are sensitive credentials and should not live in code.
A better production-style setup is:
- Store secrets in Google Secret Manager
- Grant Cloud Run access to those secrets
- 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:
- Keep the service small and focused
- Containerize it early with Docker
- Use environment variables for all configuration
- Start with Cloud Run before thinking about more complex infrastructure
- Add Secret Manager for production deployments
- Test the full contact form flow after every deploy
- 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:
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.