GCP docs sites (public bucket + internal Cloud Run + IAP)
Runbook for the two Nextra docs sites built by
docs-site/: a public site atoterapp.com/docsand an IAP-protected internal site atinternal-docs.oterapp.com.
Overview
| Site | URL | Backend | Access gate |
|---|---|---|---|
| Public | oterapp.com/docs/ | GCS backend bucket oterapp.com | None — synced under /docs/ |
| Internal | internal-docs.oterapp.com/ | Cloud Run service oter-docs-internal (nginx + static export) | Cloud IAP on the backend service |
Both are built by .github/workflows/publish-docs-release.yml from the same docs-site/ Nextra project; the DOCS_VARIANT env var picks the content tree.
Why Cloud Run for internal, not a GCS backend bucket? Cloud IAP doesn’t support backend buckets — Google Issue Tracker #114133245 , open since 2018. IAP on backend services (with Cloud Run via serverless NEG) is the supported pattern. Backend buckets work fine for the unauthenticated public site.
Public site
No new infra. The CI rsyncs docs-site/out (built with DOCS_VARIANT=public and basePath: /docs) into gs://oterapp.com/docs/. The existing Load Balancer / CDN that already serves oterapp.com from that bucket covers /docs/* automatically.
Verify after a deploy:
curl -I https://oterapp.com/docs/
curl -I https://oterapp.com/docs/getting-started/Both should return 200. Hashed assets under /docs/_next/static/ carry Cache-Control: public, max-age=31536000, immutable; HTML carries max-age=0, must-revalidate.
Internal site (one-time GCP setup)
The CI workflow handles build + image push + gcloud run deploy. These commands wire up the front door (LB + IAP) and only need to be run once per environment.
Set the variables you’ll reuse below:
export PROJECT_ID="oter-490318"
export REGION="us-east1"
export DOMAIN="internal-docs.oterapp.com"1. Create the Cloud Run service (or let the workflow create it)
The first run of Publish Docs Release with variants=internal will create the service. If you want to pre-create an empty one:
gcloud run deploy oter-docs-internal \
--image=us-docker.pkg.dev/cloudrun/container/hello \
--region="${REGION}" --project="${PROJECT_ID}" \
--platform=managed --no-allow-unauthenticated \
--port=8080 --cpu=1 --memory=256Mi --min-instances=0 --max-instances=32. Create the serverless NEG pointing at Cloud Run
gcloud compute network-endpoint-groups create oter-docs-internal-neg \
--region="${REGION}" \
--network-endpoint-type=serverless \
--cloud-run-service=oter-docs-internal \
--project="${PROJECT_ID}"3. Create the backend service and wire the NEG to it
gcloud compute backend-services create oter-docs-internal-backend-service \
--global --load-balancing-scheme=EXTERNAL_MANAGED \
--project="${PROJECT_ID}"
gcloud compute backend-services add-backend oter-docs-internal-backend-service \
--global \
--network-endpoint-group=oter-docs-internal-neg \
--network-endpoint-group-region="${REGION}" \
--project="${PROJECT_ID}"4. Swap the URL map’s default service from the old backend bucket to the new backend service
gcloud compute url-maps set-default-service oter-docs-internal-urlmap \
--default-service=oter-docs-internal-backend-service \
--global --project="${PROJECT_ID}"5. Enable IAP on the backend service
First time only: configure the OAuth consent screen in the GCP console (APIs & Services → OAuth consent screen). For a single-account setup pick External type, leave it in Testing mode, and add your accounts as Test users.
Then create an OAuth 2.0 Client ID (Web application type) and enable IAP:
gcloud iap web enable \
--resource-type=backend-services \
--oauth2-client-id="<CLIENT_ID>" \
--oauth2-client-secret="<CLIENT_SECRET>" \
--service=oter-docs-internal-backend-service \
--project="${PROJECT_ID}"Grant your user the IAP-secured Web App User role:
gcloud iap web add-iam-policy-binding \
--resource-type=backend-services \
--service=oter-docs-internal-backend-service \
--member=user:esteban505r@gmail.com \
--role=roles/iap.httpsResourceAccessor \
--project="${PROJECT_ID}"6. Verify
curl -I https://internal-docs.oterapp.com/
# Expect: 302 redirect to https://accounts.google.com/...Open the URL in an incognito window → Google sign-in → docs load.
Decommissioning the old backend bucket
If you previously set up oter-docs-internal-prod and the broken backend-bucket IAP, clean up:
# Remove the bucket backend from the URL map (step 4 above already replaces it as default)
gcloud compute backend-buckets delete oter-docs-internal-backend \
--project="${PROJECT_ID}" --quiet
# Re-secure the (now unused) bucket before deletion
gcloud storage buckets remove-iam-policy-binding gs://oter-docs-internal-prod \
--member=allUsers --role=roles/storage.objectViewer 2>/dev/null || true
gcloud storage buckets update gs://oter-docs-internal-prod \
--public-access-prevention 2>/dev/null || true
# Delete the bucket once you've confirmed Cloud Run is serving
gcloud storage rm --recursive gs://oter-docs-internal-prod
gcloud storage buckets delete gs://oter-docs-internal-prodYou can also drop the GCP_DOCS_INTERNAL_BUCKET repo variable in GitHub — the new workflow doesn’t reference it.
Local development
cd docs-site
DOCS_VARIANT=internal npm run dev
# Or test the production image locally:
docker build -f docs-site/Dockerfile -t docs-internal:local ..
docker run --rm -p 8080:8080 docs-internal:local
# Open http://localhost:8080/