Skip Navigation
Show nav
Dev Center
  • Get Started
  • Documentation
  • Changelog
  • Search
  • Get Started
    • Node.js
    • Ruby on Rails
    • Ruby
    • Python
    • Java
    • PHP
    • Go
    • Scala
    • Clojure
    • .NET
  • Documentation
  • Changelog
  • More
    Additional Resources
    • Home
    • Elements
    • Products
    • Pricing
    • Careers
    • Help
    • Status
    • Events
    • Podcasts
    • Compliance Center
    Heroku Blog

    Heroku Blog

    Find out what's new with Heroku on our blog.

    Visit Blog
  • Log inorSign up

Getting Started on Heroku Fir with Node.js

Introduction

Complete this tutorial to deploy a sample Node.js app to Heroku Private Spaces on the Fir generation of the platform. To deploy the app to the Common Runtime or Cedar Private Spaces, follow this guide instead.

The tutorial assumes that you have:

  • A verified Heroku Account
  • An existing Fir Private Space
  • A team admin role or the app creation permission
  • An SSH key added to your Heroku account
  • Node.js and npm installed locally
  • Postgres installed locally

Using dynos and databases to complete this tutorial counts towards your usage. We recommend using dyno-1c-0.5gb dynos and an Essential-0 Postgres database to complete this tutorial. Delete all resources after completing the tutorial.

Set Up

Install the Heroku Command Line Interface (CLI). Use the CLI to manage and scale your app, provision add-ons, view your logs, and run your app locally.

The Heroku CLI requires Git, the popular version control system. If you don’t already have Git installed, complete the following before proceeding:

  • Git installation
  • First-time Git setup

Download and run the installer for your platform:

apple logomacOS

Install Homebrew and run:

$ brew install heroku/brew/heroku

windows logoWindows

Download the appropriate installer for your Windows installation:

64-bit installer

32-bit installer

You can find more installation options for the Heroku CLI here.

After installation, you can use the heroku command from your command shell.

On Windows, start the Command Prompt (cmd.exe) or Powershell to access the command shell.

To log in to the Heroku CLI, use the heroku login command:

$ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/***
heroku: Waiting for login...
Logging in... done
Logged in as me@example.com

This command opens your web browser to the Heroku login page. If your browser is already logged in to Heroku, click the Log In button on the page.

This authentication is required for the heroku and git commands to work correctly.

If you have any problems installing or using the Heroku CLI, see the main Heroku CLI article for advice and troubleshooting steps.

If you’re behind a firewall that uses a proxy to connect with external HTTP/HTTPS services, set the HTTP_PROXY or HTTPS_PROXY environment variables in your local development environment before running the heroku command.

Clone the Sample App

If you’re new to Heroku, it’s recommended that you complete this tutorial using the Heroku-provided sample application.

To deploy an existing application, follow this article instead.

Clone the sample application to get a local version of the code. Execute these commands in your local command shell or terminal:

$ git clone https://github.com/heroku/node-js-getting-started.git
$ cd node-js-getting-started

You now have a functioning Git repository that contains a simple application. It includes a package.json file, which a Node.js dependency manager (like npm), can use to install dependencies.

Create Your App in a Fir Space

Delete your app and database as soon as you’re done to control costs.

 

You can get a list of all Heroku spaces by running $ heroku spaces

Create an app on Heroku to prepare the platform to receive your source code by replacing <space-name> with the name of your Fir space in the command below:

$ heroku create --space <space-name>
Creating app in space <space name>...
Creating app in space <space name>... done, damp-thicket-04285
http://damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com/ | https://git.heroku.com/damp-thicket-04285.git

When you create an app, a Git remote called heroku also gets created and associated with your local Git repository. Git remotes are versions of your repository that live on other servers. You deploy your app by pushing its code to that special Heroku-hosted remote associated with your app.

Heroku generates a random name for your app, in this case, damp-thicket-04285. You can specify your own app name.

Deploy the App

Using a dyno to complete this tutorial counts towards your usage. Delete your app and database as soon as you’re done to control costs.

Deploy your code. This command pushes the main branch of the sample repo to your heroku remote, which then deploys to Heroku:

$ git push heroku main
remote: Updated 18 paths from 7189542
remote: Compressing source files... done.
remote: Building source:
remote: Heroku Labs: Build Time Config Vars are *not enabled*.
remote: By default, your config vars are only available at runtime. See https://devcenter.heroku.com/build-time-config-vars for details on Build Time Config Vars.
remote: Waiting on build...
remote: Extracting source
remote: Image with name "damp-thicket-04285/builds" not found
remote: 3 of 6 buildpacks participating
remote: heroku/nodejs-engine      3.6.1
remote: heroku/nodejs-npm-install 3.6.1
remote: heroku/procfile           4.2.1
remote:
remote: ## Heroku Node.js Engine
remote:
remote: - Checking Node.js version
remote:   - Detected Node.js version range: `>=18.0.0 <19.0.0-0||>=20.0.0 <21.0.0-0||>=22.0.0 <23.0.0-0`
remote:   - Resolved Node.js version: `22.15.0`
remote: - Installing Node.js distribution
remote:   - Downloading Node.js `22.15.0 (linux-arm64)` from https://nodejs.org/download/release/v22.15.0/node-v22.15.0-linux-arm64.tar.gz ... (0.6s)
remote:   - Verifying checksum
remote:   - Extracting Node.js `22.15.0 (linux-arm64)`
remote:   - Installing Node.js `22.15.0 (linux-arm64)` ... (< 0.1s)
remote: - Done (finished in < 0.1s)
remote:
remote: ## Heroku Node.js npm Install
remote:
remote: - Installing node modules
remote:   - Using npm version `10.9.2`
remote:   - Creating npm cache
remote:   - Configuring npm cache directory
remote:   - Running `npm ci "--production=false"`
remote:
remote:       npm warn config production Use `--omit=dev` instead.
remote:
remote:       added 331 packages, and audited 332 packages in 4s
remote:
remote:       45 packages are looking for funding
remote:         run `npm fund` for details
remote:
remote:       1 moderate severity vulnerability
remote:
remote:       To address all issues, run:
remote:         npm audit fix
remote:
remote:       Run `npm audit` for details.
remote:
remote:   - Done (4.0s)
remote: - Running scripts
remote:   - No build scripts found
remote: - Configuring default processes
remote:   - Skipping default web process (Procfile detected)
remote: - Done (finished in 4.6s)
remote:
remote: ## Procfile Buildpack
remote:
remote: - Processes from `Procfile`
remote:   - web: `node index.js`
remote: - Done (finished in < 0.1s)
remote: Adding layer 'heroku/nodejs-engine:available_parallelism'
remote: Adding layer 'heroku/nodejs-engine:dist'
remote: Adding layer 'heroku/nodejs-engine:web_env'
remote: Adding layer 'heroku/nodejs-npm-install:npm_runtime_config'
remote: Adding layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Adding layer 'buildpacksio/lifecycle:launcher'
remote: Adding layer 'buildpacksio/lifecycle:config'
remote: Adding layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving damp-thicket-04285/builds...
remote: *** Images (sha256:a63c9ed78fcd3b4fd9948fbb18fd202e18ee3bd0aecdca0ea55b2ba726961c2a):
remote:       damp-thicket-04285/builds:0a74a217-87d6-4775-b03c-7d64e50b0671
remote: Adding cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-npm-install:npm_cache'
remote: Uploading cache
remote: Launching...
remote: http://damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com/ deployed to Heroku
remote: Verifying deploy.... done.
To https://git.heroku.com/damp-thicket-04285.git
 * [new branch]      main -> main

The app is now deployed. The default dyno size for Fir Private Spaces is dyno-1c-0.5gb.

Visit the app at the URL shown in the logs. As a shortcut, you can also open the website as follows:

$ heroku open

View Logs

Fir apps do not retain log history like Cedar apps. To view an event in your Fir logs, you must run the logging command while that event occurs.

Heroku treats logs as streams of time-ordered events, aggregated from the output streams of all your app and Heroku components. Heroku provides a single stream for all events. View information about your running app by using one of the logging commands:

$ heroku logs
Fetching logs...

2025-04-28T22:32:53.044778+00:00 heroku-router[web]: at=info method=GET path="/" host=damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com request_id=2461d3c8-e401-ccf2-6187-8ca62c90f94b fwd="123.456.789.0" dyno=web-7f6f4744bd-b6fpx connect=0ms service=4ms status=200 bytes=9111 protocol=http tls_version=tls1.3

To generate more log messages, refresh the app in your browser.

To stop streaming the logs, press Ctrl+C.

Define a Procfile

Use a Procfile, a text file in the root directory of your application, to explicitly declare what command to execute to start your app.

The Procfile in the example app looks like this:

web: node index.js

This Procfile declares a single process type, web, and the command needed to run it. The name web is important here. It declares that this process type is attached to Heroku’s HTTP routing stack and receives web traffic when deployed. This command uses the node executable to run the index.js file which starts the application.

A Procfile can contain additional process types. For example, you can declare a background worker process that processes items off a queue.

Declare App Dependencies

Heroku recognizes an app as Node.js from a package.json file in the root directory. For your own apps, you can create one by running npm init --yes.

The demo app you deployed already has a package.json that looks like:

{
  "name": "node-js-getting-started",
  "private": true,
  "description": "A sample Node.js app using Express",
  "engines": {
    "node": "18.x || 20.x || 22.x"
  },
  ...
  "dependencies": {
    "ejs": "^3.1.10",
    "express": "^5.1.0"
  },
  ...

The package.json file determines the version of Node.js to run your application on Heroku and the dependencies to install with your application.

Run this command in your local directory to install the dependencies, preparing your system to run the app locally.

$ npm install
added 332 packages, and audited 333 packages in 1s

45 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

In your development environment, a package-lock.json file is generated or updated when npm install is run. It’s important that changes to this lockfile are always committed to git before deploying your app since, if the dependencies in the lockfile are out of sync with package.json, the application will fail to build.

Run the App Locally

Start your application locally using the heroku local command, which installed as part of the Heroku CLI.

$ heroku local web --port 5006
[OKAY] Loaded ENV .env File as KEY=VALUE Format
5:32:55 PM web.1 |  Listening on 5006

Just like Heroku, heroku local examines the Procfile to determine what to run.

Open http://localhost:5006 with your web browser to see your app running locally.

To stop the app from running locally, in the CLI, press Ctrl+C to exit.

Push Local Changes

In this step, you learn how to propagate a local change to the application through to Heroku. As an example, lets modify the application to add a dependency and the code to use it.

Begin by adding a dependency for cool-ascii-faces in package.json. Run this command.

$ npm install cool-ascii-faces
added 11 packages, and audited 344 packages in 1s

45 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

Modify index.js so that it requires this module at the start. Also add a new route (/cool) that uses it. You want the final code to look like:

const express = require('express')
const path = require('path')
const cool = require('cool-ascii-faces')

const port = process.env.PORT || 5006

const app = express()

app.use(express.static(path.join(__dirname, 'public')))
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  console.log(`Rendering 'pages/index' for route '/'`)
  res.render('pages/index')
})

app.get('/cool', (req, res) => {
  console.log(`Rendering a cool ascii face for route '/cool'`)
  res.send(cool())
})

const server = app.listen(port, () => {
  console.log(`Listening on ${port}`)
})

process.on('SIGTERM', async () => {
  console.log('SIGTERM signal received: gracefully shutting down')
  if (server) {
    server.close(() => {
      console.log('HTTP server closed')
    })
  }
})

Test locally.

When you visit your application at http://localhost:5001/cool, the sample application shows you cute faces on each refresh: ( ⚆ _ ⚆ ).

Deploy. Almost every deploy to Heroku follows this same pattern. First, add the modified files to the local Git repository.

$ git add .

Commit the changes to the repository.

$ git commit -m "Add cool face API"
[main 01d79e2] Add cool face API
 3 files changed, 99 insertions(+)

Deploy, just as you did previously.

$ git push heroku main
remote: Updated 18 paths from 9267c72
remote: Compressing source files... done.
remote: Building source:
remote: Heroku Labs: Build Time Config Vars are *not enabled*.
remote: By default, your config vars are only available at runtime. See https://devcenter.heroku.com/build-time-config-vars for details on Build Time Config Vars.
remote: Extracting source
remote: Restoring cache
remote: Restoring data for SBOM from previous image
remote: 3 of 6 buildpacks participating
remote: heroku/nodejs-engine      3.6.1
remote: heroku/nodejs-npm-install 3.6.1
remote: heroku/procfile           4.2.1
remote: Restoring metadata for "heroku/nodejs-engine:dist" from app image
remote: Restoring metadata for "heroku/nodejs-engine:web_env" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_runtime_config" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_cache" from cache
remote: Restoring data for "heroku/nodejs-engine:dist" from cache
remote: Restoring data for "heroku/nodejs-npm-install:npm_cache" from cache
remote:
remote: ## Heroku Node.js Engine
remote:
remote: - Checking Node.js version
remote:   - Detected Node.js version range: `>=18.0.0 <19.0.0-0||>=20.0.0 <21.0.0-0||>=22.0.0 <23.0.0-0`
remote:   - Resolved Node.js version: `22.15.0`
remote: - Installing Node.js distribution
remote:   - Reusing Node.js 22.15.0 (linux-arm64)
remote: - Done (finished in < 0.1s)
remote:
remote: ## Heroku Node.js npm Install
remote:
remote: - Installing node modules
remote:   - Using npm version `10.9.2`
remote:   - Restoring npm cache
remote:   - Configuring npm cache directory
remote:   - Running `npm ci "--production=false"`
remote:
remote:       npm warn config production Use `--omit=dev` instead.
remote:
remote:       added 342 packages, and audited 343 packages in 3s
remote:
remote:       45 packages are looking for funding
remote:         run `npm fund` for details
remote:
remote:       1 moderate severity vulnerability
remote:
remote:       To address all issues, run:
remote:         npm audit fix
remote:
remote:       Run `npm audit` for details.
remote:
remote:   - Done (2.7s)
remote: - Running scripts
remote:   - No build scripts found
remote: - Configuring default processes
remote:   - Skipping default web process (Procfile detected)
remote: - Done (finished in 3.0s)
remote:
remote: ## Procfile Buildpack
remote:
remote: - Processes from `Procfile`
remote:   - web: `node index.js`
remote: - Done (finished in < 0.1s)
remote: Reusing layers from image 'damp-thicket-04285/builds@sha256:a63c9ed78fcd3b4fd9948fbb18fd202e18ee3bd0aecdca0ea55b2ba726961c2a'
remote: Reusing layer 'heroku/nodejs-engine:available_parallelism'
remote: Reusing layer 'heroku/nodejs-engine:dist'
remote: Reusing layer 'heroku/nodejs-engine:web_env'
remote: Reusing layer 'heroku/nodejs-npm-install:npm_runtime_config'
remote: Reusing layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Reusing layer 'buildpacksio/lifecycle:launcher'
remote: Reusing layer 'buildpacksio/lifecycle:config'
remote: Reusing layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving damp-thicket-04285/builds...
remote: *** Images (sha256:0b546747b0262297d4ee0de5e1409b851c26f14a716e766357a7a3253504bd60):
remote:       damp-thicket-04285/builds
remote:       damp-thicket-04285/builds:7fb5cfe3-0aea-4f1b-98d6-256a623259cf
remote: Reusing cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-npm-install:npm_cache'
remote: Uploading cache
remote: Launching...
remote: https://damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/damp-thicket-04285.git
   26557c7..01d79e2  main -> main

Check that everything works.

$ heroku open cool

If everything is in working order, you see another face.

Debugging

The Heroku Node.js Cloud Native Buildpack (CNB) turns your code into an Open Container Initiative (OCI) container image when you deploy to Fir. This image gets executed on our dynos.

You can use this image locally to reproduce and debug deployment problems. Build an OCI image from your application to debug locally by using the Heroku Node.js CNB. If you’re interested, check out the Node.js CNB tutorial.

Start a Console

The heroku run command to launch an interactive one-off dyno is unavailable for Fir. As an alternative, use heroku run:inside to access a running dyno until we add heroku run for Fir.

You must add an SSH key to your Heroku account before running this command.

To execute the command you need the name of a currently running process. You can see a list with heroku ps:

$ heroku ps
=== web (dyno-1c-0.5gb): node index.js (1)

web-556c78c966-znsmn: up 2025/04/28 17:35:12 -0500 (~ 7s ago)

Use that dyno name to run an interactive bash session:

$ heroku run:inside web-556c78c966-znsmn "bash"
Running launcher bash on damp-thicket-04285...
Running launcher bash on damp-thicket-04285... up, web-556c78c966-znsmn
heroku@web-556c78c966-znsmn:/workspace$ ls -lah
total 224K
drwxrwsrwx.   6 heroku heroku  16K Jan  1  1980 .
drwxr-xr-x.   1 root   root     17 Apr 28 22:35 ..
-rw-r--r--.   1 heroku heroku  226 Jan  1  1980 .env
drwxr-xr-x.   2 heroku heroku   46 Jan  1  1980 .github
-rw-r--r--.   1 heroku heroku  139 Jan  1  1980 .gitignore
-rw-r--r--.   1 heroku heroku   19 Jan  1  1980 Procfile
-rw-r--r--.   1 heroku heroku 3.0K Jan  1  1980 README.md
-rw-r--r--.   1 heroku heroku  243 Jan  1  1980 app.json
-rw-r--r--.   1 heroku heroku  818 Jan  1  1980 index.js
drwxr-sr-x. 270 heroku heroku  16K Jan  1  1980 node_modules
-rw-r--r--.   1 heroku heroku 159K Jan  1  1980 package-lock.json
-rw-r--r--.   1 heroku heroku  601 Jan  1  1980 package.json
drwxr-xr-x.   3 heroku heroku   62 Jan  1  1980 public
-rw-r--r--.   1 heroku heroku 1.6K Jan  1  1980 test.js
drwxr-xr-x.   4 heroku heroku   35 Jan  1  1980 views
heroku@web-556c78c966-znsmn:/workspace$ exit
exit

If you receive an error, Error connecting to process, configure your firewall.

Type exit to exit the shell.

Define Config Vars

With Heroku, you can externalize configuration, storing data such as encryption keys or external resource addresses in config vars.

At runtime, config vars are exposed as environment variables to the application.

Config vars for Fir-generation apps are only available at runtime by default. You can enable Build Time Config Vars to allow for both build and runtime availability.

For example, lets modify index.js to introduce a new route, /times, that repeats an action depending on the value of the TIMES environment variable. Copy the following into index.js:

const express = require('express')
const path = require('path')
const cool = require('cool-ascii-faces')

const port = process.env.PORT || 5006

const app = express()

app.use(express.static(path.join(__dirname, 'public')))
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  console.log(`Rendering 'pages/index' for route '/'`)
  res.render('pages/index')
})

app.get('/cool', (req, res) => {
  console.log(`Rendering a cool ascii face for route '/cool'`)
  res.send(cool())
})

app.get('/times', (req, res) => {
  const times = process.env.TIMES || 5
  console.log(`Rendering a count from 1 to ${times} for route '/times'`)
  let result = ''
  for (let i = 1; i <= times; i++) {
    result += i + ' '
  }
  res.send(result)
})

const server = app.listen(port, () => {
  console.log(`Listening on ${port}`)
})

process.on('SIGTERM', async () => {
  console.log('SIGTERM signal received: gracefully shutting down')
  if (server) {
    server.close(() => {
      console.log('HTTP server closed')
    })
  }
})

heroku local automatically sets up the environment based on the contents of the .env file in your local directory. In the top-level directory of your project, the .env file already contains the following:

TIMES=2

If you run the app with heroku local --port 5006 and then open http://localhost:5001/times, you see two numbers generated every time.

To set the config var on Heroku, execute this command.

$ heroku config:set TIMES=2
Setting TIMES and restarting damp-thicket-04285...
Setting TIMES and restarting damp-thicket-04285... done, v5
TIMES: 2

View the config vars that are set using heroku config.

$ heroku config | grep TIMES
TIMES: 2

Add the modified files to the local Git repository.

$ git add .

Commit the changes to the repository.

$ git commit -m "Add /times route"
[main df8238b] Add /times route
 1 file changed, 10 insertions(+)

Deploy the changes to Heroku.

$ git push heroku main
remote: Updated 18 paths from 85d4025
remote: Compressing source files... done.
remote: Building source:
remote: Heroku Labs: Build Time Config Vars are *not enabled*.
remote: By default, your config vars are only available at runtime. See https://devcenter.heroku.com/build-time-config-vars for details on Build Time Config Vars.
remote: Extracting source
remote: Restoring cache
remote: Restoring data for SBOM from previous image
remote: 3 of 6 buildpacks participating
remote: heroku/nodejs-engine      3.6.1
remote: heroku/nodejs-npm-install 3.6.1
remote: heroku/procfile           4.2.1
remote: Restoring metadata for "heroku/nodejs-engine:dist" from app image
remote: Restoring metadata for "heroku/nodejs-engine:web_env" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_runtime_config" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_cache" from cache
remote: Restoring data for "heroku/nodejs-engine:dist" from cache
remote: Restoring data for "heroku/nodejs-npm-install:npm_cache" from cache
remote:
remote: ## Heroku Node.js Engine
remote:
remote: - Checking Node.js version
remote:   - Detected Node.js version range: `>=18.0.0 <19.0.0-0||>=20.0.0 <21.0.0-0||>=22.0.0 <23.0.0-0`
remote:   - Resolved Node.js version: `22.15.0`
remote: - Installing Node.js distribution
remote:   - Reusing Node.js 22.15.0 (linux-arm64)
remote: - Done (finished in < 0.1s)
remote:
remote: ## Heroku Node.js npm Install
remote:
remote: - Installing node modules
remote:   - Using npm version `10.9.2`
remote:   - Restoring npm cache
remote:   - Configuring npm cache directory
remote:   - Running `npm ci "--production=false"`
remote:
remote:       npm warn config production Use `--omit=dev` instead.
remote:
remote:       added 342 packages, and audited 343 packages in 3s
remote:
remote:       45 packages are looking for funding
remote:         run `npm fund` for details
remote:
remote:       1 moderate severity vulnerability
remote:
remote:       To address all issues, run:
remote:         npm audit fix
remote:
remote:       Run `npm audit` for details.
remote:
remote:   - Done (2.6s)
remote: - Running scripts
remote:   - No build scripts found
remote: - Configuring default processes
remote:   - Skipping default web process (Procfile detected)
remote: - Done (finished in 2.9s)
remote:
remote: ## Procfile Buildpack
remote:
remote: - Processes from `Procfile`
remote:   - web: `node index.js`
remote: - Done (finished in < 0.1s)
remote: Reusing layers from image 'damp-thicket-04285/builds@sha256:0b546747b0262297d4ee0de5e1409b851c26f14a716e766357a7a3253504bd60'
remote: Reusing layer 'heroku/nodejs-engine:available_parallelism'
remote: Reusing layer 'heroku/nodejs-engine:dist'
remote: Reusing layer 'heroku/nodejs-engine:web_env'
remote: Reusing layer 'heroku/nodejs-npm-install:npm_runtime_config'
remote: Reusing layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Reusing layer 'buildpacksio/lifecycle:launcher'
remote: Reusing layer 'buildpacksio/lifecycle:config'
remote: Reusing layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving damp-thicket-04285/builds...
remote: *** Images (sha256:5eaaa878a86108d16a66cd0880cd0d7a29194d6c3a6b2f1b810aaecdcc5ded52):
remote:       damp-thicket-04285/builds:fdaf9dc1-79a5-4ad2-85d5-d2fb452e4460
remote: Reusing cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-npm-install:npm_cache'
remote: Uploading cache
remote: Launching...
remote: https://damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/damp-thicket-04285.git
   01d79e2..df8238b  main -> main

Then visit it by running heroku open times.

Provision a Database

The sample app requires a database. Provision a Heroku Postgres database, an add-on available through the Elements Marketplace. Add-ons are cloud services that provide out-of-the-box additional services for your application, such as logging, monitoring, databases, and more.

An essential-0 Postgres size costs $5 a month, prorated to the minute. At the end of this tutorial, we prompt you to delete your database to minimize costs. For production apps in Private Spaces, you might want to use a private database plan such as private-0 or higher.

$ heroku addons:create heroku-postgresql:essential-0 --confirm damp-thicket-04285 -- --region us
Creating heroku-postgresql:essential-0 on damp-thicket-04285...
Creating heroku-postgresql:essential-0 on damp-thicket-04285... ~$0.007/hour (max $5/month)
Database should be available soon
postgresql-concave-98878 is being created in the background. The app will restart when complete...
Use heroku addons:info postgresql-concave-98878 to check creation progress
Use heroku addons:docs heroku-postgresql to view documentation

You can wait for the database to provision by running this command:

$ heroku pg:wait

After that command exits, your Heroku app can access the Postgres database. The DATABASE_URL environment variable stores your credentials, which your app is configured to connect to. You can see all the add-ons provisioned with the addons command:

$ heroku addons

 Add-on                                       Plan        Price        Max price State
 ──────────────────────────────────────────── ─────────── ──────────── ───────── ───────
 heroku-postgresql (postgresql-concave-98878) essential-0 ~$0.007/hour $5/month  created
  └─ as DATABASE

The table above shows add-ons and the attachments to the current app (damp-thicket-04285) or other apps.

Use npm to add node-postgres to your dependencies.

$ npm install pg
added 13 packages, and audited 357 packages in 1s

45 packages are looking for funding
  run `npm fund` for details

1 moderate severity vulnerability

To address all issues, run:
  npm audit fix

Run `npm audit` for details.

Edit your index.js file to use this module to connect to the database specified in your DATABASE_URL environment variable. On line 4 of index.js, add:

const { Pool } = require('pg')

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false
  }
})

Then, to execute a query against the database, add a new /db route that will return all rows in the test_table table. On line 38 of index.js, add:

app.get('/db', async (req, res) => {
  console.log(`Rendering the results of a database query for route '/db'`)
  try {
    const client = await pool.connect()
    const result = await client.query('SELECT * FROM test_table')
    res.render('pages/db', {
      results: result ? result.rows : null
    })
    client.release()
  } catch (err) {
    console.error(err);
    res.send("Error " + err);
  }
})

Now that we’re connecting to a database, we also want to make sure to release our pool of connections when the app shuts down. After line 62, add the following to the shutdown handler:

  pool.end().then(() => {
    console.log('PG pool closed')
  })

The index.js file should now look like this:

const express = require('express')
const path = require('path')
const cool = require('cool-ascii-faces')
const { Pool } = require('pg')

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: false
  }
})

const port = process.env.PORT || 5006

const app = express()

app.use(express.static(path.join(__dirname, 'public')))
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'ejs')

app.get('/', (req, res) => {
  console.log(`Rendering 'pages/index' for route '/'`)
  res.render('pages/index')
})

app.get('/cool', (req, res) => {
  console.log(`Rendering a cool ascii face for route '/cool'`)
  res.send(cool())
})

app.get('/times', (req, res) => {
  const times = process.env.TIMES || 5
  console.log(`Rendering a count from 1 to ${times} for route '/times'`)
  let result = ''
  for (let i = 1; i <= times; i++) {
    result += i + ' '
  }
  res.send(result)
})

app.get('/db', async (req, res) => {
  console.log(`Rendering the results of a database query for route '/db'`)
  try {
    const client = await pool.connect()
    const result = await client.query('SELECT * FROM test_table')
    res.render('pages/db', {
      results: result ? result.rows : null
    })
    client.release()
  } catch (err) {
    console.error(err);
    res.send("Error " + err);
  }
})

const server = app.listen(port, () => {
  console.log(`Listening on ${port}`)
})

process.on('SIGTERM', async () => {
  console.log('SIGTERM signal received: gracefully shutting down')
  if (server) {
    server.close(() => {
      console.log('HTTP server closed')
    })
  }
  pool.end().then(() => {
    console.log('PG pool closed')
  })
})

Add the modified files to the local Git repository.

$ git add .

Commit the changes to the repository.

$ git commit -m "Add /db route"
[main 565a5a4] Add /db route
 3 files changed, 167 insertions(+), 2 deletions(-)

Deploy the changes to Heroku.

$ git push heroku main
remote: Updated 18 paths from 3dff7b2
remote: Compressing source files... done.
remote: Building source:
remote: Heroku Labs: Build Time Config Vars are *not enabled*.
remote: By default, your config vars are only available at runtime. See https://devcenter.heroku.com/build-time-config-vars for details on Build Time Config Vars.
remote: Extracting source
remote: Restoring cache
remote: Restoring data for SBOM from previous image
remote: 3 of 6 buildpacks participating
remote: heroku/nodejs-engine      3.6.1
remote: heroku/nodejs-npm-install 3.6.1
remote: heroku/procfile           4.2.1
remote: Restoring metadata for "heroku/nodejs-engine:dist" from app image
remote: Restoring metadata for "heroku/nodejs-engine:web_env" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_runtime_config" from app image
remote: Restoring metadata for "heroku/nodejs-npm-install:npm_cache" from cache
remote: Restoring data for "heroku/nodejs-engine:dist" from cache
remote: Restoring data for "heroku/nodejs-npm-install:npm_cache" from cache
remote:
remote: ## Heroku Node.js Engine
remote:
remote: - Checking Node.js version
remote:   - Detected Node.js version range: `>=18.0.0 <19.0.0-0||>=20.0.0 <21.0.0-0||>=22.0.0 <23.0.0-0`
remote:   - Resolved Node.js version: `22.15.0`
remote: - Installing Node.js distribution
remote:   - Reusing Node.js 22.15.0 (linux-arm64)
remote: - Done (finished in < 0.1s)
remote:
remote: ## Heroku Node.js npm Install
remote:
remote: - Installing node modules
remote:   - Using npm version `10.9.2`
remote:   - Restoring npm cache
remote:   - Configuring npm cache directory
remote:   - Running `npm ci "--production=false"`
remote:
remote:       npm warn config production Use `--omit=dev` instead.
remote:
remote:       added 355 packages, and audited 356 packages in 3s
remote:
remote:       45 packages are looking for funding
remote:         run `npm fund` for details
remote:
remote:       1 moderate severity vulnerability
remote:
remote:       To address all issues, run:
remote:         npm audit fix
remote:
remote:       Run `npm audit` for details.
remote:
remote:   - Done (2.7s)
remote: - Running scripts
remote:   - No build scripts found
remote: - Configuring default processes
remote:   - Skipping default web process (Procfile detected)
remote: - Done (finished in 3.0s)
remote:
remote: ## Procfile Buildpack
remote:
remote: - Processes from `Procfile`
remote:   - web: `node index.js`
remote: - Done (finished in < 0.1s)
remote: Reusing layers from image 'damp-thicket-04285/builds@sha256:5eaaa878a86108d16a66cd0880cd0d7a29194d6c3a6b2f1b810aaecdcc5ded52'
remote: Reusing layer 'heroku/nodejs-engine:available_parallelism'
remote: Reusing layer 'heroku/nodejs-engine:dist'
remote: Reusing layer 'heroku/nodejs-engine:web_env'
remote: Reusing layer 'heroku/nodejs-npm-install:npm_runtime_config'
remote: Reusing layer 'buildpacksio/lifecycle:launch.sbom'
remote: Added 1/1 app layer(s)
remote: Reusing layer 'buildpacksio/lifecycle:launcher'
remote: Reusing layer 'buildpacksio/lifecycle:config'
remote: Reusing layer 'buildpacksio/lifecycle:process-types'
remote: Adding label 'io.buildpacks.lifecycle.metadata'
remote: Adding label 'io.buildpacks.build.metadata'
remote: Adding label 'io.buildpacks.project.metadata'
remote: Setting default process type 'web'
remote: Saving damp-thicket-04285/builds...
remote: *** Images (sha256:988e677e14a876a9d1dc6de806d4cae1303891cb41a1e076f59fa9d0af60bd28):
remote:       damp-thicket-04285/builds:7b7c13ba-8189-46f8-9f40-5369e64c27e1
remote: Reusing cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-engine:dist'
remote: Adding cache layer 'heroku/nodejs-npm-install:npm_cache'
remote: Uploading cache
remote: Launching...
remote: https://damp-thicket-04285-47569dc725e7.aster-virginia.herokuapp.com/ deployed to Heroku
remote: Verifying deploy... done.
To https://git.heroku.com/damp-thicket-04285.git
   df8238b..565a5a4  main -> main

If you access /db, you receive an error because there’s no table in the database. Assuming that you have Postgres installed locally, use the heroku pg:psql command to connect to the remote database, create a table, and insert a row.

$ heroku pg:psql -c "create table test_table (id integer, name text); insert into test_table values (1, 'hello database');"
--> Connecting to postgresql-concave-98878
CREATE TABLE
INSERT 0 1

Now when you access your app’s /db route with heroku open db, you see something like:

Database results are 1 hello database

Read more about Heroku PostgreSQL.

You can use a similar technique to install MongoDB or Redis add-ons.

Delete Your App

Remove the app from your account. We only charge you for the resources you used.

This action permanently deletes your application and any add-ons attached to it.

$ heroku apps:destroy

You can confirm that your app is gone with this command:

$ heroku apps --all

Next Steps

You now know how to configure and deploy a Node.js app, view logs, and start a console.

To learn more, see:

  • How Heroku Works
  • Preparing a Codebase for Heroku Deployment
  • Heroku Node.js Documentation

Information & Support

  • Getting Started
  • Documentation
  • Changelog
  • Compliance Center
  • Training & Education
  • Blog
  • Support Channels
  • Status

Language Reference

  • Node.js
  • Ruby
  • Java
  • PHP
  • Python
  • Go
  • Scala
  • Clojure
  • .NET

Other Resources

  • Careers
  • Elements
  • Products
  • Pricing
  • RSS
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku Blog
    • Heroku News Blog
    • Heroku Engineering Blog
  • Twitter
    • Dev Center Articles
    • Dev Center Changelog
    • Heroku
    • Heroku Status
  • Github
  • LinkedIn
  • © 2025 Salesforce, Inc. All rights reserved. Various trademarks held by their respective owners. Salesforce Tower, 415 Mission Street, 3rd Floor, San Francisco, CA 94105, United States
  • heroku.com
  • Legal
  • Terms of Service
  • Privacy Information
  • Responsible Disclosure
  • Trust
  • Contact
  • Cookie Preferences
  • Your Privacy Choices