Migrate Node.js apps from Heroku to Cloud Run

This tutorial describes how to migrate Node.js web apps that are running onHeroku to Cloud Run on Google Cloud. This tutorialis intended for architects and product owners who want to migrate their appsfrom Heroku to managed services on Google Cloud.

Cloud Run is a managed compute platform that lets you runstateless containers that are invocable through HTTP requests. It is built onopen sourceKnative,which enables portability across platforms and supports container workflows andstandards for continuous delivery. The Cloud Run platform is wellintegrated with the Google Cloud product suite and makes it easier for youto design and develop apps that are portable, scalable, and resilient.

In this tutorial, you learn how to migrate an app to Google Cloud that'swritten in Node.js and usesHeroku Postgres as a backing service on Heroku. The web app is containerized and hosted inCloud Run and uses Cloud SQL for PostgreSQL as its persistencelayer.

In the tutorial, you use a simple app called Tasks that lets you view and createtasks. These tasks are stored in Heroku Postgres in the current deployment ofthe app on Heroku.

This tutorial assumes that you are familiar with the basic functionality ofHeroku and that you have a Heroku account (or access to one). It also assumes you arefamiliar withCloud Run,Cloud SQL,Docker,andNode.js.

Objectives

  • Build a Docker image to deploy the app to Cloud Run.
  • Create a Cloud SQL for PostgreSQL instance to serve as thebackend after migration to Google Cloud.
  • Review the Node.js code to understand how Cloud Runconnects to Cloud SQL and to see the code changes (if any) thatare required in order to migrate to Cloud Run from Heroku.
  • Migrate data from Heroku Postgres to Cloud SQL for PostgreSQL.
  • Deploy the app to Cloud Run.
  • Test the deployed app.

Costs

In this document, you use the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use thepricing calculator.

New Google Cloud users might be eligible for afree trial.

You might also be charged for the resources you use on Heroku.

Before you begin

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains theresourcemanager.projects.create permission.Learn how to grant roles.
    Note: If you don't plan to keep the resources that you create in this procedure, create a project instead of selecting an existing project. After you finish these steps, you can delete the project, removing all resources associated with the project.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Cloud SQL, Cloud Build, Cloud Run, Container Registry, Service Networking, Serverless VPC Access APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains theserviceusage.services.enable permission.Learn how to grant roles.

    Enable the APIs

  5. Make sure that you have the following role or roles on the project: Cloud Run > Cloud Run Admin, Cloud Storage > Storage Admin, Cloud SQL > Cloud SQL Admin, Compute Engine > Compute Network Admin, Resource Manager > Project IAM Admin, Cloud Build > Cloud Build Editor, Serverless VPC Access > Serverless VPC Access Admin, Logging > Logs Viewer, Service Accounts > Service Account Admin, Service Accounts > Service Account User, and Service Usage > Service Usage Consumer

    Check for the roles

    1. In the Google Cloud console, go to theIAM page.

      Go to IAM
    2. Select the project.
    3. In thePrincipal column, find all rows that identify you or a group that you're included in. To learn which groups you're included in, contact your administrator.

    4. For all rows that specify or include you, check theRole column to see whether the list of roles includes the required roles.

    Grant the roles

    1. In the Google Cloud console, go to theIAM page.

      Go to IAM
    2. Select the project.
    3. ClickGrant access.
    4. In theNew principals field, enter your user identifier. This is typically the email address for a Google Account.

    5. In theSelect a role list, select a role.
    6. To grant additional roles, clickAdd another role and add each additional role.
    7. ClickSave.
  6. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains theresourcemanager.projects.create permission.Learn how to grant roles.
    Note: If you don't plan to keep the resources that you create in this procedure, create a project instead of selecting an existing project. After you finish these steps, you can delete the project, removing all resources associated with the project.

    Go to project selector

  7. Verify that billing is enabled for your Google Cloud project.

  8. Enable the Cloud SQL, Cloud Build, Cloud Run, Container Registry, Service Networking, Serverless VPC Access APIs.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains theserviceusage.services.enable permission.Learn how to grant roles.

    Enable the APIs

  9. Make sure that you have the following role or roles on the project: Cloud Run > Cloud Run Admin, Cloud Storage > Storage Admin, Cloud SQL > Cloud SQL Admin, Compute Engine > Compute Network Admin, Resource Manager > Project IAM Admin, Cloud Build > Cloud Build Editor, Serverless VPC Access > Serverless VPC Access Admin, Logging > Logs Viewer, Service Accounts > Service Account Admin, Service Accounts > Service Account User, and Service Usage > Service Usage Consumer

    Check for the roles

    1. In the Google Cloud console, go to theIAM page.

      Go to IAM
    2. Select the project.
    3. In thePrincipal column, find all rows that identify you or a group that you're included in. To learn which groups you're included in, contact your administrator.

    4. For all rows that specify or include you, check theRole column to see whether the list of roles includes the required roles.

    Grant the roles

    1. In the Google Cloud console, go to theIAM page.

      Go to IAM
    2. Select the project.
    3. ClickGrant access.
    4. In theNew principals field, enter your user identifier. This is typically the email address for a Google Account.

    5. In theSelect a role list, select a role.
    6. To grant additional roles, clickAdd another role and add each additional role.
    7. ClickSave.

Set up your environment

  1. Open Cloud Shell.

    OPEN Cloud Shell

  2. In Cloud Shell, set environment variables and default values forthe Google Cloud CLI used in this tutorial.

    gcloud config set projectPROJECT_IDgcloud config set run/region us-central1

    ReplacePROJECT_ID with your project ID.

Architecture

The following figures outline the web app's architecture on Heroku (as is) andits architectural layout on Google Cloud (that you will build).

As-is architecture on Heroku.
Figure 1. As-is architecture on Heroku

The Tasks app that's currently deployed in Heroku consists of one or more webdynos. Web dynos are able to receive and respond to HTTP traffic, unlike workerdynos, which are better suited for background jobs and timed tasks. The appserves an index page that displays tasks stored in a Postgres database, usingthe Mustache templating library for Node.js.

You can access the app at an HTTPS URL. A/tasks route at that URL lets youcreate new tasks.

To-be architecture on Heroku.
Figure 2. Architecture that you build on Google Cloud

On Google Cloud, Cloud Run is used as the serverlessplatform to deploy the Tasks app. Cloud Run is designed to runstateless, request-driven containers. It is well suited for when you need yourmanaged service to support containerized apps that autoscale and also scale tozero when they're not serving traffic.

Map components used in Heroku to Google Cloud

The following table maps components in the Heroku platform to Google Cloud.This mapping helps you translate the architecture outlined in this tutorial fromHeroku to Google Cloud.

ComponentHeroku platformGoogle Cloud
ContainersDynos:Heroku uses the container model to build and scale Heroku apps. These Linuxcontainers are calleddynos and can scale to a number that you specifyto support resource demands for your Heroku app. You can select from a rangeof dyno types based on your app's memory and CPU requirements.Cloud Run containers: Google Cloudsupports running containerized workloads in stateless containers that can be runin a fully managed environment or in Google Kubernetes Engine (GKE)clusters.
Web appHeroku app: Dynos are the building blocks of Heroku apps.Apps usually consist of one or more dyno types, usually a combination ofweb and worker dynos.Cloud Run service: A web app can be modeledas a Cloud Run service. Each service gets its own HTTPSendpoint and can automatically scale up or down from 0 to N based on thetraffic to your service endpoint.
DatabaseHeroku Postgres is Heroku's database as a service (DaaS) based onPostgreSQL.Cloud SQL is a managed databaseservice for relational databases on Google Cloud.

Deploy the sample Tasks web app to Heroku

The next sections show how to set up the command-line interface (CLI) forHeroku, clone the GitHub source repository, and deploy the app to Heroku.

Set up the command-line interface for Heroku

This tutorial runs the Heroku CLI in Cloud Shell and mustauthenticate using a Heroku API key. When running in Cloud Shell, the Heroku CLI cannot authenticate using a password or web-based authentication.

Alternatively, if you do run the sample on a local terminal, you can use any Heroku CLI authentication method. When running the tutorial on a local terminal, you must also installGoogle Cloud CLI,git, andDocker.

  1. Log in to theweb console for Heroku,and then from your account settings page, copy the value of your API key.

  2. In Cloud Shell,install the Heroku CLI

  3. In Cloud Shell, authenticate the Heroku CLI. When prompted for yourpassword, enter the value of your API key that you copied from the Herokuconsole, not the password you use to sign in to the console.

    heroku login --interactive

Clone the source repository

  1. In Cloud Shell, clone the sample Tasks app GitHub repository:

    git clone https://github.com/GoogleCloudPlatform/migrate-webapp-heroku-to-cloudrun-node.git
  2. Change directories to the directory created by cloning the repository:

    cd migrate-webapp-heroku-to-cloudrun-node

    The directory contains the following files:

    • A Node.js script calledindex.js with the code for theroutes served by the web app.
    • package.json andpackage-lock.json files thatoutline the web app's dependencies. You must install these dependenciesin order for the app to run.
    • AProcfile file that specifies the command that the app executeson startup. You create aProcfile file to deploy your app toHeroku.
    • Aviews directory, with the HTML content served by the webapp on the "/" route.
    • A.gitignore file.

Deploy an app to Heroku

  1. In Cloud Shell, create a Heroku app:

    heroku create

    Make note of the name created for the app. You need this value inthe next step.

  2. Create an environment variable for the Heroku app name:

    export APP_NAME=APP_NAME

    ReplaceAPP_NAME withthe app name returned by theheroku create command.

  3. Add the Heroku Postgres add-on to provision a PostgreSQL database:

    heroku addons:create heroku-postgresql:mini
  4. Make sure the add-on was successfully added:

    heroku addons

    If the Postgres add-on was successfully added, you see a messagesimilar to the following:

    Add-on               Plan     Price       State-----------------    -----    --------    -----heroku-postgresql    mini     5$/month    created
  5. Deploy the app to Heroku:

    git push heroku master
  6. Run the following command to confirm the value of DATABASE_URL.

    heroku config

    Make note of the retrieved value for DATABASE_URL. You need this value inthe next step.

  7. Run a Docker container.

    docker run -it --rm postgres psql "DATABASE_URL"

    ReplaceDATABASE_URL withthe Heroku Postgres URL that you noted in the previous step.

  8. In the Docker container, create theTASKS table by using thefollowing command:

    CREATE TABLE TASKS(DESCRIPTION TEXT NOT NULL);
  9. Exit the container:

    exit
  10. In Cloud Shell, get the Web URL for your Heroku app by running the following command:

    heroku info
  11. Open the web URL in a browser window. The app looks the followingscreenshot (although your version won't have the tasks listed):

    To-do app in the web browser.

  12. Create sample tasks in your app from the browser. Make sure the tasksare retrieved from the database and visible in the UI.

Prepare the web app code for migration to Cloud Run

This section details the steps that you need to complete to prepare your web appfor deployment to Cloud Run.

Build and publish your Docker container to Container Registry

You need a Docker image to build the app container so it can run inCloud Run. You can build the container manually or by usingBuildpacks.

Build container manually

  1. In Cloud Shell, create a Dockerfile in the directory created bycloning the repository for this tutorial:

    cat<<"EOF" > Dockerfile# Use the official Node image.# https://hub.docker.com/_/nodeFROM node:10-alpine# Create and change to the app directory.WORKDIR /app# Copying this separately prevents re-running npm install on every code change.COPY package*.json ./RUN npm install# Copy local code to the container image.COPY . /app# Configure and document the service HTTP port.ENV PORT 8080EXPOSE $PORT# Run the web service on container startup.CMD ["npm", "start"]EOF
  2. Build your container withCloud Build and publish the image toContainer Registry:

    gcloud builds submit --tag gcr.io/PROJECT_ID/APP_NAME:1 \  --gcs-log-dir=gs://PROJECT_ID_cloudbuild
  3. Create an environment variable to hold the name of the Dockerimage that you created:

    export IMAGE_NAME="gcr.io/PROJECT_ID/APP_NAME:1"

Build container with Buildpacks

  1. In Cloud Shell,install thepack CLI.

  2. Set the pack CLI to use the Heroku builder by default:

    pack config default-builder heroku/buildpacks:22
  3. Create an environment variable to hold the Docker image name:

    export IMAGE_NAME=gcr.io/PROJECT_ID/APP_NAME:1
  4. Build the image using thepack command and push or publish the imageto Container Registry:

    pack build --publish $IMAGE_NAME

Create a Cloud SQL for PostgreSQL instance

You create a Cloud SQL for PostgreSQL instance to serve as the backendfor the web app. In this tutorial, PostgreSQL is best suited as the sample appdeployed on Heroku, which uses a Postgres database as its backend. For purposesof this app, migrating to Cloud SQL for PostgreSQL from a managedPostgres service requires no schema changes.

Note: Database migration from on-premises or hosted environments toGoogle Cloud is a multi-step process that requires careful planning,schema and data migration, and testing. For more information, see theCloud Run documentation andimporting data into Cloud SQL.
  1. Prepare your network for Cloud SQL with a private IP address.

    gcloud compute addresses create google-managed-services-default \  --global \  --purpose=VPC_PEERING \  --prefix-length=16 \  --description="peering range for CloudSQL Private Service Access" \  --network=defaultgcloud services vpc-peerings connect \  --service=servicenetworking.googleapis.com \  --ranges=google-managed-services-default \  --network=default \  --project=PROJECT_ID
  2. Create an environment variable calledCLOUDSQL_DB_NAME to hold the name of the database instance that you create in the next step:

    export CLOUDSQL_DB_NAME=tasks-db
  3. Create the database:

    gcloud sql instances create $CLOUDSQL_DB_NAME \--cpu=1 \--memory=4352Mib \--database-version=POSTGRES_15 \--region=us-central1 \--network default \--no-assign-ip

    The instance might take a few minutes to initialize.

  4. Set a password for the Postgres user:

    gcloud sql users set-password postgres \    --instance=$CLOUDSQL_DB_NAME  \    --password=POSTGRES_PASSWORD

    ReplacePOSTGRES_PASSWORD with the password thatyou want to use for the Postgres database.

Import data into Cloud SQL from Heroku Postgres

There are multiple migration patterns that you can use to migrate data intoCloud SQL. Generally, the best approach that requires littleor no downtime is to configure Cloud SQL as a replica to the databasebeing migrated, and making Cloud SQL the primary instance aftermigration. Heroku Postgres doesn't support external replicas (followers), so inthis tutorial, you use open source tools to migrate the app's schema.

For the Tasks app in this tutorial, you use thepg_dump utility to export data from Heroku Postgres to a Cloud Storage bucketand then import it into Cloud SQL. This utility can transfer dataacross homogeneous versions or when the destination database's version is newerthan the source database.

  1. In Cloud Shell, get the database credentials for your HerokuPostgres database that's attached to the sample app. You need thesecredentials in the next step.

    heroku pg:credentials:url

    This command returns the connection information string and connectionURL for your application. The connection information string has thefollowing format:

    "dbname=DATABASE_NAMEhost=FQDNport=5432user=USER_NAMEpassword=PASSWORD_STRINGsslmode=require"

    You need the values shown in the connection string in the next step.

    For an example of anFQDN(fully qualified domain name)value in a connection information string, see theHeroku documentation.

  2. Set environment variables to hold Heroku values that you use insubsequent steps:

    export HEROKU_PG_DBNAME=DATABASE_NAMEexport HEROKU_PG_HOST=FQDNexport HEROKU_PG_USER=USER_NAMEexport HEROKU_PG_PASSWORD=PASSWORD_STRING

    Replace the following:

    • DATABASE_NAME: the database name shown in theinformation string.
    • FQDN: the FQDN shown in theinformation string.
    • USER_NAME: the user name shown in theinformation string.
    • PASSWORD_STRING: the password string shown in theinformation string.
  3. Create a SQL format backup of your Heroku Postgres database:

    docker run \  -it --rm \  -e PGPASSWORD=$HEROKU_PG_PASSWORD \  -v $(pwd):/tmp \  --entrypoint "pg_dump" \  postgres \  -Fp \  --no-acl \  --no-owner \  -h $HEROKU_PG_HOST \  -U $HEROKU_PG_USER \  $HEROKU_PG_DBNAME > herokudump.sql
  4. Create an environment variable to hold the name of yourCloud Storage bucket:

    export PG_BACKUP_BUCKET=gs://PROJECT_ID-pg-backup-bucket
  5. Create a Cloud Storage bucket:

    gcloud storage buckets create $PG_BACKUP_BUCKET \  --location=us-central1 \  --public-access-prevention \  --uniform-bucket-level-access
  6. Upload the SQL file to this bucket:

    gcloud storage cp herokudump.sql $PG_BACKUP_BUCKET/herokudump.sql
  7. Authorize your Cloud SQL instance with thenecessary roles to import the SQL filefrom the Cloud Storage bucket:

    gcloud projects add-iam-policy-bindingPROJECT_ID \  --member=serviceAccount:$(gcloud sql instances describe $CLOUDSQL_DB_NAME --format='get("serviceAccountEmailAddress")') \  --role=roles/storage.objectAdmingcloud projects add-iam-policy-bindingPROJECT_ID \  --member=serviceAccount:$(gcloud sql instances describe $CLOUDSQL_DB_NAME --format='get("serviceAccountEmailAddress")') \  --role=roles/cloudsql.editor
  8. Import the SQL file to the Cloud SQL instance:

    gcloud sql import sql $CLOUDSQL_DB_NAME $PG_BACKUP_BUCKET/herokudump.sql \  --database=postgres \  --user=postgres

    When prompteddo you want to continue (y/n), enter "y".

How Cloud Run accesses the Cloud SQL database

Just as the web app deployed to Heroku needs to connect to the managed instanceof Heroku Postgres, Cloud Run requires access toCloud SQL in order to be able to read and write data.

Cloud Run communicates with Cloud SQL using theCloud SQL proxy that's automatically activated and configured when youdeploy the container to Cloud Run. The database doesn't need tohave external IP addresses approved because all the communication that itreceives is from the proxy using secure TCP.

Your code needs to invoke database operations (such as fetching data from thedatabase or writing to it) by invoking the proxy over a UNIX socket.

Because this web app is written in Node.js, you use thepg-connection-stringlibrary to parse a database URL and create aconfig object. The advantage ofthis approach is that it makes connecting to the backend database seamlessacross Heroku and Cloud Run.

In the next step, you pass the database URL as an environment variable when youdeploy the web app.

Deploy the sample app to Cloud Run

  1. In Cloud Shell, configureserverless vpc accessto allow private traffic from Cloud Run to Cloud SQL:

    gcloud compute networks subnets create serverless-connector-subnet \--network=default \--range=10.0.0.0/28 \--region=us-central1gcloud compute networks vpc-access connectors create serverless-connector \--region=us-central1 \--subnet=serverless-connector-subnet
  2. In Cloud Shell, create an environment variable that holds theconnection name of the Cloud SQL instance that you created:

    export DB_CONN_NAME=$(gcloud sql instances describe $CLOUDSQL_DB_NAME --format='value(connectionName)')
  3. Create an environment variable calledDATABASE_URL to hold the connectionstring to connect to the Cloud SQL Proxy over a UNIX port.

    export DATABASE_URL="socket:/cloudsql/${DB_CONN_NAME}?db=postgres&user=postgres&password=POSTGRES_PASSWORD"
  4. Create a service account for Cloud Run with an IAM role to connect to the database:

    gcloud iam service-accounts create sa-run-db-clientgcloud projects add-iam-policy-bindingPROJECT_ID \    --member=serviceAccount:sa-run-db-client@PROJECT_ID.iam.gserviceaccount.com \    --role=roles/cloudsql.client
  5. Deploy the web app to Cloud Run:

    gcloud run deploy tasksapp-PROJECT_ID \    --image=$IMAGE_NAME \    --service-account=sa-run-db-client@PROJECT_ID.iam.gserviceaccount.com \    --set-env-vars=DATABASE_URL=$DATABASE_URL \    --add-cloudsql-instances $DB_CONN_NAME \    --vpc-connector serverless-connector \    --allow-unauthenticated

    The preceding command also links your Cloud Run container tothe Cloud SQL database instance that you created. The command setsan environment variable for Cloud Run to point to theDATABASE_URL string that you created in the previous step.

    Note: Because this sample tasks app is a public facing web app and uses the--allow-unauthenticated flag, deployment will fail if your organizationhas theOrganization Policy iam.allowedPolicyMemberDomainsenforced. To deploy a Cloud Run sample app that ispublic-facing and unauthenticated, disable the policy on your tutorialproject. For a real app that requires authentication for end users,reviewauthenticating end users.

Test the application

  1. In Cloud Shell, get the URL at which Cloud Runserves traffic:

    gcloud run services list

    You can also review the Cloud Run Service in theGoogle Cloud console.

  2. Make sure that your web app is accepting HTTP requests by navigating to yourCloud Run Service URL.

Cloud Run creates, orspins up, a new container when an HTTPrequest is sent to the serving endpoint and if a container is not alreadyrunning. This means that the request that causes a new container to spin upmight take a bit longer to be served. Given that extra time, take into accountthe number of concurrent requests that your app can support and any specificmemory requirements it might have.

For this app, you use the default concurrency settings, which allow for aCloud Run service to serve 80 requests concurrently from a singlecontainer.

Clean up

To avoid incurring charges to your Google Cloud account for the resourcesused in this tutorial. You might also want to delete the resources created inHeroku for this tutorial.

Delete the Google Cloud project

    Caution: Deleting a project has the following effects:
    • Everything in the project is deleted. If you used an existing project for the tasks in this document, when you delete it, you also delete any other work you've done in the project.
    • Custom project IDs are lost. When you created this project, you might have created a custom project ID that you want to use in the future. To preserve the URLs that use the project ID, such as anappspot.com URL, delete selected resources inside the project instead of deleting the whole project.
  1. In the Google Cloud console, go to theManage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then clickDelete.
  3. In the dialog, type the project ID, and then clickShut down to delete the project.

Delete the Heroku App

To delete the sample app you deployed to Heroku and the associated PostgreSQLadd-on, run the following command:

heroku apps:destroy -aAPP_NAME

What's next

Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 2025-12-15 UTC.