Heroku was the best way to deploy a web app for years. It still is — if your team is pre-product-market-fit and spending money on infrastructure feels like waste. But once you’re past that stage, the math changes fast. A standard Heroku dyno costs $25/month. Two dynos for redundancy: $50/month. Add Heroku Postgres at Standard-0: another $50/month. You’re at $100+ before traffic spikes, addons, or real scale.

Google Cloud Run charges only for requests served. A medium-traffic startup with 10M requests/month and 256MB-second compute time runs roughly $20–40. Zero-downtime rollout and rollback are built in. And if you’re already in GCP for other services, one less cloud to manage.

This guide walks you through the actual migration — not the “read the docs and figure it out” version, but the sequence that works.

Step 1: Dockerfile audit — does your app containerize cleanly?

Cloud Run requires your app to run in a container that listens on the PORT environment variable. Before anything else, check:

# Good: reads PORT from env
CMD ["node", "server.js"]  # server.js: const port = process.env.PORT || 3000;

# Bad: hardcodes a port
EXPOSE 3000
CMD ["./myapp", "-port", "3000"]

Fix: Replace any hardcoded ports with $PORT. Cloud Run sets it to 8080 by default, but your Dockerfile should not hardcode this.

Also verify:


Step 2: Move env vars to Secret Manager

On Heroku you set heroku config:set DATABASE_URL=.... In Cloud Run, you have two options:

  1. Cloud Run environment variables — fine for non-sensitive config (feature flags, service URLs, log levels)
  2. GCP Secret Manager — required for anything sensitive (database passwords, API keys, OAuth secrets)

For each secret from Heroku:

# Create the secret
echo -n "your-secret-value" | gcloud secrets create DATABASE_URL 
  --data-file=-

# Grant Cloud Run access
gcloud secrets add-iam-policy-binding DATABASE_URL 
  --member="serviceAccount:your-sa@your-project.iam.gserviceaccount.com" 
  --role="roles/secretmanager.secretAccessor"

In your Cloud Run service configuration, mount the secret as an environment variable. App code doesn’t change — it still reads process.env.DATABASE_URL. Only the source changes from Heroku’s config layer to GCP Secret Manager.

Don’t forget to migrate Heroku Postgres. Options:

For a zero-downtime database migration, use pg_dump and pg_restore during low-traffic hours, then flip the connection string.


Step 3: First deploy to Cloud Run

Once the container is ready and secrets are in Secret Manager:

# Build and push to Artifact Registry
gcloud builds submit --tag gcr.io/YOUR_PROJECT/myapp:latest

# Deploy to Cloud Run
gcloud run deploy myapp 
  --image gcr.io/YOUR_PROJECT/myapp:latest 
  --region us-central1 
  --allow-unauthenticated 
  --set-env-vars "NODE_ENV=production" 
  --update-secrets="DATABASE_URL=DATABASE_URL:latest" 
  --min-instances 0 
  --max-instances 10

Cloud Run gives you a *.run.app URL immediately. Test it thoroughly before touching DNS.

Traffic split during rollout: Cloud Run lets you split traffic between revisions. Use this for your first migration:

# Deploy new revision, send 10% of traffic to it
gcloud run services update-traffic myapp 
  --to-revisions=REVISION_ID=10,PREVIOUS_REVISION=90

Watch error rates for 15–30 minutes. If stable, bump to 50%, then 100%.


Step 4: Rollback in 30 seconds

If the new revision has problems, rollback is one command:

gcloud run services update-traffic myapp 
  --to-revisions=PREVIOUS_REVISION=100

Or click “Edit traffic” in the Cloud Console. Unlike Heroku — where a bad git push heroku main leaves you scrambling to revert and re-push — Cloud Run keeps all previous revisions alive until you explicitly delete them. Rollback is instant, not a re-deploy.


Step 5: DNS cutover

Once the new Cloud Run revision is stable and serving test traffic:

  1. Add a custom domain: Cloud Console → Cloud Run → your service → Manage Custom Domains
  2. Add the CNAME or A record your DNS provider shows you
  3. Wait for propagation (5 minutes to a few hours depending on TTL)
  4. Verify HTTPS is live — Cloud Run auto-provisions a managed TLS certificate
  5. Remove the old Heroku custom domain

Keep the Heroku app running for 48 hours after DNS cutover, in case any traffic was cached to the old endpoint.


Cost comparison: Heroku vs Cloud Run

Setup Heroku Cloud Run
2 dynos (Standard-2x) $100/mo ~$15–30/mo
Postgres (Standard-0, 25 GB) $50/mo Cloud SQL ~$25/mo
Custom domain + SSL Included Included
Auto-scaling Paid add-on Built-in
Zero-downtime deploy Built-in Built-in
Rollback Manual re-deploy Revision traffic split
Typical startup total $150–300+/mo $40–80/mo

The gap widens at scale. Heroku’s dyno pricing is fixed regardless of traffic. Cloud Run’s per-request model means a slow weekend costs almost nothing.


Common gotchas

Sticky sessions. Cloud Run is stateless and load-balanced across instances. If your app relies on sticky sessions (common with Socket.io or server-side session stores), move session state to Redis (Cloud Memorystore) before migrating.

Scheduled jobs (Heroku Scheduler). Cloud Run has no built-in scheduler. Use Cloud Scheduler to trigger a Cloud Run job on a cron schedule, or migrate to Cloud Tasks for queue-driven work.

Background workers. If you use Heroku worker dynos for queue consumers, deploy them as a separate Cloud Run service with --min-instances 1 so it doesn’t scale to zero while waiting for queue messages.

WebSocket apps. Cloud Run supports WebSockets, but you need HTTP/2 enabled and sessions are not sticky. Architect for reconnection from the client side.


The migration in one glance

Step What to do Watch for
1. Dockerfile audit PORT from env, stateless, fast startup Hardcoded ports, local disk writes
2. Secrets Move to Secret Manager, grant SA access Don’t forget DB credentials
3. First deploy gcloud run deploy, test on *.run.app URL Cold start latency, health check
4. Traffic split 10% → 50% → 100%, watch errors Error rate spike = rollback
5. DNS cutover Map custom domain, managed TLS Keep Heroku alive 48 h

What’s next

Cloud Run is a natural step up from Heroku — same simplicity, better economics, and it grows with you from 100 to 1M users without re-architecting. Once you’re on Cloud Run, the next step is wiring a CI/CD pipeline so every push to main builds, tests, and deploys automatically — without anyone running gcloud run deploy by hand.

Read Your First CI/CD Pipeline Checklist for the exact setup, and the DevOps for Startups guide for the full platform picture.

Migrating from Heroku or a VM and want someone to review your setup before you cut over DNS? Book a free intro call — we’ll audit your architecture and hand you a migration checklist.

Leave a Reply

Your email address will not be published. Required fields are marked *