Jay Gould

Run React + Node on one AWS EC2 instance with Nginx - Part 2

July 20, 2019

This is part 2 of a 2 part post running a modern React and Node app with a PostgreSQL database on a single AWS EC2 instance with PM2 and Nginx for a reverse proxy.

  • Part 1: Setting up your AWS EC2 instance and running multiple Node processes simultaneously - view here
  • Part 2: Pointing your domain name at the EC2 instance for free (without the use of AWS Route 53), and installing and configuring a PostgreSQL database - [you are here]

Setting up Nginx and pointing your domain

If you don’t already know, Nginx is a super fast web server, performing the same job as something like Apache. We will be using it to achieve two objectives:

  • Using it to reverse proxy your application.
  • Allow us to point a custom domain name at our server without using Amazons Route 53.

The reverse proxy means that rather than accessing your app through a URL and port like:

http://ec2…eu-west-2.compute.amazonaws.com:8081

We can omit the port and just access it via:

http://ec2…eu-west-2.compute.amazonaws.com

Also, we don’t want to be using the standard URL given to us by Amazon. Instead we want to use a custom domain name which we already own. If you Google something like “pointing custom domain name at EC2 instance”, you’ll get tonnes of articles telling you to move your domain’s name servers over to Route 53, another of AWS’s services. This service costs $0.50 per month to host a domain name, and although this cost is low, I don’t like the idea of moving my domain names as I prefer to have all my domains managed in one place. An added benefit of using Nginx is that we can use our domain name DNS from anywhere we want, without using Route 53.

Firstly, you’ll want to install Nginx. Fortunately for us, Nginx is easy to download by using Amazon Extras. Amazon Extras are pre-packaged services available to install on the Amazon Linux 2 instances, and are great because Amazon keeps them updated and secure for us. View the available Amazon Extras by running:

amazon-linux-extras

This will show you a list of available packages. There may be one or two already enabled, but we want to enable Nginx, so run:

sudo amazon-linux-extras install nginx1.12

At the time of writing, Nginx is at version 1.12 on Amazon Linux Extras, but you should substitute with which ever version is available for you.

Now Nginx is installed we can visit our Amazon provided public DNS and we will be shown a default Nginx home page. If this is not shown, you may need to start Nginx, so just run sudo service nginx start.

9

Run cd /etc/nginx to go to the main Nginx config area. ls to view the configuration, and you’ll see a bunch of files. The main config file for Nginx is the nginx.conf file, so you could edit this and run all your config in this file, but it’s best to have your own config abstracted into other files to keep things readable and understandable. If you Google Nginx config, you’ll see mention of a sites-available and sites-enabled directories. These don’t come as standard on Amazon Linux installation of Nginx (I believe it’s a Red Hat or Debian thing, but I’m not too sure). As it’s such a common pattern though, I like to follow it and create the directory, so run:

sudo mkdir sites-available && sudo nano my-app.com

Part of the pattern is to have separate “sites-available” and “sites-enabled” directories, and creating symlinks to manage which sites are enabled. This confuses things slightly in my opinion, and I want to keep this as straight forward as possible, so all our config will be in our “sites-available” directory. If you want to learn more about the widely used pattern, this is a good thread.

We are creating what the Nginx community call “Server blocks”. Inside your “my-app.com” file, add the following config:

server {
  listen 80;
  listen [::]:80;
  server_name my-app.com www.my-app.com;

  location / {
    proxy_pass http://127.0.0.1:8081;
  }
}

This instructs Nginx to listen on port 80 (default web port) on the URL “my-app.com” (change this to your desired domain). Nginx will listen on port 80, but pass all requests back to ”http://127.0.0.1:8081” which is the localhost and port which the Node JS app is running on.

This has created a reverse proxy for our client side of our app, but this will need replicating for the server side, so create another file inside “sites-available” and follow the same process but change the domain and port to the details for your server. I like to have the API for my app as “api.my-app.com”, but you can assign a new domain all together if you like.

Save this file, then run sudo nano /etc/nginx/nginx.conf as we now want to include this new file in our main Nginx config file. Like I said earlier, I prefer to not edit the main config for Nginx, so we will just include our custom config.

Add the following line to your “nginx.conf” file:

  ...

  # Load modular configuration files from the /etc/nginx/conf.d directory.
  # See http://nginx.org/en/docs/ngx_core_module.html#include
  # for more information.
  include /etc/nginx/conf.d/*.conf;

  # Load custom settings - ADD THESE TWO LINES < < < < < < < < < <
  include /etc/nginx/sites-available/*;

  server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;
    root         /usr/share/nginx/html;

    # Load configuration files for the default server block.
    include /etc/nginx/default.d/*.conf;
    ...

Now save your file. You will need to restart Nginx and your Node processes, so run:

sudo service nginx restart && pm2 restart all

Now the final step is to point our domain name! Amazon assigns an IP address to each instance, but these IP addresses can change when the servers restart. In order to keep a long lasting IP address, Amazon offer free Elastic IP addresses. These are needed so we can point our domain name A records at an IP address which is not subject to change.

On the EC2 dashboard, go to “NETWORK AND SECURITY” in the left side menu, and select “Elastic IPs”.

10

Click “Allocate new address” and follow the instructions to assign the IP to your EC2 instance.

Now you have the correct domain name pointing to your server, don’t forget to update your .env file if you have any URLs included there, and be sure to restart Nginx and your PM2 instances as shown above.

And that’s it! You can now visit your own domain which points to your EC2 instance, with no ports visible in the URL, all handled by Nginx reverse proxy.

Installing and managing a PostgreSQL database

A final part I wanted to tack on at the end here is how to connect to a database. As I mentioned earlier, I had tried to run my client/server/DB setup on Amazon’s Elastic Beanstalk, which required a separate instance to be ran just for Postgres. Heroku offer a free Postgres service which you can technically connect to from your EC2 instance, but I don’t like the idea of having a small application spread over multiple service providers. Why not just install Postgres on the same EC2 instance as your app is running?

For production applications which are more than just side projects, it might make sense to have your database handled outside of your EC2 instance as it keeps your DB isolated, and also services which solely run databases offer backups and other services. This post aims to get a small application running for a low cost.

Postgres comes as part of the Amazon Linux Extras I mentioned earlier, so install by running:

sudo amazon-linux-extras install postgresql10

Once installed you can try to run psql, but it will give you an error message because what we just installed is the Postgres client tool, not the Postgres server. We need both in order to run a database, so to install the PostgreSQL server, run:

sudo yum install -y postgresql-server postgresql-devel

Once installed, we want to initialize a database:

/usr/bin/postgresql-setup --initdb

Then enable and start our database:

sudo systemctl enable postgresql && sudo systemctl start postgresql

Now we would usually be able to run psql to access our Postgres CLI (which we need to do in order to create users, databases, tables etc.), but when this command is run, you’ll get an error message like psql: FATAL: role "ec2-user" does not exist. This is because of how Postgres works. By default, Postgres creates a “postgres” user and a “postgres” database. The only way to connect to the “postgres” database is to be logged in as the “postgres” linux user. This is called “Ident” authentication. We are currently logged in as the “ec2-user” user, so Postgres thinks we are trying to connect to the “ec2-user” database. Obviously that doesn’t exist.

To see all users on your EC2 instance, run cat /etc/passwd. You’ll see the ec2-user and postgres users are both listed.

We want to interact with the Postgres DB as “postgres”, so we can run the following command to run a process as a specific user:

sudo -u postgres psql

This runs the psql command as the postgres user with the use of the -u option. For the purposes of this post we will use the “postgres” database and user to run our application, but in production you’d want to create new users and databases for each app. There’s a great Digital Ocean article which goes in to detail about how to achieve this.

Once you are on the psql CLI, you can run the following helpful commands to get a feel for what’s going on:

  • \du - lists database users
  • \l - lists databases
  • \q - quits the CLI
  • ALTER USER postgres PASSWORD 'myPassword’;

That last option is required to set the password of our database which we add to our application.

Now our database is running and we have a user and password, we can add this info to our application. I have my database credentials in my .env file most of the time as it’s good practice to run different databases in different environments. So your .env might include something like:

DB_NAME=postgres
DB_USER=postgres
DB_PASSWORD=myPassword
DB_HOST=localhost

Note how the host is localhost as we are running the database on the same instance as our API server which is connecting.

Before we start using our database from our application we must do two more things. Firstly we want to update our Postgres database config file to use password authentication instead of Ident auth. Ident is the default config for accessing the database on a new installation, but that won’t work as all requests from our app will be ran as the user who ran the Node process (ec2-user in our case). So run:

sudo nano /var/lib/pgsql/data/pg_hba.conf

Scroll to the bottom of this file and update the IPv4 and IPv6 methods to be md5, like this:

11

Save the file, and restart your Postres service: sudo systemctl restart postgresql.

You should then be able to run your application in your live environment, connecting to your Postgres database using your credentials defined in the .env file.

If you also would like to connect to your database using a desktop DB tool (I recommend TablePlus), then you must edit the following file:

sudo nano /var/lib/pgsql/data/postgresql.conf

And update the listen_addresses to:

listen_addresses = '*'

You will also need to add port 5432 to the AWS incoming port list (where the other ports for your app were also added previously).

This does open up your DB from accepting connections from everywhere though, so ensure your password is SUPER secure and maybe even put the DB on a different port.

Thanks for reading

I have covered a lot in this article so if there’s any incorrect info just drop me an email and let me know, I’d be glad to update the content. I hope someone finds it helpful as it’s not the easiest thing to do, but when it’s written down all in one place it makes it digestible.


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.