A video for this project is coming soon (like this). The source code is here.

What I Built

I deployed a full stack Node messaging app on a single EC2 instance. I set up the instance and connected it to a MySQL database on RDS. I used Nginx as a reverse proxy and PM2 to keep the server running. Then I put the app on a subdomain with Route53 and secured it with a Certbot SSL certificate.


Getting the Code Ready

The app worked locally, but it needed a few changes before it could run on a server. You can see every change here.

Server Changes

  • Before: Apollo Server had no path set for subscriptions. It used the default /graphql path for both HTTP requests and WS connections. That was fine locally. Once deployed I hit routing issues.
  • After: I added subscriptions: { path: '/' } to Apollo Server. This sets the subscription path explicitly. I also updated the server console output to show the new subscription URL.

Client Changes

  • Before: Apollo Client was hardcoded to http://localhost:4000 for HTTP and ws://localhost:4000/graphql for WS.
  • After: The client uses relative paths. HTTP goes to /graphql/ so it adapts to whatever host serves the app. WS uses ws://${host}/graphql/, which I build from the window.location object.

Environment Variables

  • I moved all database settings into a .env file. The Node app reads them at runtime through process.env. This keeps secrets out of the source code.

  • I added .env to .gitignore so it never gets committed.

  • I created config.js in a new config directory. It exports the database settings from process.env. I import it in models/index.js to set up the database connection.


Setting Up EC2

  1. Open the EC2 dashboard: I started in the AWS Management Console.

  2. Create the instance: I picked the latest Ubuntu Server LTS that is free tier eligible. I used a ’t2.micro’ instance type, which is also free tier.

  3. Configure the instance: I left most settings at their defaults. I set the security group to allow SSH (port 22) and HTTP/HTTPS (ports 80 and 443) from any source.

  4. Launch it: I reviewed the settings and launched. I saved the key pair for SSH access.

  5. Fix the key permissions and SSH in:

    • The downloaded private key file is too permissive by default.
    • I changed it to 600 with chmod 600 /path/to/your/key.pem.
    • Then I connected with the public IP and the ‘ubuntu’ user using ssh -i /path/to/your/key.pem ubuntu@<public-ip>.
  6. Update packages: I ran sudo apt update and sudo apt upgrade.

  7. Install NodeJS: First I added the NodeSource repository for Node.js.

curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/nodesource-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/nodesource-archive-keyring.gpg] https://deb.nodesource.com/node_14.x $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/nodesource.list

Then I installed Node with sudo apt install nodejs. I checked it with node -v and npm -v.

  1. Clone the private repo:

    • I made the GitHub repo private.
    • On the instance I moved into the .ssh directory with cd .ssh.
    • I created an SSH key for the server with ssh-keygen -t rsa -b 4096 -C “your_email_address@example.com”.
    • I opened the public key with vim id_rsa.pub, copied it, and exited Vim with :q.
    • I added that key to GitHub as a deploy key.
    • Then I cloned the repo with git clone git@github.com:<username>/<privaterepo>.git <target-directory>.

    Replace <username>/<privaterepo>.git with your GitHub username and repo name. Replace <target-directory> with the folder you want.

Setting Up RDS

  1. Create the database: I created a new MySQL instance in the AWS console. I set a DB name, username, and password. I made it publicly accessible, which still requires the credentials to log in. I used a db.t2.micro instance to stay in the free tier.

  2. Open the port: Once the instance was running, I edited the security group inbound rules. I allowed MySQL traffic on port 3306 from anywhere.

  3. Connect to it: I installed the client with sudo apt install mysql-client so I could use the mysql command. Then I connected with the RDS endpoint and my credentials using mysql -h <endpoint> -u <username> -p. It prompts for the password.

  4. Test the connection: I ran show databases;. It returned the default MySQL databases information_schema, mysql, performance_schema, and sys. That was expected because I had not seeded anything yet.

Connecting EC2 to RDS

  1. Create the .env file: In the cloned project folder I opened Nano with nano .env. I added these variables:
NODE_ENV=production
DB_USERNAME=your_database_username
DB_PASSWORD=your_database_password
DB_DATABASE=your_database_name
DB_HOST=your_database_endpoint
DB_DIALECT=mysql
JWT_SECRET=your_jwt_secret

I saved with Ctrl + O and exited with Ctrl + X.

  1. Run Sequelize: I used Sequelize to create, migrate, and seed the database. I installed it globally with sudo npm install -g sequelize-cli. Then I ran sequelize db:create to create the database. After that I ran sequelize db:migrate and sequelize db:seed:all.

  2. Check the results: I connected again with mysql -h <endpoint> -u <username> -p. I ran show databases; to confirm the new database existed. Then I ran use <db>; and SELECT * FROM users; to confirm the tables were created and the fake users were seeded.

Deploying the App

  1. Install dependencies: I ran npm install for the server. Then I ran cd client and npm install again for the client.

  2. Build the client: I ran npm run build in the client folder. This creates the static front end files that Nginx serves.

  3. Keep the server running:

    • node server.js starts the server, but it dies when the SSH session closes. I used PM2 to fix that.
    • I installed it globally with sudo npm install pm2 -g.
    • I started the server with pm2 start server.js --name "Server". Now it keeps running after I log out.
  4. Configure Nginx:

    • I installed Nginx with sudo apt install nginx and restarted it with sudo systemctl restart nginx. The EC2 public IP then showed the Nginx welcome page.
    • Next I set up a folder for the client files:
      • I created a directory in /var/www/ with sudo mkdir /var/www/<frontend>. Replace <frontend> with your folder name.
      • I changed ownership with sudo chown -R $USER:$USER /var/www/<frontend>.
      • I copied the build output with cp -r client/build/* /var/www/<frontend>.
    • Then I edited the Nginx config:
      • I opened it with sudo nano /etc/nginx/sites-available/default.

      • I set the root to /var/www/<frontend> and added index.html.

      • I routed / to the client files and /graphql/ to the Node server.

      • Here is the config:

          root /var/www/<frontend>;
        
          # Add index.php to the list if you are using PHP
          index index.html index.htm index.nginx-debian.html;
        
          server_name _;
        
          location / {
                   # First attempt to serve request as file, then
                   # as directory, then fall back to displaying a 404.
                   try_files $uri /index.html;
          }
        
          location /graphql/ {
                   proxy_pass http://localhost:4000/;
                   proxy_http_version 1.1;
                   proxy_set_header Upgrade $http_upgrade;
                   proxy_set_header Connection "upgrade";
          }
        
      • I restarted Nginx with sudo systemctl restart nginx to apply it.

Testing It

  • Loading the app:
    • I opened the EC2 public IP in a browser and the React app loaded.

    • Then I checked the backend:

      • I clicked the log in button. It returned an error response, which told me the backend was up and answering.
    • The proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade"; lines let the connection upgrade from HTTP to WS.

    • I could see the protocol switch in the browser network tab.

    • I also tested real time messaging. I watched the payload go to the server in the network tab.

Adding a Subdomain and SSL

1. Give the instance a static IP

The instance needed an IP that would not change. Otherwise DNS would break on every restart.

  • I opened the AWS Management Console.
  • I searched for “Elastic IPs”.
  • I allocated a new Elastic IP and associated it with the instance.
  • Now the subdomain always points to the right server.

2. Create the subdomain in Route53

Next I pointed a subdomain at the Elastic IP.

  • I opened Route53.
  • I created a hosted zone for my main domain.
  • I added a new record for the subdomain.
  • I chose an A record (IPv4 address) and pointed it at the Elastic IP.

3. Add SSL

Last, I secured the subdomain with a certificate.

  • On the instance I installed the Nginx Certbot plugin with sudo apt-get install certbot python3-certbot-nginx.
  • I ran sudo certbot --nginx and followed the prompts.
  • I entered my email, agreed to the terms, and picked the subdomain.
  • Certbot updated Nginx to use SSL and redirect HTTP to HTTPS.
  • I visited the subdomain and saw the lock icon next to the URL.

Wrapping Up

This was a fun project to build and document. If you try it yourself, delete the EC2 and RDS instances when you finish so you don’t get charged.