Cloud platforms are the default answer for hosting Next.js, but as a project grows, cost, data location and control over the server start to matter. For a site aimed at users in Uzbekistan, a local VPS can also make pages load faster. With next start, Next.js runs as an ordinary Node.js app, so any Linux server will do.
Preparing the server
Start from a fresh Ubuntu or Debian server, create a non-root user and install an LTS release of Node.js. Check your Next.js version's documentation for its minimum Node.js version.
sudo apt update && sudo apt upgrade -y
sudo adduser deploy
sudo usermod -aG sudo deploy
su - deploy
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts
Cloning and building
Pull the code with Git, install exact dependency versions and build. The build is memory-hungry; on a small VPS it can fail with "JavaScript heap out of memory", so add swap or build elsewhere.
cd /var/www
sudo mkdir misol-next && sudo chown deploy:deploy misol-next
git clone https://github.com/misol/misol-next.git misol-next
cd misol-next
npm ci
cp .env.example .env.production
npm run build
Values prefixed with NEXT_PUBLIC_ are inlined at build time, so changing them requires a rebuild.
Running under PM2
PM2 keeps the app alive, restarts it on crash and starts it at boot. Binding to 127.0.0.1 means port 3000 cannot be reached directly from outside, only through Nginx. Run the sudo line that pm2 startup prints.
npm install -g pm2
pm2 start npm --name misol-next -- start -- -p 3000 -H 127.0.0.1
pm2 save
pm2 startup
Nginx as a reverse proxy
Nginx accepts public traffic, caches static assets and handles SSL.
server {
listen 80;
server_name example.uz www.example.uz;
location /_next/static/ {
proxy_pass http://127.0.0.1:3000;
expires 365d;
add_header Cache-Control "public, immutable";
}
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
sudo ln -s /etc/nginx/sites-available/misol-next /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Files under /_next/static/ have content hashes in their names, so long caching is safe.
Domain and free SSL
Point the domain's A record at the VPS IP in the DNS section of the sayt.uz cabinet. Once it propagates, Certbot obtains a Let's Encrypt certificate, edits the Nginx config and installs a renewal timer.
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d example.uz -d www.example.uz
sudo certbot renew --dry-run
Shipping updates
Wrap the repeated steps in a script with set -e, so a failed build stops before the reload and the old version keeps serving.
#!/bin/bash
set -e
cd /var/www/misol-next
git pull
npm ci
npm run build
pm2 reload misol-next
Note that next build overwrites the existing .next folder, so the running app can throw errors during the build. Larger projects should build in a separate directory or on CI and copy the result.
Example: a dental clinic in Tashkent
A dental clinic in Tashkent runs example.uz on Next.js: static service pages plus an online booking form backed by an API route. It sits on a 2 GB VPS deployed exactly as above, with 2 GB of swap added for builds. Pages load quickly thanks to Nginx caching, bookings stay in a database on the clinic's own server, and the administrator just runs the deploy script after adding a new service.