TL;DR
Step-by-step guide to deploying a self-hosted Next.js application on AWS EC2 with standalone output, PM2, nginx, and CloudWatch.
Key facts
- Topology
- Next.js on AWS EC2
TL;DR
This guide walks through deploying a self-hosted Next.js application on AWS EC2 from instance selection through PM2, nginx, CloudWatch integration, and deployment automation — a production-grade setup that gives you full control over your Next.js infrastructure.
Step 1 — EC2 instance selection
For a typical Next.js application:
- Small/medium traffic:
t3.medium(2 vCPU, 4 GB) — burstable, cost-effective - Sustained traffic:
c6g.large(2 vCPU ARM, 4 GB) — better price/performance on Graviton - Heavy SSR workloads:
c6g.xlarge(4 vCPU ARM, 8 GB) — dedicated compute for server-side rendering
Use an Ubuntu 24.04 AMI. EBS volume: 30 GB gp3 minimum (gp3 is cheaper and faster than gp2).
Step 2 — Security groups
Create a security group with these inbound rules:
| Port | Source | Purpose |
|---|---|---|
| 22 | Your IP / VPN CIDR | SSH access |
| 80 | 0.0.0.0/0 | HTTP (redirects to HTTPS) |
| 443 | 0.0.0.0/0 | HTTPS |
aws ec2 create-security-group --group-name nextjs-sg --description "Next.js production"
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
--protocol tcp --port 22 --cidr YOUR_IP/32
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
--protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-name nextjs-sg \
--protocol tcp --port 443 --cidr 0.0.0.0/0
Step 3 — Server setup and Node.js
SSH into the instance and prepare the environment:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git build-essential nginx
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
Step 4 — Deploy with standalone output mode
sudo mkdir -p /var/www/nextapp
sudo chown ubuntu:ubuntu /var/www/nextapp
cd /var/www/nextapp
git clone git@github.com:yourorg/nextapp.git .
npm ci
Ensure your next.config.js has output: 'standalone', then build:
npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static
Step 5 — PM2 ecosystem configuration
Create ecosystem.config.js:
module.exports = {
apps: [{
name: 'nextapp',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '1G',
node_args: '--max-old-space-size=1536',
env: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '0.0.0.0',
},
wait_ready: true,
listen_timeout: 10000,
kill_timeout: 5000,
error_file: '/var/log/pm2/nextapp-error.log',
out_file: '/var/log/pm2/nextapp-out.log',
merge_logs: true,
}],
};
sudo mkdir -p /var/log/pm2
sudo chown ubuntu:ubuntu /var/log/pm2
pm2 start ecosystem.config.js
pm2 startup systemd
pm2 save
Step 6 — nginx with proper proxy headers
Create /etc/nginx/sites-available/nextapp:
upstream nextjs_backend {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name example.com;
gzip on;
gzip_types text/plain application/json application/javascript text/css image/svg+xml;
location /_next/static {
alias /var/www/nextapp/.next/static;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://nextjs_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_read_timeout 120s;
proxy_connect_timeout 10s;
}
}
sudo ln -s /etc/nginx/sites-available/nextapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Step 7 — SSL and firewall
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable
Step 8 — CloudWatch integration
Install the CloudWatch agent for system-level metrics:
wget https://s3.amazonaws.com/amazoncloudwatch-agent/ubuntu/amd64/latest/amazon-cloudwatch-agent.deb
sudo dpkg -i amazon-cloudwatch-agent.deb
Create a config at /opt/aws/amazon-cloudwatch-agent/etc/config.json to track memory, disk, and PM2 log files. Start the agent:
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/config.json
Set up CloudWatch alarms for CPU > 80%, memory > 85%, and disk > 80%.
ALB considerations
For multi-instance deployments, place an Application Load Balancer in front:
- Target group health check:
/api/healthon port 3000 - Sticky sessions: Enable if your app uses server-side sessions
- SSL termination: Let the ALB handle SSL and send HTTP to instances (cheaper, simpler)
Deployment script
#!/bin/bash
set -euo pipefail
APP_DIR="/var/www/nextapp"
cd "$APP_DIR"
git fetch origin main
git reset --hard origin/main
npm ci
npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static
pm2 reload ecosystem.config.js
sleep 5
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/api/health)
if [ "$HTTP_CODE" != "200" ]; then
echo "Health check failed — investigate immediately"
exit 1
fi
echo "Deployment successful"
With Reflex
Reflex provides unified monitoring and deployment orchestration for Next.js on EC2. It tracks PM2 process health, nginx upstream errors, memory trends, and disk usage across your instances. During deployments, Reflex orchestrates rolling updates with health-gated rollouts, and can automatically roll back if the health check fails — replacing manual deployment scripts with audited, repeatable automation. See How it works.