
A step by step guide to deploying a Next.js application on a VPS. Learn how to set up Node.js, PostgreSQL, Nginx as a reverse proxy, configure a domain name, and secure everything with a free SSL certificate using PM2 for process management.

Francis K Njenga
Senior Developer
Deploying a Next.js application to a production server can seem daunting, but with the right approach, it becomes a straightforward process. This guide will walk you through hosting a Next.js project on a Virtual Private Server (VPS), connecting it to a domain name, setting up a PostgreSQL database, and ensuring your application stays running reliably.
For this tutorial, we'll use a portfolio project as our example. The steps covered here apply to most Node.js applications, not just Next.js.
Before uploading your project to the server, you need to package it properly. On your local machine, compress your project files while excluding unnecessary development dependencies and build artifacts.
Exclude these folders when creating your archive:
node_modules
– These will be reinstalled on the server
.next
– This is the Next.js build folder that will be regenerated
.git
– Version control files aren't needed in production
.env.local
– Environment variables should be set directly on the server
If you're using a terminal, you can create the zip file with the command shown below.
# Navigate to your project root cd /path/to/your/project # Create zip file excluding node_modules and .next zip -r project.zip . -x "node_modules/*" ".next/*"
Command to compress your project excluding development folders
When selecting a VPS provider, focus on finding a reliable service with a good price-to-performance ratio. Based on experience, a VPS with the following specifications works well for most Node.js applications:
For this guide, we'll use Ubuntu 22.04 LTS or Ubuntu 24.04 LTS as the operating system. After creating your server, your hosting provider will give you:
# Connect to your VPS ssh root@your-server-ip-address # When prompted, enter your VPS password # If using SSH keys, the authentication will be automatic
Establishing an SSH connection to your remote server
Once logged into your VPS, the first step is to update the system packages to ensure everything is current and secure. This is a critical security practice that should be performed regularly on any production server.
The update command refreshes the package list, while the upgrade command installs newer versions of packages that have security patches or bug fixes.
# Update package list and upgrade all packages sudo apt update && sudo apt upgrade -y # Optional: Remove unnecessary packages sudo apt autoremove -y
Updating and upgrading Ubuntu packages
Since Next.js is a Node.js framework, we need to install Node.js on the server. The recommended approach is to use the NodeSource repository, which provides more recent versions of Node.js than the default Ubuntu repositories.
For Next.js applications, Node.js version 18 or higher is recommended. This guide uses version 20, which is a Long-Term Support (LTS) release.
# Add NodeSource repository and install Node.js curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs # Verify installation node -v npm -v
Installing Node.js using the NodeSource repository
To keep your Node.js application running continuously—even after you log out of the server—we need a process manager. PM2 is an excellent choice for Node.js applications. It provides:
Automatic restart if the application crashes
Restart after server reboot pm2 startup
Log management for debugging
Monitoring capabilities pm2 monit
Note: For Python projects, alternatives like Gunicorn (often paired with Supervisor or systemd) are commonly used instead of PM2.
# Install PM2 globally npm install pm2 -g # Verify installation pm2 --version
Installing PM2 globally on the server
Nginx will serve as a reverse proxy, directing incoming HTTP requests from your domain to your Next.js application running on a specific port. It will also handle:
SSL Termination
For HTTPS connections
Load Balancing
If scaling horizontally
Static File Serving
Efficient delivery of assets
Security & Rate Limiting
Headers and request throttling
# Install Nginx sudo apt install nginx -y # Enable Nginx to start on boot sudo systemctl enable nginx # Start Nginx if not already running sudo systemctl start nginx # Check Nginx status sudo systemctl status nginx
Installing Nginx and enabling the service
Your Next.js application likely needs a database. PostgreSQL is a powerful, open-source relational database that pairs well with Node.js applications. It offers:
ACID Compliance
Guaranteed data integrity and reliability
Advanced Indexing
Powerful query optimization capabilities
JSON Data Support
Flexible schema with native JSON types
High Performance
Optimized for read/write operations
The postgresql-contrib package includes additional utilities and extensions that can be useful for production deployments.
# Install PostgreSQL and contrib package sudo apt install postgresql postgresql-contrib -y # Verify PostgreSQL is running sudo systemctl status postgresql # PostgreSQL will automatically start on port 5432
Installing PostgreSQL and additional contrib packages
Now it's time to transfer your project files to the server. We'll create a dedicated directory and upload the zip file we created earlier.
There are several ways to upload files to your VPS:
Secure Copy — Best for single files or small projects
Useful if you prefer a GUI client like FileZilla
Ideal if your code is hosted on GitHub or GitLab
This guide uses SCP for simplicity, but using Git is often more convenient for ongoing deployments.
# On the server: Create a project directory mkdir frankcodes cd frankcodes # From your local machine (not on the server): Upload the zip file scp /path/to/local/project.zip root@your-server-ip:/root/frankcodes/ # Back on the server: Extract the project unzip project.zip # Remove the zip file after extraction rm project.zip # Install project dependencies npm install
Creating a directory and uploading the project zip file
Before building and running your application, you need to create the database and user that your application will use. This involves:
Switching to the postgres system user
Entering the PostgreSQL shell psql
Creating a dedicated database user
Creating the database with that user as the owner
Granting appropriate privileges
Security Note:
The credentials shown in this example are for demonstration purposes. In production, always use strong, randomly generated passwords and store them in environment variables, not in code.
# Switch to postgres user sudo -i -u postgres # Enter PostgreSQL shell psql -- Create a database user (replace with your credentials) CREATE USER franktech WITH PASSWORD 'your_secure_password'; -- Create the database with the user as owner CREATE DATABASE your_database_name OWNER franktech; -- Grant all privileges on the database to the user GRANT ALL PRIVILEGES ON DATABASE your_database_name TO franktech; -- Exit PostgreSQL shell \q -- Exit postgres user session exit
Setting up a PostgreSQL user and database
If your project uses an ORM like Drizzle, Prisma, TypeORM, or Sequelize, you'll need to run database migrations to set up the schema. This example uses Drizzle ORM, but the concept is similar across ORMs:
Creates migration files based on your schema definitions
npm run db:generate
Applies those migrations to the database
npm run db:migrate
For development, pushes schema changes without migration files
npm run db:push
Before running these commands, ensure your database connection string is properly configured in your .env file or environment variables.
# Generate migration files (Drizzle example) npm run db:generate # Apply migrations to the database npm run db:migrate # For Prisma, commands would be: # npx prisma migrate dev --name init # npx prisma db push
Generating and applying database migrations
Now that the database is ready, we can build the Next.js application and start it with PM2. The build process compiles your application into optimized production assets.
When starting with PM2, consider:
Set the port using
-- --port 3001
Avoid conflicts with other services
Name your process with
--name my-app
Easier management and monitoring
Run
pm2 save
pm2 startup
Keep processes running after server restart
Pro Tip: After running pm2 startup, follow the instructions to generate the startup script. This ensures your app automatically starts when the server boots.
# Build the Next.js application npm run build # Start the application with PM2 on port 3001 pm2 start npm --name "nextjs-app" -- start -- --port 3001 # Verify the application is running pm2 status # Test if the app is responding curl http://localhost:3001 # Save the PM2 process list to restart on server reboot pm2 save # Generate startup script pm2 startup # Follow the instructions from the startup command
Building the Next.js app and launching it with PM2
To make your application accessible via a custom domain, you need to configure DNS records. This example uses a domain registered with Hostinger, but the process is similar with any registrar.
Understanding DNS Record Types:
Maps a domain to an IPv4 address
example.com → 192.0.2.1
Maps a domain to an IPv6 address
example.com → 2001:db8::1
Aliases one domain to another
www.example.com → example.com
192.0.2.1
192.0.2.1
For most setups, you'll need an A record for the root domain (@) and optionally for the www subdomain. Setting the TTL (Time To Live) to 300 seconds (5 minutes) during setup speeds up propagation so changes take effect faster.
Type: A Name: @ Value: your-vps-ipv4-address TTL: 300 Type: A Name: www Value: your-vps-ipv4-address TTL: 300 Optional - If your VPS supports IPv6: Type: AAAA Name: @ Value: your-vps-ipv6-address TTL: 300
DNS records needed for domain configuration
Now we'll set up Nginx to proxy requests from your domain to your Next.js application. The configuration file should be created in /etc/nginx/sites-available/ and then enabled via a symbolic link to /etc/nginx/sites-enabled/.
Key configuration directives explained:
Specifies which domain names this server block responds to
server_name example.com www.example.com;
Forwards requests to your application on localhost:3001
proxy_pass http://localhost:3001;
Preserves original request information (host, IP, protocol)
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;
Required for WebSocket support in Next.js
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
server {
listen 80;
server_name example.com www.example.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_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;
}
}
Pro Tip: Always test your Nginx configuration with sudo nginx -t before reloading. This catches syntax errors that could otherwise take down your server.
server { listen 80; listen [::]:80; server_name yourdomain.com www.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_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; } }
Nginx server block for proxying to Next.js
# Create a symbolic link to enable the site sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/ # Test the Nginx configuration for syntax errors sudo nginx -t # If the test is successful, reload Nginx to apply changes sudo systemctl reload nginx # If you encounter issues, check the error log sudo tail -f /var/log/nginx/error.log
Activating the site configuration and reloading Nginx
To secure your website with HTTPS, we'll use Certbot to obtain a free SSL certificate from Let's Encrypt. Certbot automatically:
Obtains a trusted SSL certificate
For your domain from Let's Encrypt
Configures Nginx
To use the certificate automatically
Sets up automatic renewal
Certificates expire after 90 days
Redirects HTTP to HTTPS
Ensures all traffic is encrypted
Why this matters: This step is crucial for security, SEO rankings, and user trust. Modern browsers mark HTTP sites as Not Secure, which can deter visitors.
# Install Certbot and the Nginx plugin sudo apt install certbot python3-certbot-nginx -y # Verify installation certbot --version # Obtain and install SSL certificate for your domain sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com # Follow the prompts: # - Enter your email address for renewal notices # - Agree to the terms of service # - Choose whether to redirect HTTP to HTTPS (recommended: yes) # Test automatic renewal sudo certbot renew --dry-run
Using Certbot to obtain and install a Let's Encrypt SSL certificate
If your application requires initial data—such as an admin user, default categories, or configuration settings—run the seed command to populate the database. Seeding is typically done after migrations and before the application goes live.
Your package.json might contain scripts like:
Runs the seed script to populate the database with initial data
Resets and reseeds the database from scratch
Runs migrations and seeds together in one command
Best Practices:
npm run db:seed
npm run db:reset
npm run db:setup
db:reset in production will delete all existing data. Use with extreme care!
# Run the seed script npm run db:seed # If you have a custom seed command, run it directly # node scripts/seed.js # Verify the data was inserted (optional) # psql -d your_database -c "SELECT * FROM users;"
Running the database seed script
Your Next.js application is now successfully deployed on a VPS with PostgreSQL as the database, Nginx as a reverse proxy, and a valid SSL certificate for secure HTTPS connections. The application is managed by PM2, ensuring it stays running even after server reboots.
Keeps your Node.js application running in the background, restarts on crash, and survives server reboots
Acts as a reverse proxy, handles SSL termination, and serves static assets efficiently
Provides a robust, ACID-compliant database backend for your application data
Secures your site with free, auto-renewing SSL certificates from Let's Encrypt
Regular Backups
Set up backups for database and application files
Firewall Configuration
Configure ufw to restrict access to necessary ports only
Monitoring
Implement monitoring with pm2 monit or external services
CI/CD Pipeline
Set up automated deployments for streamlined updates
System Updates
Keep your server updated with regular sudo apt update && sudo apt upgrade
With this foundation, you can deploy additional applications, configure multiple domains, or scale your setup as your needs grow.
Happy deploying!
Built with Love using Next.js, PostgreSQL, Nginx, and PM2

Senior Developer
Francis Njenga is an experienced Lead Developer specializing in React, Next.js, and modern JavaScript frameworks, with a strong background in web development.