What you’ll build
- EC2 (Ubuntu) runs your Next.js server (next start) managed by PM2.
- Nginx sits in front as a reverse proxy on :80/:443, terminates TLS, serves gzip/brotli, and forwards to Node on :3000.
- Let’s Encrypt issues a free certificate (auto-renew).
- Zero-downtime deploys with pm2 reload.
Prerequisites
- Domain pointing to your EC2 public IP (e.g., A record for app.example.com).
- An EC2 t2.micro / t3.small+ (Arm or x86 is fine).
- Security Group: allow 22, 80, 443 (lock 22 to your IP if possible).
- Your app builds locally with Node 18+ (Next.js requires modern Node).
1) Launch & harden the instance
SSH in:
ssh -i ~/.ssh/your-key.pem ubuntu@your-ec2-public-ip
Update & base tools:
sudo apt update && sudo apt -y upgrade
sudo apt -y install curl git ufw
Firewall (optional but recommended):
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
2) Install Node.js (nvm) & PM2
# nvm
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.nvm/nvm.sh
nvm install --lts
node -v && npm -v
# PM2 (global)
npm i -g pm2
pm2 -v
If you use pnpm or yarn, also install Corepack:
3) Fetch your Next.js app & set env
Put your code in /var/www/app (or /home/ubuntu/app if you prefer):
sudo mkdir -p /var/www/app
sudo chown -R ubuntu:ubuntu /var/www
cd /var/www/app
git clone https://github.com/your-org/your-nextjs-repo.git .
# or scp your build artifacts
Environment variables:
cp .env.example .env # or create .env.production
nano .env # set NEXT_PUBLIC_* and server secrets
Install deps & build:
npm ci # or npm i / pnpm i / yarn
npm run build
Sanity test (temporarily):
npm run start -- -p 3000
# open http://YOUR_EC2_IP:3000 (then Ctrl+C)
4) Run with PM2 (and auto-restart)
Option A: simple start
pm2 start npm --name "next-app" -- start -- -p 3000
Option B: ecosystem file (recommended)
Create /var/www/app/ecosystem.config.js:
module.exports = {
apps: [
{
name: "next-app",
cwd: "/var/www/app",
script: "npm",
args: "start -- -p 3000",
env: {
NODE_ENV: "production",
PORT: "3000"
},
instances: 1, // set to "max" for cluster if purely stateless
exec_mode: "fork", // "cluster" is OK for Next.js pages/API; avoid for dev SSR with websockets unless tested
autorestart: true,
watch: false,
max_memory_restart: "512M",
}
]
}
Start & save:
pm2 start ecosystem.config.js
pm2 status
pm2 save
pm2 startup systemd -u ubuntu --hp /home/ubuntu
# follow the printed command, then:
sudo systemctl status pm2-ubuntu
Logs:
Optional log rotation:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 14
5) Install & configure Nginx
sudo apt -y install nginx
sudo systemctl enable --now nginx
Create a site file at /etc/nginx/sites-available/next-app:
server {
listen 80;
listen [::]:80;
server_name app.example.com;
# Optional: serve a quick health endpoint
location /healthz { return 200 "ok\n"; add_header Content-Type text/plain; }
location / {
proxy_pass http://127.0.0.1:3000;
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-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 60s;
}
# Static compression hints (Next already serves optimized assets)
gzip on;
gzip_types text/plain text/css application/json application/javascript application/xml+rss image/svg+xml;
}
Enable and test:
sudo ln -s /etc/nginx/sites-available/next-app /etc/nginx/sites-enabled/next-app
sudo nginx -t
sudo systemctl reload nginx
Point your DNS (app.example.com) to the EC2 public IP and verify http://app.example.com routes to your app via Nginx.
6) Add HTTPS (Let’s Encrypt)
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com --redirect -m you@domain.com --agree-tos -n
sudo systemctl status certbot.timer # auto-renew
Nginx will be updated to HTTP/2 + 301 redirect to https://.
7) Zero-downtime deploys
Create a small deploy script /var/www/app/deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR=/var/www/app
cd $APP_DIR
git fetch --all
git reset --hard origin/main
npm ci
npm run build
# Atomically reload without dropping connections
pm2 reload next-app
Make it executable:
chmod +x /var/www/app/deploy.sh
Deploy with:
If you serve APIs with websockets, validate pm2 reload doesn’t drop sessions; fall back to gracefulReload patterns if needed.
8) Multiple apps or staging
Add another PM2 app on a different port (e.g., 3001) and create a second Nginx server block for staging.example.com that proxies to 127.0.0.1:3001. Certbot can issue a cert for the new host as well.
9) Health checks & monitoring
- Health route (already included): GET /healthz → 200 ok.
- PM2 tools:
pm2 monit
pm2 list
pm2 logs
- Ship logs to CloudWatch/ELK (optional).
- Alarms: CPU > 70%, Memory > 75%, Nginx 5xx rate.
10) Common issues & fixes
502 Bad Gateway (Nginx)
- PM2 app not running or wrong port; check pm2 status, pm2 logs, and proxy_pass target.
Build fails on server
- Node version mismatch. Ensure nvm use --lts and match local Node.
- Missing system libs for sharp/next-image: sudo apt -y install build-essential python3 make g++ then rebuild.
TLS works but mixed content
- Use absolute HTTPS URLs for any external assets; set X-Forwarded-Proto (we already do).
Huge images slow pages
- Use <Image /> with next/image and set images.domains in next.config.js.
11) Hardening & operations
- Disable password SSH; use keys only (/etc/ssh/sshd_config).
- Regular security updates: unattended-upgrades.
- Backups: AMIs + S3 backups for .env (encrypted) and any persistent uploads (ideally S3 only).
- If you must allow uploads: never store user files on the EC2 root; always S3 + CloudFront.
12) Scaling beyond one EC2
- Vertical: bump to t3.medium/t3.large.
- Horizontal: bake an AMI, put two+ instances in an Auto Scaling Group behind an Application Load Balancer (ALB). Terminate TLS at ALB; keep Nginx if you still want per-node caching/routing, or remove Nginx and point ALB → Node directly.
- Global: front with CloudFront (static caching) and regional ALBs for dynamic content.
Quick TL;DR (commands)
# On fresh Ubuntu EC2
sudo apt update && sudo apt -y upgrade
sudo apt -y install curl git nginx ufw
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.nvm/nvm.sh && nvm install --lts
npm i -g pm2
sudo mkdir -p /var/www/app && sudo chown -R ubuntu:ubuntu /var/www
cd /var/www/app && git clone https://github.com/your/repo.git . && npm ci && npm run build
pm2 start npm --name "next-app" -- start -- -p 3000
pm2 save && pm2 startup systemd -u ubuntu --hp /home/ubuntu
# Nginx reverse proxy & HTTPS
# (create site file as shown), then:
sudo nginx -t && sudo systemctl reload nginx
sudo apt -y install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com --redirect -m you@domain.com --agree-tos -n