How to Deploy a Node.js App to AWS EC2 (Step-by-Step Guide)
On this page
Deploying a Node.js app to AWS EC2 is one of those tasks that looks intimidating until you've done it once. Then it becomes second nature. The issue is that most guides either oversimplify it (missing production essentials like process management and reverse proxies) or bury you in AWS console screenshots that change every quarter. This guide cuts through the noise and gets you running with a properly configured, production-grade setup in about 30 minutes.
TL;DR: Deploy Node.js apps to AWS EC2 with this production-ready guide. Learn instance setup, PM2, Nginx, SSL, and automated deployments.
Why EC2 for Node.js?
Before we dive in, let's be clear about the trade-offs. EC2 gives you full control and flexibility—perfect for complex applications with specific requirements. It's cheaper than managed platforms like Heroku or Railway when you're scaling beyond a hobby project. The downside? You're responsible for updates, security patches, and monitoring. For most mid-sized applications, this is the right call. If you want fully managed hosting with zero ops work, consider Vercel or Railway instead.
Prerequisites
You'll need:
- An AWS account (free tier works, but you'll likely use paid resources)
- A Node.js application ready to deploy (git repo preferred)
- SSH client on your machine (built-in on Mac/Linux, use Git Bash or WSL on Windows)
- Basic familiarity with the Linux command line
Step 1: Launch an EC2 Instance
Log into your AWS Console and navigate to EC2. Click "Launch Instances."
Configuration settings:
- AMI: Select "Ubuntu Server 24.04 LTS" (stable, well-documented for Node.js)
- Instance type:
t3.microfor testing,t3.smallort3.mediumfor production depending on traffic - Key pair: Create a new one if you don't have one. Download the
.pemfile and store it securely. Never commit this to git. - Network settings: Create a new security group or use existing. You'll configure it in the next step.
- Storage: 20 GB is fine to start; adjust based on your needs.
Click "Launch instance" and wait 30 seconds for it to initialize.
Step 2: Configure Security Groups
In the EC2 dashboard, find your instance and click on its security group. Edit inbound rules to allow:
| Protocol | Port | Source |
|---|---|---|
| SSH | 22 | Your IP (or 0.0.0.0/0 for testing only) |
| HTTP | 80 | 0.0.0.0/0 |
| HTTPS | 443 | 0.0.0.0/0 |
Save the rules. In production, restrict SSH to your specific IP. If you don't know your IP, run:
curl ifconfig.me
Step 3: SSH Into Your Instance
Copy your instance's public IPv4 address from the EC2 dashboard. Then:
chmod 600 /path/to/your/key.pem
ssh -i /path/to/your/key.pem ubuntu@your-instance-ip
You're now inside your EC2 instance. Immediately update the system:
sudo apt update && sudo apt upgrade -y
Step 4: Install Node.js and npm
Using NodeSource's repository ensures you get the latest LTS version:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Verify installation:
node --version
npm --version
You should see Node.js 22.x and npm 10.x or later.
Step 5: Clone Your Application
Create a deployment directory and clone your repo:
mkdir -p /home/ubuntu/apps
cd /home/ubuntu/apps
git clone https://github.com/yourusername/your-app.git
cd your-app
Install dependencies:
npm install --production
The --production flag skips dev dependencies, reducing install time and instance size.
Step 6: Set Up Environment Variables
Create a .env file in your app directory:
sudo nano /home/ubuntu/apps/your-app/.env
Add your environment variables:
NODE_ENV=production
PORT=3000
DATABASE_URL=your-database-url
API_KEY=your-api-key
Save with Ctrl+X, then Y, then Enter. Make sure this file is in your .gitignore so you never commit secrets.
Step 7: Install and Configure PM2
PM2 is a production process manager that keeps your Node.js app running and automatically restarts it if it crashes.
sudo npm install -g pm2
Start your application with PM2:
cd /home/ubuntu/apps/your-app
pm2 start npm --name "my-app" -- start
If you run npm start in your package.json, this works out of the box. If your entry file is different:
pm2 start server.js --name "my-app"
Configure PM2 to start on system reboot:
pm2 startup
pm2 save
Follow the instructions output by pm2 startup—it'll give you a sudo command to run. This ensures your app survives instance restarts.
Verify PM2 is managing your app:
pm2 list
pm2 logs my-app
Step 8: Install and Configure Nginx
Nginx acts as a reverse proxy, routing traffic from port 80 (HTTP) to your Node.js app on port 3000. It also handles SSL termination and static file serving efficiently.
sudo apt install -y nginx
Create an Nginx configuration file:
sudo nano /etc/nginx/sites-available/default
Replace the entire file with this configuration:
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name your-domain.com www.your-domain.com;
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;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Test the configuration:
sudo nginx -t
You should see "syntax is ok" and "test is successful". Reload Nginx:
sudo systemctl restart nginx
Now visit http://your-instance-ip in your browser. You should see your Node.js app running.
Step 9: Set Up HTTPS with Let's Encrypt
Never run a production app without HTTPS. Use Certbot with Let's Encrypt for free SSL certificates:
sudo apt install -y certbot python3-certbot-nginx
Obtain a certificate. Replace your-domain.com with your actual domain (and make sure you've pointed DNS to your instance):
sudo certbot certonly --nginx -d your-domain.com -d www.your-domain.com
Now update your Nginx configuration to use SSL:
sudo nano /etc/nginx/sites-available/default
Replace it with:
server {
listen 80;
listen [::]:80;
server_name your-domain.com www.your-domain.com;
location / {
return 301 https://$server_name$request_uri;
}
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your-domain.com www.your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
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;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Test and reload:
sudo nginx -t
sudo systemctl restart nginx
Certbot automatically renews your certificate 30 days before expiration. Verify renewal will work:
sudo certbot renew --dry-run
Step 10: Set Up Automated Deployments (Optional but Recommended)
Deploying updates manually via SSH is painful. Use a simple git hook workflow instead.
On your EC2 instance, create a bare git repository:
mkdir -p /home/ubuntu/repos/your-app.git
cd /home/ubuntu/repos/your-app.git
git init --bare
Create a post-receive hook:
nano hooks/post-receive
Add this script:
#!/bin/bash
WORK_TREE=/home/ubuntu/apps/your-app
git --work-tree=$WORK_TREE --git-dir=$(pwd) checkout -f
cd $WORK_TREE
npm install --production
pm2 restart my-app
Make it executable:
chmod +x hooks/post-receive
On your local machine, add this remote to your git config:
git remote add production ssh://ubuntu@your-instance-ip/home/ubuntu/repos/your-app.git
Now deployments are as simple as:
git push production main
Your app will automatically pull the latest code, reinstall dependencies, and restart.
Monitoring and Maintenance
Check your app status regularly:
pm2 list
pm2 monit
View logs in real-time:
pm2 logs my-app
Monitor system resources:
top
df -h
Common Pitfalls
App crashes on restart: Ensure your .env file has all required variables and the file permissions allow PM2 to read it.
"Cannot GET /" error: Check that your app is listening on the correct port (3000 by default). Verify with curl http://localhost:3000.
502 Bad Gateway: Nginx can't reach your Node.js app. Check that PM2 is running and Nginx configuration is correct.
Port already in use: If port 3000 is already in use, kill it or change the port in your app and Nginx config.
What's Next?
Once you've got the basics running, consider:
- Setting up CloudWatch monitoring and alarms for CPU/memory
- Using RDS for managed databases instead of self-hosting
- Implementing automated backups and disaster recovery
- Using AWS CodePipeline for CI/CD instead of git hooks
- Scaling horizontally with load balancing across multiple instances
This setup handles most production workloads efficiently. You've got process management, a reverse proxy, SSL, and automated deployments—the core essentials for a reliable Node.js deployment.
Sources
Looking for more? Check out Adaptels.
Related Articles
How to Debug Node.js Memory Leaks (Step-by-Step Guide)
Learn how to detect, diagnose, and fix Node.js memory leaks using heap snapshots, Chrome DevTools, and clinic.js — with real code examples.
How to Set Up GitHub Actions for CI/CD (Beginner-Friendly Guide)
Learn how to set up GitHub Actions for CI/CD pipelines — from your first workflow file to automated deployments with real YAML examples.
Running Local LLMs With Ollama: Developer Setup Guide
Set up Ollama to run local LLMs on your machine. Covers installation, model selection, API usage, and integrating local models into your dev workflow.