Deploying PERN stack on Ubuntu 20.04
Detailed step by step procedure to deploying PERN(Postgres, Express, React, Node) stack on Ubuntu 20.04 with NGINX and SSL
1. Install and Configure PostgreSQL
Update packages
sudo apt update && sudo apt upgrade -y
install PostgreSQL
sudo apt install postgresql postgresql-contrib -y
PostgreSQL will create an initial user called postgres
. In addition, PostgreSQL uses what's reffered to as peer authentication for local connections. This means that PostgreSQL obtains the username from the linux kernel and uses that to connect to the database. This requires that any user configured on postgres to have an equivalent user defined on Ubuntu. Postgres installation will have automatically created a postgres
user on Ubuntu as well to allow local connection. this can be verified by running the command:
ubuntu@ip-172-31-20-1:~$ sudo cat /etc/passwd | grep -i postgres
postgres:x:113:120:PostgreSQL administrator,,,:/var/lib/postgresql:/bin/bash
To connect to Postgres, switch to the postgres
user and run psql:
ubuntu@ip-172-31-20-1:~$ sudo -i -u postgres
postgres@ip-172-31-20-1:~$ psql
psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.
postgres=#
For this tutorial, the node process process will be run under the ubuntu
user and for the sake of simplicity, an ubuntu
user will be created on Postgres as well. If you want to use a different user feel free to create a different user in postgres.
To create a Postgres user run the following command which will give an interactive prompt for configuring the new user. For the sake of simplicity, the ubuntu user will be a superuser, which is the equivalent of being a root user on linux. The super user will have the ability to create/delete/modify databases and users.
postgres@ip-172-31-20-1:~$ createuser --interactive
Enter name of role to add: ubuntu
Shall the new role be a superuser? (y/n) y
login to postgres using the postgres
user for now to verify the new ubuntu
user was created successfully
postgres@ip-172-31-20-1:~$ psql
psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.
postgres=# \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-----------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
sanjeev | Superuser, Create role, Create DB | {}
test | | {}
test1 | Superuser, Create role, Create DB | {}
ubuntu | Superuser, Create role, Create DB | {}
postgres=#
Exit out of the psql by running \q
and also exit out of the postgres
user by running exit
on the command line
Let's try to run psql
as the ubuntu user now. An error similar to the one below should be observed
ubuntu@ip-172-31-20-1:~$ psql
psql: error: could not connect to server: FATAL: database "ubuntu" does not exist
The reason for this is that Postgres by default tries to connect to a database that is the same name as the user. Since the user is ubuntu
it tries to connect to a database called ubuntu
as well which does not exist. We can go in and create a database called ubuntu
so that it will automtically connect, however I find this unnecessry. Instead we can pass in the -d
flag and connect to a database that we know exists like the postgres
psql -d postgres
Right now the ubuntu
user in Postgres does not have a password associated with it. We will need to add a password:
ubuntu@ip-172-31-20-1:~$ psql -d postgres
psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.
postgres=# \q
ubuntu@ip-172-31-20-1:~$ psql -d postgres
psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.
postgres=# ALTER USER ubuntu PASSWORD 'password';
ALTER ROLE
postgres=#
2. Migrate Database Scheme & data
As with most sql database PostgreSQL allows us to easily copy our database schema and data from our local development postgres instance and copy it over to the Postgres isntance running in our production server.
On your local dev machine, open up a terminal and run:
pg_dump -U postgres -f yelp.pgsql -C yelp
The -U
flag specifies the user u want to login as, if you are using a different username, update it accordingly.
The -f yelp.pgsql
flag will write the database schema and data to a file called yelp.pgsql
in the current directory
-C
flag add the create database command to the file as well
yelp
is the name of the database in our local dev server that we want to dump. If your database is called something else update that accordingly. If you leave out the database name alltogether it will dump all databases
Copy the yelp.pgsql file to the production server
scp -i [path to pem file] [path to yelp.pgsql] username@[server-ip]:[directory to copy file to]
In this example the pem file and yelp.gsql file are located in the current directory and my server ip is 1.1.1.1
and the username is ubuntu
scp -i yelp.pem yelp.pgsql [email protected]:/home/ubuntu/
Login to the production server and create a database called yelp
ubuntu@ip-172-31-20-1:~$ psql -d yelp
postgres=# create database yelp;
CREATE DATABASE
Import the database schema & data from the yelp.pgsql
file
psql yelp < /home/ubuntu/yelp.pgsql
login to the yelp database and verify that everything looks ok
ubuntu@ip-172-31-20-1:~$ psql -d yelp
psql (12.4 (Ubuntu 12.4-0ubuntu0.20.04.1))
Type "help" for help.
yelp=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------------+----------+----------
public | restaurants | table | postgres
public | restaurants_id_seq | sequence | postgres
public | reviews | table | postgres
public | reviews_id_seq | sequence | postgres
(4 rows)
yelp=# select * from restaurants;
id | name | location | price_range
----+--------------------------+-----------------+-------------
2 | taco bell | san fran | 3
3 | taco bell | New York | 4
4 | cheesecake factory | dallas | 2
5 | cheesecake factory | dallas | 2
6 | cheesecake factory | houston | 2
10 | chiptle | virgini | 1
11 | chiptle | virgini | 1
13 | california pizza kitchen | vegas | 1
1 | wendys | Lincoln | 4
14 | california pizza kitchen | New mexico city | 3
(10 rows)
yelp=#
3. Copy github repo to sever
Find a place to store your application code. In this example in the ubuntu
home directory a new directory called apps
will be created. Within the new apps
directory another directory called yelp-app
. Feel free to store your application code anywhere you see fit
cd ~
mkdir apps
cd apps
mkdir yelp-app
move inside the yelp-app
directory and clone the project repo
cd yelp-app
git clone https://github.com/Sanjeev-Thiyagarajan/PERN-STACK-DEPLOYMENT.git .
4. Install Node
To install Node on Ubuntu follow the steps detailed in: https://github.com/nodesource/distributions/blob/master/README.md
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
sudo apt-get install -y nodejs
5. Install and Configure PM2
We never want to run node directly in production. Instead we want to use a process manager like PM2 to handle running our backend server. PM2 will be responsible for restarting the App if/when it crashes 😁
sudo npm install pm2 -g
Point pm2 to the location of the server.js file so it can start the app. We can add the --name
flag to give the process a descriptive name
pm2 start /home/ubuntu/apps/yelp-app/server/server.js --name yelp-app
Configure PM2 to automatically startup the process after a reboot
ubuntu@ip-172-31-20-1:~$ pm2 startup
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
The output above gives you a specific command to run, copy and paste it into the terminal. The command given will be different on your machine depending on the username, so do not copy the output above, instead run the command that is given in your output.
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u ubuntu --hp /home/ubuntu
Verify that the App is running
pm2 status
After verify App is running, save the current list of processes so that the same processes are started during bootup. If the list of processes ever changes in the future, you'll want to do another pm2 save
pm2 save
6. Deploy React Frontend
Navigate to the client directory in our App code and run npm run build
.
This will create a finalized production ready version of our react frontent in directory called build
. The build folder is what the NGINX server will be configured to serve.
ubuntu@ip-172-31-20-1:~/apps/yelp-app/client$ ls
README.md build node_modules package-lock.json package.json public src
ubuntu@ip-172-31-20-1:~/apps/yelp-app/client$ cd build/
ubuntu@ip-172-31-20-1:~/apps/yelp-app/client/build$ ls
asset-manifest.json favicon.ico index.html logo192.png logo512.png manifest.json precache-manifest.ee13f4c95d9882a5229da70669bb264c.js robots.txt service-worker.js static
ubuntu@ip-172-31-20-1:~/apps/yelp-app/client/build$
7. Install and Configure NGINX
Install and enable NGINX
sudo apt install nginx -y
sudo systemctl enable nginx
NGINX is a feature-rich webserver that can serve multiple websites/web-apps on one machine. Each website that NGINX is responsible for serving needs to have a seperate server block configured for it.
Navigate to '/etc/nginx/sites-available'
cd /etc/nginx/sites-available
There should be a server block called default
ubuntu@ip-172-31-20-1:/etc/nginx/sites-available$ ls
default
The default server block is what will be responsible for handling requests that don't match any other server blocks. Right now if you navigate to your server ip, you will see a pretty bland html page that says NGINX is installed. That is the default
server block in action.
We will need to configure a new server block for our website. To do that let's create a new file in /etc/nginx/sites-available/
directory. We can call this file whatever you want, but I recommend that you name it the same name as your domain name for your app. In this example my website will be hosted at sanjeev.xyz so I will also name the new file sanjeev.xyz
. But instead of creating a brand new file, since most of the configs will be fairly similar to the default
server block, I recommend copying the default
config.
cd /etc/nginx/sites-available
sudo cp default sanjeev.xyz
open the new server block file sanjeev.xyz
and modify it so it matches below:
server {
listen 80;
listen [::]:80;
root /home/ubuntu/apps/yelp-app/client/build;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name sanjeev.xyz www.sanjeev.xyz;
location / {
try_files $uri /index.html;
}
location /api {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Let's go over what each line does
The first two lines listen 80
and listen [::]:80;
tell nginx to listen for traffic on port 80 which is the default port for http traffic. Note that I removed the default_server
keyword on these lines. If you want this server block to be the default then keep it in
root /home/ubuntu/apps/yelp-app/client/build;
tells nginx the path to the index.html file it will server. Here we passed the path to the build directory in our react app code. This directory has the finalized html/js/css files for the frontend.
server_name sanjeev.xyz www.sanjeev.xyz;
tells nginx what domain names it should listen for. Make sure to replace this with your specific domains. If you don't have a domain then you can put the ip address of your ubuntu server.
The configuration block below is needed due to the fact that React is a Singe-Page-App. So if a user directly goes to a url that is not the root url like https://sanjeev.xyz/restaurants/4
you will get a 404 cause NGINX has not been configured to handle any path ohter than the /
. This config block tells nginx to redirect everything back to the /
path so that react can then handle the routing.
location / {
try_files $uri /index.html;
}
The last section is so that nginx can handle traffic destined to the backend. Notice the location is for /api
. So any url with a path of /api
will automatically follow the instructions associated with this config block. The first line in the config block proxy_pass http://localhost:3001;
tells nginx to redirect it to the localhost on port 3001 which is the port that our backend process is running on. This is how traffic gets forwarded to the Node backend. If you are using a different port, make sure to update that in this line.
Enable the new site
sudo ln -s /etc/nginx/sites-available/sanjeev.xyz /etc/nginx/sites-enabled/
systemctl restart nginx
8. Configure Environment Variables
We now need to make sure that all of the proper environment variables are setup on our production Ubuntu Server. In our development environment, we made use of a package called dotenv to load up environment variables. In the production environment the environment variables are going to be set on the OS instead of within Node.
Create a file called .env
in /home/ubuntu/
. The file does not need to be named .env
and it does not need to be stored in /home/ubuntu
, these were just the name/location of my choosing. The only thing I recommend avoid doing is placing the file in the same directory as the app code as we want to make sure we don't accidentally check our environment variables into git and end up exposing our credentials.
Within the .env file paste all the required environment variables
PORT=3001
PGUSER=postgres
PGHOST=localhost
PGPASSWORD=password123
PGDATABASE=yelp
PGPORT=5432
NODE_ENV=production
You'll notice I also set NODE_ENV=production
. Although its not required for this example project, it is common practice. For man other projects(depending on how the backend is setup) they may require this to be set in a production environment.
In the /home/ubuntu/.profile
add the following line to the bottom of the file
set -o allexport; source /home/ubuntu/.env; set +o allexport
For these changes to take affect, close the current terminal session and open a new one.
Verify that the environment variables are set by running the printenv
# printenv
9. Enable Firewall
sudo ufw status
sudo ufw allow ssh
sudo ufw allow http
sudo ufw allow https
sudo ufw enable
sudo ufw status
10. Enable SSL with Let's Encrypt
Nowadays almost all websites use HTTPS exclusively. Let's use Let's Encrypt to generate SSL certificates and also configure NGINX to use these certificates and redirect http traffic to HTTPS.
The step by step procedure is listed at: https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.html
Install Certbot
sudo snap install --classic certbot
Prepare the Certbot command
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Get and install certificates using interactive prompt
sudo certbot --nginx
Authors
- Sanjeev Thiyagarajan - CEO of Nothing