Deploy to Cloud Run
Your site works locally. Now turn it into a public URL anyone can hit. Cloud Run takes your code, builds a container, runs it on demand, and scales to zero when nobody's looking — you only pay for actual traffic.
Pre-flight check
Before you run the deploy, confirm the things you set up in step 1 are still in place. Two commands:
gcloud config get-value project
gcloud auth list
The first prints your project ID. The second shows the active Google account marked with *. If either is wrong, fix it before you deploy or you'll burn credits in the wrong place.
Review the Dockerfile
The starter already includes a multi-stage Dockerfile. You don't need to edit it, but it's worth understanding what Cloud Build will execute.
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
In plain terms: stage 1 installs only production dependencies in a clean Node 20 image; stage 2 copies those deps and the app source into a fresh slim image, exposes port 3000, and starts the server with node server.js.
Already comfortable with multi-stage Docker builds? Skip ahead to the deploy section.
Grant Cloud Build permission to deploy
The first time you deploy from source on a fresh project, Google needs the Cloud Build service account to be allowed to create a Cloud Run service on your behalf. The official way to do this is one IAM binding:
PROJECT_ID=$(gcloud config get-value project)
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="roles/run.builder"
You only need to do this once per project. If you skip it, the first gcloud run deploy --source call will fail with a permission error pointing at this exact role.
Deploy the site
From the project root (the starter directory), run:
gcloud run deploy --source .
--source . tells Cloud Run to package the current directory and ship it to Cloud Build, which builds the container, pushes it to Artifact Registry, and rolls it out as a Cloud Run service.
The first time you run it, gcloud asks four questions in order. Recommended answers for the workshop:
- Service name — press Enter to accept the default (the directory name) or type something like
career-site. - Region — pick a region close to you (
europe-west1for most of Europe,us-central1for the US). Cloud Run will remember this default for next time. - Create an Artifact Registry repository? — answer
Y. This is where your container image will live. - Allow unauthenticated invocations? — answer
yfor a public website.
The first deploy takes 2–3 minutes — Cloud Build pulls base images, installs dependencies, and pushes the layers fresh. Subsequent deploys are much faster because the layers are cached.
Verify the public URL
When the deploy finishes, gcloud prints a Service URL that looks like:
Service URL: https://career-site-xxxxxxxx-ew.a.run.app
Open it in your browser and check:
- The page loads with your design and the content from
persona.json. - The FAQ section is visible.
- It works on mobile — pull it up on your phone.
Share the URL in the workshop chat. This is your live career site.
One-line redeploy after changes
Once you've answered the prompts the first time, gcloud remembers your defaults. Iteration deploys are now a single command:
gcloud run deploy --source .
Press Enter through the prompts and Cloud Build does the rest.
Or: explicit one-shot command
Prefer to script it without prompts? This is the explicit form. Useful when you want to wire deploy into a CI job later:
gcloud run deploy career-site \
--source . \
--region europe-west1 \
--allow-unauthenticated \
--set-env-vars "GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project),LOCATION=europe-west1"
The env vars are pre-wired for the bonus chatbot — they cost nothing if you never use them.
Or: use the bundled deploy script
The starter ships a deploy.sh wrapper that runs the explicit form above plus an APIs sanity-check. From inside starter/:
./deploy.sh
It exists so you can re-deploy without remembering flags. Look inside the file if you're curious — it's about 30 lines of shell.
Watch the logs (optional)
If something looks off after deploy, stream the recent logs from your service:
gcloud run services logs read career-site --region europe-west1 --limit 20
Optional: custom domain
Skip this if you don't already own a domain. Cloud Run can serve your site from a custom domain via domain mappings:
gcloud run domain-mappings create \
--service career-site \
--domain your-domain.com \
--region europe-west1
You'll need to add the DNS records Cloud Run shows you with your domain registrar. Propagation can take a few minutes to a few hours.
If something goes wrong
- "Permission denied on Cloud Build service account" — you skipped the IAM binding above. Run it, then retry.
- "API has not been used in project" — re-run the API enable from step 1; it sometimes takes a minute to propagate.
- "No active configuration" —
gcloud config set project YOUR_PROJECT_ID. - Container fails to start with a 500 — your app must listen on the port from the
PORTenv var (defaults to 8080 on Cloud Run). Checkserver.jsusesprocess.env.PORT || 3000. - Cloud Build fails on
npm ci— yourpackage-lock.jsonis out of sync. Runnpm installlocally, commit the lock, redeploy.
The full troubleshooting page is at troubleshooting.
This deploy is intentionally public so you can share the URL. If you're curious about deploying private services with auth and IAM, the Deploy a Secure MCP Server on Cloud Run codelab walks through the same flow with --no-allow-unauthenticated and identity tokens.
gcloud run deploy --source .takes you from local code to a public HTTPS URL in one command.- Cloud Build packages the container; Cloud Run runs it and scales to zero when idle.
- One-time IAM grant for Cloud Build's service account is the most common first-deploy gotcha.
- Default region is remembered after the first deploy — re-deploys become a single Enter-through call.
- The bundled
./deploy.shwraps the explicit form for convenience.