Deploying Node.js Apps to AWS EC2 with Docker

Introduction

Once you've written a web application, there are dozens of offerings to get your app online and usable by other people. One well known offering is part of the Amazon Web Services (AWS) platform - Elastic Compute Cloud (EC2).

EC2 is a core part of AWS, and a lot of AWS' other services are built on top of it - therefore it's good to get an understanding of what EC2 is and how to deploy to it.

In this tutorial, we're going to create a basic Node.js app with Docker, start and configure an EC2 instance, and deploy our app to it. At the end of this tutorial you'll have your Node app running on AWS, and a better understanding of how to interact with a core AWS service.

Prerequisites

AWS Account

Amazon Web Services (AWS) is a collection of tools for building applications in the cloud. As EC2 is an AWS service, we'll need to set up an AWS account.

AWS has a free tier for a lot of awesome stuff, and EC2 is no exception - you're free to use 750 hours (31 days) of EC2 a month in the free tier for a whole year.

Docker

Docker allows us to bundle up our applications into small, easily deployable units that can be run anywhere where Docker is installed. This means no more of that 'but it works on my machine!'

This article will assume basic familiarity with Docker, and won't be going into any depth on it - however, if you'd like to do a deeper dive then check out Deploying a Node.js App to a DigitalOcean Droplet with Docker.

Node Application

Let's make a really simple Node application that responds to a request. To do this, we'll open up a terminal and run:

$ mkdir node-ec2
$ cd node-ec2
$ npm init

This will create a new folder, move into that folder, and then initialize a new Node application.

Let's stick with the NPM defaults for now - this article will assume you left the entry point as index.js. This will generate our package.json file, which is essentially a configuration file for our app.

Once the package.json file is created, open it up and add the following line to the beginning of the scripts section:

"start": "node index.js",

By doing this, instead of running node index.js, we'll be using npm start, which will run everything in our script. In this specific case, it just runs node index.js, though in reality, it could be much more than that. For example, if we can add flags to the command without having to type it out each time, or we could set some environment variables like NODE_ENV=production node index.js.

To serve our requests, we're going to be using the Express framework - it's minimalistic, and easy to get started with:

$ npm install express --save

Our package.json should now look something like this:

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1"
  }
}

Wait for the install, and then we're going to open our preferred code editor to create another new file in the same location called index.js.

The file will set up Express and define a request handler:

const express = require('express');
const app = express();
const port = 3000;

app.get('/status', (req, res) => res.send({status: "I'm alive!"}));

app.listen(port, () => console.log(`Example app listening on port ${port}!`));

This app will start on port 3000, and will serve an endpoint at /status. We can verify this works by running:

$ npm start
Example app listening on port 3000!

Heading to http://localhost:3000/status - we should get a response back with {status: "I'm alive!"}. Once that's successful, make sure to stop the server with CTRL+C.

With our simple Node application ready, let's turn it into a Docker image which we'll deploy to EC2.

Dockerizing the Node Application

Create a new file in the same directory as your Node application, called Dockerfile:

FROM node:13-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000
CMD [ "node", "index.js" ]

This is a basic Dockerfile that can be used for most simple Node applications. Next, let's build the Docker image and then run it to verify it's working correctly:

$ docker build . -t ec2-app
$ docker run -p 3000:3000 ec2-app

If you navigate to http://localhost:3000/status again, you should see the same status response from earlier. Exit the process with CTRL+C again.

Finally, let's push our Docker image to Docker Hub:

$ docker login # Use your Docker Hub credentials here
$ docker tag ec2-app <YOUR_DOCKER_USERNAME>/ec2-app
$ docker push <YOUR_DOCKER_USERNAME>/ec2-app

Setting up EC2

With our application "dockerized", we need to set up an EC2 instance for it to run on.

Head to AWS and log in.

Click the 'Services' dropdown menu at the top of the page, and search for 'EC2'. AWS is currently experimenting with their interface, so you should see a page that looks something like the one below, but the center of the page may look slightly different.

Click on the 'Instances' link on the left.

EC2 Welcome Page

On the next view, click the 'Launch Instance' button. You'll see a page that looks like this:

EC2 AMI Selection

AMIs

This is where we select the Amazon Machine Image - or AMI for short. An AMI is an 'out of the box' server, and can come with multiple configurations.

For instance, we could select one of the Quick Start AMIs that have Amazon Linux 2 on them, or if you scroll down, there are instances with Ubuntu running on them, etc.

Each AMI is a frozen image of a machine with an operating system and potentially some extra software installed.

To make things easy, we can use this to build an EC2 instance with Docker already configured for us!

To do this, we need to select 'AWS Marketplace' on the left, and then in the search box we want to enter 'ECS'. We should get a few results, but we want the 'ECS Optimized Amazon Linux 2' image.

This image comes with Docker, and is optimized for running containers. Hit 'Select' on the chosen image and we'll continue to the next page:

Choosing an AMI

Instance Types

On the next view, we select what type of instance we want. Generally, this dictates the resources available to the server that we're starting up, with scaling costs for more performant machines.

The t2.micro instance type is eligible for free tier, so it's recommended to use that:

Selecting EC2 instance type

Select the appropriate checkbox, and then click 'Review and Launch' in the bottom right corner. Click 'Launch' in the same place on the next page, and you'll get a popup to select or create a key-pair.

Select the first drop-down, and select 'Create a new key pair'. Under 'Key pair name', enter what you want to call your key pair.

Make sure to 'Download the Key Pair' on the right hand side - this is what we'll use to access our EC2 instance.

By selecting 'Launch Instance' again, your EC2 instance should get started up:

Launching instance

Click the highlighted link to be taken to the instance detail page.

Security Groups

Before we try running our application, we need to make sure that we'll be able to access the application.

Most AWS resources operate under 'Security Groups' - these groups dictate how resources can be accessed, on what port, and from which IP addresses.

Click the security group highlighted here:

Security Group Link

From here, you'll be able to see details about the security group, including it's inbound and outbound rules in various tabs. Under the inbound tab, you'll hopefully see this:

Inbound rules

What this means is that traffic that comes in through port 22, using the TCP protocol, is allowed from anywhere (0.0.0.0/0 meaning anywhere). We need to add another rule to allow anybody to access our app at port 3000.

At the top of the page, click 'Actions' and then click 'Edit inbound rules'. In the dialog that opens, click 'Add rule'.

Set the port range of the rule to 3000, and under Source, click the drop-down and select 'Anywhere'. The rest should be automatically populated.

Ultimately, you should end up with something like:

Editing the inbound rules

Connecting to Your EC2 Instance

Head back to the 'Instances' page (click the link on the left) and select the instance you created earlier. The address for your EC2 instance is located above the link to the security groups under the 'Public DNS' field.

Head back to the terminal, and navigate to the folder where the key-pair you downloaded earlier is located. It will be named as whatever you entered for the key-pair name, with a .pem as its extension.

Let's change the key's permissions and then SSH into the EC2 instance:

$ chmod 400 <NAME_OF_KEYPAIR_FILE>
$ ssh -i <NAME_OF_KEYPAIR_FILE>[email protected]<PUBLIC_DNS>

From here, we just need to launch our app via Docker:

$ docker run -p 3000:3000 <YOUR_DOCKER_USERNAME>/ec2-app

You'll be able to reach the instance using the same address you used to SSH into the instance. Simply navigate in your browser to:

<PUBLIC_DNS>:3000/status

Your app should return the status endpoint to you that we saw earlier. Congratulations, you've just run your first app on EC2!

What Next?

Run Your App Headlessly

A quick win, however is to run the app "headless". As of now, your app is currently running in your shell session - and as soon as you close that session, the app will terminate!

To start the app in a way that it'll keep running in the background, run the app with the additional -d flag:

$ docker run -d -p 3000:3000 <YOUR_DOCKER_USERNAME>/ec2-app

Security

You might want to go back and tighten up the security on the instance/experiment with different configurations - such as configuring it so that only we can access the SSH port, for example.

Change the 'Source' field on the first rule to 'My IP' - AWS will automatically figure out where you're accessing it from.

Note: If you're running through this tutorial on the move, or come back to it later, your computer might have a different IP than when you initially set 'My IP'. If you encounter any difficulties later on, make sure to come back here and select 'My IP' again!

Other AMIs

There are hundreds of different AMIs, a lot from various communities, with applications already pre-installed - it's worth taking a look through to see if there's an easy way to set up something you've wanted to work with!

Adding a Domain

Now that you've got an app running on a server, you might want to set up a domain name and point that at your application.

Conclusion

EC2 really is the backbone of a lot of AWS services - for instance, RDS (AWS' database service) is really just heavily optimized EC2 instances with a nice dashboard.

Understanding this core product in AWS' arsenal is bound to open doors to new ways of implementing ideas.

In this tutorial, we've created a simple Node.js application with the help of Express, dockerized it, set up EC2 for deployment and finally - deployed it to the EC2 instance.