02 Jun 2026
If you have not chosen a VPS provider, the following are what I've used
So now you got yourself an account on a VPS provider and you plan to host your dream project(s) on it. Well done, fellow traveller. Let our journey commence!
Most VPS offerings start from a VM that has
This should roughly cost us around 5-6 USD a month.
By all means this should be more than enough for most hobby projects. How much RAM and CPU are your projects consuming when they're running on your laptop/desktop? This should be the only deciding factor for choosing the specs of your VM.
As for the operating system, if you are unsure, choose Debian GNU\Linux.
When we provision a VPS, we get a public IP assigned to our VPS/server. To connect to the VPS we'd do something like this from our terminal
dev$ ssh root@12.13.14.15
Digitalocean for example, enables root logins via ssh to our new server. For other VPS providers it maybe different. They may provide a normal user which can sudo to root.
dev$ ssh admin@12.13.14.15
If you need to specify the private key file, the command would look like
dev$ ssh admin@12.13.14.15 -i /path/to/private-key.pem
There's a handy config that let's you not worry about ips and usernames while connecting to a server.
Create $HOME/.ssh/config if it does not exist and add the following at the end
Host myvps
Hostname 12.13.14.15
User admin
IdentityFile /path/to/private-key.pem
And then you could do
dev$ ssh myvps
and we are in our server. Please note that the .ssh folder and its content needs to have their permissions in order.
If we screw up, run the following
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_*
chmod 600 ~/.ssh/config
chmod 644 ~/.ssh/*.pub
I usually keep all of my private and public keypairs in the $HOME/.ssh folder, so that it becomes easy for me to do backups.
Lets login into the VM and get cracking
server$ sudo apt update
server$ sudo apt upgrade
This updates and upgrades the installed software in our server. Now, we'll install a few handy tools to make our server management life a bit easier.
server$ sudo apt install tmux neovim git curl htop
tmux should be the first thing you run when you login into a VPS. Why? In the event that our ssh connection dies,
any commands we were running on the server would still be running inside tmux. Let's configure it a bit
server$ nano ~/.tmux.conf
Put the following in it
# Index starts from 1
set-option -g base-index 1
set-option -g pane-base-index 1
# Renumber windows when a window is closed
set-option -g renumber-windows on
# no login shell
set -g default-command "${SHELL}"
# 256-color terminal
set -g default-terminal "tmux-256color" # use 256 colors instead of 16
# Add truecolor support (tmux info | grep Tc)
set-option -ga terminal-overrides ",xterm-256color:Tc"
# Mouse
set-option -g mouse on
# Reload ~/.tmux.conf
bind-key R source-file ~/.tmux.conf \; display-message "Reloaded!"
Ctrl+o and Ctrl+x to save and quit out of nano. If you roll with vim, use it to edit the
files mentioned in the guide instead of nano.
Now run tmux
server$ tmux
We should see a green bar at the bottom indicating that we are in a tmux session. Here's a quick video on how to use tmux.
Whenever I login into my server, this is what I do
server$ tmux ls
0: 1 windows (created Tue Jun 24 23:19:42 2025)
server$ tmux a -t 0 # or which ever session
If your VPS provider sets you up with the root account by default, then its a good idea to create a normal user.
server# adduser jim
Fill in the prompts that follow. The add our new user to the sudo group so that we can run sudo commands.
Replace jim with your preferred username.
server# usermod -aG sudo jim
Let's check if jim has powers by switching to the account.
server# su - jim
Let's do something only root can do, like copying over the .ssh folder in /root to jim's home folder so that jim can login via ssh.
server$ sudo cp -r /root/.ssh ~/.ssh
[sudo] password for jim:
Provide jim's password and the command should have succeeded. Fix permissions of the new folder
server$ chmod 700 ~/.ssh
server$ chmod 644 ~/.ssh/authorized_keys
Let's now try to connect to our VPS with this new user.
dev$ ssh jim@12.13.14.15
All good?
We need to make 3 changes
Have two terminals with ssh to our server open. And do the following in one of them.
server$ sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.orig
server$ sudo vim /etc/ssh/sshd_config
Backups saves lives!
Make the following changes in the file. Find the corresponding line and change it.
Port 2202
PermitRootLogin no
PasswordAuthentication no
If the line of interest has a # infront, remove it before saving. Otherwise, it'd be considered as a comment. For example
#PermitRooLogin yes
should become
PermitRootLogin no
Then save the file and exit. Restart the sshd service on the server
server$ sudo service ssh restart
Logout from one of the terminals and connect to the server like so
dev$ ssh jim@12.13.14.15 -i /path/to/private-key.pem -p 2202
If not able to connect, we have the other terminal from which we can debug. If all is well,
make our lives easy by making the appropriate edits to $HOME/.ssh/config on our dev machine.
Host myvps
Hostname 12.13.14.15
Port 2202
User jim
IdentityFile /path/to/private-key.pem
Check once again with
dev$ ssh myvps
It is hard naming things, so unfortunately the actual physical machine that runs our webapp and the software that it has to run to serve content - like our webapp, html files, images files etc is also called a web server.
This guide will use a web server program called nginx. Almost all webserver software allows one to host multiple websites on a single vps/server. Apache pioneered this technique and this is exactly what we need to host multiple projects on a single VPS.
Here's our hypothetical webapp
Assuming, we bought the domain myapp.com ofcourse.
Head over to where you purchased the myapp.com domain(namecheap/godaddy/bigrock?) and add an A record with @ as sub-domain to it.
Set the destination to our VPS's public IP.
A @ 12.13.14.15
After this is done, all requests to myapp.com, foo.myapp.com, bar.myapp.com etc will all land on our new VPS.
We are installing node.js here as our hypothetical webapp's backend needs it to run.
server$ curl -fsSL https://deb.nodesource.com/setup_23.x -o nodesource_setup.sh
server$ sudo -E bash nodesource_setup.sh
server$ sudo apt-get install -y nodejs
server$ node -v
Refer nodesource for latest instructions on how to install node if you are reading this in the very future.
Now lets set up our database.
If your application can make due with sqlite, then please go with
sqlite. Then as your application grows, switch over to an database system like postgres or mongodb.
Most ORMs used in backend codebases support switching database systems. So, a switch like this would not pose
a challenge.
server$ sudo apt install sqlite3
Put the following in ~/.sqliterc
.headers on
.mode column
Then to open a database
server:/var/projects/dbs$ sqlite3 myapp.db
b. postgres
Just a side note, its a good idea to run database systems like postgres, mariadb/mysql, mongodb etc on a separate VPS. More on that later.
Onto installing postgres. Latest instructions for doing that are available on the postgres website. Run the following one by one on the server.
sudo apt install curl ca-certificates
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
. /etc/os-release
sudo sh -c "echo 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $VERSION_CODENAME-pgdg main' > /etc/apt/sources.list.d/pgdg.list"
sudo apt update
sudo apt -y install postgresql
Next we setup a database user with which we can access the application database from the myapp backend.
server$ sudo su - postgres -c "createuser myappdbuser --pwprompt"
The above prompts you for a password for the myappdbuser.
Create the application database, if not done so already.
server$ sudo su - postgres -c "createdb myapp_db"
Now we grant acces to myappdbuser to myapp_db.
server$ sudo -u postgres psql
psql> GRANT ALL PRIVILEGES ON DATABASE myapp_db TO myappdbuser;
The connection string for your application would look like so
postgres://myappdbuser:s3cr3tPass@localhost:5432/myapp_db
If you decided to host the database on a separate vps,
server$ sudo cp /etc/postgresql/<version>/main/postgresql.conf /etc/postgresql/<version>/main/postgresql.conf.orig
server$ sudo vim /etc/postgresql/<version>/main/postgresql.conf
Look for the line that says
#listen_addresses = 'localhost'
and change it to
listen_addresses = 'localhost,10.2.3.4'
# the above is the private ip for the database vps
Restart postgres
server$ sudo service postgresql restart
Now the connection string for your application would look like so
postgres://myappdbuser:s3cr3tPass@10.2.3.4:5432/myapp_db
c. mysql/mariadb
server$ sudo apt install mariadb-server
server$ sudo mysql_secure_installation
In the prompts that follow
Enter current password for root (enter for none):
Switch to unix_socket authentication [Y/n] n
Change the root password? [Y/n] Y
Set the new root password and we're all set. Lets create a new database user instead of using root for our day to day.
server$ sudo mariadb
Next, we'll create the user
MariaDB [(none)]> GRANT ALL ON *.* TO 'admin'@'localhost' IDENTIFIED BY 'p4ssw0rd' WITH GRANT OPTION;
MariaDB [(none)]> FLUSH PRIVILEGES;
Please note, what we did here was making a user similar to root. We'll create an application specific
user next that we'll use for our myapp webapp.
MariaDB [(none)]> CREATE DATABASE myapp_db;
MariaDB [(none)]> GRANT ALL ON myapp_db.* TO 'myappdbuser'@'localhost' IDENTIFIED BY 'N3wp4ssw0rd' WITH GRANT OPTION;
MariaDB [(none)]> FLUSH PRIVILEGES;
MariaDB [(none)]> exit
The connection string for our application would look like so
mysql://myappdbuser:N3wp4ssw0rd@localhost:3306/myapp_db
If we decide to run mariadb on a separate VPS, make the following edit in /etc/mysql/my.cnf
...
bind-address = 10.2.3.4,127.0.0.1
...
Then restart the service
server$ sudo service mariadb restart
The connection string will now look like so,
mysql://myappdbuser:N3wp4ssw0rd@10.2.3.4:3306/myapp_db
The following is just a convention I use. Feel free to put your application whereever you like.
server$ sudo mkdir -p /var/projects/myapp.com/{frontend,backend}
server$ sudo chown -R $USER /var/projects/myapp.com
Now get the code into the folders. Let's start by building and copying over the frontend project from our dev machine.
dev:frontend$ npm run build
dev:frontend$ ls
./
../
dist/
src/
package.json
The dist folder is what we want to deploy. Let's make an archive of it, so that we can copy it over easily.
dev:frontend$ tar -czf dist.tar.gz -C dist/ .
Now lets copy the archive over to our server.
dev:frontend$ scp -i ~/.ssh/private-key.pem \
-P 2202 \
dist.tar.gz jim@12.13.14.15:/var/projects/myapp/frontend
Let's extract the archive
dev:frontend$ ssh myvps "cd /var/projects/myapp/frontend; tar xzf dist.tar.gz; rm dist.tar.gz; ls"
if all went well, we should see the built react project in that folder in our server. Here's a handy
deploy.sh script that has all of the above. Put this in your project root and add to .gitignore
#!/bin/sh
npm run build
tar -czf dist.tar.gz -C dist/ .
scp -i ~/.ssh/private-key.pem \
-P 2202 \
dist.tar.gz jim@12.13.14.15:/var/projects/myapp/frontend
# or
# scp dist.tar.gz myvps:/var/projects/myapp/frontend
ssh myvps "cd /var/projects/myapp/frontend; tar xzf dist.tar.gz; rm dist.tar.gz; ls"
Lets do the same for our backend code.
dev:backend$ ls
./ ../ app/ .git/ node_modules/ package.json
Include all folders and files we need into the archive
dev:backend$ tar -czf api.tar.gz node_modules app package.json
Now lets copy the archive over to our server.
dev:backend$ scp api.tar.gz myvps:/var/projects/myapp/backend
Let's extract the archive
dev:backend$ ssh myvps "cd /var/projects/myapp/backend; tar xzf api.tar.gz; rm api.tar.gz; ls"
Here's a handy deploy.sh for this as well
#!/bin/sh
# npm run build # - if its a typescript backend
tar -czf api.tar.gz node_modules app package.json
# or if its a typescript backend
# tar -czf api.tar.gz node_modules dist package.json
scp -i ~/.ssh/private-key.pem \
-P 2202 \
api.tar.gz jim@12.13.14.15:/var/projects/myapp/backend
# or
#scp api.tar.gz myvps:/var/projects/myapp/backend
# NOTE: tweak this as per your project.
ssh myvps "cd /var/projects/myapp/backend; tar xzf api.tar.gz; rm api.tar.gz; ls"
To run the backend, I use pm2. Create a file called pm2.config.cjs in /var/projects/myapp/backend
with the following. You can check this in with git.
module.exports = {
apps: [
{
name: "myapp-api",
script: "./app/index.js",
},
],
};
And then I check if my project secrets in the file /var/projects/myapp/backend/.env are all set.
Then to run the backend, its a simple
server$ npx pm2 start pm2.config.cjs
To see the logs
server$ npx pm2 logs myapp-api
This is the webserver that we talked about earlier. Install it with
server$ sudo apt install nginx
Now on to setting up a vhost files for our webapp.
server$ sudo nvim /etc/nginx/sites-available/myapp_com.conf
Put the following in it
server {
listen 80;
root /var/projects/myapp/frontend/dist;
index index.html index.htm;
server_name myapp.com;
location / {
default_type "text/html";
try_files $uri.html $uri $uri/ /index.html;
}
# assumes the backend is running on port 3000
location /api {
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;
}
}
Remember our hypothetical webapp's deployment plan?
1 and 2 are covered by the above nginx config. We need one more file for hosting our api in its own sub-domain.
server$ sudo nvim /etc/nginx/sites-available/api_myapp_com.conf
Put the following in it
server {
listen 80;
server_name api.myapp.com;
# assumes the backend is running on port 3000
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;
}
}
Now to make nginx use these configs, we need to run the following
server$ sudo ln -s /etc/nginx/sites-available/myapp_com.conf /etc/nginx/sites-enabled/myapp_com.conf
server$ sudo ln -s /etc/nginx/sites-available/api_myapp_com.conf /etc/nginx/sites-enabled/api_myapp_com.conf
We can ofcourse create the vhost files directly inside the sites-enabled folder, but it wouldnt be best practise.
Now we restart nginx
server$ sudo service nginx restart
Anytime, we make a config change to nginx, check if there are any problems with the config with
server$ sudo nginx -t
If all green, update nginx with
server$ sudo service nginx reload
By now, visiting http://myapp.com and http://api.myapp.com should have loaded our webapp and the webapp's api. But we need that sweet sweet https.
server$ sudo apt install certbot python3-certbot-nginx
server$ sudo certbot --nginx -d myapp.com -d api.myapp.com
certbot would ask a few questions and after that would give us the ssl certificates for our domains.
Note: Let's Encrypt SSL certificates(the ones that certbot sets us up with) expires after 3 months. By installing certbot
with apt, we get a systemd timer also installed for checking the certificates' expiry. We can find it with
server$ sudo systemctl list-timers | grep certbot
Always remember to keep you server patched with updates. A simple set of
server$ sudo apt-get update
server$ sudo apt upgrade
server$ sudo apt dist-upgrade
server$ sudo apt-cache clean
would be all thats needed to do this.
Just a heads up, keep an eye on database software upgrades like postgres / mysql. apt will tell you the packages
that are about to be upgraded when you run apt upgrade.
P.S always backup your database before upgrading.
And with that, well done webmaster!
Happy Hacking & have a great day!