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
-
Launch an instance via the AWS web interface. For the Free Tier, select
t2.micro
as your instance type. -
For this post, I will assume Ubuntu has been chosen for the OS.
-
Ensure
SSH
(port 22),HTTP
(port 80) andHTTPS
(port 443) connections are allowed into the security group. -
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.
-
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 port3000
or8080
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.