# How to Learn Self-Hosting as a Developer

> Take one small app through the full cycle on a rented VPS. The order to learn self-hosting in, what to host first, and how to tell each stage is done.

Published: 2026-11-04. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/how-to-learn-self-hosting

To learn self-hosting as a developer, take one small application through the full operating cycle on a rented VPS: run it in Docker, secure the server, serve it over HTTPS, automate releases, restore a backup, and monitor it from outside.
Use disposable data until the restore works.

I self-host [**Katabench**](https://katabench.com), a .NET coding platform with a few thousand users, along with my internal tools and a Grafana observability stack.
This guide follows the order I teach.
Each stage below ends with a check you can run, so you know when to move on.

## What Is Self-Hosting?

**Self-hosting** means running software on infrastructure you manage instead of paying a platform to run it for you.
That can be a computer at home, but for a developer shipping a web application it's usually a rented **virtual private server (VPS)**.
The provider operates the physical hardware, and you choose the operating system, the runtime, and everything above it.

|  | Home server | Rented VPS | Managed platform |
| --- | --- | --- | --- |
| You operate | Hardware, power, network, OS, and apps | OS and apps | Your app and its configuration |
| Public access | Port forwarding or a tunnel | A public IP by default | Handled for you |
| Starting cost | Hardware you may already own | Around $6 per month for 2 cores and 4 GB at Hetzner | Priced per app or per resource |
| Best for | Household services and learning hardware | Learning to deploy and run web applications | Shipping without operating servers |

If you want to run services from your own hardware, [**Cloudflare Tunnel**](https://milanjovanovic.tech/blog/cloudflare-tunnel-hide-backend-ip) solves the networking part.
The rest of this guide assumes a VPS, because it lets you focus on your application instead of your home connection.

## Is Self-Hosting Worth Learning?

Managed services from Azure or AWS are convenient and reliable, and I've used them on many projects.
You pay for the infrastructure, the operational work, and the provider's margin.
Self-hosting can be around 10 times cheaper for the same setup, and you pay the difference with your time.

Updates, access, backups, and recovery all become your job.
A single server is also a single point of failure, so a hardware fault takes everything on it offline.

Self-hosting suits an application that can accept some downtime and has someone who owns the server.
Stay on a managed service if nobody can own recovery, or if your uptime target is higher than one machine can deliver.

## What Should You Self-Host First?

Start with an application you already understand, with a web interface and a database.
You need something that can save a record, read it back, and report which version is running.
Learning a new framework at the same time adds another possible explanation every time a request fails.

A side project or a small internal tool works well.
So does a paid tool you could replace, which is how I ended up running [**my own transcription app**](https://milanjovanovic.tech/blog/speech-to-text-in-dotnet-with-assemblyai) instead of renewing a $120 subscription.

Leave email for later, because deliverability is its own discipline.
Keep real customer data off the server until you have restored a backup at least once.

## The Self-Hosting Roadmap

Work through these in order.
Each stage has a concrete result that tells you it's finished.

| Stage | What you learn | You are done when |
| --- | --- | --- |
| 1. Docker | Images, containers, Compose, volumes | The app and database start from one file, and data survives recreating the containers |
| 2. Server access | SSH keys, a firewall, a non-root user | A new session logs in with a key and password login fails |
| 3. Private administration | A VPN or SSH tunnel | SSH and the deployment panel work privately and time out from the public internet |
| 4. Deployment | DNS, a reverse proxy, HTTPS, a deployment platform | The real domain serves the app with a valid certificate and no database port is published |
| 5. Releases | CI, image tags, rollback | You can name the commit running in production and redeploy the previous image |
| 6. Backups | Off-site dumps and restores | A stored backup restores into a scratch database and your data checks pass |
| 7. Monitoring | External checks, alerts, maintenance | A controlled failure sends an alert you actually receive |

## Stage 1: Run the App in Docker Before You Touch the Server

Get the application and its database running from one Compose file on your own machine.
If you work in .NET, [**Docker for .NET developers**](https://milanjovanovic.tech/blog/docker-dotnet-developers) covers the image and Compose basics, and you can [**skip the Dockerfile**](https://milanjovanovic.tech/blog/containerize-your-dotnet-applications-without-a-dockerfile) entirely with the SDK.

You need to know three things here: an image can be rebuilt, a container can be replaced, and a volume holds the data that must survive.
Prove the last one before moving on:

```bash
docker compose up -d --wait
# Save a record through the app, then replace the containers.
docker compose down
docker compose up -d --wait
```

The record should still be there.
Running `docker compose down -v` deletes the volume too, so try that only with data you can lose.

## Stage 2: Lock Down Server Access

Create an SSH key on your own machine and upload only the public half:

```bash
ssh-keygen -t ed25519 -a 64 -f ~/.ssh/prod_ed25519
```

Then check what the SSH server actually applies, because override files can change the result:

```bash
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication) '
```

A fresh `openssh-server` package on Ubuntu 24.04 reports this:

```text
permitrootlogin without-password
passwordauthentication yes
```

Your provider's image may differ, which is why you check.
You want `no` on both lines after you have created a separate admin user with sudo access.
Test that login in a second terminal before you close the first, and keep the provider's recovery console available.

Put a firewall at the provider level that allows ports `80` and `443`, plus SSH from your IP address.
A host firewall is not enough here, because [**Docker can route published ports around UFW**](https://milanjovanovic.tech/blog/docker-bypasses-ufw).
The [**Ubuntu server hardening checklist**](https://milanjovanovic.tech/blog/ubuntu-server-hardening) has the full list for this stage.

## Stage 3: Keep Administration Private

Users need ports `80` and `443`.
You are the only one who needs SSH and the deployment panel, so they don't belong on the public internet.

A [**Tailscale network**](https://milanjovanovic.tech/blog/build-your-own-vpn-with-tailscale) handles this, and Katabench runs the same idea on **Headscale**.
Joining a private network doesn't close a public port by itself, so remove the public firewall rule once private access works.

Your check runs from outside the private network:

```bash
curl -m 5 http://your-server-ip:3000
```

It should time out.
A response means your panel is reachable by anyone who finds it.

## Stage 4: Deploy Behind a Reverse Proxy

Before you automate anything, be able to explain how a browser reaches your application.
DNS resolves the hostname, a **reverse proxy** accepts HTTPS traffic, and the proxy forwards the request to your container.
The application reaches its database over an internal network that the proxy never touches.

This is the single-server system I teach, with public traffic, private administration, and recovery kept apart:

![An application runs on one VPS with public HTTPS through Traefik, private administration through Tailscale, PostgreSQL and a disposable Redis cache on an internal network, off-site backups in Cloudflare R2, and an external readiness monitor.](https://milanjovanovic.tech/images/self-hosting/architecture.svg)

A deployment platform saves you from writing all of this by hand.
I use [**Dokploy**](https://dokploy.com), which configures [**Traefik**](https://traefik.io/traefik/) routing and certificates for you, and [**Dokploy vs Coolify**](https://milanjovanovic.tech/blog/dokploy-vs-coolify) compares the two main options.
You still need to know your application's port, configuration, and dependencies, and [**this .NET walkthrough**](https://milanjovanovic.tech/blog/deploy-dotnet-app-to-vps-with-dokploy) goes through them step by step with a runnable sample.

Check the whole path at once by requesting a readiness endpoint through the public domain:

```bash
curl -i https://app.example.com/health/ready
```

A `200` proves the certificate, the route, the application, and the database connection all work together.

## Stage 5: Automate Releases

Once a manual deployment makes sense, automate it.
Test the code, build one image, tag it with the commit SHA, and deploy that exact image.
That previous tag then becomes your rollback target, because you never have to rebuild old code during an incident.

A deployment can report success while the old version keeps running.
Expose the build's commit from the application and compare it after each release:

```bash
curl -fsS https://app.example.com/version.json
```

Practice going back to the previous image, and check database compatibility first.
Reverting an image doesn't undo a migration, and an older application may not work with the newer schema.

## Stage 6: Practice Restoring a Backup

A Docker volume survives a container replacement, but it still lives on the same disk as everything else.
Write down every durable store your application uses, including uploaded files, and copy the data off the server on a schedule.

Then restore a stored backup into a scratch database and run the queries your application needs.
[**This Postgres restore drill**](https://milanjovanovic.tech/blog/test-postgres-backup-restore) has a runnable sample, and it shows why the obvious quick check isn't enough: `pg_restore --list` exits `0` on a backup cut in half.

Restoring beside the original database checks the backup and the procedure.
It doesn't prove you can rebuild on a replacement server, which is a separate exercise.

## Stage 7: Monitor From Outside the Server

Run your uptime check from outside the VPS, or a server outage takes the monitor down with it.
Point it at your readiness endpoint, then cause a failure and confirm the alert reaches you.

Docker's own status won't tell you enough.
With the database stopped in my sample, Docker still reported the API container as `healthy` while `/health/ready` returned `503`.

Add a recurring check for updates, disk usage, backup freshness, and alert delivery.
The [**VPS deployment checklist**](https://milanjovanovic.tech/blog/vps-deployment-checklist) has the commands for all of it.

## How Much Does It Cost to Learn Self-Hosting?

Very little, if you clean up after yourself.
A 2-core, 4 GB server costs around $6 per month at [**Hetzner**](https://www.hetzner.com/cloud), and that's enough to run a control plane, an application, Postgres, and Redis together.
Most providers bill by the hour, so a practice server you delete afterwards can cost cents.

You also need a domain, or a subdomain of one you already own.
[**Tailscale**](https://tailscale.com/), [**GitHub Actions**](https://github.com/features/actions), [**Cloudflare R2**](https://developers.cloudflare.com/r2/), and [**UptimeRobot**](https://uptimerobot.com/) all have free plans.
Check current prices before you start, and remember that your time is the largest cost.

## Summary

Pick one small application and take it through all seven stages on a single VPS, with disposable data.
The order matters because each stage depends on the last.

My [**self-hosting course for developers**](https://milanjovanovic.tech/pragmatic-self-hosting) teaches this sequence on one server with Dokploy and Tailscale, and the [**first lessons are free**](https://milanjovanovic.tech/pragmatic-self-hosting/free-lessons/why-self-hosting).

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### What is the best way to learn self-hosting?

Take one small application with a database through the full operating cycle on a rented VPS: run it in Docker, lock down access, serve it over HTTPS, automate releases, restore a backup, and monitor it from outside the server. Use disposable data until the restore works.

### Do I need to know Linux to self-host?

You need to be comfortable running terminal commands, editing files, and using Git, and you should understand how a web application talks to its database. You can learn Linux administration during the project, starting with SSH, a firewall, and a non-root user.

### Is a rented VPS really self-hosting?

Yes. Self-hosting means running software on infrastructure you manage. The provider operates the physical hardware, and you are responsible for the operating system, updates, access, backups, and recovery. You do not need a server at home.

### What should I self-host first?

Start with an application you already understand that has a web interface and a database, such as a side project or a small internal tool. Leave email and anything holding real customer data until you have restored a backup at least once.

### How much does it cost to learn self-hosting?

A 2-core, 4 GB VPS costs around $6 per month at Hetzner, and most providers bill by the hour, so a short practice session can cost cents if you delete the server afterwards. You also need a domain. Tools like Tailscale, GitHub Actions, and Cloudflare R2 have free tiers. Check current prices before you start.

### Is self-hosting worth it for developers?

It is worth it when you can accept some downtime and have someone who owns updates and recovery. Self-hosting can cost a fraction of managed cloud services, and you pay the difference with your own time. It also teaches you how your application actually runs in production.
