Beginner’s Guide: Deploying a Full-Stack App on AWS EC2

🛠️ Building modern web apps with React, Node.js, MongoDB & Express
If you’ve ever built a web application and wanted to make it live on the internet, AWS EC2 (Elastic Compute Cloud) is one of the most popular choices. In this guide, we’ll walk through how to:
Launch an EC2 instance.
Connect to it securely.
Install Node.js.
Deploy a React + Node.js application.
Keep it running with systemd or PM2.
Add a reverse proxy with Caddy (or Nginx) for custom domains.
This tutorial is perfect for beginners—we’ll not just run commands, but also explain why we use them.
1. Launching an EC2 Instance
Log in to your AWS Management Console.
Search for EC2 in the search bar.
Click Launch Instance.

Enter a name for your instance (e.g.,
myapp-server).Choose an Amazon Machine Image (AMI) — for simplicity, pick Ubuntu 22.04 LTS.
Create a Key Pair (important for SSH login). Download the
.pemfile and keep it safe.Create a Security Group → allow:
SSH (port 22)
HTTP (port 80)
HTTPS (port 443)
Custom ports for your app (e.g., 3000 or 4000)

Configure storage (default 8GB is fine for small apps).
Click Launch Instance.
✅ Now your server is running in the cloud.
2. Preparing Your Project to Serve the Frontend
Before deploying to the server, it’s important to set up your backend to serve the React frontend as static files. This way, you don’t need two separate servers — your Node.js backend will serve both the API and the React app.
Step 1: Move Frontend Inside Backend
Move your React project (frontend/) inside the backend folder so the structure looks like this:
app/
└── backend/
├── server.js
├── package.json
└── frontend/
├── src/
├── public/
└── package.json
Step 2: Update package.json in Backend
Add a build script so the backend can install and build the React app before deployment:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js",
"build": "cd frontend && npm install && npm run build"
}
🔹 Now, when you run npm run build inside the backend, it will go into the frontend folder, install dependencies, and build your React app into frontend/dist.
Step 3: Update server.js
Inside your backend server.js, add this code to serve React’s build files:
const express = require("express");
const path = require("path");
const app = express();
// Example backend API route
app.get("/api/hello", (req, res) => {
res.json({ message: "Hello from backend!" });
});
// Serve frontend build as static files
const __dirname = path.resolve();
app.use(express.static("./frontend/dist"));
app.get("", (req, res) => {
res.sendFile(path.resolve(__dirname, "./frontend/dist", "index.html"));
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
🔹 express.static → serves all static files (HTML, CSS, JS) from frontend/dist.
🔹 The app.get("*", …) ensures React’s routing (SPA) works correctly.
✅ Now your backend can serve your frontend automatically — no need for two separate deployments.
3. Connect to Your EC2 Instance
From the EC2 dashboard, select your instance → click Connect → choose SSH client. here you are able to get all the actual command that i give as an example below for ssh.

Open your local terminal and go to the folder where your key file (linktree.pem) is saved:
cd ~/ssh # navigate to folder with key
Make your private key secure (required by SSH):
chmod 400 linktree.pem
🔹 chmod 400 → changes file permissions so that only you can read the key. SSH refuses insecure keys.
Now connect to your server:
ssh -i "linktree.pem" ubuntu@ec2-54-82-2-19.compute-1.amazonaws.com
🔹 ssh -i → tells SSH which identity (private key) to use.
🔹 ubuntu@... → username + server address.
If successful → you’re now inside your cloud server and the terminal looks like

4. Update & Install Node.js
First, update your system:
sudo apt update && sudo apt upgrade -y
🔹 Updates package lists & installs the latest versions of software.
Now install Node.js (v20):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
🔹 curl downloads the setup script.
🔹 sudo apt-get install -y nodejs installs Node.js + npm (package manager).
Check versions:
node -v
npm -v
5. Deploy Your App
Step 1: Copy your project from local to EC2
Option 1: scp (Secure Copy)
scp -i linktree.pem -r app/ ubuntu@ec2-54-82-2-19.compute-1.amazonaws.com:/home/ubuntu/
🔹 -r → recursive (copy whole folder).
🔹 -i → use key file.
🔹 scp always re-copies everything (good for small projects).
Option 2: rsync (recommended)
rsync -avz --exclude 'node_modules' \
-e "ssh -i ./linktree.pem" \
'path_of_your_local_folder' ubuntu@ec2-54-82-2-19.compute-1.amazonaws.com:~/app
🔹 Faster, skips unchanged files, can resume broken uploads.
🔹 Perfect for deployments.
Step 2: Install dependencies
cd ~/app/backend
npm install
Run build command
npm run build
6. Run the App
Temporary run:
npm run dev
But once you close the terminal, the app stops. To keep it running, use systemd or PM2.
6. Keep the App Running
Option A: systemd
Create a service file:
sudo vim /etc/systemd/system/myapp.service
Paste:
[Unit]
Description=Node.js App
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/app/backend
ExecStart=/usr/bin/npm start
Restart=always
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable myapp
sudo systemctl start myapp
sudo systemctl status myapp
Option B: PM2 (easier for beginners)
sudo npm install -g pm2
pm2 start server.js --name myapp
pm2 save
pm2 startup systemd
🔹 PM2 handles logs, auto-restart, clustering.
🔹 pm2 monit → live monitoring.
7. Setup Reverse Proxy with Caddy
Why? Because browsers expect apps on port 80 (HTTP) or 443 (HTTPS), not random ports.
Install Caddy (from official docs):
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo tee /etc/apt/trusted.gpg.d/caddy.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy -y
Check if Caddy runs:
sudo systemctl status caddy
Now configure:
sudo vim /etc/caddy/Caddyfile
For default IP:
:80 {
reverse_proxy localhost:3000
}
For custom domain:
aws-learning.ritikg.space {
reverse_proxy localhost:3000
}
Restart Caddy:
sudo systemctl restart caddy
✅ Now your app is live on your EC2 public IP or domain 🎉
🔑 Final Notes
Use scp for quick uploads, rsync for real deployments.
Always configure security groups to allow app ports.
Use PM2 or systemd to keep apps running.
Use a reverse proxy (Caddy/Nginx) for domains + HTTPS.



