How to Deploy a Node.js App on AWS EC2 (Production-Ready Guide with Nginx, PM2 & CI/CD)
Learn how to deploy a Node.js app on AWS EC2 with a real-world production setup using Nginx, PM2, SSL, and CI/CD. Step-by-step DevOps guide.
- Published on
- /0/0/29 mins read/297 views
On this page
- Introduction
- Why deploy nodejs apps on aws ec2 in 2025
- Ec2 vs one-click platforms the honest take
- Why ec2 works especially well for nodejs
- Who should and shouldnt use ec2 for nodejs
- The goal of this deployment guide
- Production architecture overview how the deployment actually works
- The high-level request flow
- Why nodejs should never face the internet directly
- The role of pm2 in production stability
- Where cicd fits into this architecture
- Aws ec2s responsibility in the stack
- What were not adding on purpose
- Final mental model remember this
- Prerequisites amp assumptions
- What you need before deploying
- Assumptions this guide makes
- Things you do not need good news
- A note on cost and safety
- What were about to build
- Step 1 launching an aws ec2 instance ubuntu setup done right
- Choosing the right aws region
- Selecting the operating system ami
- Picking the instance type
- Creating or selecting a key pair
- Configuring the security group this matters a lot
- Storage configuration
- Launching the instance
- Quick sanity checklist
- Step 2 connecting to your ec2 instance via ssh without breaking anything
- Step 1 prepare your key file mandatory on macoslinux
- Step 2 connect to the ec2 instance
- Common ssh errors and what they actually mean
- Step 3 update the server packages
- Step 4 set the server timezone optional but recommended
- Step 5 create a clean workspace
- Pro tip from real deployments
- Step 3 installing nodejs amp npm using nvm production-safe method
- Why nvm is the smart choice
- Step 1 install nvm
- Step 2 install the recommended nodejs version
- Step 3 avoid global permission issues important
- Step 4 install essential global packages
- Step 5 verify node survives ssh reconnects
- Real-world tip
- Step 4 installing git amp cloning the nodejs application real repo flow
- Step 1 install git on the ec2 instance
- Step 2 choose the sample repository deployment-ready
- Step 3 clone the repository
- Step 4 install project dependencies
- Step 5 configure environment variables
- Step 6 test the app manually one time only
- Pro tip from production deployments
- Step 5 running the nodejs app with pm2 process management done right
- Why pm2 is non-negotiable in production
- Step 1 start the app using pm2
- Step 2 view logs when things go wrong
- Step 3 enable pm2 startup on server reboot
- Step 4 reload without downtime
- Step 5 lock pm2 to the correct node version
- Quick sanity test
- Step 6 setting up nginx as a reverse proxy for nodejs
- Why you need nginx even for small apps
- Step 1 install nginx
- Step 2 remove the default nginx config
- Step 3 create a new server block
- Step 4 enable the site
- Step 5 update security group if needed
- Step 6 test the setup
- Pro-level nginx tip
- Step 7 enabling https with free ssl lets encrypt nginx
- Prerequisites before you start
- Step 1 install certbot
- Step 2 update nginx server name
- Step 3 generate the ssl certificate
- Step 4 verify https
- Step 5 auto-renew ssl critical
- Optional add basic security headers recommended
- Step 8 setting up cicd with github actions for zero-downtime deployments
- What this cicd pipeline will do
- Step 1 prepare the server for cicd access
- Step 2 store secrets in github
- Step 3 create github actions workflow
- Step 4 first automated deployment
- Step 5 real-world hardening tips
- Step 9 production best practices monitoring amp common pitfalls from real deployments
- 1 lock down your environment variables
- 2 enable basic monitoring without overengineering
- 3 set up log rotation avoid disk full crashes
- 4 add a health check endpoint
- 5 optimize nodejs for production
- 6 common mistakes that break production apps
- 7 when this setup is enough and when it isnt
- Final takeaway
Introduction#
Deploying a Node.js application is easy deploying it the right way on AWS EC2 is where most developers struggle.
I have seen this pattern repeatedly:the app runs fine locally, works on npm start, even responds on port 3000… but the moment it hits production, things fall apart. SSH access issues, port exposure confusion, processes dying after logout, Nginx returning 502 Bad Gateway, or worse manual redeployments at 2 AM.
This guide exists to eliminate that chaos.
In this article, I’ll walk you through how I deploy Node.js applications on AWS EC2 in real production environments not a toy setup, not a one-command demo, but a battle-tested workflow that scales from personal projects to client-grade deployments.
You’ll learn:
- Why EC2 is still a solid choice for Node.js workloads in 2025
- How to set up a clean Linux server the DevOps way
- How to run Node.js reliably using PM2
- How to expose your app safely using Nginx as a reverse proxy
- And how to automate deployments using GitHub Actions (CI/CD)
This is not a copy-paste tutorial.Every step is explained with why it exists, what usually breaks, and how to avoid common production mistakes I’ve personally debugged on AWS.
If you’re an intermediate developer trying to move beyond “it works on my machine”, this guide is for you.If you’re a beginner, you’ll still be able to follow along and you’ll learn production concepts most tutorials skip.
By the end, you’ll have:
- A live Node.js app running on AWS EC2
- A production-ready setup with PM2 + Nginx
- A repeatable deployment process you can reuse for future projects
Let’s start by understanding why AWS EC2 is still one of the most practical deployment choices for Node.js applications today.
Why Deploy Node.js Apps on AWS EC2 in 2025#
With so many modern platforms promising “one-click deployments,” it’s fair to ask: why would anyone still deploy a Node.js app on AWS EC2 in 2025?
Short answer: control, predictability, and production flexibility.
Long answer and the one that actually matters in real projects is this: EC2 gives you full ownership of your runtime environment. You decide the Node.js version, the process manager, the networking layer, and how traffic flows through your application. That level of control is hard to match with fully managed platforms.
In real-world Node.js deployments, the issues rarely come from writing code. They come from how that code runs over time memory leaks, process crashes, cold starts, port exposure mistakes, and deployment downtime. EC2 lets you design around these problems instead of working around platform limitations.
EC2 vs “One-Click” Platforms (The Honest Take)#
Platforms like Vercel, Railway, or Render are fantastic for quick demos, side projects, and frontend-heavy apps. But once you move into backend-heavy workloads, things change.
Here’s where AWS EC2 still wins for Node.js deployment:
- No runtime restrictions – You’re not locked into a specific Node version or execution model
- Stable long-running processes – Ideal for APIs, workers, cron jobs, and WebSockets
- Predictable costs – A fixed EC2 instance is often cheaper than usage-based pricing at scale
- Production-grade networking – Nginx, SSL, custom ports, internal services all under your control
If you’ve ever hit memory limits, timeout caps, or unexpected pricing spikes on managed platforms, EC2 starts to make a lot more sense.
Why EC2 Works Especially Well for Node.js#
Node.js is designed to run as a long-lived process. It shines when paired with:
- A Linux server
- A process manager (like PM2)
- A reverse proxy (like Nginx)
AWS EC2 fits this model perfectly.
Instead of treating your app like a disposable function, EC2 treats it like a service something that stays alive, restarts cleanly, and handles real traffic consistently. This is especially important for:
- REST and GraphQL APIs
- Authentication services
- Background workers
- SaaS backends
- Internal admin panels
When paired with PM2, your Node.js app can auto-restart on crashes and survive server reboots. With Nginx in front, you get proper traffic handling, security, and scalability without overengineering.
Who Should (and Shouldn’t) Use EC2 for Node.js#
Let’s be clear EC2 is not the right choice for everyone.
EC2 is a great fit if you:
- Want to understand how production deployments actually work
- Need full control over your backend infrastructure
- Are running a long-lived Node.js service
- Plan to add CI/CD, monitoring, or custom networking
EC2 might not be ideal if you:
- Just need a quick demo or prototype
- Don’t want to manage servers at all
- Are building purely static or frontend-only apps
This guide assumes you’re aiming for production readiness, not shortcuts.
The Goal of This Deployment Guide#
The goal here isn’t just to “get Node.js running on AWS EC2.”
The goal is to help you build a setup that:
- Doesn’t crash when you close your SSH session
- Survives restarts and deployments
- Handles traffic through a proper reverse proxy
- Can be automated with CI/CD
- Scales with you as the project grows
In the next section, we’ll break down the exact production architecture we’re building so you understand how Nginx, PM2, Node.js, and AWS EC2 fit together before touching a single command.
Production Architecture Overview: How the Deployment Actually Works#
Before we touch AWS, SSH keys, or terminal commands, it’s important to understand what we’re building and why.
Most deployment issues don’t happen because someone typed the wrong command they happen because the architecture itself isn’t clear. When you understand the flow, debugging becomes obvious instead of painful.
So let’s zoom out for a second.
The High-Level Request Flow#
In this setup, a request doesn’t go straight to your Node.js app.
It flows like this:
User’s Browser → Nginx → Node.js App (PM2) → Response
Each layer has a clear responsibility:
- Nginx handles incoming HTTP/HTTPS traffic
- Node.js runs your application logic
- PM2 keeps the Node.js process alive and stable
- AWS EC2 provides the Linux environment where everything lives
This separation is intentional and it’s what makes the setup production-ready.
Why Node.js Should Never Face the Internet Directly#
A common beginner mistake is exposing a Node.js app directly on port 3000 and calling it “deployed.”
Technically, it works. Practically, it’s fragile.
Node.js is great at handling application logic, but it’s not optimized for:
- TLS termination
- Handling malformed requests
- Managing connection spikes
- Serving as a public-facing web server
That’s where Nginx comes in.
Nginx sits in front of your Node.js app and acts as a gatekeeper. It listens on standard ports like 80 and 443, handles SSL, and forwards clean requests to your Node process running safely behind the scenes.
This is why most serious Node.js production deployments look the same regardless of company size.
The Role of PM2 in Production Stability#
Here’s another thing most tutorials skip:Node.js does not keep itself alive.
If your process crashes, runs out of memory, or you log out of SSH, the app dies unless something is managing it.
That “something” is PM2.
PM2 acts like a supervisor for your Node.js app:
- Restarts it if it crashes
- Keeps it running after SSH logout
- Restarts it automatically after a server reboot
- Gives you logs, metrics, and process status
In real production environments, PM2 is often the difference between a minor hiccup and full downtime.
Where CI/CD Fits into This Architecture#
Manual deployments don’t scale not for teams, and not even for solo developers long-term.
In this guide, CI/CD (using GitHub Actions) fits on top of the architecture, not inside it.
The flow looks like this:
Git Push → GitHub Actions → EC2 Server → PM2 Restart
What CI/CD does:
- Pulls the latest code automatically
- Installs dependencies
- Restarts the Node.js app safely via PM2
- Eliminates manual SSH-based deployments
This means your production server stays consistent, and deployments become repeatable instead of risky.
AWS EC2’s Responsibility in the Stack#
AWS EC2 doesn’t manage your app it hosts your environment.
Think of EC2 as:
- A clean Linux machine in the cloud
- With configurable networking (security groups)
- Persistent storage
- Full OS-level access
This is powerful, but it also means you are responsible for what runs on it. That’s exactly why EC2 is such a good learning and production platform nothing is hidden from you.
What We’re NOT Adding (On Purpose)#
To keep this guide practical and focused, we’re intentionally not adding:
- Docker
- Kubernetes
- Load balancers
- Auto-scaling groups
Not because they’re bad but because they’re not required to deploy a solid Node.js production app.
Once you understand this baseline architecture, adding those layers later becomes much easier.
Final Mental Model (Remember This)#
If you remember one thing from this section, make it this:
Nginx handles traffic.PM2 handles processes.Node.js handles business logic.EC2 hosts everything.
With this mental model in place, the rest of the guide becomes straightforward instead of overwhelming.
In the next section, we’ll quickly align on prerequisites and assumptions so everyone starts from the same baseline before launching the EC2 instance.
Prerequisites & Assumptions#
Before we start provisioning servers and writing commands, let’s make sure we’re aligned on a few basics.
This guide is written to be practical, not theoretical. You don’t need to be a DevOps engineer, but you do need a working baseline. If any of the items below are missing, fix them first it’ll save you hours later.
What You Need Before Deploying#
You’ll need the following in place:
An AWS accountA standard AWS account is enough. Free Tier works for learning and small projects.
A Node.js applicationAny Node.js app will work Express, Fastify, NestJS, etc.The app should:
- Start with npm start or node index.js
- Listen on a known port (for example, 3000)
- Be production-safe (no hardcoded secrets)
A GitHub repositoryYour application code should live in a Git repository.This is required for cloning the app on EC2 and for CI/CD later.
Basic terminal familiarityYou should be comfortable with:
- Running Linux commands
- Editing files using nano or vim
- Understanding basic error output
No advanced Linux knowledge is required we’ll cover everything that matters.
Assumptions This Guide Makes#
To keep the guide focused and avoid unnecessary branches, we’ll make a few assumptions:
- You are deploying a backend Node.js application, not just static files
- You’re okay using Ubuntu Linux on AWS EC2
- You want a non-Docker, server-based deployment
- You plan to expose the app using Nginx on ports 80 and 443
- You want a setup that works today and scales later
If you’re looking for serverless, container-based, or fully managed deployments, this guide will still be useful conceptually but the commands will differ.
Things You Do Not Need (Good News)#
Let’s clear up some common misconceptions:
- ❌ You do not need Docker
- ❌ You do not need Kubernetes
- ❌ You do not need deep AWS knowledge
- ❌ You do not need to memorize networking theory
Everything we do here is intentional and minimal.
A Note on Cost and Safety#
AWS EC2 is powerful, but it’s also easy to forget a running instance.
Before you proceed:
- Make sure you understand which instance type you’re launching
- Use Free Tier–eligible instances if you’re learning
- Stop or terminate instances you’re not using
A clean deployment is useless if it quietly burns money in the background.
What We’re About to Build#
By the end of this guide, you’ll have:
- A running EC2 instance with Ubuntu
- Node.js installed via NVM
- Your Node.js app running under PM2
- Nginx acting as a reverse proxy
- An optional CI/CD pipeline for automated deployments
If that sounds good, you’re ready to move forward.
In the next section, we’ll launch the EC2 instance and configure it correctly including instance type, region, and security groups so we don’t fix infrastructure mistakes later.
Step 1: Launching an AWS EC2 Instance (Ubuntu Setup Done Right)#
Everything that comes after this step depends on one thing: a correctly configured EC2 instance.
Most Node.js deployment issues don’t come from Node or Nginx they come from poor EC2 setup decisions made in the first five minutes. So let’s do this cleanly.
Choosing the Right AWS Region#
Pick a region closest to your users. Lower distance = lower latency.
For most use cases:
- Asia → ap-south-1 (Mumbai)
- Europe → eu-west-1 (Ireland)
- US → us-east-1 (N. Virginia)
There’s no “best” region globally proximity always wins.
Selecting the Operating System (AMI)#
Choose:
Ubuntu Server 22.04 LTS (64-bit)
Why Ubuntu?
- Massive community support
- Clean package management
- Excellent Node.js compatibility
- Most production Node guides assume Ubuntu
Avoid older AMIs or experimental images unless you have a specific reason.
Picking the Instance Type#
For most Node.js apps:
- t2.micro → learning, side projects (Free Tier)
- t3.small → APIs, admin panels, early SaaS
- t3.medium → heavier workloads
Node.js doesn’t need huge CPU, but it does need stable memory. If your app randomly crashes under load, memory is usually the culprit.
You can always resize later don’t overthink this.
Creating or Selecting a Key Pair#
This key pair is your only SSH access to the server.
Best practices:
- Create a new key pair for this project
- Download the .pem file immediately
- Store it securely (password manager or encrypted folder)
If you lose this key, you lose access. Period.
Configuring the Security Group (This Matters a Lot)#
Security groups act as your server’s firewall.
You need only three inbound rules:
| Type | Port | Source |
|---|---|---|
| SSH | 22 | Your IP only |
| HTTP | 80 | Anywhere |
| HTTPS | 443 | Anywhere |
That’s it.
Do not expose:
- Port 3000
- Database ports
- Random ranges
Your Node.js app should never be publicly accessible without Nginx in front of it.
Storage Configuration#
The default storage (8–10 GB) is usually enough for Node.js apps.
Increase it only if:
- You log heavily
- You store uploads locally (not recommended)
- You run build pipelines on the server
Otherwise, keep it simple.
Launching the Instance#
Once everything is reviewed:
- Launch the instance
- Wait for it to reach Running state
- Copy the Public IPv4 address
This IP is temporary unless you attach an Elastic IP (optional). For now, it’s fine.
Quick Sanity Checklist#
Before moving on, confirm:
- EC2 instance is running
- You have the .pem key file
- Port 22 is open only to your IP
- Ports 80 and 443 are open publicly
If all of that checks out, you’ve done the hardest infrastructure part correctly.
In the next section, we’ll connect to the EC2 instance via SSH, fix key permissions, and prepare the server for Node.js installation.
Step 2: Connecting to Your EC2 Instance via SSH (Without Breaking Anything)#
You’ve launched the EC2 instance. Now it’s time to get inside the server.
SSH is how you control the machine install software, run Node.js, configure Nginx, everything. One small mistake here (mostly permissions) can block you for hours, so let’s do it properly.
Step 1: Prepare Your Key File (Mandatory on macOS/Linux)#
If you’re on macOS or Linux, your .pem file must have strict permissions.
Run this once:
1
chmod 400 your-key-name.pem
Why this matters:
- SSH will refuse to connect if the key is too open
- This is a security feature, not an error
Windows users (PowerShell / Git Bash) usually don’t need this step.
Step 2: Connect to the EC2 Instance#
Use the public IPv4 address of your instance.
1
ssh -i your-key-name.pem ubuntu@YOUR_PUBLIC_IP
Important details:
- Username for Ubuntu AMI is always ubuntu
- Do not use root
- Replace YOUR_PUBLIC_IP with the actual IP
On first connect, you’ll see a fingerprint warning. Type yes and continue.
If successful, your terminal will change to something like:
1
ubuntu@ip-172-31-xx-xx:~$
Congrats you’re inside the server.
Common SSH Errors (And What They Actually Mean)#
Permission denied (publickey)→ Wrong key file, wrong permissions, or wrong username.
Connection timed out→ Port 22 blocked in security group or wrong IP.
No route to host→ Instance isn’t running or public IP changed.
Fix the root cause don’t retry blindly.
Step 3: Update the Server Packages#
Before installing anything, update the system:
1
sudo apt update && sudo apt upgrade -y
This prevents:
- Broken dependencies
- Outdated security libraries
- Weird install errors later
It’s boring, but it saves hours.
Step 4: Set the Server Timezone (Optional but Recommended)#
Logs and cron jobs depend on time.
1
timedatectl
If needed:
1
sudo timedatectl set-timezone Asia/Kolkata
Pick the timezone that matches your operations or users.
Step 5: Create a Clean Workspace#
Keep your app organized from day one.
1
mkdir apps cd apps
We’ll deploy everything inside this directory.
Pro Tip from Real Deployments#
Do not:
- Run your app as root
- Store secrets in .env files without permission control
- Expose Node.js ports directly to the internet
We’ll fix all of this properly with PM2 + Nginx in upcoming sections.
Your server is now:
- Securely accessible
- Updated
- Ready for Node.js
Next up, we’ll install Node.js the production-safe way using NVM, so version upgrades never break your app.
Step 3: Installing Node.js & npm Using NVM (Production-Safe Method)#
One of the biggest mistakes I see in real deployments is installing Node.js directly via apt.
It works… until:
- You need to upgrade Node
- A dependency requires a different version
- Another app needs a different runtime
That’s why NVM (Node Version Manager) is non-negotiable in production.
Why NVM Is the Smart Choice#
From first-hand experience:
- Zero downtime during Node upgrades
- Easy rollback if something breaks
- Multiple Node versions on the same server
- Matches how modern DevOps teams work
If you care about long-term stability, this is the way.
Step 1: Install NVM#
Run this as the ubuntu user (not root):
1
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
After installation, reload your shell:
1
source ~/.bashrc
Verify NVM:
1
nvm --version
If you see a version number, you’re good.
Step 2: Install the Recommended Node.js Version#
For most production Node.js apps today:
- Node 18 LTS → safe, stable, widely supported
- Node 20 LTS → fine if your stack supports it
Install Node 18:
1
nvm install 18 nvm use 18
Set it as default:
1
nvm alias default 18
Confirm everything:
1
node -v npm -v
Step 3: Avoid Global Permission Issues (Important)#
NVM installs Node in user space which means:
- No sudo needed for npm
- No permission errors
- No broken global installs
This alone eliminates 50% of common Node deployment bugs.
Step 4: Install Essential Global Packages#
We’ll need these later:
1
npm install -g pm2
PM2 will:
- Keep your app running forever
- Restart on crashes
- Enable zero-downtime reloads
- Manage logs cleanly
We’ll configure it properly in a dedicated section.
Step 5: Verify Node Survives SSH Reconnects#
Log out:
1
exit
Reconnect via SSH and run:
1
node -v
If Node is still available, NVM is set up correctly.
If not:
- Your shell profile wasn’t sourced correctly
- We’ll fix it before moving forward
Real-World Tip#
Never assume Node versions.
Always lock your app with:
- .nvmrc in the repo
- engines field in package.json
This avoids “works on my machine” disasters during CI/CD.
At this point, your EC2 instance has:
- A clean Ubuntu OS
- Production-ready Node.js
- PM2 installed for process management
Next, we’ll install Git, clone the sample Node.js app, and prepare it for deployment no placeholders, actual flow.
Step 4: Installing Git & Cloning the Node.js Application (Real Repo Flow)#
Every production deployment starts the same way: pulling code from version control.If Git isn’t installed or configured properly, CI/CD and manual deployments both fall apart.
Let’s lock this in.
Step 1: Install Git on the EC2 Instance#
Run:
1
sudo apt install git -y
Verify installation:
1
git --version
If you see a version number, Git is ready.
Step 2: Choose the Sample Repository (Deployment-Ready)#
For this guide, we’ll use a minimal but production-aware Node.js app:
- Express-based
- Environment variable support
- Clean start script
- Easy to replace with your own app later
Example repo structure we’re assuming:
1
2
3
4
5
6
7
8
node-ec2-sample/
├── src/
│ └── index.js
├── package.json
├── package-lock.json
├── .env.example
├── ecosystem.config.js
└── README.md
You can replace this repo with your own app the deployment steps remain the same.
Step 3: Clone the Repository#
Move into your app directory:
1
cd ~/apps
Clone the repo:
1
git clone https://github.com/your-username/node-ec2-sample.git
Enter the project:
1
cd node-ec2-sample
Step 4: Install Project Dependencies#
Now install Node dependencies:
1
npm install
What’s happening here:
- package.json defines the dependency tree
- package-lock.json locks versions for consistency
- npm installs everything locally (no sudo)
If this step fails:
- Check Node version
- Ensure enough disk space
- Look for native module errors
Step 5: Configure Environment Variables#
Never hardcode secrets.
Create your environment file:
1
cp .env.example .env
Edit it:
1
nano .env
Example:
1
PORT=3000 NODE_ENV=production
Save and exit.
Later, we’ll secure this file properly.
Step 6: Test the App Manually (One Time Only)#
Before PM2 or Nginx, make sure the app actually runs.
1
npm run start
You should see something like:
1
Server running on port 3000
Open your browser:
1
http://YOUR_PUBLIC_IP:3000
If the app loads perfect.
Stop the app:
1
CTRL + C
Pro Tip from Production Deployments#
If your app doesn’t run manually, PM2 won’t magically fix it.
Always:
- Test locally on the server
- Fix runtime errors first
- Then introduce process managers and proxies
At this stage, you have:
- Git installed
- Code pulled from GitHub
- Dependencies installed
- App verified on the server
Next, we’ll run the app using PM2, enable auto-restarts, and make sure it survives crashes and reboots.
Step 5: Running the Node.js App with PM2 (Process Management Done Right)#
In production, your app must never depend on an open terminal.Servers reboot. SSH sessions drop. Crashes happen.
PM2 solves all of that.
Why PM2 Is Non-Negotiable in Production#
From real-world ops experience, PM2 gives you:
- Automatic restarts on crashes
- Background execution (no terminal needed)
- Built-in log management
- Zero-downtime reloads
- Startup on server reboot
If you’re running Node in production without a process manager, you’re gambling.
Step 1: Start the App Using PM2#
From your project root:
1
pm2 start src/index.js --name node-ec2-app
Or if your app uses an npm script:
1
pm2 start npm --name node-ec2-app -- run start
Check status:
1
pm2 status
You should see your app marked as online.
Step 2: View Logs (When Things Go Wrong)#
PM2 logs are lifesavers:
1
pm2 logs node-ec2-app
Or for quick debugging:
1
pm2 logs
You’ll instantly see:
- Runtime errors
- Missing env variables
- Port conflicts
Step 3: Enable PM2 Startup on Server Reboot#
This is critical.
Run:
1
pm2 startup
PM2 will output a command. Copy and execute it exactly as shown (usually with sudo).
Then save your current process list:
1
pm2 save
Now your app:
- Starts automatically on reboot
- Restores the exact process state
Step 4: Reload Without Downtime#
When you deploy new code later:
1
pm2 reload node-ec2-app
This avoids:
- Killing active connections
- Dropping requests
- Restart delays
For single-instance apps, this is still cleaner than restart.
Step 5: Lock PM2 to the Correct Node Version#
Because we’re using NVM, PM2 must use the right Node binary.
Confirm with:
1
pm2 show node-ec2-app
If you ever upgrade Node:
- Switch version using NVM
- Restart PM2
- Save again
This prevents weird “Node not found” issues after reboots.
Quick Sanity Test#
Reboot the server:
1
sudo reboot
Reconnect via SSH and run:
1
pm2 status
If your app is online PM2 is set up correctly.
At this point:
- Your Node.js app is running reliably
- It survives crashes and reboots
- Logs are centralized and readable
Next, we’ll put Nginx in front of the app to:
- Remove port numbers
- Add a reverse proxy
- Prepare for HTTPS
- Improve security and performance
Step 6: Setting Up Nginx as a Reverse Proxy for Node.js#
Your Node.js app should never be exposed directly to the internet.
Nginx sits in front of it and handles:
- Incoming traffic on ports 80/443
- Request routing
- Connection buffering
- Basic security hardening
This is standard practice in real-world Node deployments.
Why You Need Nginx (Even for Small Apps)#
From experience:
- Browsers expect port 80/443, not 3000
- Nginx handles traffic spikes better than Node
- You get cleaner URLs (no port numbers)
- SSL becomes trivial later
Think of Node as your engine and Nginx as the body.
Step 1: Install Nginx#
Run:
1
sudo apt install nginx -y
Verify status:
1
sudo systemctl status nginx
Open your browser and visit:
1
http://YOUR_PUBLIC_IP
If you see the Nginx welcome page, installation worked.
Step 2: Remove the Default Nginx Config#
We’ll create a clean config for our app.
1
sudo rm /etc/nginx/sites-enabled/default
Step 3: Create a New Server Block#
Create a config file:
1
sudo nano /etc/nginx/sites-available/node-ec2-app
Paste the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server {
listen 80;
server_name _;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Save and exit.
What this does:
- Accepts traffic on port 80
- Forwards it to your Node app on port 3000
- Supports WebSockets and modern connections
Step 4: Enable the Site#
Create a symbolic link:
1
sudo ln -s /etc/nginx/sites-available/node-ec2-app /etc/nginx/sites-enabled/
Test Nginx config:
1
sudo nginx -t
If you see syntax is ok, reload Nginx:
1
sudo systemctl reload nginx
Step 5: Update Security Group (If Needed)#
Ensure your EC2 security group allows:
- HTTP → Port 80
- HTTPS → Port 443 (we’ll use it next)
Step 6: Test the Setup#
Now open:
1
http://YOUR_PUBLIC_IP
You should see your Node.js app without port 3000.
If it doesn’t load:
- Check pm2 status
- Check pm2 logs
- Check sudo tail -f /var/log/nginx/error.log
Pro-Level Nginx Tip#
Never restart Nginx blindly.
Always:
1
sudo nginx -t sudo systemctl reload nginx
Reload applies changes without dropping connections.
At this stage, your app is:
- Served on standard web ports
- Protected behind Nginx
- Ready for HTTPS and domains
- Much more stable under load
Next, we’ll secure the app with HTTPS using a free SSL certificate and optionally map a custom domain.
Step 7: Enabling HTTPS with Free SSL (Let’s Encrypt + Nginx)#
If your app is live without HTTPS in 2026, users don’t trust it and search engines don’t either.
Good news: SSL is free, automated, and takes under 10 minutes when done right.
Prerequisites Before You Start#
Make sure you have:
- A domain name (e.g. app.yourdomain.com)
- DNS A record pointing to your EC2 public IP
- Nginx running and serving your app on port 80
If DNS isn’t ready, pause here SSL won’t work without it.
Step 1: Install Certbot#
Certbot is the official Let’s Encrypt client.
1
sudo apt install certbot python3-certbot-nginx -y
Step 2: Update Nginx Server Name#
Edit your Nginx config:
1
sudo nano /etc/nginx/sites-available/node-ec2-app
Replace:
1
server_name _;
With:
1
server_name app.yourdomain.com;
Save and test:
1
sudo nginx -t sudo systemctl reload nginx
Step 3: Generate the SSL Certificate#
Run:
1
sudo certbot --nginx -d app.yourdomain.com
Follow the prompts:
- Enter email (for expiry alerts)
- Agree to terms
- Choose redirect → Redirect HTTP to HTTPS
Certbot will:
- Generate certificates
- Update Nginx config
- Enable HTTPS automatically
No manual SSL config needed.
Step 4: Verify HTTPS#
Open your browser:
1
https://app.yourdomain.com
You should see:
- 🔒 Lock icon
- No security warnings
- App loading normally
If not:
- Check DNS propagation
- Check Nginx logs
- Re-run certbot
Step 5: Auto-Renew SSL (Critical)#
Let’s Encrypt certs expire every 90 days.
Enable auto-renew:
1
sudo certbot renew --dry-run
Ubuntu already schedules renewal via cron this just verifies it works.
Optional: Add Basic Security Headers (Recommended)#
Edit your Nginx server block:
1
2
3
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
Reload Nginx:
1
sudo systemctl reload nginx
These headers improve:
- Security posture
- SEO trust signals
- Browser compatibility
At this point, your app is:
- Running behind Nginx
- Secured with HTTPS
- Trusted by browsers
- Ready for real users
Next, we’ll add CI/CD with GitHub Actions, so deployments become one-command, zero-downtime updates.
Step 8: Setting Up CI/CD with GitHub Actions for Zero-Downtime Deployments#
If you’re still SSH-ing into servers and running git pull in 2026, you’re leaving speed and reliability on the table.
CI/CD lets you:
- Deploy on every push
- Eliminate human error
- Roll out changes consistently
- Sleep better during releases
We’ll keep this practical and minimal, not enterprise-overkill.
What This CI/CD Pipeline Will Do#
On every push to main:
- Connect to EC2 securely via SSH
- Pull the latest code
- Install dependencies (if needed)
- Reload the app using PM2 (no downtime)
Step 1: Prepare the Server for CI/CD Access#
On your local machine, generate an SSH key:
1
ssh-keygen -t ed25519 -C "github-actions"
Do not set a passphrase (GitHub Actions can’t prompt).
Copy the public key:
1
cat ~/.ssh/id_ed25519.pub
On the EC2 server:
1
nano ~/.ssh/authorized_keys
Paste the key on a new line and save.
Set correct permissions:
1
chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys
Step 2: Store Secrets in GitHub#
In your GitHub repo:
- Go to Settings → Secrets and variables → Actions
- Add the following secrets:
Secret NameValueEC2_HOSTPublic IP or domainEC2_USERubuntuEC2_KEYPrivate SSH key (id_ed25519)
⚠️ Never commit keys to your repo. Secrets only.
Step 3: Create GitHub Actions Workflow#
Create this file in your repo:
1
.github/workflows/deploy.yml
Add:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: Deploy Node App to EC2
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to EC2
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
script: |
cd ~/apps/node-ec2-sample
git pull origin main
npm install --production
pm2 reload node-ec2-app
Step 4: First Automated Deployment#
Commit and push:
1
git add . git commit -m "setup ci/cd for ec2" git push origin main
Go to:
- GitHub → Actions tab
- Watch the workflow run
If it passes, your app just deployed without SSH.
Step 5: Real-World Hardening Tips#
From real ops experience:
- Use npm ci for strict builds
- Add a health check endpoint
- Add rollback logic for large apps
- Protect main branch with reviews
CI/CD should reduce risk, not add it.
At this point, you have:
- Fully automated deployments
- Zero-downtime reloads
- No manual server access needed
- A workflow you can scale
Next, we’ll wrap up with production best practices, monitoring tips, and common mistakes to avoid the stuff most tutorials skip.
Step 9: Production Best Practices, Monitoring & Common Pitfalls (From Real Deployments)#
At this stage, your Node.js app is live, secure, and automated.Now the focus shifts from deploying to operating.
This section is pure first-hand experience the things you only learn after things break.
1. Lock Down Your Environment Variables#
Your .env file should never be world-readable.
On the server:
1
chmod 600 .env
Only the app user should access secrets.
Also:
- Never log secrets
- Never commit .env
- Rotate keys periodically
Security issues don’t come from hackers they come from carelessness.
2. Enable Basic Monitoring (Without Overengineering)#
You don’t need Datadog on day one.
Start simple:
PM2 monitoring
1
pm2 monit
Track:
- Memory spikes
- CPU usage
- Restart loops
System health
1
htop df -h free -m
If memory constantly hits 90%, upgrade the instance don’t “optimize” prematurely.
3. Set Up Log Rotation (Avoid Disk Full Crashes)#
PM2 logs can grow fast.
Install log rotation:
1
pm2 install pm2-logrotate
Default config is fine, but you can tune it later.
This alone prevents silent production outages.
4. Add a Health Check Endpoint#
Every serious app should have:
1
GET /health
Return:
1
{ "status": "ok" }
Why this matters:
- CI/CD validation
- Future load balancers
- Monitoring integrations
- Faster incident response
This is not optional at scale.
5. Optimize Node.js for Production#
In package.json:
1
2
3
"scripts": {
"start": "NODE_ENV=production node src/index.js"
}
Also:
- Disable debug logs in prod
- Use compression middleware
- Avoid synchronous I/O
Performance is about consistency, not speed.
6. Common Mistakes That Break Production Apps#
Avoid these like fire:
- Running Node as root
- Exposing port 3000 publicly
- Skipping Nginx
- No PM2 startup config
- Manual deploys on live servers
- Ignoring server updates
Every one of these causes outages guaranteed.
7. When This Setup Is Enough (And When It Isn’t)#
This EC2-based setup is perfect for:
- MVPs
- SaaS v1
- Internal tools
- APIs with moderate traffic
You should consider moving to:
- Load balancers
- Containers
- ECS / EKS
Only when:
- Traffic is unpredictable
- You need horizontal scaling
- Downtime costs money
Don’t over-engineer too early.
Final Takeaway#
You didn’t just deploy a Node.js app.
You built:
- A secure EC2 server
- A production Node runtime
- A reverse-proxied web setup
- HTTPS with auto-renewal
- Zero-downtime CI/CD
- A system that survives reboots and crashes
That’s real DevOps, not copy-paste cloud tutorials.
