Skip to content

Instantly share code, notes, and snippets.

@ChrisGV04
Last active March 19, 2024 22:47
Show Gist options
  • Save ChrisGV04/be192d14c872f0c0f2d5f10b0aab89ac to your computer and use it in GitHub Desktop.
Save ChrisGV04/be192d14c872f0c0f2d5f10b0aab89ac to your computer and use it in GitHub Desktop.
Steps to deploy a Node.js app in Ubuntu 20.4 with Nginx, PM2, Redis and MongoDB

Steps to deploy a Node.js app with NGINX and PM2

This where the steps I took to deploy one of my apps to Vultr on an Ubuntu server. It also includes the installation of the SSL certificate

Create SSH Key

If you don't have an SSH key in your local machine, you'll need to create one by runninig:

# You will be prompted to set the file name and a password
ssh-keygen -C "[email protected]"

# Use that public key inside of the server setup page
cat /path/to/your/file.pub

Log in to the server

Once the server is up and running, you can connect via SSH to it using the root user and password provided by the Hosting setup page:

ssh root@IP_ADDRESS

If you get prompted for the authenticity of the SSH handshake, just type yes.

Updates

If your server didn't do this already, update the system packages by running:

sudo apt update
sudo apt upgrade

If you get prompted by some server config, you can press Enter twice to continue.

Create sudo user & disable root login

It's best practice to user a sudo user instead of the root one for security reasons. To create the new user, use the following commands:

adduser USER_NAME # Create the user
usermod -aG sudo USER_NAME # Add sudo permissions
id USER_NAME # Check if the user was created successfully

Now, manually add the SSH key to the new user. Run:

cd /home/USER_NAME
mkdir .ssh
chmod 700 /home/USER_NAME/.ssh

cd .ssh
sudo nano authorized_keys

sudo chown -R USER_NAME:USER_NAME /home/USER_NAME # Make that user the owner of their directory

Inside the authorized_keys paste the same public SSH key from your machine just as you did on the first step. Then exit the editor with CTRL + X, then press Y and Enter to save.

Now you should be able to log in as that user:

exit # First exit the server from the root user
ssh USER_NAME@IP_ADDRESS

Finally, to disable root login, edit the sshd_config file:

# /etc/ssh/sshd_config
PermitRootLogin no
PasswordAuthentication no

Reload the sshd service to apply the changes:

sudo systemctl reload sshd

Install Node.js

To install the latest Node.js version using NVM, run the following:

# Install NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
source ~/.bashrc

# Use NVM to install the latest Node.js version
nvm install --lts

Use Github Deploy Keys

If you need to use your Github account with SSH for app deployment, you must do the following on the server:

ssh-keygen -t rsa
eval `ssh-agent -s` && ssh-add ~/.ssh/KEY_NAME

Now you can use the public key as a Deploy Key inside of your Github project and clone it via:

git clone REPO_URL local_dir_name

Instal PM2

The best process manager for node is PM2, which can be installed globally using:

npm i -g pm2
pm2 startup ubuntu # Set it up for auto-startup. Follow the steps from this command

It's also recommended to use pm2-logrotate to not store an infinite amount of logs. To install it, run the following:

pm2 install pm2-logrotate

# (Recommended) Only keep 5 log files per process at the same time. Default is 30
pm2 set pm2-logrotate:retain 5

Setup UFW Firewall

For firewall protection, enable the UFW with:

sudo ufw enable
sudo ufw status
sudo ufw allow ssh # Port 22 is for SSH

Setup Fail2Ban

It's highly recommended to add this package to prevent brute force attacks when someone tries to log into our server. The easiest way to set it up is by running the following commands:

sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl restart fail2ban

After that, the configuration file will be located at /etc/fail2ban/jail.conf. Based on this article.

Install NGINX

To install the NGINX web server, run the following:

sudo apt install nginx
sudo systemctl enable nginx
sudo ufw allow 'Nginx Full'

You can now create the configuration files for all of your websites inside of the /etc/nginx/sites-available directory. To make those sites live, run the following:

# Create link to the config file on enabled sites
sudo ln -s /etc/nginx/sites-available/YOUR_CONFIG_FILE /etc/nginx/sites-enabled/

# Test if there are no issues and apply changes
sudo nginx -t
sudo systemctl restart nginx

Install SSL Certificate

To install a basic SSL certificates on your NGINX sites, we can use Certbot which also auto-renews them:

sudo apt install certbot python3-certbot-nginx

sudo certbot --nginx -d example.com -d www.example.com
sudo systemctl enable certbot.timer # Enable auto-renewals

Install Redis

If your app requires Redis, you can install it as follows:

curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

sudo apt-get update
sudo apt-get install redis

Inside the /etc/redis/redis.conf file, search and replace the supervised option:

# /etc/redis/redis.conf
supervised systemd

And just restart the redis-server service to apply the changes:

sudo systemctl restart redis-server
sudo systemctl enable redis-server

To secure the Redis Databse, you must set a password at requirepass foobared inside /etc/redis/redis.conf. Make sure to use an extremely secure password, since Redis is able to check up to 1M passwords per second if faced with bruteforce.

# ...
requirepass USE_AN_EXTREMELY_SECURE_PASSWORD
# ...

We recommend using a random string by running the following command:

openssl rand 60 | openssl base64 -A

Once the password is set, just restart the redis service and you're good to go.

sudo systemctl restart redis

# Test that auth works
redis-cli
set key1 10 # This should throw a NOAUTH error

auth your_redis_password
set key1 10 # This should return OK

The connection string should look something like this:

redis://default:YOUR_REDIS_PASSWORD@localhost

Install MongoDB

Instructions from the MongoDB Docs.

First, we should install the MongoDB software:

wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | sudo apt-key add -
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-6.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org

# To prevent auto updates on MongoDB, run this
echo "mongodb-org hold" | sudo dpkg --set-selections
echo "mongodb-org-database hold" | sudo dpkg --set-selections
echo "mongodb-org-server hold" | sudo dpkg --set-selections
echo "mongodb-mongosh hold" | sudo dpkg --set-selections
echo "mongodb-org-mongos hold" | sudo dpkg --set-selections
echo "mongodb-org-tools hold" | sudo dpkg --set-selections

Setup Replica Set

Steps followed from this Youtube Video and this Article

⚠️ If using with Cloudflare as a DNS provider, you should add a subdomain with proxy disabled. For example: mongo.example.com. This is because Cloudflare doesn't pass mongodb:// traffic through the proxy.

First, shut down the original server:

sudo systemctl stop mongod

Now, create folders for each replica set to store the database on. For example, if we have 3 members we would create db1, db2 and db3. Then set ownership to MongoDB.

cd /var/lib
mkdir mongodb2 mongodb3
sudo chown -R mongodb:mongodb mongodb2 mongodb3

And for enabling authentication later on, also create a secure keyfile and copy it to every member of the replica set:

openssl rand -base64 756 > keyfile
sudo chmod 400 keyfile
sudo chown -R mongodb:mongodb keyfile

Now, copy the original instance's config file for each of the set members:

# Repeat for each member
sudo cp /etc/mongod.conf /etc/mongod2.conf

And then edit each config file to use the new directory for the databases and setup replication:

# /etc/mongod.conf, /etc/mongod2.conf, ...

# Replace with the path of each db folder
storage:
  dbPath: /var/lib/mongodb

net:
  # Increment port for each instance
  port: 27017
  # Optionally, add your domain name
  bindIp: 127.0.0.1,mongo.example.com

# Use the same name on each member
replication:
  replSetName: rs0

# Setup Authorization for later. Leave commented.
#security:
#  authorization: enabled
#  keyFile: /path/to/keyfile

Now we need to create a service for each of the instances, so we can copy the original one for each member:

# Repeat for each member
sudo cp /lib/systemd/system/mongod.service /lib/systemd/system/mongod2.service

And replace the ExecStart --config path to the config file of each member:

# /lib/systemd/system/mongod.service, /lib/systemd/system/mongod2.service, ...
ExecStart=/usr/bin/mongod --config /etc/mongod.conf

Once donce, we can start and enable all of the new services:

# Repeat for each member
sudo systemctl start mongod
sudo systemctl enable mongod

If you get an error with status code 14, run the following:

sudo chown -R mongodb:mongodb /var/lib/mongodb
sudo chown mongodb:mongodb /tmp/mongodb-27017.sock

# Restart every instance
sudo systemctl restart mongod.service

Initiate replica set

If all of your instances are running correctly, it's time to create the Replica Set. Connect to one of the instances:

mongosh --port 27017

And start the replica set:

rs.initiate({
  _id: 'rs0',
  members: [
    { _id: 0, host: 'mongo.example.com:27017' },
    { _id: 1, host: 'mongo.example.com:27018' },
    { _id: 2, host: 'mongo.example.com:27019' }
  ]
})

# You should see the list of members
rs.status()

If your members started on host 127.0.0.1 or localhost, you can use this guide to change the hostnames of the members after.

Enable Access Control

Steps from this YouTube video

And finally, let's enable authentication to secure the database.

First, connect to the primary member of the replica set using mongosh --port 27017 and create a user by run the following:

use admin

# (Recomended) For limited access users, use the following
db.createUser({user: "USERNAME", pwd: passwordPrompt(), roles: [{role: "readWrite", db: "YOUR_DB_NAME"}]})

# If you want a root user, use the following
db.createUser({user: "root", pwd: passwordPrompt(), roles: ["root"]})

Once the user is created, stop every replica set member's:

# Stop each member
sudo systemctl stop mongod

And now enable the security property configuration inside each config file for the replica members:

# /etc/mongod.conf, /etc/mongod2.conf, ...
security:
  authorization: enabled
  keyFile: /path/to/keyfile

Now we can restart every service:

# Restart each member
sudo systemctl restart mongod

And just like that, authorization is enabled. To authenticate using mongosh, run the following:

use admin
db.auth("USERNAME", passwordPrompt())

That's it!

Connect to the database

To connect to the database locally, we can use the following connection string template:

mongodb://<USERNAME>:<PASSWORD>@127.0.0.1:27017,127.0.0.1:27018,127.0.0.1:27019/<DATABASE-NAME>?authSource=admin&replicaSet=<REPLICA-SET-NAME>&readPreference=primary

Just replace the <USERNAME>, <PASSWORD>, <DATABASE-NAME> and <REPLICA-SET-NAME> with the appropriate data.

Remote connections

To connect remotelly, we should allow the firewall traffic on each member's port:

# Repeat for each member's port
sudo ufw allow 27017

We should also make sure that the hostnames of the members are set as any kind of public domain. For example: mongo.example.com:27017. If not, use this guide to change the hostnames of the members.

Once done, just replace 127.0.0.1 for the public domain inside the connection string for each of the members.

Done!

Install MySQL

Instructions from this video.

sudo apt install mysql-server

This will install the community version of MySQL on your server. To secure your installation run:

sudo mysql_secure_installation

I use the following prompts:

- VALIDATE PASSWORD COMPONENT: Yes
- Password strength: 2 (STRONG)
- Remove anonymous users: Yes
- Disallow remote root login: Yes
- Remove test DB: Yes
- Reload privileges table: Yes

And after that, you can log in with the command mysql which will open the basic MySQL shell.

Secure MySQL Connection

First, we'll create a new user to manage the database instead of the root user. To do that, run the SQL command:

CREATE USER 'your_username'@'localhost' IDENTIFIED BY 'your_password_here';
GRANT ALL PRIVILEGES ON *.* TO 'your_username'@'localhost' WITH GRANT OPTION;

And now you can log in as that user with the bash command mysql -u your_username -p and type your password.

You can check the current authentication status by running the following SQL command:

SELECT user,authentication_string,plugin,host FROM mysql.user;

If your user doesn't have mysql_native_password as the plugin, we must change it to allow password login by running the SQL command:

ALTER USER 'your_username'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';

That should be it! You can now use your new MySQL database locally. IF YOU NEED REMOTE ACCESS, do some research.

Install Phpmyadmin

First, follow this article to install PHP with NGINX.

If you want a visual manager for the database, you can install phpmyadmin on your system by following this article.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment