A Minimal Node.js and Express Server Setup and Deployment
Tags: express, aws, devops
Published: 2018-08-23 03:04:21

---
title: A Minimal Node.js and Express Server Setup and Deployment
published: true
description: Manually deploying a "Hello World" Express app to an AWS EC2 instance without too much tooling.
tags: express,aws,devops
---

As a software engineer I often forget the complexities of how my code is deployed. When I run the code I write, typically it is done on my local development environment, and then I push my code to a repository where through continuous deployment magic my code becomes available to my end users.

While I reap many benefits from this complexity, I often forget why it is there, and when things don't work, I am simply annoyed by the complexity.

In this article I put together a very minimal Node.js and Express application and deploy it manually onto an AWS instance. The goal of this exercise is to evaluate the advantages and disadvantages to doing my deployment like this.

_Note: It's worth mentioning by minimal I mean fewest number of pieces, not necessarily fewest number of steps. Something like Heroku will likely get you up and running much quicker._

# The Application

The application I have in mind is a simple "Hello World!" Express application. First I run a few commands in bash to set up my envrionment.

```bash
mkdir express_hello_world
cd express_hello_world
npm init -y
npm install express
```

Then I create `server.js` with the following JavaScript code.

```js
const express = require("express")
const app = express()
app.get("/", function(req, res) {
    res.send("Hello World")
})
app.listen(3000)
```

Now if I run my code with `node server.js`, I should be able to go to <http://localhost:3000/> and see the message "Hello World" in my browser.

Great! Now commit and push, and move onto the next thing right? Oh wait, we are deploying manually, we got some work to do.

# The Server

We need a computer of some sort to run our code on the web. Especially since my laptop isn't online 24/7, and we wouldn't want anyone to not be able to access our greeting. I head to the [AWS console](https://console.aws.amazon.com/).

-   Click on "EC2"
-   Click on "Launch Server"
-   Select "Amazon Linux 2 AMI"
-   Click "Review and Launch"
-   Click "Launch"
-   In dropdown select "Create a new key pair"
-   Enter "express_hello_world" as my "Key pair name"
-   Click "Download Key Pair"
-   Click "Launch Instances"

Whew! That was a few steps, most of which I didn't really take the time to evaluate the best options, but now we have some sort of "default" server to run our code on. However, we still need to install Node.js on the server, and upload our "Hello World" application to the server, and then make sure it is running.

# Connecting to the Server

The key pair we downloaded was a "pem" file which is a private key. We can use this key to connect to the server via an SSH client. We also need the public IP address of our AWS server we created. From the "EC2 Dashboard" click "Instances" on the left sidebar, and find the column "IPv4 Public IP" which has the IP adress for the instance we just created.

Now we can connect to the server.

```bash
mkdir -p ~/Documents/AWS/keys/express_hello_world/
cd ~/Documents/AWS/keys/express_hello_world/
cp ~/Downloads/express_hello_world.pem ./
chmod 400 express_hello_world.pem
ssh -i "express_hello_world.pem" ec2-user@<PUBLIC_IP_ADDRESS_OF_INSTANCE>
```

You should now be at a bash prompt on your AWS EC2 instance.

_Note: If you are using Windows, you should probably follow the instructions for [Connecting to Your Linux Instance from Windows Using PuTTY](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/putty.html?icmpid=docs_ec2_console). However, you can use "Windows Subsystem from Linux" (WSL). If you use WSL, make sure you put `express_hello_world.pem` in a WSL folder or `chmod 400 express_hello_world.pem` will not work._

# Installing Node.js

From the Node.js home page the installation instructions for the yum package manager which is what our server instance using are as follows.

```bash
curl --silent --location https://rpm.nodesource.com/setup_8.x | sudo bash -
sudo yum -y install nodejs
```

You can type `node --version` to check if it is installed and you are on the correct version. The current LTS version was 8.11.4 when this article was written. Anything greater than that should work fine.

# Creating a User

We will create a user for running our application. This prevents us from having to worry about what our application can do as root, or even potentially with sudo abilities as `ec2-user`.

```bash
sudo useradd express_hello_world
```

# Copying Application to Server

Exit from our current ssh session by typing `exit`. Find the directory where we originally put our `server.js` and `package.json` files. We will copy that entire directory to the `ec2-user` home directory on our EC2 server instance.

After we accomplish that, we want to set the permissions so the owner is the `express_hello_world` user we just created, and move the application to the `express_helo_world` home directory.

```bash
rm -rf <EXPRESS_APP_DIRECTORY>/node_modules
scp -r -i "express_hello_world.pem" \
    <EXPRESS_APP_DIRECTORY> \
    ec2-user@<PUBLIC_IP_ADDRESS_OF_INSTANCE>:express_hello_world_app
ssh -i "express_hello_world.pem" ec2-user@<PUBLIC_IP_ADDRESS_OF_INSTANCE>
sudo chmod 600 ./express_hello_world_app/*
sudo chown -R express_hello_world:express_hello_world ./express_hello_world_app
sudo mv ./express_hello_world_app /home/express_hello_world/express_hello_world_app
```

_Note: For Windows users it probably makes the most sense to use a program like [FileZilla](https://filezilla-project.org/) to copy the files to the server. Although with WSL you can use the `scp` method described here. Again make sure your private key is in a WSL directory with the correct permissions (400)._

# Checkpoint 1: Testing the Application Locally

It is a good time to do a sanity check. We should be able to run the application from the `express_hello_world` user, and use `curl` to get our "Hello World" message.

Become the `express_hello_world` user and install Express dependency. We also test here to make sure we get a "Connection Refused" since the server should not be running.

```bash
sudo su - express_hello_world
cd ./express_hello_world_app
npm install
curl localhost:3000  # Expected output "Connection Refused"
```

Now we should be able to start the server and get the "Hello World" response from it.

```bash
node server.js &
SERVER_PID=$!
curl localhost:3000  # Expected output "Hello World"
```

After we verified we get our "Hello World" response. Kill the server, and check that we are no longer accepting connections.

```bash
kill $SERVER_PID
curl localhost:3000  # Expected output "Connection Refused"
```

Finally type `exit` to return to the `ec2-user` user.

# Exposing the Application to the World

Since we are running our application as a unprivledged user, we cannot host it on port 80 (or any port below 1024). However, we can create a firewall rule to forward that port to the port we are hosting on.

```bash
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to 3000
```

We also need to setup our AWS security rules to allow traffic on port 80. From the EC2 Dashboard. Click on Instaces in the left sidebar. Right click on the instance we created, go to "Networking", then "Change Security Groups". Look at the "Security Group ID" we currently have selected, and remember it. Click "Cancel".

On the left sidebar, click "Security Groups". Right click on the row with the "Security Group ID" we found in the last step. And click "Edit inbound rules". Click "Add Rule" and from the drop down, select "HTTP". Then click "Save".

# Checkpoint 2: Testing the Application from the Web

In your browser try going to [http://&lt;PUBLIC_IP_ADDRESS_OF_INSTANCE>](<>). You should get a "This site can't be reached" message. This is because we stopped running the server in the previous step.

Go ahead and launch the server again. Run the following commands.

```bash
sudo su - express_hello_world
cd ./express_hello_world_app
node server.js &
SERVER_PID=$!
```

Now if we go to the previous URL, we should get a "Hello World" message in the browser.

Awesome! We are now live!

To shutdown the server we can kill the application like we did last time.

```bash
kill $SERVER_PID
exit  # To exit the express_hello_world user prompt
```

# Running the Server as a Daemon Process

We have a working server, but it would be much better if we didn't have the server process tied to our current SSH session. We can use the systemd process for doing this. It will also handle making sure the server is restarted if it fails, and automatically started when the server boots up.

Create a file `express_hello_world.service` in `/etc/systemd/system/`.

```bash
sudo vim /etc/systemd/system/express_hello_world.service
```

Add the following to that file. These are the settings for our daemon process. You can control things like the command we use to start the process with "ExecStart". the User/Group, what environment variables are set, and the current Working Directory.

    [Unit]
    Description=Express Hello World Application
    After=network.target

    [Service]
    ExecStart=/usr/bin/node server.js
    Restart=always
    User=express_hello_world
    Group=express_hello_world
    Environment=PATH=/usr/bin:/usr/local/bin
    Environment=NODE_ENV=production
    WorkingDirectory=/home/express_hello_world/express_hello_world_app

    [Install]
    WantedBy=multi-user.target

To start our job, we use `systemctl`. We can use the status subcommand to check that status of our daemon process too.

```bash
sudo systemctl start express_hello_world.service
sudo systemctl status express_hello_world.service
```

## Debugging

After running status, we should see "Active: active (running)". If you don't see that, something obviously went wrong. Use `sudo journalctl --unit express_hello_world.service` to examine the error log of the `node service.js` command.

One likely cause of the job failing is port 3000 is in use. This can happen when you forgot to kill the server in one of the checkpoints (This happened to me, haha). Do `ps aux | grep node`, and find the process that still is running, note it's PID, then do `sudo kill <PID>`. After this the daemon process should start without a problem.

## Rebooting

Lastly to make sure the application starts when the server is booted, run the command

```bash
sudo systemctl enable express_hello_world.service
```

### Persist iptables between reboots

If you reboot now, the iptable rule that we defined before will not be restored on boot. We can also create a systemd service that does that. Create the file `/etc/systemd/service/express_hello_world_iptables.service` and put the following systemd configuration.

    [Unit]
    Description=Forward port 80 to port 3000

    [Service]
    Type=oneshot
    ExecStart=/bin/sh -c "iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to 3000"
    ExecStop=/bin/sh -c "iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to 3000"
    RemainAfterExit=yes

    [Install]
    WantedBy=multi-user.target

Then delete the rule we already created and start the service we just defined (this recreates the rule).

```bash
iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to 3000
sudo systemctl start express_hello_world_iptables.service
sudo systemctl enable express_hello_world_iptables.service
```

_Note: It feels like there is a better way to handle this iptables stuff, but I couldn't find anything that felt very minimal. This doesn't seem like something that should be very complex._

# Checkpoint 3: Testing our Daemon Process

In your browser go to [http://&lt;PUBLIC_IP_ADDRESS_OF_INSTANCE>](<>) again. We should see the message "Hello World" if our last step succeeded.

Run the following command to stop our application.

```bash
sudo systemctl stop express_hello_world.service
```

And refresh the browser to make sure the server is no longer responding.

Now reboot the server.

```bash
sudo reboot
```

Wait for the server to come back up, and check the website. We should get the message "Hello World" again.

Assuming everything went smoothly, we now have a functional server hosting our nodejs application!

# Reflections

This was a lot of steps to set everything up. However, most of it could easily be automated. Even provisioning the AWS service can be automated through the [AWS Command Line Interface](https://aws.amazon.com/cli/).

## Re-Deploy

To deploy a new version of our app, we would just need to copy the files to our server, and restart the daemon. Another step that should be fairly easy to automate.

## Disadvantages

The application runs exactly as we set it up. To change anything, we really have to know Linux very well. I can manage ok through Googling basic commands, but doing some simple things seem like that might be a pain, for example:

-   Set up HTTPS (On Heroku this is a one click setup pretty much)
-   Move to another service. (With something like docker this might be easier, a lot of what I did I think was specific to Amazon Linux 2).
-   Scaling. (Something like docker would make it easy to run more instances of my app).

## iptables

My understanding is that iptables is deprecated, but as a novice to Linux system administration, it is unclear to me what the available replacements are. A lot of the information I could find refered to commands that just didn't exist on my system.

## Next Steps

I might try to write some scripts that automates this process to see how hard it is to deploy different apps on different servers. I certainly have some hard coded values in my systemd that I would have to deal with.

All in all it was fun to get this up and running.