Spinning up a Node.js Server in a Container on AWS with EC2
Tags: aws, docker, node, tutorial
Published: 2022-01-14 18:38:18

For a project at work, I needed to get a Node.js Express server running in a Docker container, then deploy that container to an EC2 instance on AWS. I took notes along the way because I was sure I'd need to do something like this again someday. Then, I figured—since I took notes—I might as well share my notes.

You can generalize my use case for your own needs. It doesn't have to be a Node.js server. It can be any Docker container image that you need to deploy to AWS EC2, as long as you know what port(s) on the container you need to expose to the outside world.

Are you ready to get started? Here we go!

# Build the Docker Image

For this post, we'll keep it simple by deploying a basic Node.js Express server with a single endpoint. I initialized the Node.js project, added `express`, and then wrote the following `index.js` file:

```javascript
const PORT = 8080;
const express = require('express');
const app = express();

app.get('/', async (req, res, next) => {
  res.send('Hello world.');
});

app.listen(PORT, () => {
  console.log(`Server is listening on port ${PORT}`);
});
```

Note that the server in my example is listening on port `8080`. This is what my project folder looks like:

```bash
$ tree -L 1
.
├── index.js
├── node_modules
├── package.json
└── yarn.lock

1 directory, 3 files
```

To make this project deployable as a Docker container, we write a `Dockerfile`, which we put in the project root folder.

```dockerfile
FROM node:14-alpine

WORKDIR /usr/src/app

COPY package*.json /usr/src/app
RUN npm install

COPY . .

EXPOSE 8080

CMD ["node", "index.js"]
```

You can test your container on your local machine:
```bash
$ docker run -d -p 3000:8080 my-node-server:latest
c992be3580b1c27c81f6e2af54f9f49bf82f977df36d82c7af02c30e4c3b321d

$ curl localhost:3000
Hello world.

$ docker stop c992be3580
```

Notice that I ran my container with `-p 3000:8080`,  which exposes the port `8080` on my container (the one which my Node.js server is listening on) to port `3000` on my local machine. This is what allows us to send a request to `localhost:3000` and get a response from our server. Later, when we're running in an AWS EC2, we'll expose port `8080` on our container to port `80` on our EC2 instance.

# Prepare Your AWS ECR Repository

Our EC2 machine will need to fetch our container image before it can run it. To do that, we need to [push our container image to AWS ECR](https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html). But before we can do that, we need to prepare our repository and set up access. Make sure you've installed the [AWS CLI](https://aws.amazon.com/cli/).

## Create an ECR Repository

First, at AWS ECR, create a new repository. For our AWS region, we'll use `us-east-2` (Ohio).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6jqt7rqega5q2p84s1c9.png)

We create a **private** repository called `my-node-server`, keeping all of the remaining default settings.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/czuxnlvv2opaqfc1h6e3.png)

Soon, we'll need to use `docker login` to access our repository and push our container image. To log in, we'll need an authentication token for our registry. Make sure you have [created an IAM user with programmatic access](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html#id_users_create_console) and that you've already run `aws configure` to use that IAM user's credentials.

## Create an IAM policy to Allow `ecr:GetAuthorizationToken`

Your IAM user will need permission to `ecr:GetAuthorizationToken`. On the AWS IAM policies page, create a new policy with the following JSON:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    }
  ]
}
```

Provide a name for your new policy (for example: `ecr-get-authorization-token`). Attach the policy to your IAM user.

## Create an IAM Policy to Allow Uploading to Your ECR Repository

Your IAM user will also need permission to upload container images to your ECR repository. Create another IAM policy with the following JSON, making sure to set the `Resource` to the ARN of your repository.

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:CompleteLayerUpload",
        "ecr:DescribeImages",
        "ecr:DescribeRepositories",
        "ecr:GetDownloadUrlForLayer",
        "ecr:GetRepositoryPolicy",
        "ecr:InitiateLayerUpload",
        "ecr:ListImages",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:us-east-2:1539********:repository/my-node-server"
    }
  ]
}
```

Provide a name for your new policy (for example: `ecr-upload-to-my-node-server-repo`), and attach this policy to your IAM user also.

# Push Container Image to AWS ECR

Now, we're ready to push our container image up to AWS ECR.

## Tag Your Local Container Image

The container image we created was tagged `my-node-server:latest`. We need to tag that image with our ECR registry, repository, and (optional) image tag name. For this, you will need the URI of the ECR repository you created above.

```bash
$ docker tag my-node-server:latest \
  1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server:latest

$ docker images
REPOSITORY                                                   TAG     IMAGE ID    
1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server  latest  a6df3f2bda72
my-node-server                                               latest  a6df3f2bda72
```

Your repository URI will, of course, have _your_ AWS Account ID and region.

## Log in to Your Container Registry

With your IAM user authorized to `ecr:GetAuthorizationToken`, you can get the token and use it with the `docker login` command. Make sure the region you're getting the authorization token for is the same region that you're trying to log into.

```bash
$ aws ecr get-login-password --region us-east-2 | docker login \
  --username AWS \
  --password-stdin 1539********.dkr.ecr.us-east-2.amazonaws.com

...
Login Succeeded
```

## Push Your Container to AWS ECR
Now that we're logged in, we can push our container up to our registry.

```bash
$ docker push 1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server:latest
The push refers to repository [1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server]
7ac6ec3e6477: Pushed 
f56ccac17bd2: Pushed 
91b00ce18dd1: Pushed 
58b7b5e46ecb: Pushed
0f9a2482a558: Pushed
8a5d6c9c178c: Pushed 
124a9240d0af: Pushed 
e2eb06d8af82: Pushed 
latest: digest: sha256:9aa81957bd5a74b3fc9ab5da82c7894014f6823a2b1e61cd837362107dc062e5 size: 1993
```

Our container image with our Node.js server is now at AWS ECR!

The hard part is done. Now, we can spin up our EC2 instance.

# Spin Up EC2 Instance

Head over to the main EC2 page, making sure that you're using the same region (`us-east-2`) as you did in previous steps. Click on "Launch Instance."

We'll launch a "free tier eligible" Amazon Linux 2 AMI instance. Choose the 64-bit (x86) version and click "Select."

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/89mj86sfkuqtwjcxtuvc.png)

To stay in the free tier for this learning experience, we'll choose the `t2.micro` instance type. Then, we'll skip ahead to the "Configure Security Group" page.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ox51f776gzc5wc7vepnw.png)

For our EC2 instance's security group, we will create a new security group, providing a name and a description. Our EC2 will need to allow for SSH (TCP at port `22`) and HTTP access (TCP at port `80`). AWS may warn you that you might want to restrict traffic to a whitelist of IP addresses. For a production-grade deployment, you may want to consider taking more security measures than what we're doing here for demonstration purposes.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vy4zkmmwlzbng0dxinhf.png)

Click on "Review and Launch," and then click on "Launch."

You'll see a dialog for creating a key pair for SSH access to your EC2 instance. Select "Create a new key pair," select "RSA" as the key pair type, and then give your key pair a name. Then, click on "Download Key Pair."

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j47lvy78dnj51ysuv4jh.png)

Store the downloaded private key file in a secure place. Then, click on "Launch Instances."

Your EC2 instance may take a few minutes to spin up.

# Connect to EC2 to Install and Run Docker

Once our EC2 instance is running, we'll set it up to run our Docker container.

## Connect to Your EC2 Instance

On the EC2 instances page, select the instance you just launched, and then click on "Connect." There are instructions for using an SSH client with the private key you just downloaded. You can also use the EC2 Instance Connect option here in the AWS console. Click on "Connect."

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2r14rkfzwd1awhbijvs8.png)

This will open a new tab in your browser, and you'll have an0 in-browser terminal with command-line access to your EC2 instance.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cezgl4og4o0bctvs43rg.png)

## Install and Startup Docker

In that EC2 terminal, run the following commands to [install Docker to your instance](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/docker-basics.html):

```bash
[ec2-user@ip-172-31-38-144 ~]$ sudo yum update -y
...
No packages marked for update
 
[ec2-user@ip-172-31-38-144 ~]$ sudo amazon-linux-extras install docker
...
Installed size: 285 M
Is this ok [y/d/N]: y
...

[ec2-user@ip-172-31-38-144 ~]$ sudo service docker start
 
[ec2-user@ip-172-31-38-144 ~]$ sudo chmod 666 /var/run/docker.sock
```

## Run `aws configure` to Set up IAM User Credentials

At the EC2 command line, you will need to run `aws configure`, using the same IAM user credentials as you have on your local machine so that you can run similar AWS CLI commands.

```bash
[ec2-user@ip-172-31-38-144 ~]$ aws configure
AWS Access Key ID [None]: AKIA****************
AWS Secret Access Key [None]: z8e*************************************
Default region name [None]: us-east-2
Default output format [None]: json
```

## Log in to Your Container Registry

Just like we did when pushing our image from our local machine to ECR, we need to log in to our registry (from within our EC2) so that we can pull our image.

```bash
[ec2-user@ip-172-31-38-144 ~]$ $ aws ecr get-login-password --region us-east-2 | docker login \
  --username AWS \
  --password-stdin 1539********.dkr.ecr.us-east-2.amazonaws.com

...
Login Succeeded
```

## Pull Down Container Image

Now that we're logged in, we pull down our container image.

```bash
[ec2-user@ip-172-31-38-144 ~]$ docker pull 1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server:latest
latest: Pulling from my-node-server
a0d0a0d46f8b: Pull complete 
4684278ccdc1: Pull complete 
cb39e3b315fc: Pull complete 
90bb485869f4: Pull complete 
32c992dbb44a: Pull complete 
4d7fffd328bd: Pull complete 
562d102dfc97: Pull complete 
d7de8aedebed: Pull complete 
Digest: sha256:9aa81957bd5a74b3fc9ab5da82c7894014f6823a2b1e61cd837362107dc062e5
Status: Downloaded newer image for 1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server:latest
1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server:latest
```

## Run Docker

With our container image pulled down, we can run it with Docker. Remember, we want to expose port `8080` on our container to port `80` on our EC2 instance (which is the port open to the world for HTTP access).

```bash
[ec2-user@ip-172-31-38-144 ~]$ docker run -t -i -d \
-p 80:8080 1539********.dkr.ecr.us-east-2.amazonaws.com/my-node-server

 8cb7c337b9d5f39ea18a60a69f5e1d2d968f586b06f599abfada34f3fff420c1
```

# Test With an HTTP Request

We've set up and connected all the pieces. Finally, we can test access to our server. Note that we've only set up our EC2 instance and network for responding to HTTP (not HTTPS) requests. **We'll cover the additional configurations for HTTPS and a custom domain in a future article.**

To test our setup, we simply need to make curl requests to the public IPv4 address (or the public IPv4 DNS address, which is an alias) for our EC2 instance.

```bash
$ curl http://3.14.11.142
Hello world.

$ curl http://ec2-3-14-11-142.us-east-2.compute.amazonaws.com
Hello world.
```

Note that we're not specifying a port in our requests, which means we're using the default port (`80`) for HTTP requests. When we started up our container with docker run, we associated port `80` on our EC2 instance with the open port `8080` on our container. We also set up our security group to allow traffic from port 80.

# Conclusion

Getting a basic server up and running as a container in an AWS EC2 may feel complicated. Granted, it is a lot of steps, but they are straightforward. Your own use case may be different (server implementation, container needs, ports to expose), but the process will still be pretty much the same. In our [next article](https://dev.to/alvinslee/adding-a-custom-domain-and-ssl-to-aws-ec2-1ko8), we'll take what we've done a step further by setting up a custom domain and accessing our server via SSL/HTTPS.