Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save sferdeveloper/5f31b3e5b0822415741459bfc4166ed8 to your computer and use it in GitHub Desktop.

Select an option

Save sferdeveloper/5f31b3e5b0822415741459bfc4166ed8 to your computer and use it in GitHub Desktop.
Deploy & secure a Next.js app + Postgres DB on a VPS

Self-Hosting Next.js Tutorial

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

Instructions & commands:

  1. Get your VPS server on Hostinger (Use code CODINGINFLOW for 10% off). Install Ubuntu 24 as the OS and set a root password.
  2. Log into your server as root: ssh root@<your-server-ip>
  3. Update Linux packages: apt update && apt upgrade -y
  4. Create a new user: adduser <username>
  5. Add this user to the sudo group: usermod -aG sudo <username>
  6. Logout from your server: logout
  7. Log in with your new user account: ssh <username>@<your-server-ip>
  8. Check if sudo works: sudo -v (If you don't get an error, it's good)
  9. Create the SSH key folder: mkdir ~/.ssh && chmod 700 ~/.ssh
  10. Confirm the folder exists: ls -a
  11. Logout again: logout
  12. Generate an SSH key on your local machine: ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/vps_key
  13. 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>
  1. Log back into your server: ssh <username>@<your-server-ip>
  2. Open SSH configuration: sudo nano /etc/ssh/sshd_config
  • Disable IPv6: AddressFamily inet
  • Disable password: PasswordAuthentication no
  • Disable root login: PermitRootLogin no
  1. If there is an included .conf file, make it empty. E.g. sudo nano /etc/ssh/sshd_config.d/*.conf
  2. Restart SSH: sudo systemctl restart ssh (On some distros it's sshd instead of ssh)
  3. Logout, verify that root + password logins are disabled
  4. Log back into your server
  5. Install firewall: sudo apt install ufw
  6. Whitelist ports: sudo ufw allow ssh + http + https
  7. Enable firewall: sudo ufw enable
  8. Check firewall status: sudo ufw status
  9. Open firewall rules: sudo nano /etc/ufw/before.rules
  • Add this to the INPUT block: -A ufw-before-input -p icmp --icmp-type echo-request -j DROP
  1. Reboot the server: sudo reboot. Log back in once the server is reachable again.
  2. Download & install Node.js:
  • curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
  • sudo -E bash nodesource_setup.sh
  • sudo apt-get install nodejs -y
  1. Check Node + NPM version: node -v, npm -v
  2. Install Git & check version: sudo apt install git, git -v
  3. Install PostgreSQL: sudo apt install postgresql postgresql-contrib
  4. Start PostgreSQL: sudo systemctl start postgresql.service
  5. Check if PostgreSQL is running: sudo service postgresql status
  6. Start the Postgres prompt: sudo -u postgres psql
  7. 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> postgres
  • GRANT ALL PRIVILEGES ON SCHEMA public TO <username>;
  1. Exit PostgreSQL: \q
  2. Create an apps folder: mkdir apps
  3. Navigate to /apps: cd apps/ (Use cd .. to go back)
  4. Git clone the sample repo (or use your own): git clone https://github.com/codinginflow/next-self-hosting
  5. Open the project folder: cd next-self-hosting/
  6. Create a .env file: sudo nano .env and add the following:
NEXT_PUBLIC_APP_TITLE="Self-Hosting Next.js Tutorial"
DATABASE_URL="postgresql://<username>:<password>@localhost:5432/<database_name>?schema=public"
  1. Install NPM packages: npm install
  2. Install Nginx: sudo apt install nginx
  3. Create an Nginx configuration file: sudo nano /etc/nginx/sites-available/nextjs.conf and 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;
 }
}
  1. Link your new configuration file: sudo ln -s /etc/nginx/sites-available/nextjs.conf /etc/nginx/sites-enabled/
  2. Remove the default configuration: sudo rm /etc/nginx/sites-enabled/default
  3. Check if your Nginx config is valid: sudo nginx -t
  4. Restart Nginx: sudo service nginx restart
  5. Push the database schema (This depends on your ORM/DB library): npx prisma db push
  6. Build and run the project: npm run build, npm start
  7. Now you can open your website at http://<your-server-ip> but the process will stop when you close the terminal.
  8. Install PM2: sudo npm install -g pm2
  9. Run your Next.js app via PM2: pm2 start npm --name "XXXXXX" -- start or pm2 start yarn --name "XXXXXX" --interpreter bash -- start
  10. Useful PM2 commands:
  • Check status: pm2 status
  • Show logs: pm2 logs (Close with Ctrl + C)
  • Clear logs: pm2 flush
  • Restart Next.js app (to apply changes): pm2 restart nextjs
  1. Show the PM2 startup script: pm2 startup. Copy and execute the printed command.
  2. Now, even if you reboot your server, PM2 should restart your Next.js app automatically: sudo reboot
  3. Buy a domain, for example at Namecheap
  4. Go to your Hostinger dashboard -> DNS Manager -> Add Domain and follow the instructions
  5. 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;
  1. Restart Nginx: sudo service nginx restart
  2. Install Certbot: sudo snap install --classic certbot
  3. Prepare Certbot command: sudo ln -s /snap/bin/certbot /usr/bin/certbot
  4. Install SSL certificates: sudo certbot --nginx
  5. 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!

@sferdeveloper
Copy link
Author

πŸ” 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:

# Navigate to your app folder
cd /path/to/your/app

# Install any new dependencies (optional, only if package.json changed)
yarn install

# Build the app again
yarn build

# Restart the app via PM2
pm2 restart app

If you used npm instead of yarn, 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:

  1. Clone the repo

    git clone https://github.com/your-username/your-repo.git
    cd your-repo
  2. Add your .env file
    (or if it's already in the repo and private, you're good)

  3. Install dependencies

    yarn install  # or npm install
  4. Build

    yarn build
  5. Run with PM2

    pm2 start yarn --name "newapp" -- start
  6. Set 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

sudo nano /etc/nginx/sites-available/project2.conf

Paste this in (replace project2.yourdomain.com and port if needed):

server {
    listen 80;
    server_name project2.yourdomain.com;

    location / {
        proxy_pass http://localhost:3001;
        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;

        proxy_buffering off;
        proxy_set_header X-Accel-Buffering no;
    }
}

πŸ”— 2. Enable the config

sudo ln -s /etc/nginx/sites-available/project2.conf /etc/nginx/sites-enabled/

🚫 3. (Optional) Remove default config if not already done

sudo rm /etc/nginx/sites-enabled/default

βœ… 4. Test and restart Nginx

sudo nginx -t
sudo systemctl restart nginx

🌐 5. Point subdomain in Cloudflare

Go to your Cloudflare dashboard and:

  • Add a new DNS record:
    • Type: A
    • Name: project2
    • IPv4: your VPS IP address
    • Proxy status: Proxied (orange cloud) is fine

πŸ” 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:

sudo certbot --nginx

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 /project2 instead of a subdomain β€” both are possible.

@sferdeveloper
Copy link
Author

sferdeveloper commented Apr 17, 2025

Nice! Since you're just adding a brand new domain (and not setting up the whole VPS again), you don’t need to repeat everything. Here's a breakdown of what you do and don’t need to repeat πŸ‘‡


βœ… DO THESE AGAIN (for the new domain):

  1. Create a new Nginx config file:
    sudo nano /etc/nginx/sites-available/XXXXXXX.conf
    Update server_name with your new domain (e.g., your-new-domain.com www.your-new-domain.com) and proxy to the right port.

β›”β›”β›” DO NOT FORGET THE PORT

server {
    listen 80;
    server_name XXXXXX.com www.XXXXX.com;;

    location / {
        proxy_pass http://localhost:3001; 
        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;

        proxy_buffering off;
        proxy_set_header X-Accel-Buffering no;
    }
}
  1. Link the new config:

    sudo ln -s /etc/nginx/sites-available/XXXXXXX.conf /etc/nginx/sites-enabled/
  2. Restart Nginx:

    sudo nginx -t
    sudo service nginx restart
  3. Update DNS (on Cloudflare or your registrar):
    Point the new domain/subdomain (A record) to your VPS IP.

  4. Install SSL for the new domain:

    sudo certbot --nginx
  5. (Optional but recommended) Test renewal:

    sudo certbot renew --dry-run

❌ DON’T REPEAT (already done globally on your VPS):

  • Install Nginx
  • Install Node/PM2
  • Remove default config
  • Buy a domain πŸ˜„
  • Install Certbot or create the /usr/bin/certbot link (already installed)
  • Build/install PM2 globally
  • Setup Prisma, DB schema, etc. (unless it's a different app)

⚠️ Maybe Do Again (if app is separate):

β›”β›”β›” DO NOT FORGET THE PORT

  • If the new domain points to a separate Next.js app, then:
    • Clone or pull that app
    • Run:
      yarn 
      
      npm build
      
       PORT=3001 pm2 start yarn --name "XXXXXX" --interpreter bash -- start

If it’s the same app and you're just adding a custom domain, you just need to:

  • Update Nginx
  • Add SSL
  • Update your NEXTAUTH_URL or any other env vars

To make sure your site redirects properly between www.mydomain.com and mydomain.com (like always forcing one version), here's what to do:


πŸ” Option 1: Redirect using Cloudflare Page Rules (Easy way)

  1. Go to Cloudflare dashboard β†’ choose your domain.
  2. Go to Rules β†’ Page Rules.
  3. Click "Create Page Rule".
  4. In the "If the URL matches" field, put:
    http://mydomain.com/*
    
  5. Set the setting to:
    • Forwarding URL β†’ 301 Permanent Redirect
    • Destination URL:
      https://www.mydomain.com/$1
      

πŸ‘‰ This will redirect all non-www traffic to www.

Repeat for HTTPS:

  • Create another rule for:
    https://mydomain.com/*
    
    Redirect to:
    https://www.mydomain.com/$1
    

βœ… Things to check:

  • Your DNS in Cloudflare should have both:
    • A record for @ (root domain) β†’ your VPS IP
    • A record for www β†’ same VPS IP

SSL/TLS encryption

Current encryption mode: Full (strict)

@sferdeveloper
Copy link
Author

I added socket

how to fix this $ yarn start
yarn run v1.22.22
$ NODE_ENV=production node server.js
'NODE_ENV' is not recognized as an internal or external command,
operable program or batch file.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. ?

This error is happening because you're on Windows, and the syntax NODE_ENV=production node server.js is Unix/Linux-style. Windows doesn't support setting environment variables like that directly in the command line.

βœ… Fix for Windows

You can use the cross-env package to make your scripts work cross-platform (Windows/Linux/macOS).

Steps to fix:

  1. Install cross-env:

    yarn add cross-env --dev
  2. Update your package.json script:

    Find this line in your package.json:

    "start": "NODE_ENV=production node server.js"

    Change it to:

    "start": "cross-env NODE_ENV=production node server.js"
  3. Run it again:

    yarn start

This should work on any OS now.

Let me know if you want a Windows-only workaround without cross-env.


On Linux, you don’t need cross-env β€” you can set environment variables inline just like you're doing with PORT=3001.

So if you want to set both NODE_ENV=production and PORT=3001, just combine them:

NODE_ENV=production PORT=3001 pm2 start yarn --name "XXXXXX" --interpreter bash -- start

This sets both env vars before running your script.


Quick Breakdown:

  • NODE_ENV=production β€” tells your app it's in production mode.
  • PORT=3001 β€” sets the port.
  • pm2 start yarn β€” tells PM2 to run yarn.
  • -- start β€” passes start to yarn, so it runs the start script in package.json.

Let me know if you also want it to load from an .env file or persist after reboot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment