A guide to self-hosting web apps on a VPS

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!

Table of contents

  1. Choosing a server
  2. Configuring our server
    1. tmux
    2. A new user
    3. Configuring ssh on the server
    4. Setting up a webapp on the server
  3. Finishing up

Choosing a server

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.

Configuring our server

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

1. tmux

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

2. A new user

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?

3. Configuring ssh on the server

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

4. Setting up a webapp on the server

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.

4.1 DNS

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.

4.2 Installing runtimes and dependencies

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.

a. sqlite

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

4.3 Application directories

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

4.4 Setting up nginx

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. https://myapp.com is a react.js SPA.
  2. https://myapp.com/api is a nodejs api that the above react frontend would consume.
  3. https://api.myapp.com is the same nodejs api as above.

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

4.5 Certbot HTTPS

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

Finishing up

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!