GCP web deploy (Artifact Registry and Cloud Run)
Runbook for shipping the Next.js web app (
web-frontend/) to Artifact Registry and Cloud Run from GitHub Actions, using the same Workload Identity Federation as the API server.
Overview
| Step | Tool |
|---|---|
| Build | web-frontend/Dockerfile (standalone Next.js) |
| Registry | Same Artifact Registry repo as the server (GCP_AR_REPOSITORY), image name oter-web |
| Deploy | gcloud run deploy → public HTTPS URL (map custom domain in Cloud Run console) |
| CI workflow | .github/workflows/publish-web-release.yml |
NEXT_PUBLIC_* URLs are baked in at image build time (GitHub secrets or variables). Point them at your production API (Cloud Run server URL or custom API host).
What changed from the old path
| Old (legacy) | New (GCP) |
|---|---|
DigitalOcean Container Registry (DOCR) | Artifact Registry |
EC2 scp + web-deploy.sh + docker-compose oter-web | gcloud run deploy |
Secrets DOCR_*, EC2_* for web release | Reuse GCP_WIF_* + GCP_* variables |
The VM stack (infra/docker-compose.yml, infra/nginx.conf) may still run nginx or other services; production web traffic should use Cloud Run once DNS is mapped.
Prerequisites
- Same GCP project setup as gcp-server-cloud-run.md (APIs, Artifact Registry, WIF).
- Cloud Run service for the web app (create on first deploy or ahead of time), port 3000, public ingress if the app is user-facing.
- Local/production build passes:
cd web-frontend && npm ci && npm run build.
GitHub configuration
Use the same Settings → Secrets and variables → Actions page as the server. WIF secrets are shared; web adds one required variable and build-time API URLs.
Secrets (Actions → Secrets)
| Secret | Required | Notes |
|---|---|---|
GCP_WIF_PROVIDER | Yes | Same as server — full WIF provider resource name |
GCP_WIF_SERVICE_ACCOUNT | Yes | Same deployer SA as server |
NEXT_PUBLIC_API_URL | Recommended | Production API base, e.g. https://api.estebanruano.com/api/v1. Can use a variable instead if you prefer. |
NEXT_PUBLIC_WS_URL | No | Explicit timer WebSocket base; omit to derive from API URL |
NEXT_PUBLIC_SOCKETS_URL | No | Socket.IO URL; omit to derive from API URL |
The workflow falls back to https://api.estebanruano.com/api/v1 for NEXT_PUBLIC_API_URL when unset; set the secret or variable explicitly before the first production deploy.
Variables (Actions → Variables)
| Variable | Required | Example / note |
|---|---|---|
GCP_PROJECT_ID | Yes | Same as server, e.g. oter-490318 |
GCP_REGION | Yes | e.g. us-east1 |
GCP_AR_REPOSITORY | Yes | Same Artifact Registry Docker repo id as server |
CLOUD_RUN_WEB_SERVICE | Yes | Cloud Run service name, e.g. oter-web |
GCP_WEB_RUNTIME_SERVICE_ACCOUNT | No | Runtime SA email; omit if the default Cloud Run identity is enough |
NEXT_PUBLIC_API_URL | No | Alternative to secret (public URL) |
NEXT_PUBLIC_WS_URL | No | Alternative to secret |
NEXT_PUBLIC_SOCKETS_URL | No | Alternative to secret |
Example files: infra/gcp/web.env.example, web-frontend/.env.example.
One-time checklist
- Confirm server WIF and
GCP_*variables already work (see server runbook). - Add
CLOUD_RUN_WEB_SERVICE=oter-web(or your chosen service name). - Set
NEXT_PUBLIC_API_URL(secret or variable) to the API host, not the web app host. - Run Actions → Publish Web Release → Run workflow and select the branch (e.g.
release/web/<version>ormain). - Map custom domain in Cloud Run → Manage custom domains (production:
oterapp.com→ serviceoter-web).
Runtime service account (optional)
Create oter-web-runtime@PROJECT.iam.gserviceaccount.com if the web app needs GCP APIs at runtime (usually not for a static/SSR Next app). For a public site with no GCP calls, Cloud Run’s default identity may suffice — set GCP_WEB_RUNTIME_SERVICE_ACCOUNT only when you attach a dedicated SA.
Grant the deployer SA (Service Account User on the web runtime SA) if you set one.
Custom domain (oterapp.com)
Mapping is configured in GCP: oterapp.com → Cloud Run service oter-web (us-east1).
DNS at your registrar (apex / root)
Add these records for oterapp.com (@):
| Type | Name | Value |
|---|---|---|
| A | @ | 216.239.32.21 |
| A | @ | 216.239.34.21 |
| A | @ | 216.239.36.21 |
| A | @ | 216.239.38.21 |
| AAAA | @ | 2001:4860:4802:32::15 |
| AAAA | @ | 2001:4860:4802:34::15 |
| AAAA | @ | 2001:4860:4802:36::15 |
| AAAA | @ | 2001:4860:4802:38::15 |
Remove any old A/CNAME records that still point at a VM, nginx, or DigitalOcean.
After DNS propagates, Google provisions the managed TLS certificate (often 15–60 minutes). Status:
gcloud beta run domain-mappings describe --domain=oterapp.com --region=us-east1Look for CertificateProvisioned: True and Ready: True.
Optional: www.oterapp.com
Create a second mapping (gcloud beta run domain-mappings create --service=oter-web --domain=www.oterapp.com --region=us-east1) and add the CNAME record the console shows (usually ghs.googlehosted.com).
API URL
NEXT_PUBLIC_API_URL in CI must point at the API host (api.estebanruano.com or Cloud Run server URL), not oterapp.com.
Triggers
Manual only: Actions → Publish Web Release → Run workflow (workflow_dispatch). Select the git ref to build from (release branch, tag, or main).
Verify
gcloud run services describe "${CLOUD_RUN_WEB_SERVICE}" \
--region="${GCP_REGION}" --format='value(status.url)'
curl -fsS "$(gcloud run services describe oter-web --region=us-east1 --format='value(status.url)')/api/health"Related
- gcp-server-cloud-run.md — API server on Cloud Run
- automation-options.md — automation/workflow tooling options and cost analysis
- docr-quota-fix.md — legacy DOCR (no longer used for web CI)