You already run this app on your laptop against the same Gmail inbox. Hosting it means:
POST /api/run, which reads that same Gmail.main builds that Docker image and deploys it. Secrets stay in GCP — GitHub does not need a Google key.| Piece | Where | What it does |
|---|---|---|
| Dashboard + poller | Cloud Run (one container) | Website + POST /api/run |
| Schedule | Cloud Scheduler | Hits /api/run every 5 minutes |
| Database | Neon Postgres (free) | Jobs survive when the container sleeps |
| Gmail | Your existing secrets/token.json |
Same mailbox as localhost |
| Profile | Your existing config/profile.yaml |
Same titles / skills |
Do not upload .env, secrets/, or config/profile.yaml to GitHub. Cloud Run gets those as secrets.
C:\projects\inbox-job-agentsecrets\token.json (created when you ran Gmail login locally)config\profile.yaml (your real profile).env with GEMINI_API_KEY (and GEMINI_API_KEY_2 / GROQ_API_KEY if you use them)If token.json is missing, run this on the laptop first, then come back:
cd C:\projects\inbox-job-agent
.\.venv\Scripts\python.exe -m app.auth_setup
Use the same Google account you want the cloud app to read.
Google requires a credit card to turn on billing, even if you stay in the free tier. Cloud Run with max-instances 1 and min instances 0 is meant to stay inside the free allowance for a personal dashboard. Neon is free. You are billed only if the container runs for many hours or you raise memory a lot.
If you never want a card on file, keep polling on this laptop. There is no card-free Cloud Run path.
Cloud Run’s disk is wiped when the service sleeps. SQLite from your laptop will not work in the cloud. Use Neon.
Open the connection string. It looks like:
postgresql://USER:PASSWORD@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
Change only the start from postgresql:// to postgresql+psycopg://:
postgresql+psycopg://USER:PASSWORD@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require
DATABASE_URL. Tables are created on first run.Optional check on this laptop:
cd C:\projects\inbox-job-agent
$env:DATABASE_URL = "postgresql+psycopg://USER:PASSWORD@HOST/neondb?sslmode=require"
.\.venv\Scripts\python.exe -m app.run report --days 1
An empty report (not an error) means Neon works. This does not copy your local SQLite jobs into Neon. The cloud dashboard starts empty and fills as Gmail is polled.
inbox-job-agent. Create.If you already created a project named inbox-job-agent for Gmail OAuth, reuse it. Do not make a second project.
The Project ID in Home (often inbox-job-agent or inbox-job-agent-123456) is what you type in gcloud commands. It is not always identical to the display name.
On the OAuth consent screen for that project: if the app is still Testing, Google kills the Gmail refresh token after 7 days. Publish the app (Personal use / External, no verification needed). You will see an “unverified app” warning once; that is your own desktop client reading your own mail.
gcloud to PATH.gcloud --version
If that fails, reopen the terminal again (PATH only updates in new windows).
In PowerShell:
cd C:\projects\inbox-job-agent
gcloud auth login
A browser window opens. Sign in with the same Google account, then allow access.
gcloud projects list
Copy the PROJECT_ID for inbox-job-agent, then:
gcloud config set project YOUR_PROJECT_ID
gcloud config get-value project
The second command must print YOUR_PROJECT_ID. Then enable the services this deploy uses:
gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com cloudscheduler.googleapis.com secretmanager.googleapis.com
Wait until it finishes (about a minute).
The Docker image does not contain your token or profile (see .dockerignore). Cloud Run reads them from Secret Manager.
cd C:\projects\inbox-job-agent
gcloud secrets create gmail-token --data-file=secrets\token.json
gcloud secrets create profile-yaml --data-file=config\profile.yaml
If Google says the secret already exists:
gcloud secrets versions add gmail-token --data-file=secrets\token.json
gcloud secrets versions add profile-yaml --data-file=config\profile.yaml
That token.json is the login you already did on this PC. The cloud app reads the same inbox. You do not log into Gmail again unless the token expires.
This is API_TOKEN. Anyone who knows it can open the site and trigger a poll.
In PowerShell:
-join ((65..90) + (97..122) + (48..57) | Get-Random -Count 24 | ForEach-Object {[char]$_})
Copy the result. Do not use commas in it.
Stay in C:\projects\inbox-job-agent. Replace YOUR_API_TOKEN with the password from step 6.
gcloud run deploy inbox-job-agent `
--source . `
--region us-east1 `
--allow-unauthenticated `
--timeout 900 `
--memory 1Gi `
--cpu 1 `
--max-instances 1 `
--set-env-vars "LLM_PROVIDER=gemini,GMAIL_QUERY=in:inbox -category:promotions,MIN_JOB_SCORE=0.45,API_TOKEN=YOUR_API_TOKEN" `
--set-secrets "GMAIL_TOKEN_JSON=gmail-token:latest,PROFILE_YAML=profile-yaml:latest"
--source . uploads this folder and builds Dockerfile in Cloud Build. You never run docker build yourself.
The first deploy can take 5–10 minutes. When it works, it prints a URL:
https://inbox-job-agent-xxxxx-ue.a.run.app
Save that URL.
On Home in Cloud Console, copy Project number (digits, not the project id). Then:
gcloud secrets add-iam-policy-binding gmail-token --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" --role="roles/secretmanager.secretAccessor"
gcloud secrets add-iam-policy-binding profile-yaml --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" --role="roles/secretmanager.secretAccessor"
Run the gcloud run deploy command again.
The first deploy only has Gmail + profile + a few flags. Add the rest in the browser so you do not fight quoting in PowerShell.
| Name | Value |
|---|---|
DATABASE_URL |
Neon string from step 1 (postgresql+psycopg://...) |
GEMINI_API_KEY |
from your local .env |
GEMINI_API_KEY_2 |
optional second AI Studio key |
GROQ_API_KEY |
optional, from .env |
NVIDIA_API_KEY |
optional |
OPENROUTER_API_KEY |
optional |
LLM_CHAIN_CLASSIFY |
gemini,gemini2,groq if you have two Gemini keys |
LLM_CHAIN_EXTRACT |
gemini,gemini2,groq,nvidia,openrouter |
Leave GMAIL_TOKEN_JSON and PROFILE_YAML as secrets from step 7.
API_TOKEN).The first Mail page can be empty until a check runs. That is Neon, not your laptop SQLite.
This is the whole cloud schedule. No Pub/Sub. Cloud Scheduler calls POST /api/run.
A new Cloud Run revision (each push to main) plants the cursor at deploy time, so old mail is not backfilled.
Replace YOUR_API_TOKEN with the dashboard password:
gcloud scheduler jobs create http inbox-job-agent-poll `
--location us-east1 `
--schedule "*/5 * * * *" `
--time-zone "America/New_York" `
--uri "https://inbox-job-agent-244210842384.us-east1.run.app/api/run" `
--http-method POST `
--headers "x-api-token=YOUR_API_TOKEN" `
--attempt-deadline 900s
If the job already exists:
gcloud scheduler jobs update http inbox-job-agent-poll `
--location us-east1 `
--schedule "*/5 * * * *" `
--update-headers "x-api-token=YOUR_API_TOKEN"
After GitHub is connected in Cloud Run (next section), each push to main rebuilds the Docker image and starts a new revision. Update the scheduler once; it is not recreated by the image build.
Do not put a Google key in GitHub. Secrets already on Cloud Run stay there.
Do not click Connect on the Cloud Run Overview page. That button creates a second service (inbox-job-agent-git, often in europe-west1) with no secrets. Your real dashboard is already:
https://inbox-job-agent-244210842384.us-east1.run.app
Service name inbox-job-agent, region us-east1. Open that service, then Set up continuous deployment.
You have two logins. Keep both. Do not try to make them the same.
| Where | Account | What it is |
|---|---|---|
| Google Cloud | naveen.morla04@gmail.com |
Project inbox-job-agent, Cloud Run, secrets |
| GitHub | user naveenmorla1901 |
Repo inbox-job-agent |
Google Cloud never “sees” the GitHub email. It only needs you to install Google’s GitHub app on the GitHub user that owns the repo. That is a popup. In that popup you must be naveenmorla1901, not the GCP Gmail.
naveenmorla1901. Confirm you can open github.com/naveenmorla1901/inbox-job-agent.naveen.morla04@gmail.com in this window.naveen.morla04@gmail.com.inbox-job-agent-git.inbox-job-agent-git).Google opens a GitHub window. Look at the GitHub username in the top-right. It must say naveenmorla1901.
naveen.morla04@gmail.com: click Install the GitHub App on another GitHub account (wording may be Switch account). Sign in as naveenmorla1901.inbox-job-agent only.Back in Cloud Run you should now see repository naveenmorla1901/inbox-job-agent. If the repo list is empty, you installed the app on the wrong GitHub user. Repeat C.
| Field | Value |
|---|---|
| Repository | naveenmorla1901/inbox-job-agent |
| Branch | main (or ^main$) |
| Build type | Dockerfile, or Cloud Build configuration file /cloudbuild.yaml |
| Source directory | / (leave default) |
Save. Do not change env vars or secrets on this screen if it shows them.
main.You can delete GitHub secrets GCP_SA_KEY and API_TOKEN. They are not used for this.
If the repo still does not appear: GitHub → your user naveenmorla1901 → Settings → Applications → Installed GitHub Apps → Google Cloud Build or Developer Connect → Configure → grant inbox-job-agent.
fetchReadToken / 403GitHub is already connected. Cloud Build just cannot read the repo until you grant one IAM role. This is not the Gmail vs GitHub email issue.
The error looks like:
Permission 'developerconnect.gitRepositoryLinks.fetchReadToken' denied
Do this in PowerShell (same GCP login as before):
gcloud config set project inbox-job-agent
gcloud projects add-iam-policy-binding inbox-job-agent `
--member="serviceAccount:244210842384-compute@developer.gserviceaccount.com" `
--role="roles/developerconnect.readTokenAccessor"
gcloud projects add-iam-policy-binding inbox-job-agent `
--member="serviceAccount:244210842384@cloudbuild.gserviceaccount.com" `
--role="roles/developerconnect.readTokenAccessor"
gcloud projects add-iam-policy-binding inbox-job-agent `
--member="serviceAccount:service-244210842384@gcp-sa-cloudbuild.iam.gserviceaccount.com" `
--role="roles/developerconnect.readTokenAccessor"
Or in the browser: IAM & Admin → Grant access → paste each of those three emails → role Developer Connect Read Token Accessor → Save.
Then retry (do not reconnect GitHub):
cloudrun-inbox-job-agent-... trigger → Run.A green build should then Pull → Build → Push → Deploy. Your Cloud Run service stays in us-east1; the trigger name can say europe-west1 because that is where Developer Connect stored the GitHub link. That is fine.
Connecting GitHub from Overview created a second website. Only the first one has Gmail.
| Service | Region | URL | Use it? |
|---|---|---|---|
inbox-job-agent |
us-east1 | https://inbox-job-agent-244210842384.us-east1.run.app | Yes — Gmail, Neon, password |
inbox-job-agent-git |
europe-west1 | …europe-west1.run.app |
No — empty copy from Connect |
gcloud config set project inbox-job-agent
gcloud run services update inbox-job-agent --region us-east1 --update-secrets "GMAIL_TOKEN_JSON=gmail-token:latest,PROFILE_YAML=profile-yaml:latest"
Or in the browser: Cloud Run → inbox-job-agent (us-east1) → Edit & deploy new revision → Variables & secrets → Reference a secret → secret gmail-token → Exposed as environment variable named GMAIL_TOKEN_JSON → version latest → Deploy.
gcloud run services delete inbox-job-agent-git --region europe-west1
In the console: Cloud Run → inbox-job-agent-git → Delete.
Do not copy secrets onto inbox-job-agent-git. Keep one service in us-east1.
If the secret gmail-token was never created, on the laptop:
gcloud secrets create gmail-token --data-file=secrets\token.json
If it already exists:
gcloud secrets versions add gmail-token --data-file=secrets\token.json
Then run the gcloud run services update command above.
docker build -t inbox-job-agent .
docker run --rm -p 8080:8080 -e PORT=8080 -e API_TOKEN=dev inbox-job-agent
Open http://localhost:8080 — that is the Cloud Run container, locally.
Profile still is not in git. After you edit config\profile.yaml:
gcloud secrets versions add profile-yaml --data-file=config\profile.yaml
gcloud run services update inbox-job-agent --region us-east1 --update-secrets PROFILE_YAML=profile-yaml:latest
| Symptom | Likely cause |
|---|---|
| Deploy asks for billing | Step 2: link a billing account |
gcloud is not recognized |
Reopen PowerShell after installing the SDK |
| Secret permission error | Step 7 IAM binding with project number |
Check now: No Gmail token |
You opened inbox-job-agent-git (europe-west1). Use the us-east1 URL. Attach secret gmail-token as GMAIL_TOKEN_JSON (Step 11 G). |
invalid_grant / Gmail auth error |
Re-run python -m app.auth_setup locally, then update gmail-token |
| Token dies after a week | Publish the OAuth consent screen (not Testing) |
401 on /api/run |
Scheduler header x-api-token does not match API_TOKEN |
| Site works, no new mail | Scheduler missing, or Gmail query too narrow |
| GitHub connect does not list the repo | You authorized the GitHub user for the GCP Gmail. Install the app on naveenmorla1901. Use Incognito. Click Install on another GitHub account. |
Build fails in ~10s with fetchReadToken / 403 |
GitHub is connected. Grant Developer Connect Read Token Accessor to the Cloud Build accounts (Step 11 F), then retry the build. |
| GitHub “Node.js 20 is deprecated” | Harmless warning. The Docker workflow does not use that Google action. |