Before I deployed the GeriCare DNB Portal, my cloud experience was limited to clicking through AWS consoles and following tutorials. Cloud Run was the first time I actually owned a production deployment end to end โ writing the Dockerfile, configuring the service, debugging cold starts, and watching real users hit it.
This is the guide I wish I'd had. Not a rehash of the official docs, but the actual steps, the actual errors, and the things the docs don't mention.
Why Cloud Run?
The client already used Google Cloud Platform for other services. Cloud Run specifically was a good fit because:
- It runs any containerised app โ no vendor-specific framework required
- It scales to zero between requests (cheap for a low-traffic internal tool)
- HTTPS is handled automatically, no cert management
- Deployments are atomic โ roll back with one command if something breaks
Step 1: Write a production Dockerfile
The biggest mistake beginners make is using a development image in production. Here's the Dockerfile I settled on after a few iterations:
# Use specific version, not 'latest'
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
# Runtime stage โ smaller final image
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app .
ENV NODE_ENV=production
EXPOSE 8080
CMD ["node", "server.js"]
const PORT = process.env.PORT || 8080 in your server file.Step 2: Build and push to Artifact Registry
# Authenticate
gcloud auth configure-docker asia-south1-docker.pkg.dev
# Build for the correct platform (Cloud Run runs on linux/amd64)
docker build --platform linux/amd64 -t asia-south1-docker.pkg.dev/PROJECT_ID/REPO/app:v1 .
# Push
docker push asia-south1-docker.pkg.dev/PROJECT_ID/REPO/app:v1
--platform linux/amd64 flag is critical. Without it you'll push an ARM image that silently fails on Cloud Run's x86 infrastructure.Step 3: Deploy the service
gcloud run deploy dnb-portal --image asia-south1-docker.pkg.dev/PROJECT_ID/REPO/app:v1 --region asia-south1 --platform managed --allow-unauthenticated --set-env-vars NODE_ENV=production --set-secrets JWT_SECRET=jwt-secret:latest --memory 512Mi --cpu 1 --min-instances 0 --max-instances 3
The --set-secrets flag pulls from GCP Secret Manager at startup โ no environment variable file needed, no secrets in your repository.
Connecting to Neon DB (PostgreSQL)
Neon DB gives you a standard PostgreSQL connection string. Cloud Run services can connect directly over the internet with SSL. I stored the connection string in Secret Manager and pulled it at startup:
const { Pool } = require('pg');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
max: 5, // keep pool small โ Cloud Run scales horizontally
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000
});
max too high. Each Cloud Run instance gets its own connection pool. If you have 3 instances running and max: 20, you can hit 60 simultaneous connections โ enough to exhaust Neon's free tier limits.The gotchas I hit
Cold starts. With --min-instances 0, the first request after idle takes 2โ4 seconds as the container boots. For an internal tool, that's fine. For a user-facing product, set --min-instances 1 to keep one instance warm.
Request timeout. Cloud Run's default request timeout is 60 seconds. Long-running operations (I had a PDF generation endpoint) need --timeout 300.
Missing PORT environment variable. Cloud Run injects PORT=8080 โ your app must read it with process.env.PORT.
Continuous deployment
I set up a simple GitHub Actions workflow so every push to main rebuilds and redeploys:
- name: Build and Deploy
run: |
docker build --platform linux/amd64 -t $IMAGE .
docker push $IMAGE
gcloud run deploy $SERVICE --image $IMAGE --region $REGION
Cloud Run is genuinely excellent for this kind of deployment. The combination of zero infrastructure management, automatic HTTPS, and scale-to-zero billing made it the right call for GeriCare's needs โ and I'd reach for it again on the next project.