Deploying a simple Node.js app with HTTPS on cloud providers in 2023: Heroku, Render, Fly.io, AWS, Hetzner, Google Cloud Run

Jérémy Levy
Eleven
Published in
15 min readNov 5, 2022

--

Hello 👋,

Today, in the Eleven Blog, we will try to deploy a Node.js application on different cloud providers.

Eleven is a free and open-source Codespaces alternative with automatic HTTPS and long running processes.

The application that we will try to deploy is a simple Node.js app hosted on GitHub: https://github.com/jeremylevy/node-app

The code of our application is taken from the Node.js website “Getting Started Guide”.

Table of contents

Heroku (https://heroku.com) (7$/mo)

Heroku is a platform as a service (PaaS) that enables developers to build, run, and operate applications entirely in the cloud.

1. Create an Heroku account

The first step is to create an Heroku account by going to: https://signup.heroku.com/dc

2. Install the Heroku Command Line Interface (CLI)

The Heroku CLI requires Git. If you don’t already have Git installed, you need to install it.

Now that you’ve created an Heroku account, you need to install the CLI.

The steps required to install the CLI vary depending on your operating system. Go to the following page to choose the best installer for your platform: https://devcenter.heroku.com/articles/getting-started-with-nodejs?singlepage=true#set-up

When the installation is complete, run the heroku login command to log in:

$ heroku login

3. Prepare the app

Now that the CLI is installed and configured, clone the sample app that we’ve prepared:

$ git clone https://github.com/jeremylevy/node-app.git
$ cd node-app

4. Deploy

Time to deploy!

The first step is to create an Heroku app. To to do this, run the following command in the node-app directory:

$ heroku create

A git remote called heroku will be created and associated with your local git repository.

You can now deploy your app using the git push command:

$ git push heroku main
...
remote: -----> Launching...
remote: Released v3
remote: https://salty-garden-10959.herokuapp.com/ deployed to Heroku

The application is now deployed.

Open the URL displayed to ensure that your app works fine https://salty-garden-10959.herokuapp.com:

Sadly, it doesn’t.

To view your application logs, run the heroku logs --tail command:

2022-11-04T09:27:23.537146+00:00 heroku[web.1]: Starting process with command `npm start`2022-11-04T09:27:25.266098+00:00 app[web.1]: npm ERR! Missing script: "start"

It seems that Heroku tries to find a start script in our application.

No problem! In the package.json file, add a start property to the scripts object:

"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

then, push your changes:

$ git add -A && git commit -m "add start command"
$ git push heroku main
...
remote: -----> Launching...
remote: Released v3
remote: https://salty-garden-10959.herokuapp.com/ deployed to Heroku

Once again, open the URL displayed https://salty-garden-10959.herokuapp.com:

It still not works.

Re-run the heroku logs --tail command:

$ heroku logs --tail
2022-11-04T09:41:43.374211+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

This time, Heroku tries to bind to the PORT environment variable instead of the one used in our index.js.

Update the index.js file to use the PORT environment variable if defined:

const port = process.env.PORT || 3000;

Push your changes:

$ git add -A && git commit -m "use $PORT env var if exists"
$ git push heroku main
...
remote: -----> Launching...
remote: Released v3
remote: https://salty-garden-10959.herokuapp.com/ deployed to Heroku

Another time, open the URL displayed https://salty-garden-10959.herokuapp.com:

Our app is still not starting.

Re-run the heroku logs --tail command:

$ heroku logs --tail
2022-11-04T09:41:43.374211+00:00 heroku[web.1]: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

Same error!

It seems that Heroku fails to bind to our app. The sole reason possible is that our app binds to 127.0.0.1 instead of 0.0.0.0.

Go back to the index.js file and try to replace 127.0.0.1 by 0.0.0.0:

const hostname = '0.0.0.0';

Push your changes:

$ git add -A && git commit -m "bind to '0.0.0.0' instead of '127.0.0.1'"
$ git push heroku main
...
remote: -----> Launching...
remote: Released v3
remote: https://salty-garden-10959.herokuapp.com/ deployed to Heroku

Finally, go again to the displayed URL https://salty-garden-10959.herokuapp.com:

It works! Congrats!

5. Enable HTTPS

Heroku doesn’t offer HTTPS on its free plan. As a result, you need to upgrade to a paid plan to enable HTTPS:

$ heroku ps:resize web=hobby

By default, a Heroku app is available at its Heroku domain, which has the form <app_name>.herokuapp.com.

To make your app available at a non-Heroku domain (for example, www.yourcustomdomain.com), you need to add a custom domain to it.

  • To add a custom domain with a subdomain, you need to use the domains:add command:
$ heroku domains:add www.example.com
Adding www.example.com to ⬢ example-app... done
▸ Configure your app's DNS provider to point to the DNS Target
salty-garden-10959.herokudns.com.
▸ For help, see https://devcenter.heroku.com/articles/custom-domains

The domain www.example.com has been enqueued for addition
▸ Run heroku domains:wait 'www.example.com' to wait for completion

Now, add a CNAME record that points to the DNS target provided by Heroku (salty-garden-10959.herokudns.com in this case) and you’re set.

  • To add a custom domain without a subdomain, you also need to use the domains:add command:
$ heroku domains:add example.com
Adding www.example.com to ⬢ example-app... done
▸ Configure your app's DNS provider to point to the DNS Target
salty-garden-10959.herokudns.com.
▸ For help, see https://devcenter.heroku.com/articles/custom-domains
The domain www.example.com has been enqueued for addition
▸ Run heroku domains:wait 'www.example.com' to wait for completion

This time, add an ALIAS, ANAME or CNAME (Cloudflare only)record that points to the DNS target provided by Heroku (salty-garden-10959.herokudns.com in this case) and you’re set.

Render (https://render.com) (Free) (Failed)

Render is a unified cloud to build and run all your apps and websites with free TLS certificates, global CDN, private networks and auto deploys from Git.

1. Create a Render account

The first step is to create a Render account by going to: https://dashboard.render.com/register?next=/

2. Create a new “Web Service”

Now that you’ve created a Render account, you need to create a new “Web Service” in the dashboard.

To do this, click on the “New +” button then choose “Web Service”:

At the bottom, fill the “Public Git repository” section with the address of our sample app https://github.com/jeremylevy/node-app:

Now, click on “Continue”:

And… it doesn’t work. Nothing we can do about that.

Fly.io (https://fly.io) (Free)

Deploy App Servers Close to Your Users.

1. Install Flyctl and signup

Flyctl is a command-line utility that lets you work with the Fly.io platform.

The process required to install the CLI vary depending on your operating system. Go to the following page to choose the best installer for your platform: https://fly.io/docs/hands-on/install-flyctl/

When the installation is complete, run the fly auth signup command to sign up:

$ fly auth signup

2. Prepare the app

Now that the CLI is installed and configured, clone the sample app that we’ve prepared:

$ git clone https://github.com/jeremylevy/node-app.git
$ cd node-app

3. Deploy

Time to deploy!

Run the fly launch command in the node-app directory:

$ fly launch

and… we’ve an error:

$ fly launch
Scanning source code
Detected a NodeJS app
? Choose an app name (leave blank to generate one):
automatically selected personal organization: Jeremy Levy
? Choose a region for deployment: Frankfurt, Germany (fra)
Error You must verify your email address to activate your account. https://fly.io/dashboard/jeremy-levy/billing

We first need to verify our email address. Go for it and re-run the fly launch command:

$ fly launch

and… we’ve another error:

$ fly launch
...
2022-11-04T13:59:26Z [info]2022/11/04 13:59:26 listening on [fdaa:0:cf98:a7b:8d:af1e:3937:2]:22 (DNS: [fdaa::3]:53)
2022-11-04T13:59:26Z [info]Server running at http://127.0.0.1:3000/
--> v0 failed - Failed due to unhealthy allocations - no stable job version to auto revert to and deploying as v1
--> Troubleshooting guide at https://fly.io/docs/getting-started/troubleshooting/
Error abort

but our app seems to start successfully:

2022-11-04T13:59:26Z   [info]Server running at http://127.0.0.1:3000

By going to https://fly.io/docs/getting-started/troubleshooting/, we could see that the fly CLI has a logs command that could display the app’s logs:

$ fly logs
2022-11-04T13:59:23Z runner[af1e3937] fra [info]Starting instance
2022-11-04T13:59:23Z runner[af1e3937] fra [info]Configuring virtual machine
2022-11-04T13:59:23Z runner[af1e3937] fra [info]Pulling container image
2022-11-04T13:59:25Z runner[af1e3937] fra [info]Unpacking image
2022-11-04T13:59:26Z runner[af1e3937] fra [info]Preparing kernel init
2022-11-04T13:59:26Z runner[af1e3937] fra [info]Configuring firecracker
2022-11-04T13:59:26Z runner[af1e3937] fra [info]Starting virtual machine
2022-11-04T13:59:26Z app[af1e3937] fra [info]Starting init (commit: 81d5330)...
2022-11-04T13:59:26Z app[af1e3937] fra [info]Preparing to run: `/cnb/process/web` as heroku
2022-11-04T13:59:26Z app[af1e3937] fra [info]2022/11/04 13:59:26 listening on [fdaa:0:cf98:a7b:8d:af1e:3937:2]:22 (DNS: [fdaa::3]:53)
2022-11-04T13:59:26Z app[af1e3937] fra [info]Server running at http://127.0.0.1:3000

Sadly for us, the logs seems to be perfectly fine. No error, only infos.

Further down the troubleshooting page, we see something interesting:

The Rule of thumb to fix this is to get the app to open up port 8080 on the IP address 0.0.0.0.

Ok, we’re into something. Change your app to conform to the fly.io’s requirements:

const hostname = '0.0.0.0';
const port = 8080;

and re-run the fly launch command:

$ fly launch
An existing fly.toml file was found for app falling-grass-7247
App is not running, deploy...
==> Building image
WARN Failed to start remote builder heartbeat: must provide either an organization id or app name
WARN failed to create build in graphql: input:3: createBuild Could not find App
Error failed to fetch an image or build from source: app does not have a Dockerfile or buildpacks configured. See https://fly.io/docs/reference/configuration/#the-build-section

and… another error!

By digging into the documentation, it seems that the launch command must only be run once, during app creation. Not for deployment.

To deploy another version, you need to run the fly deploy command:

$ fly deploy
...
1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing]
--> v1 deployed successfully

This times everything seems to be working fine… Excepted that we don’t have any URL displayed to check out our application.

By digging another time in the documentation, we could see an open command that opens your browser and directs it to your app. Perfect!

$ fly open

It works! Congrats!

4. Enable HTTPS

To use HTTPS in your application, you first need to run the fly ips list command to see your app's IP addresses:

$ fly ips list
VERSION IP TYPE REGION CREATED AT
v4 137.66.62.122 public global 2h26m ago
v6 2a09:8280:1::1:93df public global 2h26m ago

Then, you will need to add an A record pointing to your app’s v4 IP address for the domain that you want to use.

Once done, you could run the following command to generate SSL certificates for your domain:

$ fly certs create my-domain.com

This will starts the process of generating certificates. You can check on the progress by running:

$ fly certs show my-domain.com
Hostname = my-domain.com
Configured = true
Issued = rsa,ecdsa
Certificate Authority = lets_encrypt
DNS Provider = dnsimple
DNS Validation Instructions = CNAME _acme-challenge.example.com => example.com.o055.flydns.net.
DNS Validation Hostname = _acme-challenge.example.com
DNS Validation Target = my-domain.com.o055.flydns.net
Source = fly
Created At = 1m9s ago
Status = Ready

Configured should be true and Status should be ready when the certificates are available.

You’re set!

AWS (https://aws.amazon.com) (Usage based)

Amazon Web Services offers reliable, scalable, and inexpensive cloud computing services. Free to join, pay only for what you use.

Given the complexity of deploying something on AWS from scratch, it would have taken an entire article to describe the steps required. For comparison fairness, we’ve chosen to use Eleven.

1. Install the Eleven CLI and connect your GitHub account

The process required to install the Eleven CLI vary depending on your operating system:

  • On Linux and MacOS, the easiest way to install it is by running the following command in your terminal:
$ curl -sf https://raw.githubusercontent.com/eleven-sh/cli/main/install.sh | sh -s — -b /usr/local/bin latest
  • On Windows, the easiest way is by running the following Powershell script:
$ irm https://raw.githubusercontent.com/eleven-sh/cli/main/install.ps1 | iex

When the installation is complete, run the eleven login command to connect your GitHub account and access your repositories:

$ eleven login

2. Deploy

Time to deploy!

We’ll use the eleven init command to deploy our application:

$ eleven aws --region us-east-1 init my-app --instance-type t2.medium --runtimes node --repositories jeremylevy/node-appThe public IP of your sandbox is: 35.168.224.199To connect to your sandbox:
- With your editor: `eleven aws --region us-east-1 edit my-app`
- With SSH : `ssh eleven/my-app`
To allow TCP traffic on a port: `eleven aws --region us-east-1 serve my-app <port> [--as <domain>]`Installed runtimes: node@latest

Now, we need to start our app and make sure that it will be restarted in case of error. To do so, we need to use the forever command.

To run commands in our app, we could use an editor with an integrated terminal (like VSCode) or use ssh directly. We’ll use ssh:

$ ssh eleven/my-app forever node index.jsForever: command started. Run "forever stop" in current path to stop.

3. Enable HTTPS

To enable HTTPS in our application, we need to add an A record pointing to the public IP address displayed by the init command (see above), for the domain that we want to use.

Once done, we will use the eleven serve command to connect our domain to our application:

$ eleven aws --region us-east-1 serve my-app 3000 --as www.test.comSuccess! The port "3000" is now reachable at: https://www.test.com

(Our application listens on port “3000”, and the domain that we want to use is “www.test.com”.)

You’re set!

Hetzner (https://www.hetzner.com) (4$/month)

Hetzner Online GmbH is a company and data center operator based in Gunzenhausen, Germany.

Given the complexity of deploying something on Hetzner from scratch, it would have taken an entire article to describe the steps required. For comparison fairness, we’ve chosen to use Eleven.

1. Install the Eleven CLI and connect your GitHub account

The process required to install the Eleven CLI vary depending on your operating system:

  • On Linux and MacOS, the easiest way to install it is by running the following command in your terminal:
$ curl -sf https://raw.githubusercontent.com/eleven-sh/cli/main/install.sh | sh -s — -b /usr/local/bin latest
  • On Windows, the easiest way is by running the following Powershell script:
$ irm https://raw.githubusercontent.com/eleven-sh/cli/main/install.ps1 | iex

When the installation is complete, run the eleven login command to connect your GitHub account and access your repositories:

$ eleven login

2. Deploy

Time to deploy!

We’ll use the eleven init command to deploy our application:

$ eleven hetzner --region fsn1 init my-app --instance-type cx11 --runtimes node --repositories jeremylevy/node-appThe public IP of your sandbox is: 167.235.202.28To connect to your sandbox:
- With your editor: `eleven hetzner --region fsn1 edit my-app`
- With SSH : `ssh eleven/my-app`
To allow TCP traffic on a port: `eleven hetzner --region fsn1 serve my-app <port> [--as <domain>]`Installed runtimes: node@latest

Now, we need to start our app and make sure that it will be restarted in case of error. To do so, we need to use the forever command.

To run commands in our app, we could use an editor with an integrated terminal (like VSCode) or use ssh directly. We’ll use ssh:

$ ssh eleven/my-app forever node index.jsForever: command started. Run "forever stop" in current path to stop.

3. Enable HTTPS

To enable HTTPS in our application, we need to add an A record pointing to the public IP address displayed by the init command (see above), for the domain that we want to use.

Once done, we will use the eleven serve command to connect our domain to our application:

$ eleven hetzner --region fsn1 serve my-app 3000 www.test.comSuccess! The port "3000" is now reachable at: https://www.test.com

(Our application listens on port “3000”, and the domain that we want to use is “www.test.com”.)

You’re set!

Google Cloud Run (https://cloud.google.com/run) (Usage based)

Google Cloud Run is a fully managed compute platform that automatically scales containers, build your apps and deploy them in seconds.

1. Create a Google account

The first step is to create a Google account by going to: https://accounts.google.com/signup

Once created, you will be redirected to the Google Cloud console:

2. Create a new project

Next, we will need to create a “project”. A “project” corresponds to a set of infrastructure components.

If you remove a project, you will also remove all the infrastructure components linked to it.

Go to the following page to create your first project for our test app: https://console.cloud.google.com/projectcreate

Fill in the “Project name” field and click on the “Create” button.

3. Install the gcloud Command Line Interface (CLI)

Now that you’ve created a project, you will need to install the gcloud CLI.

The steps required to install the CLI vary depending on your operating system. Go to the following page to choose the best installer for your platform: https://cloud.google.com/sdk/docs/install

When the installation is complete, run the gcloud init command to log in and configure the CLI:

$ gcloud init

When asked to pick a project to use, choose the one created earlier:

$ gcloud init
...
Pick cloud project to use:
[1] my-node-test-app
[2] Enter a project ID
[3] Create a new project
Please enter numeric choice or text value (must exactly match list item): 1

4. Prepare the app

Now that the CLI is installed and configured, clone the sample app that we’ve prepared:

$ git clone https://github.com/jeremylevy/node-app.git
$ cd node-app

5. Deploy

Time to deploy!

Run the following command in the node-app directory:

$ gcloud run deploy node-test-app --source .
  • If you are prompted to enable the “Artifact Registry API”, respond by pressing y.
  • When prompted for region, select the region of your choice by typing its number. (If you want to use a custom domain and have HTTPS enabled for it, you need to choose one of the following regions: “asia-east1”, “asia-northeast1”, “asia-southeast1”, “europe-north1”, “europe-west1”, “europe-west4”, “us-central1”, “us-east1”, “us-east4” or “us-west1”.)
  • When prompted to create an “Artifact Registry Docker” repository, respond by pressing y.
  • When prompted to allow “unauthenticated invocations”, respond by pressing y.

Then, wait a few moments until the deployment is complete:

$ gcloud run deploy node-test-app --source .Deployment failed                                                              
ERROR: (gcloud.run.deploy) The user-provided container failed to start and listen on the port defined provided by the PORT=8080 environment variable. Logs for this revision might contain more information.
Logs URL: https://console.cloud.google.com/logs/viewer?
For more troubleshooting guidance, see https://cloud.google.com/run/docs/troubleshooting#container-failed-to-start

And… we’ve an error!

By going to the displayed “Logs URL”, we could see that there are many problems with our app:

  1. First, our app needs to listen on the PORT environment variable if defined.
  2. Secondly, our app needs to have a start script that will contain the command required to start it.
  3. Thirdly, our app needs to bind to 0.0.0.0 and not 127.0.0.1.

In the index.js file, update the hostname and port constants to conform to the Cloud Run requirements:

const hostname = '0.0.0.0';
const port = process.env.PORT || 3000;

Then, in the package.json file, add a start property to the scripts object:

"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

Now, rerun the deploy command:

$ gcloud run deploy node-test-app --source .
...
Service [node-test-app] revision [node-test-app-00002-xof] has been deployed and is serving 100 percent of traffic.
Service URL: https://node-test-app-msj2tijn6q-ue.a.run.app

Finally, go the displayed “Service URL” https://node-test-app-msj2tijn6q-ue.a.run.app:

It works! Congrats!

6. Enable HTTPS

To enable HTTPS, you will first need to verify domain ownership by running the following command:

$ gcloud domains verify <base-domain>

<base-domain> is the base domain that you want to verify. For example, if you want to enable HTTPS for subdomain.example.com, you should verify the ownership of example.com.

Your browser will open to the “Google Webmaster Central” website. Choose your domain name provider in the list and follow the instructions. Once done, click on the “Validate” button.

Now that the domain ownership is confirmed, run the following command:

$ gcloud beta run domain-mappings create --service node-test-app --domain <my-domain>
  • If you are prompted to “install beta components”, respond by pressing y.
  • When prompted for a region, choose the same region than previously.
$ gcloud beta run domain-mappings create --service node-test-app --domain test.my-domain.comNAME   RECORD TYPE  CONTENTS
test CNAME ghs.googlehosted.com.

Now, add a CNAME record that points to the DNS target provided by the gcloud CLI (ghs.googlehosted.com. in this case) and you’re set.

(Note that it can take several minutes for the managed SSL certificate to be issued.)

--

--

Jérémy Levy
Eleven

29. Software engineer & entrepreneur. Creator of @eleven-sh @recode-sh @scaffold-sh. Always building. 🇫🇷