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
/graphqlpath 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:4000for HTTP andws://localhost:4000/graphqlfor WS. - After: The client uses relative paths. HTTP goes to
/graphql/so it adapts to whatever host serves the app. WS usesws://${host}/graphql/, which I build from thewindow.locationobject.
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.jsin a newconfigdirectory. It exports the database settings from process.env. I import it inmodels/index.jsto set up the database connection.
Setting Up EC2
Open the EC2 dashboard: I started in the AWS Management Console.
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.
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.
Launch it: I reviewed the settings and launched. I saved the key pair for SSH access.
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>.
Update packages: I ran
sudo apt updateandsudo apt upgrade.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.
Clone the private repo:
- I made the GitHub repo private.
- On the instance I moved into the
.sshdirectory withcd .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>.gitwith your GitHub username and repo name. Replace<target-directory>with the folder you want.
Setting Up RDS
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.
Open the port: Once the instance was running, I edited the security group inbound rules. I allowed MySQL traffic on port 3306 from anywhere.
Connect to it: I installed the client with
sudo apt install mysql-clientso I could use themysqlcommand. Then I connected with the RDS endpoint and my credentials usingmysql -h <endpoint> -u <username> -p. It prompts for the password.Test the connection: I ran
show databases;. It returned the default MySQL databasesinformation_schema, mysql, performance_schema, and sys. That was expected because I had not seeded anything yet.
Connecting EC2 to RDS
- 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.
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 ransequelize db:createto create the database. After that I ransequelize db:migrateandsequelize db:seed:all.Check the results: I connected again with
mysql -h <endpoint> -u <username> -p. I ranshow databases;to confirm the new database existed. Then I ranuse <db>;andSELECT * FROM users;to confirm the tables were created and the fake users were seeded.
Deploying the App
Install dependencies: I ran
npm installfor the server. Then I rancd clientandnpm installagain for the client.Build the client: I ran
npm run buildin the client folder. This creates the static front end files that Nginx serves.Keep the server running:
node server.jsstarts 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.
Configure Nginx:
- I installed Nginx with
sudo apt install nginxand restarted it withsudo 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/withsudo 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>.
- I created a directory in
- 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 addedindex.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 nginxto apply it.
- I installed Nginx with
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.

- 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;andproxy_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 --nginxand 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.