Follow the instructions below to deploy a Next.js app with a local PostgreSQL database to a VPS, secure it, and connect it to a custom domain with free SSL. Watch the accompanying tutorial on YouTube: https://www.youtube.com/watch?v=2T_Dx7YgBFw
- Get your VPS server on Hostinger (Use code
CODINGINFLOWfor 10% off). Install Ubuntu 24 as the OS and set a root password. - Log into your server as root:
ssh root@<your-server-ip> - Update Linux packages:
apt update && apt upgrade -y - Create a new user:
adduser <username> - Add this user to the sudo group:
usermod -aG sudo <username> - Logout from your server:
logout - Log in with your new user account:
ssh <username>@<your-server-ip> - Check if sudo works:
sudo -v(If you don't get an error, it's good) - Create the SSH key folder:
mkdir ~/.ssh && chmod 700 ~/.ssh - Confirm the folder exists:
ls -a - Logout again:
logout - Generate an SSH key on your local machine:
ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/vps_key - Push this SSH key to your server:
- Windows:
scp $env:USERPROFILE/.ssh/vps_key.pub <username>@<server-ip>:~/.ssh/authorized_keys - Mac:
scp ~/.ssh/id_ed25519.pub <username>@<your-server-ip>:~/.ssh/authorized_keys - Linux:
ssh-copy-id <username>@<your-server-ip>
- Log back into your server:
ssh <username>@<your-server-ip> - Open SSH configuration:
sudo nano /etc/ssh/sshd_config
- Disable IPv6:
AddressFamily inet - Disable password:
PasswordAuthentication no - Disable root login:
PermitRootLogin no
- If there is an included
.conffile, make it empty. E.g.sudo nano /etc/ssh/sshd_config.d/*.conf - Restart SSH:
sudo systemctl restart ssh(On some distros it'ssshdinstead ofssh) - Logout, verify that root + password logins are disabled
- Log back into your server
- Install firewall:
sudo apt install ufw - Whitelist ports:
sudo ufw allow ssh+http+https - Enable firewall:
sudo ufw enable - Check firewall status:
sudo ufw status - Open firewall rules:
sudo nano /etc/ufw/before.rules
- Add this to the
INPUTblock:-A ufw-before-input -p icmp --icmp-type echo-request -j DROP
- Reboot the server:
sudo reboot. Log back in once the server is reachable again. - Download & install Node.js:
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.shsudo -E bash nodesource_setup.shsudo apt-get install nodejs -y
- Check Node + NPM version:
node -v,npm -v - Install Git & check version:
sudo apt install git,git -v - Install PostgreSQL:
sudo apt install postgresql postgresql-contrib - Start PostgreSQL:
sudo systemctl start postgresql.service - Check if PostgreSQL is running:
sudo service postgresql status - Start the Postgres prompt:
sudo -u postgres psql - Run these commands inside PostgreSQL to create a database + user:
CREATE DATABASE <database_name>;CREATE USER <username> WITH ENCRYPTED PASSWORD '<password>';ALTER ROLE <username> SET client_encoding TO 'utf8';ALTER ROLE <username> SET default_transaction_isolation TO 'read committed';ALTER ROLE <username> SET timezone TO 'UTC';GRANT ALL PRIVILEGES ON DATABASE <database_name> TO <username>;\c <database_name> postgresGRANT ALL PRIVILEGES ON SCHEMA public TO <username>;
- Exit PostgreSQL:
\q - Create an apps folder:
mkdir apps - Navigate to /apps:
cd apps/(Usecd ..to go back) - Git clone the sample repo (or use your own):
git clone https://github.com/codinginflow/next-self-hosting - Open the project folder:
cd next-self-hosting/ - Create a .env file:
sudo nano .envand add the following:
NEXT_PUBLIC_APP_TITLE="Self-Hosting Next.js Tutorial"
DATABASE_URL="postgresql://<username>:<password>@localhost:5432/<database_name>?schema=public"
- Install NPM packages:
npm install - Install Nginx:
sudo apt install nginx - Create an Nginx configuration file:
sudo nano /etc/nginx/sites-available/nextjs.confand insert the following (Don't forget to insert your server IP):
server {
listen 80;
server_name <your-server-ip>;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Disable buffering to allow streaming responses
proxy_buffering off;
proxy_set_header X-Accel-Buffering no;
}
}
- Link your new configuration file:
sudo ln -s /etc/nginx/sites-available/nextjs.conf /etc/nginx/sites-enabled/ - Remove the default configuration:
sudo rm /etc/nginx/sites-enabled/default - Check if your Nginx config is valid:
sudo nginx -t - Restart Nginx:
sudo service nginx restart - Push the database schema (This depends on your ORM/DB library):
npx prisma db push - Build and run the project:
npm run build,npm start - Now you can open your website at
http://<your-server-ip>but the process will stop when you close the terminal. - Install PM2:
sudo npm install -g pm2 - Run your Next.js app via PM2:
pm2 start npm --name "XXXXXX" -- startorpm2 start yarn --name "XXXXXX" --interpreter bash -- start - Useful PM2 commands:
- Check status:
pm2 status - Show logs:
pm2 logs(Close withCtrl + C) - Clear logs:
pm2 flush - Restart Next.js app (to apply changes):
pm2 restart nextjs
- Show the PM2 startup script:
pm2 startup. Copy and execute the printed command. - Now, even if you reboot your server, PM2 should restart your Next.js app automatically:
sudo reboot - Buy a domain, for example at Namecheap
- Go to your Hostinger dashboard ->
DNS Manager->Add Domainand follow the instructions - Update the server name in your Nginx config:
sudo nano /etc/nginx/sites-available/nextjs.conf:
server_name your-domain.com www.your-domain.com;
- Restart Nginx:
sudo service nginx restart - Install Certbot:
sudo snap install --classic certbot - Prepare Certbot command:
sudo ln -s /snap/bin/certbot /usr/bin/certbot - Install SSL certificates:
sudo certbot --nginx - Test automatic rewenal:
sudo certbot renew --dry-run
After the DNS changes are applied (this can take up to 48h), your Next.js website will be reachable under your domain name with an active SSL certificate. Subscribe to Coding in Flow for more Next.js and web dev tutorials!
π When you do
git pull(to update your app):That just fetches the latest code β but doesnβt build or restart the app.
After pulling changes, do this:
If you used
npminstead ofyarn, just swap the commands.π If you clone a new repo (second app):
Yes, youβll repeat some steps, but not all. Hereβs the minimal flow:
Clone the repo
git clone https://github.com/your-username/your-repo.git cd your-repoAdd your
.envfile(or if it's already in the repo and private, you're good)
Install dependencies
yarn install # or npm installBuild
Run with PM2
pm2 start yarn --name "newapp" -- startSet up Nginx with a different subdomain
Step 6 is about adding a new Nginx config so your new app runs on something like
project2.yourdomain.com.Hereβs a simple step-by-step guide:
β Let's say your second app runs on port
3001π§ 1. Create a new Nginx config file
Paste this in (replace
project2.yourdomain.comand port if needed):π 2. Enable the config
π« 3. (Optional) Remove default config if not already done
β 4. Test and restart Nginx
π 5. Point subdomain in Cloudflare
Go to your Cloudflare dashboard and:
Aproject2π 6. Enable SSL with Certbot (optional if using Cloudflare Full Strict)
If youβre using Full (Strict) and SSL is working already with your first app, you can run:
It will detect the new domain and let you install an SSL cert for
project2.yourdomain.com.Let me know if this second app runs on a different port (like
3002, etc), or you want to route everything under/project2instead of a subdomain β both are possible.