Tutorial24 min read

Deploy React + Node.js to AWS EC2 Without GitHub — The Complete Beginner's Guide

A complete, beginner-friendly guide to deploying a React + Node.js app to AWS EC2 without GitHub — SSH, SCP, npm, PM2, an Nginx reverse proxy, the production build, and fixing every common error.

Deploy React + Node.js to AWS EC2 Without GitHub — The Complete Beginner's Guide

Deploy React + Node.js to AWS EC2 Without GitHub — The Complete Beginner's Guide

You built something amazing. Your React + Node.js application works perfectly on your laptop. Users love it. But there's one problem.

It only runs on your computer.

The moment you close your laptop, your website disappears.

Most deployment guides assume your code lives on GitHub. Push → Clone → Deploy. Done.

But what if your project isn't on GitHub? What if it's just on your local machine? What if you're learning AWS and don't want to deal with Git yet?

That's exactly the situation we faced.

We built a collaborative code editor using React, Node.js, Express, Socket.IO, and Yjs. Everything worked locally. But deploying it without GitHub seemed impossible.

Every tutorial skipped the "why." They just said "run this command" without explaining what it actually does, why it's necessary, or what happens when things break.

This guide is different.

We're not just going to deploy your application. We're going to understand deployment itself. By the end, you'll know why each tool exists, how they fit together, and how to debug problems when they inevitably occur.

What You'll Learn

By the end of this guide, you'll understand:

  • Why you can't just copy your project to a server and run it
  • How SSH works and why AWS uses PEM files instead of passwords
  • How to upload files to your server without GitHub
  • Why npm install is required after uploading
  • Why npm start isn't suitable for production
  • What PM2 does and why every production server uses it
  • What Nginx is and why you need it
  • Why React needs to be built for production
  • How to configure Nginx as a reverse proxy
  • How to debug every common deployment error
  • Production best practices to keep your application secure and reliable

Your Situation

You have:

  • An AWS account
  • A created Ubuntu EC2 instance
  • A PEM key downloaded
  • A React (Vite) frontend on your laptop
  • A Node.js backend on your laptop
  • A working application locally
  • No GitHub repository

You need:

  • Your application accessible from the internet
  • It running 24/7
  • Automatic restarts if it crashes
  • A proper production architecture

Let's build it.


Part 1: Understanding Your Server

What is AWS EC2?

AWS EC2 stands for Elastic Compute Cloud.

In simple words: Amazon rents you a virtual computer.

You don't buy expensive hardware. Instead, you create a virtual machine in AWS data centers and control it remotely.

That machine behaves exactly like a normal Linux computer. You can:

  • Install software
  • Create files
  • Run Node.js
  • Install Nginx
  • Host websites

Everything you can do on your own computer, you can do on EC2.

Why Not Just Use Your Laptop?

Right now, your laptop runs your application. When someone wants to use it, they need your laptop to be:

  • Turned on
  • Connected to the internet
  • Running the application

If you close it, the application stops. That's unacceptable for production.

Production servers are:

  • Always online (24/7)
  • Always running the application
  • Automatically restarting if something breaks
  • Monitored constantly
  • Accessible from anywhere

What Does "Elastic" Mean?

Today your website has 10 users. A small EC2 instance is enough.

Tomorrow it gets 100,000 users.

Instead of buying new hardware, you simply upgrade your EC2 instance.

This flexibility is why it's called "Elastic."

Understanding Ports and Security Groups

When you created your EC2 instance, you configured something called a Security Group.

Think of it as a firewall.

Every service running on your server uses a door (called a port).

Without permission, nobody can enter.

The Security Group decides which doors are open.

Port 22 — SSH (remote control)

This lets you connect to your server and run commands. Without it, you're locked out.

Port 80 — HTTP (websites)

This is what people see in their browser. When someone visits http://YOUR_PUBLIC_IP, they connect to port 80.

Port 3000 — Backend (testing)

We'll open this temporarily to verify our backend works. Later, we'll close it for security.


Part 2: Connecting to Your Server

What is SSH?

SSH stands for Secure Shell.

Think of it as a secure remote terminal.

Your laptop is at home. Your server is in an AWS data center somewhere far away.

You use SSH to control that distant server as if you were sitting in front of it.

Your Laptop
    │
    ├─ SSH (encrypted)
    │
    ▼
AWS Server (thousands of miles away)

Every command you type is encrypted. The server executes it and sends the output back.

Why Not Use a Password?

Most people expect to log in like this:

Username: ubuntu
Password: ••••••••

AWS intentionally avoids this.

Passwords can be guessed, leaked, or brute-forced.

Instead, AWS uses public-key authentication, which is much more secure.

Understanding the PEM File

When you created your EC2 instance, AWS downloaded a file like your_pem_file.pem.

This is your private key.

Think of it as the only key that unlocks your server.

If someone gets this file, they can access your server.

If you lose it, you're locked out.

Important: Never upload this file to GitHub. Treat it like a password—but even more carefully.

The SSH Command

The command to connect is:

ssh -i ~/Documents/ssh/your_pem_file.pem ubuntu@YOUR_PUBLIC_IP

Breaking it down:

  • ssh — Start a secure remote connection
  • -i — Use this identity file (your private key)
  • ~/Documents/ssh/your_pem_file.pem — Path to your PEM file (on YOUR computer)
  • ubuntu — The username (Ubuntu instances use "ubuntu")
  • YOUR_PUBLIC_IP — The server's address (something like 13.235.xxx.xxx)

After running this, you'll see:

ubuntu@ip-172-31-41-225:~$

Notice: The prompt now shows ubuntu. You're no longer on your local machine. You're inside the EC2 server.

Common SSH Mistakes

Wrong username: Amazon Linux uses ec2-user, not ubuntu. Always check your AMI documentation.

Wrong PEM path: If SSH can't find your PEM file, it fails. Always verify: ls ~/Documents/ssh

Port 22 blocked: If your Security Group doesn't allow SSH traffic, the connection times out.

Wrong public IP: AWS can assign a new IP if you stop and start your instance. Always check the current IP in the AWS console.


Part 3: Uploading Your Project Without GitHub

The Problem

Your project lives on your laptop:

MacBook
│
├── ai-interviewer
│   ├── frontend
│   └── backend

Your server is empty:

EC2 Server
│
└── (nothing)

How do you get your project there without GitHub?

Introducing SCP

SCP stands for Secure Copy Protocol.

It's the file transfer version of SSH.

Instead of sending commands, it sends files.

Your Laptop
    │
    ├─ SCP (encrypted file transfer)
    │
    ▼
AWS Server

The SCP Command

scp -i ~/Documents/ssh/your_pem_file.pem -r \
~/Documents/Personal/ai-interviewer \
ubuntu@YOUR_PUBLIC_IP:/home/ubuntu/

Breaking it down:

  • scp — Secure Copy
  • -i ~/Documents/ssh/your_pem_file.pem — Use this key (same as SSH)
  • -r — Recursive (copy entire folders)
  • ~/Documents/Personal/ai-interviewer — Source folder (on YOUR laptop)
  • ubuntu@YOUR_PUBLIC_IP:/home/ubuntu/ — Destination (on the server)

Important: Run this from your laptop, NOT from inside the EC2 server. We made this mistake initially—Linux doesn't know what /Users/vikasrajput/ is. That's a macOS path.

Verifying the Upload

Connect to your server:

ssh -i ~/Documents/ssh/your_pem_file.pem ubuntu@YOUR_PUBLIC_IP

Check what's there:

ls

You should see:

ai-interviewer

Perfect. Your project is now on the server.

Why Not Upload node_modules?

Many beginners try to copy everything, including node_modules.

Don't.

Why?

  • It's hundreds of megabytes (slow to transfer)
  • It's platform-specific (macOS packages ≠ Linux packages)
  • It's easy to regenerate (just run npm install)

Instead, upload only your source code. Run npm install on the server. This downloads Linux-compatible packages.


Part 4: Installing Dependencies

Understanding package.json

Inside your project, you have files like package.json:

{
  "name": "backend",
  "version": "1.0.0",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^5.2.1",
    "socket.io": "^4.8.3",
    "y-socket.io": "^1.1.3"
  }
}

This file is like a recipe. It says:

"My project needs Express, Socket.IO, and y-socket.io."

But it doesn't contain those libraries. It only lists their names.

What is npm?

npm stands for Node Package Manager.

It's like the App Store for JavaScript.

When you run npm install, npm:

  1. Reads package.json
  2. Connects to the npm registry
  3. Downloads each library
  4. Installs them in a folder called node_modules

Installing Backend Dependencies

Connect to your server and navigate to the backend:

cd ~/ai-interviewer/backend

Verify you're in the right place:

pwd

Expected output:

/home/ubuntu/ai-interviewer/backend

Now install:

npm install

You'll see npm downloading and installing hundreds of packages. This may take a minute or two.

When it's done, a new folder appears: node_modules

Installing Frontend Dependencies

Do the same for the frontend:

cd ~/ai-interviewer/frontend
npm install

What Just Happened?

Your server now has:

  • Your source code
  • All required libraries
  • Everything needed to run the application

But there's still a question: how do we actually start it?


Part 5: Running Your Backend (And Why It's Not Enough)

Starting the Backend

Navigate to the backend:

cd ~/ai-interviewer/backend

Start it:

npm start

Expected output:

Server is running on port 3000

Congratulations. Your backend is now running.

Verifying It Works

Open your browser and visit:

http://YOUR_PUBLIC_IP:3000

If your backend responds, you've successfully deployed the backend.

This was a huge milestone for us. Everything worked!

Then we realized a critical problem.

The Hidden Problem

Imagine you're done for the day. You close your laptop.

What happens?

Your SSH session disconnects.

Linux kills every process attached to that terminal.

That includes your Node.js application.

Your website immediately goes offline.

Why?

When you started npm start, the process tree looked like:

SSH Terminal
    │
    ▼
npm
    │
    ▼
Node.js
    │
    ▼
server.js

Node.js belongs to the terminal. If the terminal dies, everything below it dies too.

The Real Problem

Production servers aren't supervised by someone's terminal.

They run as background services.

Think of it like an office:

  • If the manager leaves, employees don't stop working
  • Someone else supervises them

That's exactly what we need for Node.js.

We need something to:

  • Start the application
  • Keep it running in the background
  • Restart it if it crashes
  • Restart it after server reboots
  • Monitor its health

That tool is called a Process Manager.

The most popular one is PM2.


Part 6: Using PM2 (The Application Supervisor)

What is PM2?

PM2 is a Process Manager for Node.js.

Instead of you managing Node.js, PM2 does.

Think of PM2 as a supervisor.

Without PM2:
SSH Terminal
    │
    ▼
Node.js (dies if terminal closes)

With PM2:
PM2 (always running)
    │
    ▼
Node.js (stays alive even if terminal closes)

Problems PM2 Solves

SSH disconnect: Without PM2, closing your laptop stops the application. With PM2, the application keeps running.

Application crash: If Node.js throws an error, PM2 automatically restarts it.

Server reboot: After AWS restarts your server, PM2 automatically starts your application again.

Monitoring: PM2 shows CPU usage, memory usage, uptime, and restart count.

Installing PM2

npm install -g pm2

The -g means "global." PM2 becomes available everywhere on the server, not just in your project.

Starting Your Backend with PM2

Navigate to the backend:

cd ~/ai-interviewer/backend

Start it with PM2:

pm2 start server.js --name ai-interviewer-backend

This tells PM2:

"Execute node server.js and call it ai-interviewer-backend."

Viewing Running Applications

pm2 list

Output:

┌────┬────────────────────────────┬────────┐
│ id │ name                       │ status │
├────┼────────────────────────────┼────────┤
│ 0  │ ai-interviewer-backend     │ online │
└────┴────────────────────────────┴────────┘

Your application is "online." It's healthy.

Viewing Logs

To see what your application is doing:

pm2 logs ai-interviewer-backend

If there's an error, you'll see it immediately here.

Making PM2 Survive Reboots

PM2 needs two commands:

pm2 startup
pm2 save

The first tells Linux to start PM2 automatically after a reboot.

The second saves your running processes so PM2 remembers them.

Now if AWS reboots your server, PM2 automatically starts your application again. No manual work required.


Part 7: Understanding Nginx (Why Node.js Isn't Enough)

The Current Situation

Your backend is running on http://YOUR_PUBLIC_IP:3000.

Everything works.

So why install another software?

Today you have 10 users. Node.js handles it fine.

Tomorrow you have 100,000 users.

Node.js is now responsible for:

  • Serving HTML
  • Serving CSS
  • Serving JavaScript
  • Serving images
  • Serving videos
  • Handling APIs
  • Managing real-time connections

Should Node.js handle all of this?

Technically yes.

Practically no.

The Problem with Serving Static Files

Suppose someone requests /logo.png.

Does it make sense for Node.js to:

  1. Wake up
  2. Execute JavaScript
  3. Read the file
  4. Create an HTTP response
  5. Send it back

All of that just to serve an image?

No.

It's wasteful.

Meet Nginx

Nginx is a Web Server.

Written in C, it's incredibly fast and memory-efficient.

Its job is handling HTTP requests.

It specializes in:

  • Serving HTML, CSS, JavaScript
  • Serving images and videos
  • Reverse proxy (forwarding requests)
  • SSL/HTTPS
  • Compression
  • Caching

Node.js vs Nginx (A Restaurant Analogy)

Imagine a restaurant:

  • Chef (Node.js): Cooks food. Should focus on cooking.
  • Waiter (Nginx): Takes orders, delivers food, handles customers.

Would you want the chef leaving the kitchen every time someone asks for water?

No.

Production servers work the same way.

Nginx handles customers.

Node.js focuses on backend work.

The Future Architecture

After installing Nginx:

Internet
    │
    ▼
Port 80 (HTTP)
    │
    ▼
Nginx
  ├─ React Static Files (HTML, CSS, JS)
  │
  └─ /socket.io → Forward to Node.js
         │
         ▼
   Node.js (Port 3000)
         │
         ▼
   Express + Socket.IO

Users only see port 80. They never know about port 3000.

Installing Nginx

Update Ubuntu's package list:

sudo apt update

Install Nginx:

sudo apt install nginx -y

Start it:

sudo systemctl status nginx

You should see:

Active: active (running)

Now visit:

http://YOUR_PUBLIC_IP

You'll see the default Nginx welcome page. That's normal. We'll replace it with our React application.


Part 8: Building React for Production

Why Can't We Deploy React Directly?

When you develop React locally, you run:

npm run dev

This starts Vite, a development server.

Vite does amazing things:

  • Hot Module Replacement (saves instantly update your browser)
  • File watching (detects changes)
  • Source maps (easier debugging)

Perfect for development.

Terrible for production.

Should 100,000 users each start a Vite development server?

Of course not.

Understanding the Build Process

When you run:

npm run build

Vite does several things:

JSX Compilation: React code like <h1>Hello</h1> becomes JavaScript that browsers understand.

Bundling: Instead of loading hundreds of files, Vite combines everything into a few optimized bundles.

Minification: Removes whitespace, shrinks variable names, reduces file size.

Tree Shaking: Removes unused code.

Asset Optimization: Optimizes images, CSS, fonts.

The result goes into a dist folder.

Building the Frontend

Navigate to the frontend:

cd ~/ai-interviewer/frontend

Build it:

npm run build

Expected output:

✓ built in 12.3s

A new folder appears: dist

Inside, you'll find:

dist/
├── assets
│   ├── index-ABC123.js
│   └── index-XYZ789.css
└── index.html

This is what we deploy. Not the src folder. Not node_modules. Only dist.

The Memory Problem We Faced

We got this error:

Killed

Nothing else. Just one word.

It confused us for hours.

Then we checked memory:

free -h

Output:

Mem:    908 MB
Swap:   0 B

Our EC2 instance had less than 1 GB RAM.

Building React + Monaco Editor + Rollup optimization = huge memory consumption.

Linux killed the process to prevent a system crash.

The Solution: Swap Memory

We created temporary disk-based memory:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Now:

free -h

Output:

Swap:   2.0 GB

We rebuilt:

npm run build

This time it worked.


Part 9: Deploying React Using Nginx

Where Should We Copy the Build?

Nginx serves files from:

/var/www/html

When someone visits http://YOUR_PUBLIC_IP, Nginx looks here.

We need to copy our production build there:

sudo cp -r ~/ai-interviewer/frontend/dist/* /var/www/html/

Breaking it down:

  • sudo — Need administrator privileges to modify system folders
  • cp — Copy
  • -r — Recursive (copy folders)
  • ~/ai-interviewer/frontend/dist/* — Everything inside dist (on your server)
  • /var/www/html/ — Nginx's website folder

Verifying the Deployment

ls -la /var/www/html

You should see:

assets
index.html
vite.svg

Testing in the Browser

Visit:

http://YOUR_PUBLIC_IP

Your React application should appear.

Congratulations! Your frontend is now live.

The Biggest Mistake We Made

Our React UI loaded perfectly.

But nothing worked.

Opening Developer Tools showed:

WebSocket connection to ws://localhost:3000/socket.io
ERR_CONNECTION_REFUSED

The backend was running. Port 3000 was open. Everything looked healthy.

So why was the browser trying to connect to localhost?

Understanding the localhost Problem

Inside our React code, we had:

new SocketIOProvider("http://localhost:3000", ...)

This worked perfectly during development.

Both frontend and backend ran on our laptop.

But in production:

When a visitor opens your website, their browser runs the JavaScript.

localhost means their own computer.

Unless they also have a backend running on port 3000, the connection fails.

That's exactly what we saw.

The Solution: Use window.location.origin

Instead of hardcoding:

"http://localhost:3000"

We changed it to:

window.location.origin

Now the same code works everywhere:

Locally: window.location.origin = http://localhost:5173

Production: window.location.origin = http://YOUR_PUBLIC_IP

No hardcoding. No environment variables. Just smart code.


Part 10: Configuring Nginx as a Reverse Proxy (Fixing WebSockets)

Everything Looks Perfect...

After deploying React, we thought we were done.

The UI loaded.

The backend was healthy.

But the collaborative editor didn't work.

Chrome Developer Tools showed:

WebSocket connection to ws://YOUR_PUBLIC_IP/socket.io
failed: Unexpected response code: 404

Why 404?

The browser requested:

GET /socket.io

Nginx looked inside /var/www/html for a folder called socket.io.

It didn't exist.

So Nginx returned: 404 Not Found

But the backend was alive and listening on port 3000.

The problem wasn't the backend. The problem was Nginx didn't know how to reach it.

Introducing the Reverse Proxy

A reverse proxy forwards requests to another server.

Browser → Nginx → (Browser doesn't know Node.js exists)
              │
              ↓
            Node.js

We needed to tell Nginx:

"If someone requests /socket.io, forward it to Node.js on port 3000."

Configuring Nginx

Open the Nginx config:

sudo nano /etc/nginx/sites-available/default

Add this section:

location /socket.io/ {
    proxy_pass http://127.0.0.1:3000/socket.io/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400;
}

Let's understand each line:

location /socket.io/

"When a request starts with /socket.io, handle it specially."

proxy_pass http://127.0.0.1:3000/socket.io/;

"Forward this request to Node.js on port 3000."

127.0.0.1 is localhost. It's faster than using the public IP.

proxy_http_version 1.1

WebSockets require HTTP/1.1. Without this, the connection upgrade fails.

proxy_set_header Upgrade $http_upgrade

"Tell Node.js: The browser wants to upgrade this connection to WebSocket."

proxy_set_header Connection "upgrade"

"Explicitly switch protocols."

proxy_set_header Host $host

"Pass the original host information to Node.js."

proxy_read_timeout 86400

Collaborative editors keep connections open for hours. Without this timeout, Nginx closes idle connections after 30 seconds.

Validating the Configuration

Always test before applying:

sudo nginx -t

Expected output:

syntax is ok
test is successful

Applying the Configuration

sudo systemctl reload nginx

Note: We use reload, not restart. A reload re-reads the configuration without dropping existing connections.

Verifying the Proxy Works

Test without opening the browser:

curl "http://localhost/socket.io/?EIO=4&transport=polling"

Before the fix, this returned: 404

After the fix, it returns something like: 0{"sid":"...","upgrades":["websocket"]}

This tiny response proves Nginx is successfully forwarding requests to Socket.IO.

The Final Architecture

After configuring the reverse proxy:

Internet
    │
    ▼
http://YOUR_PUBLIC_IP
    │
    ▼
Nginx (Port 80)
  ├─ Static Files → /var/www/html
  │
  └─ /socket.io → 127.0.0.1:3000
         │
         ▼
   Node.js (PM2 manages it)
         │
         ▼
   Express + Socket.IO

Notice something beautiful:

Users see only one URL. They never know about port 3000, PM2, or Node.js.

Everything works seamlessly behind the scenes.


Part 11: Debugging Every Error (And How We Fixed It)

The Debugging Mindset

Whenever something doesn't work, don't assume your code is broken.

Instead, think in layers:

Browser
    │
Internet
    │
Security Group
    │
Nginx
    │
PM2
    │
Node.js
    │
Application Code

The problem usually exists in only one layer.

Your job is finding which one.

Error 1: Permission denied (publickey)

Message:

Permission denied (publickey)

Causes:

  • Wrong PEM file
  • Wrong username
  • Wrong public IP
  • Port 22 blocked in Security Group

How We Fixed It:

Verify your key exists:

ls ~/Documents/ssh

Check the username (Ubuntu uses ubuntu, Amazon Linux uses ec2-user)

Check the public IP in the AWS console

Try connecting again.

Error 2: No such file or directory (SCP)

Message:

scp: stat local
No such file or directory

Root Cause:

We accidentally ran SCP inside the EC2 terminal.

Our prompt was:

ubuntu@ip-172...

Linux doesn't have /Users/vikasrajput/ because that's a macOS path.

How We Fixed It:

Exit the SSH session:

exit

Now the prompt shows your local machine:

vikasrajput@MacBook %

Run SCP again.

Error 3: vite: not found

Message:

vite: not found

Root Cause:

We re-uploaded the frontend folder. The upload replaced it, and node_modules disappeared.

Vite is inside node_modules.

How We Fixed It:

cd ~/ai-interviewer/frontend
npm install
npm run build

Error 4: Killed (out of memory)

Message:

Killed

No stack trace. No error message. Just one word.

Root Cause:

EC2 instance had less than 1 GB RAM.

Building React = huge memory consumption.

Linux's Out Of Memory (OOM) Killer terminated the process to prevent system crash.

How We Fixed It:

Create swap memory:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Rebuild:

npm run build

Error 5: WebSocket 404

Message:

WebSocket connection to ws://YOUR_PUBLIC_IP/socket.io failed
Unexpected response code: 404

Root Cause:

Nginx didn't know about /socket.io. It searched for a folder in /var/www/html and didn't find one.

How We Fixed It:

Add reverse proxy configuration (as we did in Part 10).

Error 6: localhost doesn't work

Problem:

React code has:

"http://localhost:3000"

Works locally. Fails in production.

Why?

The browser connects to itself, not your server.

How We Fixed It:

Use window.location.origin:

window.location.origin

Same code works everywhere.

Error 7: npm command not found

Message:

sudo: npm: command not found

But npm -v works fine.

Root Cause:

Node.js was installed using NVM (Node Version Manager).

NVM only installs for your user.

When we used sudo, Linux switched to root user.

Root doesn't know where npm is.

How We Fixed It:

Don't use sudo:

npm install
npm run build

No sudo needed.

Error 8: Nginx 404 for React routes

Problem:

You visit http://YOUR_PUBLIC_IP/editor/1.

Nginx returns 404.

Why?

Nginx looks for a file called editor/1 in /var/www/html.

Doesn't find one.

But this is a React route, not a file.

How We Fixed It:

Tell Nginx to redirect all unmatched routes to index.html:

location / {
    try_files $uri $uri/ /index.html;
}

React's router handles the rest.


Part 12: Production Best Practices

Now that your application is working, here are critical improvements before using it in production.

1. Close Port 3000

After Nginx forwards requests, users don't need direct access to your backend.

Update your AWS Security Group and remove the inbound rule for port 3000.

Your backend stays healthy, but it's no longer directly exposed to the internet.

2. Use HTTPS (SSL)

HTTP sends data unencrypted.

HTTPS encrypts everything.

Install a free SSL certificate using Let's Encrypt:

sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --nginx -d your-domain.com

Then configure Nginx to redirect HTTP → HTTPS.

3. Use a Domain Name

Instead of:

http://13.235.xxx.xxx

Use:

https://editor.yourdomain.com

This looks professional and makes future server changes easier.

If you change servers, the domain stays the same. Only the DNS changes.

4. Keep Your Server Updated

Regularly update packages:

sudo apt update
sudo apt upgrade

Security updates are just as important as feature updates.

5. Monitor Your Application

Use PM2's monitoring commands:

pm2 list        # See status
pm2 logs        # See recent errors
pm2 monit       # Real-time monitoring

If something breaks, you'll see it immediately.

6. Backup Your Code

Even though your code is on EC2, always keep a backup on your laptop or GitHub.

If the server dies, you lose everything without a backup.

7. Use Environment Variables

Never hardcode sensitive information:

// Bad
const DB_URL = "postgres://user:password@localhost/db";

// Good
const DB_URL = process.env.DATABASE_URL;

Create an .env file on the server (don't commit to Git):

DATABASE_URL=postgres://user:password@localhost/db
API_KEY=your-secret-key

8. Enable PM2 Monitoring

PM2 can send alerts if your application crashes:

pm2 web

This starts PM2's web dashboard on port 9615.


The Final Architecture

After completing this entire guide, your deployment looks like this:

                    Internet
                        │
                        ▼
        https://your-domain.com
                        │
                        ▼
              Nginx (Port 80/443)
          ┌─────────────┴──────────────┐
          │                            │
          ▼                            ▼
    React Production Build        /socket.io
    (/var/www/html)                    │
                                       ▼
                              PM2 Process Manager
                                       │
                                       ▼
                              Node.js (Port 3000)
                                       │
                                       ▼
                         Express + Socket.IO

This is the exact same architecture you'll find in production Node.js applications at startups and enterprises.


Key Takeaways

1. Every tool solves a specific problem:

  • SSH: Remote access
  • SCP: File transfer
  • npm: Dependency management
  • PM2: Process management
  • Nginx: Web server + reverse proxy
  • React build: Optimization

2. Layers matter:

Always debug from the bottom up. Verify each layer is working before blaming the layer above.

3. Understand the why:

Commands are easy to copy. Understanding why they're necessary is what makes you a better developer.

4. Production is different:

Your laptop's development environment is nothing like production. Localhost works locally. Window.location.origin works everywhere.

5. Monitoring is essential:

Production applications need monitoring. PM2 logs help you debug problems before users report them.


What's Next?

Your application is now running in production.

But deployment is just the beginning.

Next, consider:

  • Setting up a domain name (we recommend Route 53)
  • Adding HTTPS with Let's Encrypt
  • Implementing database backups
  • Setting up monitoring and alerting
  • Implementing CI/CD pipelines (for automatic deployments)
  • Scaling to multiple servers

But those topics are for another guide.

For now, you've accomplished something most beginners never do.

You've deployed a real application to production.

You understand how every piece fits together.

You can debug problems confidently.

That's exactly the mindset production engineers have.


Final Thought

When we started this deployment, we thought AWS was the hard part.

It wasn't.

The real challenge was understanding how all the pieces fit together:

  • Linux
  • SSH
  • SCP
  • Node.js
  • npm
  • PM2
  • Nginx
  • React
  • Vite
  • WebSockets
  • Reverse proxies

Each one solves a specific problem.

Once you understand why each tool exists, deployment stops being intimidating.

You stop blindly copying commands from tutorials.

You start understanding deployment itself.

And that's the entire point of this guide.

Happy deploying! 🚀


Written by: EditMyStuff Team

Based on: Real deployment experience with React + Node.js + Socket.IO to AWS EC2

Updated: 2026


Need Help?

Having issues with your deployment? Here's a quick debugging checklist:

  1. Is the backend running? pm2 list
  2. Is the backend listening? sudo ss -tulpn | grep 3000
  3. Is Nginx healthy? sudo nginx -t
  4. Can Nginx reach Node.js? curl http://localhost/socket.io
  5. Is the configuration applied? sudo systemctl status nginx
  6. Check the logs: pm2 logs and sudo tail -f /var/log/nginx/error.log

Fix one layer at a time. You got this! 💪

Edit PDFs & images in your browser — free, private, no upload.

Everything runs in your browser. Nothing leaves your device.

Explore the tools