React client + FastAPI server, deployed as two Docker images on one GCE VM (Mumbai / asia-south1).
Production uses the same entrypoint as server/Dockerfile:
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]Locally (from server/):
cd server
cp .env.example .env
# fill DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD, API_KEY
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python3 -m uvicorn main:app --reload --host 0.0.0.0 --port 8000- API: http://localhost:8000
- Health: http://localhost:8000/health
- OpenAPI docs: http://localhost:8000/docs
--reload is for local only; do not use it in the Docker image.
cd client
cp .env.example .env # if present; set VITE_API_BASE_URL=http://localhost:8000
npm install
npm run devcp server/.env.example server/.env
# fill DB_* and API_KEY
docker compose up --build- Client: http://localhost (port 80)
- API: http://localhost:8000
- Browser API via nginx proxy: http://localhost/api/...
- GCP project +
gcloudauth - APIs — Cloud Build, Artifact Registry, Compute, Secret Manager
- Secret Manager secret
catalog-service— one JSON withclient+serversections - Artifact Registry repo
catalog-serviceinasia-south1 - GCE VM in
asia-south1-cwith Docker Compose + firewall TCP80 - IAM for Cloud Build SA (push images, read secrets, SSH to VM) and VM SA (pull images)
- Trigger on
main, or rungcloud builds submit --config=cloudbuild.yaml
Pipeline flow:
- Fetch
catalog-service→ writeclient/.env(from.client) +server/.env(from.server) - Build/push client + server images (client bakes Vite env; server stays secret-free)
- MIG restart; instance startup loads
.serverintoserver.envand pulls:latest
gcloud services enable \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com \
compute.googleapis.com \
secretmanager.googleapis.comCreate a single secret named catalog-service. Payload shape:
{
"client": {
"VITE_API_BASE_URL": "/api",
"VITE_GOOGLE_CLIENT_ID": "your-google-client-id.apps.googleusercontent.com"
},
"server": {
"DB_HOST": "x.x.x.x",
"DB_PORT": "5432",
"DB_NAME": "catalog_service",
"DB_USER": "postgres",
"DB_PASSWORD": "...",
"USER_SERVICE_DB_NAME": "user_service",
"API_KEY": "...",
"GOOGLE_CLIENT_ID": "your-google-client-id.apps.googleusercontent.com",
"SESSION_JWT_SECRET": "replace-with-a-long-random-secret",
"SESSION_TTL_HOURS": "24",
"GCS_BUCKET": "your-bucket",
"REGION": "asia-south1",
"SERVICE_CLIENTS": {
"catalog-workflows": "shared-token-also-in-catalog-service-cloud-secret"
}
}
}Do not put GOOGLE_CLOUD_PROJECT in the secret — project comes from ADC on
GCE. REGION is required under server; Cloud Build and instance startup
read it from Secret Manager (not hardcoded in cloudbuild.yaml).
# Create (once), then add versions when config changes
gcloud secrets create catalog-service --replication-policy=automatic
gcloud secrets versions add catalog-service --data-file=your-merged.jsonSERVICE_CLIENTS is the allowlist of machine callers (client-id → token).
fetch-secrets.sh / instance-startup.sh flatten it to SERVICE_CLIENT_<ID> env
vars for the running process; local server/.env stays flat for easy testing.
You can delete the old catalog-service-client / catalog-service-server secrets after migrating.
export PROJECT_ID=$(gcloud config get-value project)
export REGION=asia-south1
export AR_REPO=catalog-service
gcloud artifacts repositories create "${AR_REPO}" \
--repository-format=docker \
--location="${REGION}" \
--description="Catalog service images"export VM_NAME=catalog-service-1
export VM_ZONE=asia-south1-cInstall Docker + Compose on the VM (auto-installs if missing). Deploy uses
SSH via IAP, a short-lived Cloud Build access token for docker login on the VM,
then pull/run — so the VM SA does not need Artifact Registry Reader for image pulls.
Firewall: allow TCP 80. Open 8000 only if you need direct API access.
Grant the VM service account roles/artifactregistry.reader.
Grant PROJECT_NUMBER@cloudbuild.gserviceaccount.com:
roles/secretmanager.secretAccessorroles/artifactregistry.writerroles/compute.instanceAdmin.v1roles/iam.serviceAccountUser- SSH / OS Login access for
gcloud compute ssh/scp
The SSH user on the VM also needs passwordless sudo for mkdir/chown under /opt (common on GCE images).
gcloud builds triggers create github \
--name=catalog-service-main \
--repo-name=catalog-service \
--repo-owner=opptra \
--branch-pattern='^main$' \
--build-config=cloudbuild.yaml \
--substitutions=_AR_REPO=catalog-service,_MIG_NAME=catalog-servicegcloud builds submit --config=cloudbuild.yaml| Image | Port on VM |
|---|---|
catalog-client (nginx) |
80 |
catalog-server (uvicorn) |
8000 |