Below is a solid “general-purpose” AWS EC2 setup for a Next.js app in /var/www/myapp, fronted by Nginx, running as a PM2-managed systemd service, with HTTPS (Let’s Encrypt). This is the standard production pattern: Nginx (80/443) → localhost:3000 (Next.js).
Assumptions: Ubuntu 22.04/24.04 on EC2, you have a domain (recommended), and you can SSH in.
- AWS prerequisites (security group + DNS)
Security Group inbound rules
TCP 22 from your IP (SSH)
TCP 80 from 0.0.0.0/0 (HTTP)
TCP 443 from 0.0.0.0/0 (HTTPS)
DNS
Create an A record: yourdomain.com → your EC2 public IPv4
Optional: www.yourdomain.com → same
- Base packages + firewall sudo apt update sudo apt -y upgrade
sudo apt -y install nginx ufw git curl build-essential
sudo ufw allow OpenSSH sudo ufw allow 'Nginx Full' sudo ufw --force enable
sudo systemctl enable --now nginx
Check Nginx:
curl -I http://localhost 2) Create a deploy user (optional but recommended)
If you’re currently using ubuntu user, that’s fine. The important part is: don’t run your app as root.
sudo adduser deploy sudo usermod -aG sudo deploy
Then (optional) let deploy manage /var/www/myapp:
sudo mkdir -p /var/www/myapp sudo chown -R deploy:deploy /var/www/myapp sudo chmod -R 755 /var/www
Log in as deploy:
sudo su - deploy 3) Install Node.js LTS (via NodeSource)
(Use current LTS line; this installs a modern Node suitable for Next.js.)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt -y install nodejs node -v npm -v 4) Put your Next.js app in /var/www/myapp
If you already have it there, skip. Otherwise:
cd /var/www/myapp
either git clone into it OR copy files here
Example:
git clone <your_repo_url> .
Install deps:
cd /var/www/myapp npm ci
Create/adjust production env:
nano /var/www/myapp/.env.production
Build:
npm run build
Quick local test:
npm run start -- -p 3000
CTRL+C
- Install PM2 and run the app
Install PM2 globally:
sudo npm i -g pm2 pm2 -v Option A (simple): run npm start cd /var/www/myapp pm2 start npm --name myapp -- start -- -p 3000 pm2 save pm2 status Option B (recommended): ecosystem file (supports multiple processes cleanly)
Create ecosystem.config.cjs:
cd /var/www/myapp cat > ecosystem.config.cjs <<'EOF' module.exports = { apps: [ { name: "myapp", cwd: "/var/www/myapp", script: "node_modules/next/dist/bin/next", args: "start -p 3000", env: { NODE_ENV: "production" }, // If you use .env.production, Next loads it automatically in production, // but you can also use env_file if you prefer: // env_file: "/var/www/myapp/.env.production", autorestart: true, max_restarts: 10 } ] }; EOF
Start it:
pm2 start ecosystem.config.cjs pm2 save 6) Make PM2 a systemd service (auto-start on reboot)
Run this as the user that runs PM2 (e.g., deploy):
pm2 startup systemd
PM2 will print a command that starts with sudo env PATH=... pm2 startup systemd -u deploy --hp /home/deploy. Copy/paste that exact command.
Then:
pm2 save sudo systemctl status pm2-deploy
Now your app will come back after reboot.
- Configure Nginx reverse proxy (HTTP → localhost:3000)
Create a server block:
sudo nano /etc/nginx/sites-available/myapp
Paste (replace yourdomain.com):
server { listen 80; listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
# Optional: increase body size for uploads
client_max_body_size 25m;
# Basic security headers (safe defaults)
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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;
# WebSockets / upgrade (useful if you add them later)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
proxy_send_timeout 60s;
}
}
Enable it:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t sudo systemctl reload nginx
Test from server:
curl -I http://yourdomain.com 8) Add HTTPS with Let’s Encrypt (Certbot)
Install certbot:
sudo apt -y install certbot python3-certbot-nginx
Issue cert + auto-edit Nginx:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Verify renewal timer:
sudo systemctl status certbot.timer sudo certbot renew --dry-run
At this point you have:
HTTPS on 443
HTTP redirected to HTTPS (if you chose that option in Certbot)
- Operational checks & common admin commands App health pm2 status pm2 logs myapp curl -I http://127.0.0.1:3000 curl -I https://yourdomain.com Nginx logs sudo tail -f /var/log/nginx/access.log sudo tail -f /var/log/nginx/error.log Restart flows pm2 restart myapp sudo systemctl reload nginx After deploying new code
Typical flow:
cd /var/www/myapp git pull npm ci npm run build pm2 restart myapp 10) “General purpose” extras you’ll likely want A) Provide a standard place for shared env/secrets
Keep app env in: /var/www/myapp/.env.production
Set permissions:
sudo chown deploy:deploy /var/www/myapp/.env.production sudo chmod 600 /var/www/myapp/.env.production B) Enable swap on small instances (helps builds)
For t2/t3 small instances, building Next.js can OOM. Example 2GB swap:
sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab free -h C) Multiple processes behind Nginx
Run additional services on other localhost ports (e.g., API on 4000) using PM2.
Add more location blocks or separate subdomains in Nginx.
Minimal “ready-to-deploy” summary
You now have:
Nginx running on 80/443
HTTPS via Certbot
Next.js served by PM2 on localhost:3000
systemd auto-start for PM2 on reboot
