Migrating Node.js apps from Heroku to Cloud Run

This tutorial describes how to migrate Node.js web apps that are running on Heroku to Cloud Run on Google Cloud Platform (GCP). This tutorial is intended for architects and product owners who want to migrate their apps from Heroku to managed services on GCP.

Cloud Run is a managed compute platform that lets you run stateless containers that are invocable through HTTP requests. It is built on open source Knative, which enables portability across platforms and supports container workflows and standards for continuous delivery. The Cloud Run platform is well integrated with the GCP product suite and makes it easier for you to design and develop apps that are portable, scalable, and resilient.

In this tutorial, you learn how to migrate an app to GCP that's written in Node.js and uses Heroku Postgres as a backing service on Heroku. The web app is containerized and hosted in Cloud Run and uses Cloud SQL for PostgreSQL as its persistence layer.

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

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

Objectives

  • Build a Docker image to deploy the app to Cloud Run.
  • Create a Cloud SQL for PostgreSQL instance to serve as the backend after migration to GCP.
  • Review the Node.js code to understand how Cloud Run connects to Cloud SQL and to see the code changes (if any) that are 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

This tutorial uses the following billable components of Google Cloud:

To generate a cost estimate based on your projected usage, use the pricing calculator. New Google Cloud users might be eligible for a free trial.

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

Before you begin

  1. Sign in to your Google Account.

    If you don't already have one, sign up for a new account.

  2. In the Cloud Console, on the project selector page, select or create a Google Cloud project.

    Go to the project selector page

  3. Make sure that billing is enabled for your Google Cloud project. Learn how to confirm billing is enabled for your project.

  4. Enable the Cloud SQL and Cloud Run APIs.

    Enable the APIs

Setting up your environment

  1. Open Cloud Shell.

    OPEN Cloud Shell

  2. In Cloud Shell, assign default settings for values that are used throughout the tutorial, such as region and zone In this tutorial, you use us-central1 as the default region and us-central1-a as the default zone.

    gcloud config set compute/region us-central1
    gcloud config set compute/zone us-central1-a
    
  3. Configure the gcloud command-line tool to use us-central1 as the default region for Cloud Run:

    gcloud config set run/region us-central1
    
  4. Create an environment variable to hold a default app name for this tutorial:

    export APP_NAME=tasks-web-app
    

Architecture

The following figures outline the web app's architecture on Heroku (as is) and its architectural layout on GCP (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 web dynos. Web dynos are able to receive and respond to HTTP traffic, unlike worker dynos, which are better suited for background jobs and timed tasks. The app serves an index page that displays tasks stored in a Postgres database, using the Mustache templating library for Node.js.

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

As-is architecture on Heroku.
Figure 2. Architecture that you build on GCP

On GCP, Cloud Run is used as the serverless platform to deploy the Tasks app. Cloud Run is designed to run stateless, request-driven containers. It is well suited for when you need your managed service to support containerized apps that autoscale and also scale to zero when they're not serving traffic.

Mapping components used in Heroku to GCP

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

Component Heroku platform GCP
Containers Dynos: Heroku uses the container model to build and scale Heroku apps. These Linux containers are called dynos and can scale to a number that you specify to support resource demands for your Heroku app. You can select from a range of dyno types based on your app's memory and CPU requirements. Cloud Run containers: GCP supports running containerized workloads in stateless containers that can be run in a fully managed environment or in Google Kubernetes Engine (GKE) clusters.
Web app Heroku app: Dynos are the building blocks of Heroku apps. Apps usually consist of one or more dyno types, usually a combination of web and worker dynos. Cloud Run service: A web app can be modeled as a Cloud Run service. Each service gets its own HTTPS endpoint and can automatically scale up or down from 0 to N based on the traffic to your service endpoint.
Database Heroku Postgres is Heroku's database as a service (DaaS) based on PostgreSQL. Cloud SQL is a managed database service for relational databases on GCP. It offers two variants: PostgreSQL and MySQL.

Deploying the sample Tasks web app to Heroku

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

Set up the command-line interface for Heroku

  1. In Cloud Shell, run the following command to set up the Heroku CLI:

    curl https://cli-assets.heroku.com/install-ubuntu.sh | sh
    
  2. Log in to your Heroku account:

    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 called index.js with the code for the routes served by the web app.
    • package.json and package-lock.json files that outline the web app's dependencies. You must install these dependencies in order for the app to run.
    • A Procfile file that specifies the command that the app executes on startup. You create a Procfile file to deploy your app to Heroku.
    • A views directory, with the HTML content served by the web app on the "/" route.
    • A .gitignore file.

Deploy an app to Heroku

  1. In Cloud Shell, create a Heroku app:

    heroku create
    
  2. Add the Heroku Postgres add-on to provision a PostgreSQL database:

    heroku addons:create heroku-postgresql:hobby-dev
    
  3. Make sure the add-on was successfully added:

    heroku addons
    

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

    Owning-App               Add-on                      Plan                          Price    State
    ----------------------   ------------------------    --------------------------    -----    -----
    sample-nodejs-todo-app   postgresql-adjacent-95585   heroku-postgresql:hobby-dev   free     created
    
  4. Deploy the app to Heroku:

    git push heroku master
    
  5. Retrieve the Heroku Postgres URI from the Heroku console. Alternatively, run the following command to retrieve the URI:

    heroku config
    

    Make note of the retrieved URI. You need it in the next step.

  6. Run a Docker container. Replace database-uri with the Heroku Postgres URI that you noted in the previous step.

    docker run -it --rm postgres psql "database-uri"
    
  7. In the Docker container, create the TASKS table by using the following command:

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

    exit
    
  9. In Cloud Shell, get your Heroku app URL:

    heroku info
    
  10. Open the app's URL in a browser window. The app looks like this (although your version won't have the tasks listed):

    To-do app in the web browser.

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

Preparing the web app code for migration to Cloud Run

This section details the steps that you need to complete to prep your web app for 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 in Cloud Run. You can build the container manually or by using Buildpacks.

Build container manually

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

    cat <<EOF > Dockerfile
    # Use the official Node image.
    # https://hub.docker.com/_/node
    FROM 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 8080
    EXPOSE $PORT
    
    # Run the web service on container startup.
    CMD ["npm", "start"]
    EOF
    
  2. Build your container with Cloud Build and publish the image to Container Registry:

    gcloud builds submit --tag gcr.io/${DEVSHELL_PROJECT_ID}/$APP_NAME:1
    
  3. Create an environment variable to hold the name of the Docker image that you created:

    export IMAGE_NAME="gcr.io/${DEVSHELL_PROJECT_ID}/$APP_NAME:1"
    

Build container with Buildpacks

  1. In Cloud Shell, install the pack CLI:

    wget https://github.com/buildpack/pack/releases/download/v0.2.1/pack-v0.2.1-linux.tgz
    tar xvf pack-v0.2.1-linux.tgz
    rm pack-v0.2.1-linux.tgz
    sudo mv pack /usr/local/bin/
    
  2. Set the pack CLI to use the Heroku builder by default:

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

    export IMAGE_NAME=gcr.io/${DEVSHELL_PROJECT_ID}/$APP_NAME:1
    
  4. Build the image using the pack command and push or publish the image to 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 backend for the web app. In this tutorial, PostgreSQL is best suited as the sample app deployed on Heroku, which uses a Postgres database as its backend. For purposes of this app, migrating to Cloud SQL for PostgreSQL from a managed Postgres service requires no schema changes.

gcloud

  1. Create an environment variable called CLOUDSQL_DB_NAME to hold the name of the database instance that you create in the next step:

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

    gcloud sql instances create $CLOUDSQL_DB_NAME  \
        --cpu=1 \
        --memory=4352Mib \
        --database-version=POSTGRES_9_6 \
        --region=us-central1
    

    The instance might take a few minutes to initialize.

  3. Set a password for the Postgres user:

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

Console

  1. In the GCP Console, go to the Cloud SQL Instances page:

    GO TO THE CLOUD SQL INSTANCES PAGE

  2. Click Create instance.

  3. Select PostgreSQL and click Next.

  4. Enter tasks-db as the name of the instance.

  5. Create a password for the postgres user.

  6. For the region, select us-central1.

  7. Do not modify other defaults.

Import data into Cloud SQL from Heroku Postgres

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

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

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

    heroku pg:credentials:url
    

    This command returns the connection information string and connection URL for your application.

    The connection information string has the following format:

    "dbname=database-name host=host-name.compute-1.amazonaws.com port=5432 user=user-name password=password-string sslmode=require"
    
  2. Set environment variables to hold Heroku parameters that you use in subsequent steps. Replace the host, user, password, and dbname parameters with their corresponding values in the connection information string.

    export HEROKU_PG_HOST=host
    export HEROKU_PG_USER=user
    export HEROKU_PG_PASSWORD=password
    export HEROKU_PG_DBNAME=dbname
    
  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 your Cloud Storage bucket. This name should be unique. The environment variable should have the format gs://bucket-name.

    export PG_BACKUP_BUCKET=gs://bucket-name
    
  5. Create a Cloud Storage bucket using the gsutil command-line tool:

    gsutil mb -c regional -l us-central1 $PG_BACKUP_BUCKET
    
  6. Upload the SQL file to this bucket:

    gsutil cp herokudump.sql $PG_BACKUP_BUCKET
    
  7. In the GCP Console, go to the Cloud SQL Instances page:

    GO TO THE CLOUD SQL INSTANCES PAGE

  8. Select the instance in order to open its Instance details page.

  9. In the button bar, click Import.

    Importing the SQL dump file from Cloud Storage.

  10. For Cloud Storage file, enter the path to the SQL dump file that you uploaded to Cloud Storage.

  11. For Format of import, select SQL.

  12. For Database, select postgres.

  13. Expand Advanced Options, and for User, select postgres.

  14. Click Import to start the import.

  15. Validate that the import is successful by reviewing the updates in the Operations tab for the instance.

How Cloud Run accesses the Cloud SQL database

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

Cloud Run communicates with Cloud SQL using the Cloud SQL proxy that's automatically activated and configured when you deploy the container to Cloud Run. The database doesn't need to have external IP addresses approved (whitelisted) because all the communication that it receives is from the proxy using secure TCP.

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

Because this web app is written in Node.js, you use the pg-connection-string library to parse a database URL and create a config object. The advantage of this approach is that it makes connecting to the backend database seamless across Heroku and Cloud Run.

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

Deploy the sample app to Cloud Run

  1. In Cloud Shell, create an environment variable that holds the connection name of the Cloud SQL instance that you created:

    export DB_CONN_NAME=$(gcloud sql instances describe $CLOUDSQL_DB_NAME --format='value(connectionName)')
    
  2. Create an environment variable called DATABASE_URL to hold the connection string to connect to the Cloud SQL Proxy over a UNIX port. Replace your-db-password with the password you created for your database instance.

    export DATABASE_URL="socket:/cloudsql/${DB_CONN_NAME}?db=postgres&user=postgres&password=your-db-password"
    
  3. Deploy the web app to Cloud Run.

    gcloud beta run deploy tasksapp-$DEVSHELL_PROJECT_ID \
        --image=$IMAGE_NAME \
        --set-env-vars=DATABASE_URL=$DATABASE_URL \
        --add-cloudsql-instances $DB_CONN_NAME \
        --allow-unauthenticated \
        --platform managed
    

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

Testing the application

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

    gcloud beta run services list --platform managed
    

    You can also review the Cloud Run Service in the GCP Console.

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

Cloud Run creates, or spins up, a new container when an HTTP request is sent to the serving endpoint and if a container is not already running. This means that the request that causes a new container to spin up might take a bit longer to be served. Given that extra time, take into account the number of concurrent requests that your app can support and any specific memory requirements it might have.

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

Cleaning up

To avoid incurring charges to your GCP account for the resources used in this tutorial. You might also want to delete the resources created in Heroku for this tutorial.

Delete the GCP project

  1. In the Cloud Console, go to the Manage resources page.

    Go to the Manage resources page

  2. In the project list, select the project you want to delete and click Delete .
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Delete the Heroku App

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

heroku apps:destroy -a $APP_NAME

What's next

Was this page helpful? Let us know how we did:

Send feedback about...