In this post, I will cover the process of setting up and running a Node.js server on an Amazon EC2 instance. Whilst more time-efficient solutions exist (e.g. using Heroku or AWS Elastic Beanstalk), it can be quite fun to actually experience setting up the server by hand.

This article serves as a note-to-self on the process (I've encountered more than one issue before!) and a reference for others, should you need it.

As a prerequisite, you will need an AWS account with a valid payment method, and a Node.js application ready to be deployed.

Preparing the instance

  1. Launch an instance via the AWS web interface. For the Free Tier, select t2.micro as your instance type.

  2. For this post, I will assume Ubuntu has been chosen for the OS.

  3. Ensure SSH (port 22), HTTP (port 80) and HTTPS (port 443) connections are allowed into the security group.

  4. For most instances, you will need to attach an EBS volume in order to store your program files locally. I have found 8GB tends to be enough for a simple Node.js setup.

  5. Once the instance is running, connect via SSH:

    ssh -i /path/my-key-pair.pem ubuntu@my-instance-public-dns-name

    You will need to make sure you have downloaded the correct key whilst creating your instance. The DNS address of the instance can be found on the AWS web interface.

Installing node

Node is not automatically installed, so we must do so ourselves:

sudo apt update

curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
sudo apt-get install -y nodejs

node -e "console.log('Running Node.js ' + process.version)"

This installs node version manager (nvm) and activates it. Then, node is installed, and the installation is tested.

Git, GitHub and SSH

Before cloning your repo from GitHub, it is necessary to create an SSH key on the EC2 instance to authenticate with GitHub:

ssh-keygen -t ed25519 -C "your_email@example.com"

eval "$(ssh-agent -s)"

touch ~/.ssh/config

nano ~/.ssh/config

For the contents of the config file:

Host *
  AddKeysToAgent yes
  IdentityFile ~/.ssh/id_ed25519

And then add the key to the agent:

ssh-add ~/.ssh/id_ed25519

Remember to add the SSH key to your the SSH keys page of your GitHub account. To get the text to copy:

cat ~/.ssh/id_ed25519.pub

Finally, test your connection with:

ssh -T git@github.com

Now you can clone your GitHub repo like normal.

git clone git@github.com:username/repo-name

SSH Error

If this doesn't work on the grounds of user permissions for the SSH config file, make sure you have the correct access permissions for the config file, like below. This is not always an issue but can occasionally be an issue.

sudo chmod 600 ~/.ssh/config

Running the app

At this point, we can successfully run the Node.js app:

node app.js

However, two issues remain:

  • The app terminates whenever the SSH client disconnects
  • Incoming HTTP traffic is directed at port 80 but your application likely listens on port 3000 or 8080

Running a node server forever

To keep the application running even after the current terminal session, we can use the package pm2:

sudo apt install npm

sudo npm install pm2 -g

pm2 start app.js

If you have access to multiple cores and you would like Node.js to take advantage of them, use one of the following:

pm2 start app.js -i <NUM_CORES>

pm2 start app.js -i 'max'

If you want to make pm2 automatically start the app when the instance boots, use the following:

pm2 startup

pm2 save

To restart your application without any downtime, use the following command:

pm2 reload all

Port forwarding

It is likely that your Node.js server runs on port 3000. However, to serve HTTP and HTTPS traffic, ports 80 and 443 are needed. It is best practice to forward traffic to the ports rather than set the Node.js server on those ports directly. Here's how to do it with nginx:

Let's first install nginx:

sudo apt-get install nginx

And then ensure it is running:

sudo systemctl start nginx

After this, you should be able to see a site at port 80 in your browser. Now to configure nginx:

sudo rm /etc/nginx/sites-enabled/default

Now for the actual config contents:

sudo nano /etc/nginx/sites-available/myconfig
server {
  listen 80;
  server_name YOUR_IP_OR_DOMAIN;
  location / {
    proxy_set_header  X-Forwarded-For $remote_addr;
    proxy_set_header  Host $http_host;
    proxy_pass        "http://127.0.0.1:3000";
  }
}

Now the config file is made, we need to link it into the sites-enabled directory.

sudo ln -s /etc/nginx/sites-available/myconfig /etc/nginx/sites-enabled/myconfig

sudo service nginx restart

The server should now work - give it a try in your browser!

Let's Encrypt SSL

If you'd like to serve encrypted HTTPS traffic, you'll need an SSL certificate to verify your identity. Thankfully, you can get one for free from the Let's Encrypt certificate authority using Certbot.

sudo yum install python3 augeas-libs

sudo python3 -m venv /opt/certbot/

sudo /opt/certbot/bin/pip install --upgrade pip

sudo /opt/certbot/bin/pip install certbot certbot-nginx

sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot

sudo certbot --nginx

Note that using the --nginx flag on certbot automatically configures our nginx server to use HTTPS and the new certificates.

To schedule a cron task to automatically renew the certificate:

echo "0 0,12 * * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' && certbot renew -q" | sudo tee -a /etc/crontab > /dev/null

And to test the automatic certificate renewal:

sudo certbot renew --dry-run

Finally to update certbot:

sudo /opt/certbot/bin/pip install --upgrade certbot certbot-nginx

Conclusion

This post has been extremely technical but hopefully now you have a good idea of the steps involved in launching your own server manually. In production applications, it would of course be necessary to set up a deployment pipeline so you don't need to SSH into your instance and git pull to update your app. For small hobby projects, however, this is a great setup that gives you control of the server from start to finish.