The first time I deployed a Next.js app I made the mistake of treating it like a plain React build — ran npm run build, uploaded the out folder, pointed Nginx at it. Worked fine until I needed SSR or API routes. Then nothing worked.
Next.js is more flexible than most frameworks, which also means there's more to understand about deployment. If you're using server-side rendering or API routes, you need a running Node.js process. If your app is purely static, you can export it and serve plain files. Both approaches are valid — you just need to know which one applies to you.
This guide covers both deployment modes: running Next.js as a full Node.js server, and exporting it as static files. I'll explain which approach fits which use case.
I run this on Tencent Cloud Lighthouse. Next.js with SSR works well on the 2 vCPU / 4 GB RAM plan. For static export, the Starter plan is sufficient. A practical advantage for Next.js deployments specifically: Lighthouse's snapshot feature makes rolling back a deployment simple — take a snapshot before each major update, and if the new version has a problem you can restore the previous state in minutes. The OrcaTerm browser terminal also means you can check logs and restart the PM2 process from any device without needing your development machine.
Key Takeaways
- SSR mode needs PM2 + Nginx; static export just needs Nginx serving files
- Run
next buildbefore every deployment andnext startto serve it- PM2's
--watchflag breaks production — use CI/CD for updates instead- For image optimization in production, configure the
images.domainsinnext.config.js- WebSocket connections (for Next.js live features) require Nginx WebSocket proxy headers
| Node.js Server | Static Export | |
|---|---|---|
| SSR / ISR | ✅ Yes | ❌ No |
| API routes | ✅ Yes | ❌ No |
| Image optimization | ✅ Built-in | ⚠️ Requires workaround |
| Server requirements | Node.js process running | Just Nginx serving files |
| Use when | Full-stack app, dynamic data | Content-only site, no API routes |
Choose the Node.js server approach if you use:
getServerSidePropsnext/imageChoose static export if your app is:
| Requirement | Notes |
|---|---|
| Cloud server | Tencent Cloud Lighthouse Ubuntu 22.04 |
| Node.js 18+ | We'll install via NVM |
| A Next.js project | Next.js 13+ (App Router or Pages Router both work) |
ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y
sudo apt install -y git nginx
sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
nvm install 20
nvm use 20
nvm alias default 20
node --version # v20.x.x
npm --version
Install PM2 (process manager):
npm install -g pm2
This is the approach for apps with SSR, API routes, or dynamic features.
mkdir -p ~/apps && cd ~/apps
git clone https://github.com/your-username/your-nextjs-app.git
cd your-nextjs-app
npm install
npm run build
Test it starts:
npm start
# Next.js starts on port 3000
# Visit http://YOUR_SERVER_IP:3000 to verify
# Ctrl+C to stop
Start with PM2:
pm2 start npm --name "nextjs-app" -- start
pm2 startup
# Run the command PM2 outputs
pm2 save
Check it's running:
pm2 status
pm2 logs nextjs-app
For pure static sites, Next.js can export to HTML/CSS/JS files.
Update next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true, // Recommended for static hosts
images: {
unoptimized: true, // Required for static export
},
}
module.exports = nextConfig
Build locally and upload:
# On your local machine
npm run build
# Build creates an /out directory
ls out/
# Upload to server
scp -r out/* ubuntu@YOUR_SERVER_IP:/var/www/nextjsapp/
Or build on the server:
# On the server
cd ~/apps/your-nextjs-app
npm install && npm run build
sudo cp -r out/* /var/www/nextjsapp/
sudo nano /etc/nginx/sites-available/nextjsapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
# Proxy all requests to Next.js
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-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
sudo mkdir -p /var/www/nextjsapp
sudo nano /etc/nginx/sites-available/nextjsapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/nextjsapp;
index index.html;
# Cache static assets (Next.js hashes filenames)
location /_next/static/ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# All routes fall back to index.html
location / {
try_files $uri $uri.html $uri/ /index.html;
}
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_vary on;
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/nextjsapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Create .github/workflows/deploy.yml:
name: Deploy Next.js
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SERVER_IP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd ~/apps/your-nextjs-app
git pull origin main
npm install
npm run build
pm2 reload nextjs-app --update-env
name: Deploy Next.js Static
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Build
run: npm ci && npm run build
- name: Upload to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SERVER_IP }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "out/"
target: "/var/www/nextjsapp"
strip_components: 1
Next.js's <Image> component uses a server-side image optimization pipeline. When you use output: 'export', this pipeline doesn't exist — so you need to either disable it or provide an external image optimization service.
The quick fix is unoptimized: true in next.config.js:
images: {
unoptimized: true,
}
This tells Next.js to skip optimization and serve images as-is. Fine for most static sites.
If you want image optimization with static export, use an external CDN that supports image transformation. Tencent Cloud EdgeOne supports image processing at the edge — you can route image requests through EdgeOne for on-the-fly resizing and format conversion without a Next.js server.
Next.js has two categories of environment variables:
| Variable | Available | Use for |
|---|---|---|
NEXT_PUBLIC_* |
Browser + server | Public values (API URLs, analytics IDs) |
| Everything else | Server-side only | Secrets (API keys, database passwords) |
# .env.production
NEXT_PUBLIC_API_URL=https://api.yourdomain.com
DATABASE_URL=postgresql://user:password@localhost/mydb # Server-only
For the Node.js server deployment, set environment variables in PM2:
pm2 start npm --name "nextjs-app" -- start --env production
# or in ecosystem.config.js:
module.exports = {
apps: [{
name: 'nextjs-app',
script: 'npm',
args: 'start',
env_production: {
NODE_ENV: 'production',
DATABASE_URL: 'postgresql://...',
}
}]
}
| Issue | Likely Cause | Fix |
|---|---|---|
| Connection refused | Service not running or wrong port | Check systemctl status SERVICE and verify firewall rules |
| Permission denied | Wrong file ownership or permissions | Check file ownership with ls -la and use chown/chmod to fix |
| 502 Bad Gateway | Backend service not running | Restart the backend service; check logs with journalctl -u SERVICE |
| SSL certificate error | Certificate expired or domain mismatch | Run sudo certbot renew and verify domain DNS points to server IP |
| Service not starting | Config error or missing dependency | Check logs with journalctl -u SERVICE -n 50 for specific error |
| Out of disk space | Logs or data accumulation | Run df -h to identify usage; clean logs or attach CBS storage |
| High memory usage | Too many processes or memory leak | Check with htop; consider upgrading instance plan if consistently high |
| Firewall blocking traffic | Port not open in UFW or Lighthouse console | Open port in Lighthouse console firewall AND sudo ufw allow PORT |
Should I use SSR mode or static export for my Next.js app?
Use SSR (Node.js server mode) if your app has API routes, uses getServerSideProps, or needs server-side rendering. Use static export (output: 'export') if your app is purely static with no server-side logic.
How many PM2 workers should I run for Next.js?
Start with one instance per available CPU core. For a 2-vCPU server, 2 instances is reasonable. Monitor memory — Next.js is more memory-heavy than lightweight Node.js apps.
What is the difference between next build and next start?
next build compiles your application for production. next start runs the compiled application. You must run next build before next start each time you deploy changes.
How do I handle Next.js image optimization on a VPS?
Next.js image optimization requires a running server (SSR mode). For static exports, disable it with images: { unoptimized: true } or use EdgeOne CDN for image processing at the edge.
/api/*) function normally. For static export, API routes are not included and must be hosted separately.Deploy your Next.js app today:
👉 Tencent Cloud Lighthouse — Node.js-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers