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
Choosing a server
Most VPS offerings start from a VM that has
- 2 cpu cores
- 25 GB ssd storage
- 1 GB of RAM.
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
- prevent root ssh logins
- only allow ssh logins with private keys i.e no password logins
- change port to something other than 22
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
- https://myapp.com is a react.js SPA.
- https://myapp.com/api is a nodejs api that the above react frontend would consume.
- https://api.myapp.com is the same nodejs api as above.
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.
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 announcing-litebb.md announcing-poof.md announcing-spb-lil.md autologin-sway-debian.md better-font-rendering-linux.md color-me-baby.md copy-paste-vim-slackware.md dwm-st-installation.md hello-lc230.md install-docker-linux.md javascript-callbacks-promises-async-await.md lubuntu-i3wm-awesomeness.md minimal-unix-setup-sowm.md multiple-ssh-keys-for-services.md no-more-blurry-fonts.md openbsd-getting-started.md polkit-thunar-mount.md self-hosting-webapps.md setting-up-neomutt-offlineimap-msmtp.md __skeleton.md__ slackware-install-lvm-luks.md slackware-kernel-patch-install.md slackware-post-install.md swaywm-on-debian-11.md vim-to-neovim.md xml.xml 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?
- https://myapp.com is a react.js SPA.
- https://myapp.com/api is a nodejs api that the above react frontend would consume.
- 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!























































































