Guides · Cloud Run & Google Cloud · Published 2026-09-12 · 2 min read

Deploy a Node.js Express App to Google Cloud Run from Source — Dockerfile, PORT, Scale to Zero, and the npm Script That Makes Redeploys One Command

A minimal, production-ready way to run an Express app on Cloud Run: the Dockerfile, reading PORT, a deploy script in package.json, scaling to zero with a small memory limit, and the mistakes that cost the first hour.

Cloud Run is the least-effort way to put a small Node.js site on the internet with HTTPS, autoscaling and a bill that rounds to zero when nobody visits. This guide shows the exact setup we use for a family of Express sites, deployed straight from source with one command.

The app must read PORT

Cloud Run injects the port at runtime. Hard-coding 3000 is the most common first failure ("container failed to start and listen on the port defined by PORT").

const PORT = parseInt(process.env.PORT, 10) || 3000;
app.listen(PORT, () => console.log(`listening on ${PORT}`));

Also add a cheap health route. Do not call it /healthz: on Cloud Run that exact path is intercepted at Google's edge and never reaches your container (see the /healthz gotcha). /health works.

Dockerfile

FROM node:22-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "app.js"]

Copying package*.json first lets Docker cache the npm ci layer, so a content-only change rebuilds in seconds. Add a .dockerignore with node_modules, .env, .git and anything else that should not ship.

One-command deploy from package.json

"scripts": {
  "deploy": "gcloud run deploy my-site --source . --project my-project --region us-central1 --allow-unauthenticated --set-env-vars SITE_HOST=example.com --min-instances 0 --max-instances 2 --memory 256Mi --quiet"
}

--source . uploads the folder, builds the image with Cloud Build, pushes it and deploys, all in one step. --quiet suppresses prompts so it runs from scripts. --min-instances 0 scales to zero; --memory 256Mi is plenty for an Express site serving Markdown. Then:

npm run deploy

The first run takes a few minutes; later runs usually finish in about two.

Environment variables and secrets

Pass non-secret config with --set-env-vars. Put real secrets in Secret Manager and reference them with --set-secrets NAME=secret:latest; never bake them into the image or commit .env. Locally, load .env with dotenv as the first line of the app so the same code works in both places.

Verify

curl -sI https://my-site-xxxx.us-central1.run.app/health

Look for HTTP/2 200 and the x-cloud-trace-context header, which proves the response came from your container rather than Google's edge.

Costs

With scale to zero, a low-traffic site costs a few cents a month at most: you pay only for request time and a small amount for container image storage. Keeping --max-instances small protects you from a traffic spike or a bot turning into a bill.

Common mistakes

Summary

Read PORT, add a /health route, use a two-stage-copy Dockerfile, and put the full gcloud run deploy --source . command in an npm script. From then on every change is npm run deploy.

Related guides