Skip to Content
InfraGCP: internal docs site

GCP docs sites (public bucket + internal Cloud Run + IAP)

Runbook for the two Nextra docs sites built by docs-site/: a public site at oterapp.com/docs and an IAP-protected internal site at internal-docs.oterapp.com.

Overview

SiteURLBackendAccess gate
Publicoterapp.com/docs/GCS backend bucket oterapp.comNone — synced under /docs/
Internalinternal-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=3

2. 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-prod

You 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/