What You'll Need
Before you start, make sure you have these ready. The whole deploy takes about 20–30 minutes once they're in place.
- A VPS running Ubuntu (any provider — Hostinger, DigitalOcean, Linode, Hetzner, etc.) and its public IP
- A domain name you control
- Your full-stack Next.js app pushed to a GitHub repository
- Basic comfort with the terminal — you'll copy/paste commands
yourdomain.com, the database name, username and password with your own values everywhere they appear below.
Point Your Domain at the VPS
In your domain registrar's DNS settings, add two A records that point to your VPS's public IP address:
| Type | Name / Host | Value | TTL |
|---|---|---|---|
| A | @ | Your VPS IP | 60 |
| A | www | Your VPS IP | 60 |
DNS can take a few minutes to propagate. Confirm it's pointing to the right server:
nslookup yourdomain.com
Once it resolves to your IP, SSH into the server:
ssh root@yourdomain.com
A TTL of 60 seconds means DNS changes propagate fast while you're setting things up. You can raise it later once everything is stable.
Install PostgreSQL & Create the Database
Update the package list and install PostgreSQL:
sudo apt update
sudo apt install -y postgresql postgresql-contrib
Switch to the postgres system user and open the PostgreSQL shell:
sudo -i -u postgres
psql
Now create a login role and a database that role owns. Run these inside the postgres=# prompt:
CREATE ROLE myapp_user WITH LOGIN PASSWORD 'strong_password_here';
CREATE DATABASE myapp_db OWNER myapp_user;
GRANT ALL PRIVILEGES ON DATABASE myapp_db TO myapp_user;
\q
Type exit to leave the postgres user. Your Next.js app will connect using this string:
postgresql://myapp_user:strong_password_here@localhost:5432/myapp_db
SUPERUSER rights. It's all your app needs and keeps the database far safer.
Install Node.js & Build the App
Install Node.js using fnm (Fast Node Manager) so you can manage versions cleanly, then activate the latest LTS:
curl -fsSL https://fnm.vercel.app/install | bash
source ~/.bashrc
fnm install --lts
fnm use --lts
node -v
Clone your repository. Use the HTTPS URL for a public repo, or the SSH URL for a private one:
# public repo
git clone https://github.com/username/your-nextjs-app.git
# private repo (SSH)
git clone git@github.com:username/your-nextjs-app.git
cd your-nextjs-app
Install dependencies, create your .env, and add the database URL plus any secrets your app uses:
npm install
cp .env.example .env
nano .env
DATABASE_URL="postgresql://myapp_user:strong_password_here@localhost:5432/myapp_db"
NODE_ENV=production
If you use an ORM like Prisma or Drizzle, run your migrations now, then create the production build:
# example — Prisma
npx prisma migrate deploy
npm run build
A successful npm run build is your green light. If it fails here, fix it before going further — a broken build will never serve correctly behind the proxy.
Keep It Running with PM2
If you just run npm run start, the app dies the moment you close your SSH session. PM2 keeps it alive in the background and restarts it automatically. Install it globally and start your app:
npm install -g pm2
pm2 start npm --name "nextjs-app" -- run start
Next.js listens on port 3000 by default. Make PM2 relaunch your app after a server reboot:
pm2 startup
pm2 save
Handy commands to manage it later:
pm2 list— see running apps and their statuspm2 logs nextjs-app— tail live logspm2 restart nextjs-app— restart after pulling new code
Serve It with Caddy (Automatic HTTPS)
Your app runs on port 3000, but visitors should reach it on https://yourdomain.com. Caddy is a web server that reverse-proxies traffic to your app and provisions a free SSL certificate automatically. Install it:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install -y caddy
Open the Caddy config file:
sudo nano /etc/caddy/Caddyfile
Replace its contents with this — it proxies your domain to the Next.js server and redirects www to the root domain:
yourdomain.com {
reverse_proxy localhost:3000
}
www.yourdomain.com {
redir https://yourdomain.com{uri}
}
Save (Ctrl+O, Enter, Ctrl+X), then reload Caddy to apply it:
sudo systemctl reload caddy
https://yourdomain.com and your full-stack Next.js app is live and secured. 🎉
Shipping Updates Later
When you push new code to GitHub, redeploying on the VPS is three commands:
git pull
npm install && npm run build
pm2 restart nextjs-app
Quick Troubleshooting
| Symptom | Likely fix |
|---|---|
| 502 / Bad Gateway | App isn't running — check pm2 list and pm2 logs. |
| Site won't load at all | DNS not pointed yet — re-check the A records with nslookup. |
| Database connection error | Verify DATABASE_URL credentials and that PostgreSQL is running. |
| No HTTPS / cert error | Ports 80 & 443 must be open in your VPS firewall for Caddy. |
Wrapping Up
You've taken a full-stack Next.js app from a GitHub repo to a live, HTTPS-secured domain on your own server — DNS, PostgreSQL, Node.js, PM2 and Caddy, all wired together. The same pattern scales to almost any Node backend; swap the database or add an api.yourdomain.com block in the Caddyfile when you need it.
Want to learn this end to end with real projects and mentorship? Our Full Stack AI Bootcamp covers building and deploying production apps.
Discussion