DEV Community

Cover image for How to Securely Deploy Node App to Ubuntu Server
coder7475
coder7475

Posted on

How to Securely Deploy Node App to Ubuntu Server

This is blog about how you can deploy a Node app to a server with Nginx, whether it is a VPS, VDS, or dedicated server. This assumes you're familiar with basic Linux & git commands. This will work for any Node app that runs a server, be it an Express app, Next.js app, Remix app, etc. Another thing to note is that we will deploy application code; the database will be separated.

Assumptions

  1. A hosting service with full root access and a domain
  2. OS: Ubuntu 22.04
  3. IPv4: 172.172.172.172
  4. IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334
  5. Domain Name: example.com
  6. Using a bash terminal on your local computer
  7. Code is hosted on GitHub

Workflow Steps

1. Secure Your Server


A. Login to Your Server

  1. Open your terminal (bash, zsh, etc.)
ssh root@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

You will be prompted to provide the root password. After entering it, you will be logged into your server. You should see something like this:

root@vm172048:~$
Enter fullscreen mode Exit fullscreen mode

If so, you have successfully logged in as the root user.

B. Server Hardening

Now that we are inside our machine, we can start installing the necessary packages and software, but before that, let's upgrade our system. Enter the command below in your terminal:

apt update && apt upgrade -y
Enter fullscreen mode Exit fullscreen mode

Now, all the current packages are updated to the latest patch version, which keeps the system safe from "unpatched vulnerability exploitation."

C. Create Non-Root User

Deploying as a root user is not recommended, as it has full access to all server resources. So let's create a non-root user called admin and add it to the sudo group to use commands that need root privileges.

To create a sudo user:

useradd -m -s /bin/bash admin
Enter fullscreen mode Exit fullscreen mode

This will create a new user with the name admin, and you can check the groups of the user using the groups command.

groups admin
Enter fullscreen mode Exit fullscreen mode

Let's add the admin user to the sudo group:

usermod -aG sudo admin
Enter fullscreen mode Exit fullscreen mode

This will add the user to the sudo group without removing them from the original admin group.

Now let's create a password for the user:

sudo passwd admin
Enter fullscreen mode Exit fullscreen mode

You will be prompted to enter a new password and retype it.

To check if the password is set properly, open a new terminal and check it by typing:

ssh admin@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

You will be prompted for your new password. After entering it, you should see something like:

admin@vm172048:~$
Enter fullscreen mode Exit fullscreen mode

D. Connect to the Server Using SSH

Using password login is not recommended. You want to use SSH (Secure Shell) and make sure that SSH is the only way to log in.

If you are a user of git, chances are that you already have an ssh key set up. If you don't already have an ssh key, use the command below to generate a new SSH key on your local machine:

ssh-keygen -t ed25519 -C "your_email@example.com"
Enter fullscreen mode Exit fullscreen mode

Follow the instructions; it should ask you where you want to save the file and if you want a passphrase or not. Just press the enter button for each prompt. Make sure you set a strong passphrase. To copy the public key over to your server, run the following command on your local machine:

ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

Now, you should be able to log in without a password. If it still does not work, use cat ~/.ssh/id_ed25519.pub to copy the key. Then paste it into the server's ~/.ssh/authorized_keys:

touch ~/.ssh/authorized_keys
sudo nano ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

After all of this, you should be able to log in without using a password.

E. Disable Root and Password Login on the Server

To turn off username and password login, type in:

sudo nano /etc/ssh/sshd_config
Enter fullscreen mode Exit fullscreen mode

Find these values and set them as follows:

Port 1234 # Change the default port (use a number between 1024 and 65535)
PermitRootLogin no # Disable root login
PasswordAuthentication no # Disable password authentication
PubkeyAuthentication yes # Enable public key authentication
AuthorizedKeysFile .ssh/authorized_keys # Specify authorized_keys file location
AllowUsers admin # Only allow specific users to log in
Enter fullscreen mode Exit fullscreen mode

This disallows every login method besides SSH under the user you copied your public key to. It stops login as root and only allows the user you specify to log in. Hit CTRL+S to save and CTRL+X to exit the file editor. Restart SSH:

sudo service ssh restart
Enter fullscreen mode Exit fullscreen mode

Now try logging in as root to see if it disallows you. Since you changed the default SSH port from 22 to 1234, you need to mention the port when logging in.

ssh -p 1234 root@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

This will disallow you since root login is disabled. To log in, use:

ssh -p 1234 admin@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

Also, it should go without saying, but you need to keep the private key safe, and if you lose it, you will not be able to get in remotely anymore.

F. Firewall Configuration

Ubuntu comes with the ufw firewall by default. If not, you can install it with the command:

sudo apt install ufw -y
Enter fullscreen mode Exit fullscreen mode

To see the current status of ufw, enter:

sudo ufw status
Enter fullscreen mode Exit fullscreen mode

This will show the current status of the firewall. To enable the firewall, run the following command:

sudo ufw enable
Enter fullscreen mode Exit fullscreen mode

First, run some default policies with:

sudo ufw default deny incoming && sudo ufw allow outgoing
Enter fullscreen mode Exit fullscreen mode

Now, since we changed the SSH port to 1234, allow it through the firewall. Aside from that, we will be using ports 80 and 443 for web serving via HTTP and HTTPS, so allow them too.

sudo ufw allow 1234,80,443
Enter fullscreen mode Exit fullscreen mode

To further improve brute force login via SSH, use the command below:

sudo ufw limit 1234
Enter fullscreen mode Exit fullscreen mode

This will limit port 1234 to 6 connections in 30 seconds from a single IP. If you opened any port incorrectly, you can deny the connection with:

sudo ufw deny <port_number>
Enter fullscreen mode Exit fullscreen mode

Now, to see the current status, use:

sudo ufw status verbose
Enter fullscreen mode Exit fullscreen mode

Restart the ufw to make sure all rules are applied:

sudo ufw reload
Enter fullscreen mode Exit fullscreen mode

After enabling the firewall, never exit from your remote server connection without enabling the rule for the ssh connection. Otherwise, you won't be able to log into your own server.

G. Fail2Ban Configuration

Fail2Ban provides a protective shield for Ubuntu 22.04 that is specifically designed to block unauthorized access and brute-force attacks on essential services like SSH and FTP. To install fail2ban, use the command below:

sudo apt install fail2ban
Enter fullscreen mode Exit fullscreen mode

After the installation is complete, start the Fail2ban service with:

sudo systemctl start fail2ban
Enter fullscreen mode Exit fullscreen mode

To enable Fail2ban on Ubuntu 22.04 so that it starts automatically when your system boots up, use:

sudo systemctl enable fail2ban
Enter fullscreen mode Exit fullscreen mode

Next, we need to verify if Fail2ban is up and running without any issues using the following command:

sudo systemctl status fail2ban
Enter fullscreen mode Exit fullscreen mode

Now let's configure Fail2ban. The main configuration is located in /etc/fail2ban/jail.conf, but it's recommended to create a local configuration file:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Enter fullscreen mode Exit fullscreen mode

Now open the jail.local file and tweak the values in the Default section:

bantime = 10m
findtime = 10m
maxretry = 5
Enter fullscreen mode Exit fullscreen mode

Fail2Ban works by banning an IP for a specified ban time after detecting repeated failures within a defined find time. The max retry setting determines the number of failures allowed before an IP is banned.

Now restart the Fail2ban service:

sudo systemctl restart fail2ban
Enter fullscreen mode Exit fullscreen mode

To see which service for which jail is activated, enter:

sudo fail2ban-client status
Enter fullscreen mode Exit fullscreen mode

If you have come this far, congratulations! You have secured your server. Now it's ready to deploy your webApp. Enter exit to end the session.

exit
Enter fullscreen mode Exit fullscreen mode

2. DNS Configuration


Our website will be known by a domain name, in this case, example.com. To make the domain name point to our server, we need to do some DNS configuration on the side of our domain provider.

a. Login to your domain provider's website.

b. Navigate to example.com and then manage DNS Management.

c. Now update the A and AAAA records for IPv4 & IPv6 Address.

Record Type Host Name Address
A @ 172.172.172.172
AAAA @ 2001:0db8:85a3:0000:0000:8a2e:0370:7334

d. Next, update the CNAME Record to forward www.example.com to example.com.

Record Type Host Name Address
CNAME www example.com

CNAME maps a subdomain to another domain name.

Now it might take a few minutes to propagate to all DNS servers. To check if example.com resolves to your host IP address, check DNS propagation using this online tool: DNS Checker.

3. Deploy the Web App


To deploy, we will need to install several packages. First, we will be using git to clone the repository from GitHub. Since it's a Node app, we will need Node installed on our system; I will be using Node version 20. We will be using npm, which comes with Node. Finally, to manage the app as a background process, we will be using pm2.

First, log in to your server using:

ssh -p 1234 admin@172.172.172.172
Enter fullscreen mode Exit fullscreen mode

A. Install All Necessary Dependencies

a. First, check the installation:

nginx -v
node -v
npm -v
git --version
pm2 --version
Enter fullscreen mode Exit fullscreen mode

b. Then update to the latest version & remove unnecessary packages:

sudo apt update && sudo apt upgrade -y && sudo apt autoremove -y
Enter fullscreen mode Exit fullscreen mode

c. Install the latest version of git available:

sudo apt install git -y
Enter fullscreen mode Exit fullscreen mode

d. Install Node version 20 & its accompanying npm (Node Package Manager):

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt-get install -y nodejs
Enter fullscreen mode Exit fullscreen mode

e. Install the nginx web server:

sudo apt install -y nginx
Enter fullscreen mode Exit fullscreen mode

B. Clone the Code Repo from GitHub

Our code repo will be hosted on GitHub. We will be cloning the repo to our server using a deploy key.

a. First, generate a deploy key named site0:

ssh-keygen -t ed25519 -C "fahad@octopusx.io" -f ~/.ssh/example
Enter fullscreen mode Exit fullscreen mode

You will be prompted for a passphrase; just press enter. This will generate a public-private key pair called example.pub & example in your ~/.ssh folder.

b. Make sure the ~/.ssh folder is owned by admin:

sudo chown -R admin ~/.ssh
Enter fullscreen mode Exit fullscreen mode

c. Add GitHub's SSH server public key to the server's known_hosts file:

ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts
Enter fullscreen mode Exit fullscreen mode

d. Next, copy the SSH Public key after outputting the key to the terminal:

cat ~/.ssh/example.pub
Enter fullscreen mode Exit fullscreen mode

e. Use the copied key as a deploy key in GitHub:

  • Go to your GitHub Repo
  • Click on the Settings Tab
  • Click on Deploy Keys option from the sidebar
  • Click on the Add Deploy Key Button and paste the copied SSH Public Key with a name of your choice
  • Click on Add Key

f. Clone the project from your GitHub Repo to your server's home using:

git clone git@github.com:admin7374/example_app.git
Enter fullscreen mode Exit fullscreen mode

Here, admin7374 is the GitHub username and example_app is the Node app we are about to deploy. This will clone the code repo on the server.

C. Run the App with pm2

Now it's time to build and run the Node app in the background:

a. Navigate to the project folder:

cd ~/example_app
Enter fullscreen mode Exit fullscreen mode

b. Create a .env file:

touch .env
Enter fullscreen mode Exit fullscreen mode

c. Open the .env file and paste your environmental variables:

sudo nano .env
Enter fullscreen mode Exit fullscreen mode

Example .env file:

PORT = 8001
DATABASE_URL = "database_url"
Enter fullscreen mode Exit fullscreen mode

After pasting, click CTRL + S and CTRL + X to save and exit.

d. Create an ecosystem.config.cjs file in your repo code (best created inside the GitHub repo):

touch ecosystem.config.cjs
sudo nano ecosystem.config.cjs
Enter fullscreen mode Exit fullscreen mode

Then paste the code below:

module.exports = {
  apps : [
      {
        name: "example_app",
        script: "npm start",
        port: 8001 // optional, if have port set in app
      }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The above code will run the Node app at port 8001; make sure it matches the port defined in the application. The script is usually how the Node app generally runs. It assumes there is an npm start script inside your package.json to run the build code.

e. Next, install the necessary Node modules using:

npm ci
Enter fullscreen mode Exit fullscreen mode

The above command creates a node_modules folder with all necessary packages to run the code.

f. Now, let's build the code. Type:

npm run build
Enter fullscreen mode Exit fullscreen mode

The above script will build the code for distribution using the build script defined in package.json.

g. Add PM2 Process on Startup:

sudo pm2 startup
Enter fullscreen mode Exit fullscreen mode

h. Start the Node App using pm2:

pm2 start ecosystem.config.cjs
Enter fullscreen mode Exit fullscreen mode

i. Save the PM2 Process:

pm2 save
Enter fullscreen mode Exit fullscreen mode

This will save the process to keep running in the background.

j. List all PM2 processes running in the background:

pm2 list
Enter fullscreen mode Exit fullscreen mode

k. If you need to reload for redeployment, use:

pm2 reload example_app
Enter fullscreen mode Exit fullscreen mode

l. To check the PM2 process logs, use:

pm2 monit
Enter fullscreen mode Exit fullscreen mode

This will open an interactive terminal that will show you logs and metadata for each process. Enter q to quit.

m. Check if the app is working properly using:

curl localhost:8001
Enter fullscreen mode Exit fullscreen mode

This should output properly if the app is working.

D. Serve the App with NGINX

Now it's time to finally serve the app using Nginx. Nginx is a powerful web server, reverse proxy, and load balancer. In this case, we will use the nginx reverse proxy feature to serve the app running on localhost:8001 to the internet.

a. Start and enable nginx:

sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

b. Verify nginx is up and running:

sudo systemctl status nginx
Enter fullscreen mode Exit fullscreen mode

If everything went well, the output should indicate that the Nginx service is active (running).

c. If you wish to confirm Nginx's operation via a web browser, navigate to:

http://example.com
Enter fullscreen mode Exit fullscreen mode

This should show the default nginx page.

d. If it's not showing, the ufw firewall may be blocking ports 80 and 443. To allow them through the ufw firewall, use:

sudo ufw allow 'Nginx Full'
Enter fullscreen mode Exit fullscreen mode

e. Nginx, like many server software, relies on configuration files to dictate its behavior. Begin by creating a configuration file for your website:

sudo nano /etc/nginx/sites-available/example.com
Enter fullscreen mode Exit fullscreen mode

f. Inside this file, input the following proxy pass configuration:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://localhost:8001;

        # Proxy Params - pass client request information to the proxied server
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # If you need to upload files larger than 1M, use the below directive
        # client_max_body_size 500M;

        # Web socket upgrade configuration
        # Uncomment the following lines if you're using websockets in your app
        # proxy_http_version 1.1;
        # proxy_set_header Upgrade $http_upgrade;
        # proxy_set_header Connection "upgrade";
    }

    # Logging
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log warn;
}
Enter fullscreen mode Exit fullscreen mode

The proxy pass configuration serves files directly; it proxies requests to a local application (in this case, running on port 8001).

g. With the configuration file created, it isn't live yet. To activate it, you'll create a symbolic link to the sites-enabled directory:

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com
Enter fullscreen mode Exit fullscreen mode

Think of this step as "publishing" your configuration, making it live and ready to handle traffic.

h. Test the configuration before going live:

sudo nginx -t
Enter fullscreen mode Exit fullscreen mode

Nginx will then parse your configurations and return feedback. A successful message indicates that your configurations are error-free.

i. Time to go live. This requires a reload:

sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

j. Now check in the web browser, go to:

http://example.com
Enter fullscreen mode Exit fullscreen mode

k. Our current website configuration serves content over HTTP on port 80, which is unencrypted. Let's encrypt it via Let's Encrypt. First, install certbot:

sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:certbot/certbot
sudo apt update
sudo apt install certbot python3-certbot-nginx
Enter fullscreen mode Exit fullscreen mode

l. Generate SSL Certificates using certbot:

sudo certbot --nginx -d example.com -d www.example.com
Enter fullscreen mode Exit fullscreen mode

Just follow the instructions in the prompts. Your website will be encrypted with a proper SSL/TLS encryption certificate. Check your website in the browser to see if it's installed.

  1. Nowadays, certbot, when getting a new cert, will set up auto-renew for you, so it's a sit-and-forget kind of task. But to make sure it worked, you can run:
sudo systemctl status certbot.timer
Enter fullscreen mode Exit fullscreen mode

Now, big congratulations! You have successfully deployed your web app using Nginx. If you want to optimize Nginx, I recommend following this post: Basic Nginx Setup.

This concludes my documentation on how to deploy a Node app securely with Nginx.

References - For More Information

  1. Server Setup & Hardening
  2. Server Hardening Best Practices
  3. Server Setup Basics
  4. Install fail2ban
  5. Fail2ban Ubuntu Configs
  6. Deploy Node.js to VPS
  7. Nginx Configs
  8. How to Change Default SSH Port
  9. Uncomplicated Firewall
  10. DNS Cheatsheet
  11. SCP
  12. PM2 Guide

Top comments (0)