Skip to main content

Command Palette

Search for a command to run...

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

Published
5 min read
Beginner’s Guide: Deploying a Full-Stack App on AWS EC2
R

🛠️ 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:

  1. Launch an EC2 instance.

  2. Connect to it securely.

  3. Install Node.js.

  4. Deploy a React + Node.js application.

  5. Keep it running with systemd or PM2.

  6. 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

  1. Log in to your AWS Management Console.

  2. Search for EC2 in the search bar.

  3. Click Launch Instance.

  4. Enter a name for your instance (e.g., myapp-server).

  5. Choose an Amazon Machine Image (AMI) — for simplicity, pick Ubuntu 22.04 LTS.

  6. Create a Key Pair (important for SSH login). Download the .pem file and keep it safe.

  7. Create a Security Group → allow:

    • SSH (port 22)

    • HTTP (port 80)

    • HTTPS (port 443)

    • Custom ports for your app (e.g., 3000 or 4000)

  1. Configure storage (default 8GB is fine for small apps).

  2. 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.