5

Deploy to Cloud Run

20 min

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:

  1. Service name — press Enter to accept the default (the directory name) or type something like career-site.
  2. Region — pick a region close to you (europe-west1 for most of Europe, us-central1 for the US). Cloud Run will remember this default for next time.
  3. Create an Artifact Registry repository? — answer Y. This is where your container image will live.
  4. Allow unauthenticated invocations? — answer y for a public website.
First deploy is slow

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:

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

Common deploy errors
  • "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 PORT env var (defaults to 8080 on Cloud Run). Check server.js uses process.env.PORT || 3000.
  • Cloud Build fails on npm ci — your package-lock.json is out of sync. Run npm install locally, commit the lock, redeploy.

The full troubleshooting page is at troubleshooting.

Want to lock the door?

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.

Key takeaways
  • 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.sh wraps the explicit form for convenience.