What Is Self-Hosting in May 2026? A Practical Beginner Guide to Running Your First App on a $5 VPS
Categories: what is self hosting ( may 2026 )
What is self-hosting in May 2026, and why are so many beginners suddenly interested in it? In simple terms, self-hosting means running an app on infrastructure you control instead of relying completely on a third-party SaaS provider. That could be a notes app, RSS reader, bookmark manager, or dashboard running on a cheap $5 VPS with your own domain and backups.
For a lot of people, self-hosting stops sounding like a niche hobby the moment a subscription price goes up, a feature disappears, or an app starts locking away data that should be easy to export. At that point, the real appeal is not “being advanced.” It is control, portability, and the ability to understand exactly where your app lives and how it works.
The good news is that self-hosting in 2026 is much more beginner-friendly than it used to be. You do not need to build a complex homelab or learn Kubernetes before you start. For a first project, one Ubuntu VPS, Docker Compose, and a reverse proxy like Caddy are enough to run a real app securely on the public internet.
This guide walks you through the practical version of self-hosting: when it is worth doing, what you are responsible for, and how to deploy your first app step by step on a small VPS. By the end, you will have a clear mental model of self-hosting and a realistic path to running your first service with HTTPS, backups, and fewer beginner mistakes.
You are trying to run one useful app on a small server you control, with your own domain, your own backups, and a setup you can understand when something breaks.
The first time I configured a self-hosted app on a cheap VPS, the hard part was not Docker. It was knowing which steps actually mattered and which rabbit holes to ignore. This guide is for that exact point: you know basic Linux, but you have never put your own service on the internet.
What You'll Learn
- What self-hosting means in May 2026, and when it is actually worth your time
- How to set up a cheap Ubuntu VPS with a subdomain, SSH hardening, and a basic firewall
- How to install Docker and deploy a real app with Docker Compose
- How to put Caddy in front of your app for HTTPS without fighting certificates manually
- How to add backups, test the service, and troubleshoot the first common failures
Prerequisites
- A VPS with at least
1 vCPU,1 GB RAM, and25 GBSSD storage - Ubuntu
24.04 LTSinstalled on that VPS - A domain you control, with access to DNS records
- An SSH keypair on your laptop or workstation
- Basic comfort with Linux commands, editing files, and reading logs
- About 30 to 45 minutes if you follow this straight through
What Self-Hosting Means in May 2026
Self-hosting in 2026 is not just “running something on your own server.” It usually means running software on infrastructure you control so you can decide where the data lives, how backups work, how updates happen, and when the service changes.
That definition matters because the current reason people self-host is not ideology alone. It is usually SaaS fatigue, privacy concerns, export risk, or simple cost control. If you are paying for four or five tiny tools every month, one small VPS can replace a few of them.
You should also be honest about what self-hosting is not. It is not free, even on a $5 VPS. You pay with setup time, maintenance, security responsibility, and the occasional Saturday night spent reading logs.
Here is when self-hosting is worth it for most beginners in 2026:
- You use an app every week and the data matters to you.
- The app has a solid Docker image and clear documentation.
- You can tolerate a little downtime while you learn.
- You are willing to own backups and updates.
Here is when it is usually not worth it yet:
- The app stores money, production customer data, or business-critical email.
- You have no backup plan.
- You do not want to patch the host or read logs.
- You are doing it only because it sounds “more advanced.”
My rule is simple: start with something useful but non-catastrophic. Bookmarks, RSS, notes, recipes, and personal dashboards are good first apps. Your company auth stack is not.
Step 1: Choose a $5 VPS and Point a Subdomain to It
A cheap VPS is enough for your first app. In 2026, a basic instance from a mainstream VPS provider usually gives you enough CPU and memory for a lightweight container, reverse proxy, and small backup job.
For a first setup, use one server and one subdomain. Do not start with a swarm, a Kubernetes cluster, or five containers you found in a Reddit thread. That is how people turn a clean first project into abandoned infrastructure.
Pick the server
Choose Ubuntu 24.04 LTS when the provider asks for an image. After the server comes up, connect as root using the IP address your provider gives you.
ssh root@YOUR_SERVER_IP
Expected output:
Welcome to Ubuntu 24.04 LTS (GNU/Linux ...)
root@your-vps:~#
Check the OS version so you know you are following the rest of this guide on the same base system.
cat /etc/os-release
Expected output:
PRETTY_NAME="Ubuntu 24.04 LTS"
VERSION_ID="24.04"
Create a DNS record
Create an A record for a subdomain like links.yourdomain.com pointing to your server IP. I like using a dedicated subdomain for each app because it keeps reverse proxy configs simple and makes future moves easier.
After you add the record, test DNS resolution from your own machine.
dig +short links.yourdomain.com
Expected output:
203.0.113.10
If you do not have dig, this works too:
nslookup links.yourdomain.com
Expected output:
Name: links.yourdomain.com
Address: 203.0.113.10
Do not continue until DNS resolves to the right IP. HTTPS setup later depends on this.
Step 2: Harden Ubuntu Before You Deploy Anything
A fresh VPS is not “secure enough because it is obscure.” The bots will find it fast, especially on port 22. Your first job is reducing the obvious risk before you expose an app.
1. Update the system
Start with packages and security updates.
apt update
Expected output:
Fetched ...
Reading package lists... Done
apt upgrade -y
Expected output:
Setting up ...
Processing triggers for ...
2. Create a non-root sudo user
Running your day-to-day admin work as root is lazy and dangerous. Create a regular user and give it sudo access.
adduser tawkir
Expected output:
Adding user `tawkir' ...
Adding new group `tawkir' ...
Adding new user `tawkir' ...
usermod -aG sudo tawkir
Expected output:
No output is normal here.
Verify the group membership:
id tawkir
Expected output:
uid=1000(tawkir) gid=1000(tawkir) groups=1000(tawkir),27(sudo)
3. Install your SSH public key
On your local machine, print your public key so you can copy it.
cat ~/.ssh/id_ed25519.pub
Expected output:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... yourname@laptop
On the server, create the .ssh directory for the new user.
mkdir -p /home/tawkir/.ssh
Expected output:
chmod 700 /home/tawkir/.ssh
Expected output:
Add your public key to authorized_keys.
nano /home/tawkir/.ssh/authorized_keys
Expected output:
GNU nano ...
Paste the public key, save the file, then fix ownership and permissions.
chmod 600 /home/tawkir/.ssh/authorized_keys
Expected output:
chown -R tawkir:tawkir /home/tawkir/.ssh
Expected output:
Now test login from your local machine in a new terminal before you disable root SSH.
ssh tawkir@YOUR_SERVER_IP
Expected output:
Welcome to Ubuntu 24.04 LTS (GNU/Linux ...)
tawkir@your-vps:~$
4. Lock down SSH
Open the SSH daemon config.
sudo nano /etc/ssh/sshd_config
Use settings like this:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
Restart SSH after saving.
sudo systemctl restart ssh
Expected output:
Check the service status:
sudo systemctl status ssh --no-pager
Expected output:
Active: active (running)
This will not work if you disable password authentication before confirming key login works. Test first, then tighten.
5. Enable UFW
Open only what you actually need: SSH, HTTP, and HTTPS.
sudo ufw allow OpenSSH
Expected output:
Rule added
Rule added (v6)
sudo ufw allow 80/tcp
Expected output:
Rule added
Rule added (v6)
sudo ufw allow 443/tcp
Expected output:
Rule added
Rule added (v6)
sudo ufw enable
Expected output:
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
Verify the rules:
sudo ufw status
Expected output:
Status: active
To Action From
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
6. Install fail2ban
Fail2ban is not magic, but it is still a cheap way to slow down noisy SSH brute-force attempts.
sudo apt install fail2ban -y
Expected output:
Setting up fail2ban ...
sudo systemctl enable --now fail2ban
Expected output:
Created symlink ...
sudo systemctl status fail2ban --no-pager
Expected output:
Active: active (running)
Step 3: Install Docker and the Compose Plugin
Docker is the fastest path for a beginner because it keeps app setup reproducible. You are not hand-installing dependencies, and when you move the app later, the same compose file mostly comes with you.
Install the required packages first.
sudo apt install ca-certificates curl gnupg -y
Expected output:
Setting up ca-certificates ...
Setting up curl ...
Create the Docker keyring directory.
sudo install -m 0755 -d /etc/apt/keyrings
Expected output:
Download Docker’s official GPG key.
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
Expected output:
Fix the permissions.
sudo chmod a+r /etc/apt/keyrings/docker.asc
Expected output:
Add the Docker repository.
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Expected output:
Refresh package metadata.
sudo apt update
Expected output:
Hit:1 ...
Get:2 https://download.docker.com/linux/ubuntu ...
Reading package lists... Done
Install Docker Engine, CLI, and Compose plugin.
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y
Expected output:
Setting up docker-ce ...
Setting up docker-compose-plugin ...
Verify Docker itself:
sudo docker version
Expected output:
Client: Docker Engine - Community
Server: Docker Engine - Community
Verify Compose:
sudo docker compose version
Expected output:
Docker Compose version v2...
Add your user to the Docker group so you do not need sudo for every command.
sudo usermod -aG docker tawkir
Expected output:
Apply the new group membership by logging out and back in.
exit
Expected output:
logout
Connection to YOUR_SERVER_IP closed.
Reconnect:
ssh tawkir@YOUR_SERVER_IP
Expected output:
Welcome to Ubuntu 24.04 LTS ...
Check group membership:
id
Expected output:
uid=1000(tawkir) gid=1000(tawkir) groups=1000(tawkir),27(sudo),999(docker)
Step 4: Deploy Your First App With Docker Compose
For a first self-hosted app, I recommend Linkding instead of Vaultwarden. Vaultwarden is excellent, but a bookmark app is a lower-stakes first deployment. You still learn DNS, Docker, reverse proxying, volumes, and backups without putting your password vault on day-one infrastructure.
1. Create the project directory
mkdir -p ~/apps/linkding
Expected output:
cd ~/apps/linkding
Expected output:
2. Create the app secret
Generate a real secret value on the server.
openssl rand -hex 32
Expected output:
4f3d2b6e8b4d7c9f1a2e3d4c5b6a7980112233445566778899aabbccddeeff00
Save that in an environment file.
nano .env
Use content like this:
LD_SUPERUSER_NAME=admin
LD_SUPERUSER_PASSWORD=change-this-right-now
LD_DB_ENGINE=sqlite
LD_SECRET_KEY=4f3d2b6e8b4d7c9f1a2e3d4c5b6a7980112233445566778899aabbccddeeff00
You should replace the admin password with your own real value before deployment.
3. Write the compose file
nano compose.yaml
Use this:
services:
linkding:
image: sissbruecker/linkding:latest
container_name: linkding
restart: unless-stopped
env_file:
- .env
volumes:
- ./data:/etc/linkding/data
ports:
- "9090:9090"
This does two important things. It persists data in ./data, and it exposes the app on port 9090 locally on the server so you can test it before HTTPS.
4. Start the container
docker compose up -d
Expected output:
[+] Running 2/2
✔ Network linkding_default Created
✔ Container linkding Started
Check that the container is up:
docker ps
Expected output:
CONTAINER ID IMAGE STATUS PORTS
abc123def456 sissbruecker/linkding:latest Up 10 seconds 0.0.0.0:9090->9090/tcp
Check the logs if you want to confirm startup:
docker compose logs --tail=50
Expected output:
linkding | Starting gunicorn ...
linkding | Listening at: http://0.0.0.0:9090
Test locally on the server before you touch the proxy layer.
curl -I http://127.0.0.1:9090
Expected output:
HTTP/1.1 302 Found
Server: gunicorn
Location: /login/
That redirect is a good sign. The app is alive.
Step 5: Put Caddy in Front for HTTPS
You can use Nginx Proxy Manager if you prefer a web UI, but Caddy is cleaner for a first deployment. You write a tiny config, point it at your app, and it handles Let’s Encrypt automatically.
1. Create a directory for Caddy
mkdir -p ~/apps/caddy
Expected output:
cd ~/apps/caddy
Expected output:
2. Create the Caddyfile
nano Caddyfile
Use this:
links.yourdomain.com {
reverse_proxy 127.0.0.1:9090
}
Use the real subdomain you already pointed at your VPS.
3. Create the compose file for Caddy
nano compose.yaml
Use this:
services:
caddy:
image: caddy:2
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./data:/data
- ./config:/config
4. Start Caddy
docker compose up -d
Expected output:
[+] Running 1/1
✔ Container caddy Started
Check the logs while it requests certificates.
docker compose logs --tail=100
Expected output:
caddy | serving initial configuration
caddy | obtaining certificate
caddy | certificate obtained successfully
Now test the public endpoint from the server itself:
curl -I https://links.yourdomain.com
Expected output:
HTTP/2 302
server: Caddy
location: /login/
At this point, you should also open the subdomain in your browser. You should see the Linkding login page with a valid HTTPS certificate.
Practical Example: What You Just Built
You now have a real self-hosted service with the pieces that matter in production-lite form. The app runs in a container, the data lives on disk outside the container, HTTPS is automatic, and the public traffic goes through a reverse proxy instead of exposing every app directly.
That may not sound dramatic, but it is the core pattern behind a lot of self-hosting in 2026. Swap Linkding for a notes app, dashboard, photo service, or RSS reader, and the structure stays almost the same.
If you check both containers, you should see the pattern clearly.
docker ps
Expected output:
CONTAINER ID IMAGE STATUS PORTS
abc123def456 sissbruecker/linkding:latest Up ... 0.0.0.0:9090->9090/tcp
789ghi012jkl caddy:2 Up ... 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
This is why self-hosting becomes practical once you learn one stack well. You do not re-learn everything per app. You re-use a stable deployment pattern.
Step 6: Add Backups Before You Forget
Beginners usually delay backups because “it is just one app.” Then the first broken volume permission, accidental delete, or failed move teaches the lesson the hard way.
For this setup, the minimum backup set is:
- The app data directory
- The
.envfile - The
compose.yamlfile - The
Caddyfile
Create a local backup directory first.
mkdir -p ~/backups
Expected output:
Create a compressed backup file of the Linkding app directory.
tar -czf ~/backups/linkding-$(date +%F).tar.gz -C ~/apps linkding
Expected output:
Create a compressed backup of the Caddy directory too.
tar -czf ~/backups/caddy-$(date +%F).tar.gz -C ~/apps caddy
Expected output:
Verify the files exist:
ls -lh ~/backups
Expected output:
-rw-rw-r-- 1 tawkir tawkir ... caddy-2026-05-28.tar.gz
-rw-rw-r-- 1 tawkir tawkir ... linkding-2026-05-28.tar.gz
If you want one very simple scheduled backup, use cron.
crontab -e
Add this line:
0 3 * * * tar -czf /home/tawkir/backups/linkding-$(date +\%F).tar.gz -C /home/tawkir/apps linkding && tar -czf /home/tawkir/backups/caddy-$(date +\%F).tar.gz -C /home/tawkir/apps caddy
This is still not a complete backup strategy. Production needs off-server backups too, because a perfect backup on a dead VPS is not a backup.
Step 7: Verify the Service the Right Way
A browser test is useful, but it is not enough. You want at least four checks: DNS, HTTPS, containers, and logs.
1. Verify DNS
dig +short links.yourdomain.com
Expected output:
203.0.113.10
2. Verify HTTPS
curl -I https://links.yourdomain.com
Expected output:
HTTP/2 302
server: Caddy
3. Verify containers
docker ps
Expected output:
STATUS
Up ...
Up ...
4. Verify logs
docker logs linkding --tail=50
Expected output:
Listening at: http://0.0.0.0:9090
docker logs caddy --tail=50
Expected output:
server is listening only on the HTTPS port but has no TLS connection policies
certificate obtained successfully
If one of those four checks fails, that narrows the problem fast. That is the difference between random guesswork and actual troubleshooting.
Troubleshooting the First Problems You Will Probably Hit
DNS is not resolving
This usually means the record is wrong or has not propagated yet. Check the subdomain again and confirm it returns your VPS IP.
dig +short links.yourdomain.com
Expected output:
203.0.113.10
If this returns nothing, fix DNS first. Caddy cannot get a certificate for a name that does not resolve.
Port 80 or 443 is blocked
If HTTP or HTTPS never responds, check UFW and confirm both ports are open.
sudo ufw status
Expected output:
80/tcp ALLOW
443/tcp ALLOW
Some VPS providers also have their own cloud firewall. I ran into this once on a fresh server where UFW was correct, but the provider-level firewall still blocked 80 and 443.
Container restart loop
If a container keeps restarting, inspect it directly.
docker ps -a
Expected output:
STATUS
Restarting (1) ...
Then read the logs.
docker logs linkding --tail=100
Expected output:
Traceback ...
Permission denied
Restart loops are often config mistakes, bad environment variables, or volume permission issues.
Bad volume permissions
This is a common one with self-hosted apps. The container starts, but it cannot write to the mounted directory.
Check ownership of the data directory:
ls -ld ~/apps/linkding/data
Expected output:
drwxr-xr-x 2 tawkir tawkir ...
If ownership looks wrong after experimenting with sudo, fix it:
sudo chown -R tawkir:tawkir ~/apps/linkding/data
Expected output:
Then restart the app:
docker compose -f ~/apps/linkding/compose.yaml restart
Expected output:
Container linkding Restarting
Container linkding Started
What to Self-Host Next in 2026
Once you have one app running cleanly, you have enough skill to host a few more practical services. Good next candidates are Linkding, FreshRSS, Homepage, Mealie, Immich, or a small uptime monitor.
What I would not self-host yet is anything that would seriously hurt if you misconfigured it. That includes primary email, business auth, critical databases for paying users, or your family password vault if you still do not trust your backup and restore process.
The useful progression looks like this:
- One low-risk personal app
- Reverse proxy and HTTPS
- Backups and restore testing
- Monitoring and update workflow
- More sensitive apps later
That order saves you from the classic beginner mistake of hosting important data before you know how to recover it.
Next Steps
Today, do one concrete thing: buy or reuse a cheap VPS, create one subdomain, and deploy Linkding exactly as shown here. If you can get one app online with HTTPS and a working backup archive, you are no longer “reading about self-hosting.” You are doing it.
Comments (0)