mailwal

🚀 Pragmatic Self-Hosting is open

Milan Jovanović

You can build a useful app and still feel unsure about putting it on your own server.

Getting it online leaves more questions. Which ports should be public? How do you ship the next change? Will your backup actually restore? Piecing together those answers can keep a project on your laptop long after the code is ready.

I use apps I host myself to collect testimonials and transcribe recordings for my business. They replaced my Senja and TurboScribe subscriptions, which cost $590 and $120 a year. That's $710/year in subscriptions - now running on a $5/mo VPS.

I use Dokploy for my own applications. Owning those tools means I can change them to fit how I work and choose how they're hosted. It also makes me responsible for their updates, access, and recovery. These are the deployment foundations I want to teach you.

If you already build software, learning to operate it gives you another option: launch a small product or run your own business tools on infrastructure you control. You'll also learn when managed hosting is the better choice.

I've opened enrollment for Pragmatic Self-Hosting: Foundations to help you make that step.

After working through it, you'll be able to deploy a complex app, explain how traffic and administration reach it, verify a release, and test recovery of its data. You'll have a repeatable process to adapt to your next project.

Launch week starts today. The launch price is $149 (regular price $249).

You'll practice on TaskFlow, the included .NET and React app. This is the system you'll understand and operate by the end:

One VPS hosts Traefik, TaskFlow, PostgreSQL, Redis and Dokploy. Public HTTPS enters through Traefik. Tailscale protects administration and CI requests. GHCR supplies images, R2 stores backups, and an external monitor checks readiness.
Modules 0–9 · The system you will build. One VPS, one failure domain. Open the map on the course page to explore each route.

Each part has a check you perform yourself.

Start by deciding when self-hosting makes sense for your app, including the running costs and responsibility that come with one VPS.

You'll provision an Ubuntu VPS on Hetzner and control who can reach it. Configure the firewall and key-only SSH.

You'll keep administration off the public internet. Use Tailscale for SSH and Dokploy, verify the private route, then remove public SSH access.

Inside the Tailscale private network, your computer, with its tailnet identity and SSH key, has private routes to SSH on port 22 with key authentication and to the Dokploy panel and API on port 3000. No public SSH or panel.
Module 4 · Private operator access through Tailscale. Before relying on the private route, you confirm tailnet SSH works and public SSH fails.

With Docker and Dokploy, you can configure and deploy an app through a panel that stays private and requires two-factor authentication.

Dokploy, the panel and API on port 3000, deploys and configures the TaskFlow container. Traefik, which carries user requests, and the data services are dimmed.
Module 5 · Dokploy configures and deploys TaskFlow. You check that the panel loads over Tailscale, fails on the public address and requires two-factor authentication.

Trace a request through your domain, HTTPS and Traefik to the app container. Understand why PostgreSQL and Redis stay on a private Docker network, and how a running container can still be unready to serve traffic.

Traefik terminates TLS and forwards HTTP to TaskFlow on dokploy-network. TaskFlow also joins the Compose project network, where PostgreSQL stores data on a named volume and Redis is a disposable cache. No app, database or cache ports are published.
Module 6 · Overlapping Docker networks share only the app. Check HTTPS and a saved task. Stop PostgreSQL and watch readiness fail, then recover when the database restarts. The app process keeps running throughout.

For each release, you'll test the exact image, publish it to GHCR and use a private deployment request to Dokploy. You'll be able to verify the live commit SHA and roll back an image, while understanding why the database schema must remain compatible. GitHub Actions automates the release pipeline.

GitHub Actions tests, builds and smoke-tests one image per commit and publishes it to GHCR, a private registry, with a full SHA tag and image digest. The VPS pulls the image into Docker with separate registry credentials. The short-lived CI identity is dimmed.
Module 7 · A tested image reaches the server through GHCR. The running app must report the full commit SHA you pushed, and the previous one after a rollback.

You'll prove a database backup can restore. Send separate Dokploy and PostgreSQL backups to R2, restore an exact dump into a temporary database, and compare its schema and rows.

Dokploy archives and PostgreSQL pg_dump backups go separately to Cloudflare R2. One exact object is restored into the taskflow_verify test database, beside the live taskflow database, and its schema and row counts are verified.
Module 8 · Off-site backups and a restore into a temporary database. Restore one exact dump into a temporary database and compare its schema and row counts without replacing the live database.

You'll know when the app stops serving traffic and what to check next. Set up external readiness alerts, inspect logs and resource use, and follow a maintenance checklist for updates and backup checks.

An external monitor checks /health/ready from outside the server, through the provider firewall, Traefik and TaskFlow. It sends DOWN and UP notifications to your alert channel.
Module 9 · External readiness checks and a separate alert channel. You check that the external monitor is green and that test DOWN and UP alerts arrive.

You'll keep the TaskFlow source code, diagrams and runbook so you can repeat the process. I also included seven agent skills for assisting with taught tasks. You'll learn the manual workflow and verify results yourself.

The launch price is $149 (regular price $249).

~ Milan

P.S. You can reply to this email if you've got any questions.

Milan Jovanović