In case you prefer video tutorial, here’s the link to the video:
Loading Video...
Pros:
- avoid vendor lock-in like Vercel or Netlify
- it’s free (while using free tier VM)
- full control over your machine and server
- it performs better then some other solutions
- you learn a lot while doing so
- ability to scale (simply choose another machine)
Cons:
- extra setup required (this tutorial is enough for basic setup though)
- downtime while updating the app, meaning this solution might not be suitable for large projects, but OK for small projects with no need for 100% uptime
- if you decide to upgrade your VM you will pay for machine all the time instead of pay-per-usage like Cloud Run
P.S. This blog is self hosted with Google Cloud VM for free. You can see for yourself how it performs.
Once you finish this tutorial, you will know how to use Google Cloud VM (VPS) with Ubuntu, Ngnix and Node.js to self host Nextjs for free.
- Cloud Cloud Platform Project with billing enabled
- GitHub account (if you want to clone your app from GitHub)
- Optional: Domain name
I use Hostinger to purchase and manage domain names.
V1) Create your own app and make sure it’s ready to be cloned from GitHub
I recommend starting with cloning one of these templates:
- Portfolio Starter Kit (pages router)
- Next.js App Router Playground (app router)
npx create-next-app@latest
on VM via SSH to create your. If you choose this option, you don't need GitHub repo. Just go to the next step.If you already have one - skip this step.
Make sure to:
- enable Compute Engine API before creating VM
- choose free tier settings for VM
- choose operating system compatible with control panel of choise (if you are planning to intall any). For example if you choose Hestia Control Panel, the system must be Ubuntu 20.04
- Optional: Install Opt Agent for monitoring VM’s load (CPU, processes, memory, disk, netword etc)
- Name: any
- Region: us-central1 (iowa)
- Zone: any
- Machine type: e2-micro (preset > shared core)
- VM provisioning model: standard
- Boot disk: Ubuntu 20.4 LTS (x86/64, amd64 focal image), Standard persistend disk, 30 GB
- Firewall: Allow HTTP & HTTPS traffic
- Networking > Network interfaces > Network Service Tier: Standard (us-central1)
After choosing settings as above, you should see that your Monthly estimate is around $7.31 (this might change in the future). In actuality you will not be charged this amount. Free credits will be used instead, since this is a free tier.
Google Cloud > Cloud Engine > Choose created VM > Settings > Metadata > SSH keys.
Metadata settings work for all intances inside a project, so if you have multiple VMs inside one project - you only have to add SSH key once.
V1: Use Google Cloud Console > Cloud Engine > Choose created VM > Connect > SSH.
V2: Use any SSH client.
ssh -i shh/key/path username@hostname
ssh -i C:\Users\admin\.ssh\id_rsa robert@32.118.82.984
Command’s breakdown:
ssh
: The command to initiate an SSH connection.i shh/key/path
: Specifies the path to the private key file.username@hostname
: Replaceusername
with your remote username andhostname
with the IP address or domain name of the server you want to connect to.
username
part of this command might just be a first part of your Gmail address.While running this command the first time, you might encounter an error:
"The authenticity of host can't be established. This key is not known by any other names Are you sure you want to continue connecting (yes/no/[fingerprint])?"
yes
and hit enter.If you want to connect your domain to your VM, you need to add DNS records.
Record type | Name | Value | Comment |
---|---|---|---|
A | @ | VM external IP, example: 35.208.156.147 | Points domain to VM’s IP. |
CNAME | www | yourdomain.com | Enables traffic from www.yourdomain.com |
A | admin | VM external IP, example: 35.208.156.147 | [Optional] Points admin.yourdomain.com to VM’s IP. |
sudo su -
root@instance-name
in terminal after that.Run following commands to install ...
nvm:
- Check if node is installed:
nvm -v
- If not installed, run
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
nvm
- Check current nvm version:
nvm -v
Node:
- Install latest version of node.js:
nvm install --lts
- Check current node version:
node -v
npm:
- Install npm:
sudo apt-get install npm
(this might take a few minutes) - Check installed version of npm:
npm -v
Don't worry if you get stuck on "Processing triggers for man-db ...". Just wait for it to finish.
which git
to check if git is installed.If not installed, run:
sudo apt-get update
sudo apt-get install git
which git
git clone https://github.com/user/repo-name.git
6.5.1. Generate “Personal access token”. As a test you can give this token access to everything.
6.5.2. Modify and use this command on VM:
git clone https://<your_personal_access_token>@github.com/<user_name>/<repo_name>.git
Example:
git clone https://ghp_awpKsivWBSadwagTdacirasWDCCwacI@github.com/githubuser/test.git
npx create-next-app@latest
if you don't have an app that you want to deploy.hash -r
- Go to project’s folder using
cd foldername/
- Run
npm install
(this might take a few minutes if you have a lot of dependencies) - Run
npm run build
(this might take 5-15 minutes) - Run
npm run start
In order to make sure that your app is accessible from outside, you need to open firewall for port 3000. Go to your VM’s settings > Firewall > Add firewall rule and create rule with following settings:
- Name: allow-3000
- Type: Ingress
- Action: Allow
- Targets: All instances in the network
- Filters: IP ranges: 0.0.0.0/0
- Protocol and ports: TCP:3000
vm.ip.address:3000
, for example 35.208.17.104:3000
You can also try stopping current process (ctrl + c) or close the terminal. After that the app should not be available, since we didn’t configure pm2 process to run even without active terminal.
pm2
and configure background process to run even when we don’t have active SSH terminal opened.pm2
:- Install pm2:
sudo npm install -g pm2
- Go to project’s folder using
cd appfoldername/
- Add & run “npm run start” process to pm2 with name “process-name”:
pm2 start npm --name “process-name” -- start
- Check whether this process is running:
pm2 status
- Configure pm2 to run this process on startup:
pm2 startup
- Save this pm2 configuration:
pm2 save
Additional info:
- To stop this process, use
pm2 stop processid
orpm2 stop processname
- To remove this process from reboot use
pm2 unstartup systemd
- To restart the process after it was stopped use
pm2 restart processid
orpm2 restart processname
In order for app to be available not only on port 3000, we need to install and configure ngnix
Ensure that your Google Cloud VM firewall rules allow incoming traffic on both port 3000 and the default HTTP port (80). You may need to update your firewall rules to permit traffic on port 80.
Using Google Cloud Shell, run:
gcloud compute firewall-rules create allow-3000 --allow tcp:3000 --project <YOUR_PROJECT_ID>
gcloud compute firewall-rules create allow-80 --allow tcp:80 --project <YOUR_PROJECT_ID>
<YOUR_PROJECT_ID>
insert project ID of your Google Cloud Project.You can also create these rules manually via Google Cloud Dashboard.
Make sure you are in root directory and run:
sudo apt-get update
sudo apt-get upgrade
(this might take a few minutes)sudo apt-get install nginx
- Edit the Nginx configuration to create a reverse proxy to your Next.js app.
/etc/nginx/sites-available/nextjs.conf
sudo nano /etc/nginx/sites-available/nextjs.conf
and paste the following configuration.Make sure to include your VM’s IP in this config file!
server {
listen 80;
# Include your VM external IP below
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;
}
}
- Create a symbolic link to enable the configuration:
- Create symbolic link by running:
sudo ln -s /etc/nginx/sites-available/nextjs.conf /etc/nginx/sites-enabled/
- Delete default ngnix config:
rm /etc/nginx/sites-enabled/default
- You can check whether that worked using
ls /etc/nginx/sites-enabled/
. This should only log file that you created. - Verify Symlink:
ls -l /etc/nginx/sites-enabled/
- Test ngnix config:
sudo nginx -t
- Restart Nginx for the changes to take effect:
sudo service nginx restart
- Check whether ngnix is working
Your app should now be available not only on port 3000, but also on your VM’s external IP (without port). If you configured DNS for your domain - your app should be avaiable under your domain as well.
If while creating symbolic link you are running into this error:
ln: failed to create symbolic link '/etc/nginx/sites-enabled/nextjs.conf': File exists”
Tt means that you already have a config file in /etc/nginx/sites-enabled/.
sudo rm /etc/nginx/sites-enabled/nextjs.conf
and try againg.sudo nginx -t
you might get this error:nginx: [warn] conflicting server name "35.208.197.116" on 0.0.0.0:80, ignored nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
/etc/nginx/sites-enabled/nextjs.conf
and try again. You should only have one nextjs.conf file in /etc/nginx/sites-available/
directory.- Install certbot:
sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx
certbot --version
- Obtain a Certificate:
sudo certbot certonly --nginx -d your_domain.com
your_domain.com
with your actual domain. This command will prompt you to enter an email address and agree to the terms of service.- Update ngnix configuration
sudo nano /etc/nginx/sites-available/nextjs.conf
and paste the following configuration.Make sure to include your VM’s IP and domain name in this config file!
server {
listen 443 ssl;
listen [::]:443 ssl;
# Replace YOUR_SERVER_IP with your VM's IP address
server_name YOUR_DOMAIN_IP;
# Configuration for SSL certificate
# Replace YOUR_DOMAIN.NAME with your domain name
ssl_certificate /etc/letsencrypt/live/YOUR_DOMAIN_NAME/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOUR_DOMAIN_NAME/privkey.pem;
# Additional SSL configurations go here...
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;
}
}
# Redirect HTTP traffic to HTTPS
server {
listen 80;
listen [::]:80;
server_name YOUR_DOMAIN_IP;
return 301 https://$host$request_uri;
}
- Setup Automatic Certificate Renewal:
Let's Encrypt certificates expire after 90 days, so it's crucial to set up automatic renewal. Certbot can automatically renew certificates that are near expiry.
sudo certbot renew --dry-run
- Check Firewall Rules
Ensure that your firewall allows traffic on both port 80 and 443.
- Verify SSL Configuration:
Sometimes when leave your VM running overnight, the following might happen:
- the app with stop working on IP and connected domain
- SSH access will be unavailable
To avoid that, simply add the following key-value pair to VM’s metadata:
- key:
enable-guest-attributes
- value:
true
You can also simply restart the VM and it will (temporarily) fix the issue.
While in app folder:
- stop pm2 process:
pm2 stop process_id
- pull changes from remote origin:
git pull
- rebuild your app:
npm run build
- restart pm2 process:
pm2 restart process_id
Following instruction is valuable for apps with a lot of static content that is often updated.
Note, that running this script will cause app downtime.
- Create a file (script) called
update_and_restart.sh
usingnano update_and_restart.sh
- Add following code to this file:
#!/bin/bash
# Navigate to your project directory
cd path/to/your/nextjs/app
// Stop pm2 process
pm2 stop all
// Pull the latest changes from the GitHub repo
git pull origin main
// Install or update dependencies
npm install
// Build your Next.js app
npm run build
// Restart pm2 process
pm2 restart all
- Make the script executable by running
chmod +x update_and_restart.sh
- Test the script by running
./update_and_restart.sh
This should execute the steps in your script and update your Next.js app.
- Schedule the Script with Cron
- Open the crontab configuration by running:
sudo crontab -e
- Add the following line to schedule the script to run every day at 23:59:
59 23 * * * root/update_and_restart.sh
Now, your script will automatically run at 23:59 every day to stop pm2, pull the latest changes, build the app, and restart pm2.
- Install VS Code extension called Remote - SSH from Microsoft
- Open comand pallete using
ctrl + shift + p
and chooseRemote SSH: Open SSH Config file
and include the following:
Host your.server.ip HostName your.server.ip User user_name IdentityFile path_to_ssh_key
Here’s the example file:
Host 123.160.8.160 HostName 123.160.8.160 User username IdentityFile C:\Users\admin\.ssh\id_rsa
- Then from command line choose
Remote SSH: Connect to host
- Open file explorer > Open folder > Choose a folder that contains your app
Now you can edit contents of your app in VS Code 🎊
localhost:3001
. To do that:- Open Terminal
- Ports
- Forward a port
- Port: 3001, Forwarded address: localhost:3001
- Open in browser
npm install
, stuck on "reify:fsevents: timing reifyNode:node_modules/@img/sharp-darwin-"Solution - downgrade to node version 14:
npm install -g n
n v14
Source:
Hopefully, you have a good understanding of how to deploy a Next.js app to a server. Now, let's have some fun and test your knowledge.
Can you deploy and self host a Next.js app on a virtual machine for free?
Do you need to have a domain name to deploy to a VM?
What do you need to install before building and deploying and self hosting your Next.js app on VM?
Kudos to the following people/organisations for their valuable resources:
- Free cloud providers list
- How to Deploy a Next.js app to a Custom Server - NOT Vercel! (Full Beginner Tutorial)
- YouTube video: Deploy your Next.js app to a VPS (EASY!)
- Google Cloud Platform (Free Tier) WordPress Setup
- YouTube Video: Deploy Next.js on AWS Ubuntu Server
- SSH into Remote VM with VS Code | Tunneling into any cloud | GCP Demo