Reverse Proxy Explained: One Domain, Many Containers

A reverse proxy is one program that answers every request to your server and hands each one to the right app behind it. That single sentence is why one small VPS can run your analytics, your automations, and your whiteboard at once, each on its own domain, each with HTTPS. To prove it, I booted a fresh DigitalOcean droplet, started three containers, and put all three on the internet behind one nginx. Every config and every number on this page came off that server.

TL;DR

Your server has one IP address, and browsers only really use two ports: 80 and 443. Three apps cannot all own port 443. So none of them get it. A reverse proxy (I use nginx here) takes the port, reads which domain each visitor asked for, and forwards the request to the right container on an internal port. Below: the concept in plain words, then the real build. Three containers, three subdomains, one certbot command for HTTPS on all of them. About 30 minutes to copy.

I have run this pattern in production for years. It is also the first thing my Self Hosting 2.0 course quietly sets up for you when you install Coolify: its proxy does exactly what this page teaches, just automatically. Understanding the manual version is what makes the automatic one debuggable.

New to servers? The jargon on this page, translated
Port
A numbered door on your server. Every service answers behind one: SSH behind 22, HTTPS behind 443. Browsers knock on 80 and 443 and nothing else.
Container
An app packed with everything it needs, run by Docker. Each of the three demo apps is one container.
Host header
The domain name your browser writes into every request, like a visitor telling the front desk who they came to see. Routing runs on this.
A record
A DNS entry that says "this name lives at this IP address". All three demo subdomains point at the same IP.
Loopback (127.0.0.1)
The machine's private line to itself. An app listening only here is invisible to the internet.
server block
One routing rule in nginx: "requests for this domain go to this app". One small file per app.
Let's Encrypt
A free certificate authority. Its certbot tool gets you the HTTPS padlock and renews it forever, for $0.
TLS handshake
The one-time hello where browser and server agree on encryption. Costs some milliseconds once, then the connection is reused.
What you'll have at the end
  • A clear picture of what a reverse proxy does, with the request path drawn out
  • Three real apps on one server: Uptime Kuma, Excalidraw, whoami, each on its own subdomain
  • HTTPS on all three from one certbot command, with auto-renewal proven
  • Proof from outside that the app ports themselves are unreachable
  • A measured answer to "does the proxy slow things down" (spoiler: 0.1 ms)
Before you start (for the hands-on part)
  • What you need: a small VPS running Ubuntu with root SSH, Docker installed, and a domain you control. Any provider works; I used a DigitalOcean droplet (2 vCPU / 4 GB, more than this demo needs). Still picking a box? My VPS directory compares the real prices.
  • Docker not installed yet? The official Docker install docs for Ubuntu take about five minutes.
  • Tested on: Ubuntu 24.04.4, nginx 1.24.0, Docker 29.6.2, certbot 2.9.0, on 11 August 2026.
⚙ Personalize this guide

Your domain & server IP (optional)

Type your base domain and your server's IP address. Every copyable command below updates to use them, so you can paste with no edits.

Values stay in your browser only. Nothing leaves this page.

What is a reverse proxy?

A reverse proxy is a server program that sits in front of your apps and answers every request on their behalf. It reads which domain the visitor asked for, forwards the request to the right app on an internal port, and carries the answer back. The visitor never talks to the app directly. They only ever talk to the proxy.

The analogy that made it click for me is a hotel front desk. Guests do not wander the corridors knocking on room doors. Everyone comes through one entrance, tells the desk who they are here for, and the desk routes them. The rooms do not need street addresses. The building needs one.

Here is that sentence as an actual request, using one of the demo apps you will build below:

One request, start to finish

What happens when a browser opens kuma.yourdomain.com, step by step.

Browserwants kuma.yourdomain.com
203.0.113.42:443the server's one public IP
nginxreads the Host header:
"kuma.yourdomain.com"
127.0.0.1:8081the Uptime Kuma container

The routing decision is made on the Host header, the domain name the browser writes into every request. Not the IP. Not the port. Three domains can share one IP and still land on three different apps. That is the whole trick.

"Reverse" only describes which side the proxy works for. A regular (forward) proxy works for clients going out. A reverse proxy works for servers receiving traffic in. More on that below.

The problem it solves: one IP, many apps

Say you self-host three apps on one VPS. Uptime monitoring, a whiteboard, whatever comes next month. Each runs in its own container, each listens on its own port. Without a proxy, your options look like this:

Without a reverse proxy vs with one

The same three containers, exposed two different ways.

Without · every app owns a public port

203.0.113.42:8081Uptime Kuma, no HTTPS
203.0.113.42:8082Excalidraw, no HTTPS
203.0.113.42:8083whoami, no HTTPS

With · one entrance, names instead of numbers

kuma.yourdomain.com🔒 HTTPS
draw.yourdomain.com🔒 HTTPS
whoami.yourdomain.com🔒 HTTPS

The left side is what "just publish the ports" gets you: URLs with port numbers nobody remembers, no padlock, and three app ports open to every scanner on the internet. The right side is the same server after 30 minutes of this guide.

The deeper constraint: browsers assume port 80 for http:// and 443 for https://. A URL without a port number only ever reaches those two. And a certificate for a bare IP with a port is somewhere between painful and impossible to get. So the app that owns 443 gets a clean URL and a padlock, and every other app gets :8082 stapled to its name. With three apps, nobody wins that fight.

The reverse proxy dissolves it. nginx takes 80 and 443 once, and every app becomes a name. Adding a fourth app next month costs one DNS record and one small config file. No new open ports, no new certificates to think about, as you will see below.

Reverse proxy vs forward proxy

Both are middlemen. The difference is which side they work for, and which side they hide.

Forward proxy vs reverse proxy

Same trick, opposite direction.

Forward proxy · works for the clients, hides who is asking

Many usersoffice, school, VPN
Forward proxyone outgoing identity
The internetsees the proxy, not the users

Reverse proxy · works for the servers, hides what is serving

The internetmany visitors
Reverse proxyone public entrance
Many appssee the proxy, not the visitors' mess
Forward proxyReverse proxy
Sits next tothe clientsthe servers
Hideswho is askingwhat is serving
Configured bythe user or their networkthe server owner (you)
Typical usecorporate filtering, privacy, geo-unblockingmany apps on one server, HTTPS termination, caching, load balancing
ExamplesSquid, a VPN's proxy modenginx, Caddy, Traefik, HAProxy, Cloudflare

One name from that last row worth a sentence: when you put a site behind Cloudflare, you are using a reverse proxy you rent instead of run. Their edge answers for your domain and forwards to your server, the same mechanic as the nginx you are about to configure, at planetary scale.

Reverse proxy vs load balancer

These get mixed up because the same software does both. The difference is the shape of the routing:

  • A reverse proxy sends different requests to different apps. kuma.* to the monitor, draw.* to the whiteboard.
  • A load balancer spreads identical requests across copies of the same app, so no single copy drowns.

nginx does both, sometimes in the same config file. For self-hosting a handful of different apps on one box, the reverse proxy behavior is the one you need. Load balancing starts to matter when one app outgrows one machine. Different problem, different guide.

The tools people actually use

Every tool below does the job on this page. The differences are in how much they automate:

ToolWhat it isPick it when
nginxThe workhorse. Text configs, runs everywhere, decades of answers online.You want to understand what is happening. This guide uses it for exactly that reason.
CaddyModern proxy with HTTPS built in. A two-line Caddyfile replaces a server block plus certbot.You want the shortest possible config and accept more magic.
TraefikWatches Docker and configures itself from container labels. This is what Coolify runs underneath.Containers come and go often and you want zero manual config.
HAProxyThe heavy-traffic specialist, strongest at load balancing.Raw throughput is the problem you actually have.
Nginx Proxy ManagerA web UI over nginx: add hosts and certificates by clicking.You want this page's result without editing files.

I am teaching nginx here because it hides nothing. After you have written one server block by hand, every other tool on this list becomes readable: you know what the label, the Caddyfile line, or the UI form is generating for you.

The proof: an nginx reverse proxy for three Docker apps

Enough concept. I booted a fresh Ubuntu 24.04 droplet and built the exact picture from the diagrams above. Three real apps:

AppWhat it isContainer portSubdomain
Uptime Kumaself-hosted uptime monitor127.0.0.1:8081kuma.yourdomain.com
Excalidrawthe whiteboard app127.0.0.1:8082draw.yourdomain.com
whoamitiny server that prints the request it receives, our teaching tool127.0.0.1:8083whoami.yourdomain.com

My demo ran on the lab domain lab.selfhostschool.com, which is what you will see in the screenshots and terminal output. Swap in your own domain (the personalize card above does it for you in every command).

Step 1: point three subdomains at the server

In your DNS provider's panel, create one A record per app, all pointing at the same server IP:

kuma.yourdomain.com     A    203.0.113.42
draw.yourdomain.com     A    203.0.113.42
whoami.yourdomain.com   A    203.0.113.42

Three names, one IP. This looks wrong the first time you do it. It is the point: DNS gets every visitor to the building, and the Host header gets them to the right room.

Step 2: run the containers, invisible to the internet

SSH into the server and start the three apps. The part that matters is 127.0.0.1: in every -p flag:

docker run -d --name kuma --restart unless-stopped \
  -p 127.0.0.1:8081:3001 -v kuma-data:/app/data louislam/uptime-kuma:1

docker run -d --name draw --restart unless-stopped \
  -p 127.0.0.1:8082:80 excalidraw/excalidraw:latest

docker run -d --name whoami --restart unless-stopped \
  -p 127.0.0.1:8083:80 traefik/whoami

-p 127.0.0.1:8081:3001 means: publish the container's port 3001 on the server's loopback address only. The app runs, the server can talk to it, and the internet cannot see it at all. Here is the server's view after all three started:

Three apps up, zero public ports

Output from the demo droplet, 11 August 2026.

root@cf-reverse-proxy: ~
root@cf-reverse-proxy:~# docker ps --format 'table {{.Names}}\t{{.Ports}}\t{{.Status}}'
NAMES     PORTS                      STATUS
whoami    127.0.0.1:8083->80/tcp     Up About a minute
draw      127.0.0.1:8082->80/tcp     Up About a minute (healthy)
kuma      127.0.0.1:8081->3001/tcp   Up About a minute (healthy)
root@cf-reverse-proxy:~# ss -tlnp | grep -E ':(8081|8082|8083)'
LISTEN 0  4096  127.0.0.1:8081  0.0.0.0:*  users:(("docker-proxy",pid=2779,fd=8))
LISTEN 0  4096  127.0.0.1:8082  0.0.0.0:*  users:(("docker-proxy",pid=2952,fd=8))
LISTEN 0  4096  127.0.0.1:8083  0.0.0.0:*  users:(("docker-proxy",pid=3068,fd=8))

And here is the internet's view. I probed those ports from my laptop, with port 22 as the control that proves the probe works:

Probed from outside: the apps do not exist

A raw TCP connect from my laptop to the droplet's public IP.

you@laptop: ~
you@laptop:~$ bash probe.sh 178.128.194.165
port 22:   OPEN  (the control: SSH answers, so the probe works)
port 8081: CLOSED/FILTERED  (no TCP connect within 5s)
port 8082: CLOSED/FILTERED
port 8083: CLOSED/FILTERED

This is the security half of the reverse proxy pattern: the apps themselves are unreachable. Not firewalled. Simply not listening on any address the internet can route to. There is nothing to attack yet.

The mistake this prevents

The common shortcut is -p 8081:3001 without the 127.0.0.1:. That publishes the port on every interface, straight past ufw, because Docker writes its own firewall rules. I measured what happens to exposed ports on a fresh server in the Coolify VPS hardening guide: the first uninvited SSH attempt arrived 2 minutes 35 seconds after boot. Bind to loopback and the question never comes up.

Step 3: install nginx and write one server block per app

Now the front desk. Install nginx:

apt update && apt install -y nginx

Then give each app one file in /etc/nginx/sites-available/. Here is /etc/nginx/sites-available/kuma, the fullest of the three because Kuma's live dashboard also needs websocket support (the two Upgrade lines at the end):

server {
    listen 80;
    server_name kuma.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Read it top to bottom and it says exactly what the diagram said: server_name is the Host header to match, proxy_pass is where to forward. The proxy_set_header lines pass the visitor's real address through to the app; without them every visitor would look like 127.0.0.1. That is measurable, below.

The blocks for the other two apps are the same shape minus the websocket lines. Only the name and port change. /etc/nginx/sites-available/draw:

server {
    listen 80;
    server_name draw.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8082;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

And /etc/nginx/sites-available/whoami:

server {
    listen 80;
    server_name whoami.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:8083;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

One more file, and it is my favorite detail of the whole setup. Replace the contents of /etc/nginx/sites-available/default with a catch-all that answers requests matching no app, including anyone poking the bare IP:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

444 is an nginx special: close the connection without answering at all. A scanner sweeping IP ranges gets silence. Only visitors who asked for one of your domains by name get anything back.

Enable the three sites, test the config, reload:

ln -s /etc/nginx/sites-available/kuma   /etc/nginx/sites-enabled/kuma
ln -s /etc/nginx/sites-available/draw   /etc/nginx/sites-enabled/draw
ln -s /etc/nginx/sites-available/whoami /etc/nginx/sites-enabled/whoami
nginx -t && systemctl reload nginx

Does the Host header really decide everything? Easy to prove. Ask the same IP three ways:

Same IP, three different answers

The only thing changing between these requests is the Host header.

you@laptop: ~
you@laptop:~$ curl http://178.128.194.165/
curl: (52) Empty reply from server          # bare IP, no Host match: 444, silence

you@laptop:~$ curl http://kuma.lab.selfhostschool.com/
Found. Redirecting to /dashboard            # same IP, Kuma answers

you@laptop:~$ curl -H "Host: whoami.lab.selfhostschool.com" http://178.128.194.165/
Hostname: 44232a95e32f
GET / HTTP/1.1
Host: whoami.lab.selfhostschool.com
X-Forwarded-For: 203.0.113.7              # bare IP + a Host header: routed anyway

The third request is the concept laid bare: I sent it to the raw IP but wrote the Host header by hand, and nginx routed it to whoami as if I had used the domain. Routing is the Host header. Everything else is delivery. (Client IP in the output replaced with a placeholder.)

Step 4: HTTPS on all three apps with one command

Here is where the pattern pays off hardest. Because nginx is the only thing facing the internet, HTTPS is the proxy's problem, once, not each app's problem three times. The apps keep speaking plain HTTP on loopback and never know encryption exists.

apt install -y certbot python3-certbot-nginx

certbot --nginx \
  -d kuma.yourdomain.com \
  -d draw.yourdomain.com \
  -d whoami.yourdomain.com --redirect

certbot proves to Let's Encrypt that you control the three names, gets one certificate covering all of them, rewrites the three server blocks to use it, and adds the HTTP to HTTPS redirect. On the demo box the whole thing took under a minute:

One certificate, three names, zero dollars

certbot 2.9.0 on the demo droplet.

root@cf-reverse-proxy: ~
Requesting a certificate for kuma.lab.selfhostschool.com and 2 more domains

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/kuma.lab.selfhostschool.com/fullchain.pem
This certificate expires on 2026-11-09.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for kuma.lab.selfhostschool.com to /etc/nginx/sites-enabled/kuma
Successfully deployed certificate for draw.lab.selfhostschool.com to /etc/nginx/sites-enabled/draw
Successfully deployed certificate for whoami.lab.selfhostschool.com to /etc/nginx/sites-enabled/whoami
Congratulations! You have successfully enabled HTTPS on https://kuma.lab.selfhostschool.com,
https://draw.lab.selfhostschool.com, and https://whoami.lab.selfhostschool.com

Renewal is not your job either. certbot installed a systemd timer that checks twice a day and renews when under 30 days remain. I verified both halves on the box: systemctl list-timers certbot.timer showed the next run scheduled the same night, and certbot renew --dry-run came back "all simulated renewals succeeded".

That is the build done. Three apps, live, each on its own padlocked subdomain:

Uptime Kuma's first-run setup page served over HTTPS at kuma.lab.selfhostschool.com, shown in a browser frame with the padlock visible in the URL bar
Uptime Kuma answering on its own subdomain over HTTPS, fresh out of the container. The app itself was never exposed: nginx carried this whole page over the loopback connection to port 8081.
Excalidraw's whiteboard canvas served over HTTPS at draw.lab.selfhostschool.com, shown in a browser frame
Excalidraw on the same server, same IP, same nginx. Different Host header.

What the proxy sees (and what it adds)

This is where the whoami container earns its place in the demo. Its whole job is to print the request it receives, so it shows you exactly what your app sees from behind the proxy. I asked it twice, once directly on loopback and once through nginx from the internet:

The same app, asked directly and through the proxy

whoami prints every request it receives. Spot the three new headers.

Direct · on the server, no proxy

root@cf-reverse-proxy: ~
root@cf-reverse-proxy:~# curl 127.0.0.1:8083
Hostname: 44232a95e32f
RemoteAddr: 172.17.0.1:38880
GET / HTTP/1.1
Host: 127.0.0.1:8083
User-Agent: curl/8.5.0
Accept: */*

Through nginx · via the public HTTPS domain

root@cf-reverse-proxy: ~
root@cf-reverse-proxy:~# curl https://whoami.lab.selfhostschool.com/
Hostname: 44232a95e32f
RemoteAddr: 172.17.0.1:38886
GET / HTTP/1.1
Host: whoami.lab.selfhostschool.com
User-Agent: curl/8.5.0
Accept: */*
Connection: close
X-Forwarded-For: 178.128.194.165
X-Forwarded-Proto: https
X-Real-Ip: 178.128.194.165

Those three green headers are the proxy_set_header lines from Step 3 doing their job. To the app, every request now arrives from nginx, so X-Forwarded-For and X-Real-Ip carry the visitor's actual address, and X-Forwarded-Proto says HTTPS was used. Apps that show visitor IPs or build absolute links read these. When an app's docs say "behind a reverse proxy, enable trusted proxy mode", these headers are what that setting trusts.

The proxy also gives you something quieter: one access log for everything. Every request to every app lands in /var/log/nginx/access.log, one file to watch, one file to feed fail2ban.

And that log taught me something I did not plan to put in this guide. Requesting the certificate announced my subdomains. Let's Encrypt publishes every certificate it issues to public Certificate Transparency logs (an append-only public record designed to catch bad certificates), and scanners watch those logs. The first third-party request to the new subdomains, by name, arrived about three minutes after certbot finished. A crawler identifying itself as "RecordedFuture Global Inventory Crawler" showed up four minutes in. Nobody typed those names anywhere public. The certificate was the announcement.

The demo server lived 85 minutes. In that time its access log recorded 516 requests from 75 different IP addresses. One internet-scanning service, leakix.net, sent 186 of them by itself. My own tests and the Let's Encrypt validation checks account for under 200; most of the rest was the background noise every public HTTPS site gets. None of it reached an app port, because there was nothing else listening.

Why that matters

The moment an app has a certificate, assume the internet knows its address. Apps with a setup screen (Uptime Kuma's "create admin account" page, for example) should get their admin account created immediately after they go live, not tomorrow. On my box the scanners arrived faster than a coffee break. The catch-all 444 block and loopback-only ports mean they found nothing else to touch.

Does it slow things down? I measured it

Every request now takes an extra hop, so it must cost something. I measured how much on the demo box: 20 requests to whoami direct on loopback, then 20 through nginx with HTTPS, from the server itself so network jitter stays out of the numbers.

direct to the app (loopback, no proxy) median 1.5 ms
through nginx + HTTPS, connection reused (how browsers work) mean 1.5 ms
the TLS handshake, paid once per connection 64 ms, once

Read that middle row again. With the connection kept alive, which is what every browser does, going through nginx plus encryption cost the same 1.5 ms as skipping both. The proxy hop itself is a rounding error, around a tenth of a millisecond. The only real cost is the TLS handshake, and that is the price of HTTPS, not of the proxy. You would pay it with the app exposed directly too, if the app could even do TLS.

My first measurement said something different: 70 ms per request through the proxy. That number was real but it was measuring a fresh TLS handshake on every single request, which no browser does. If you benchmark your own proxy and get scary numbers, check whether your tool is reusing connections before you blame nginx.

Do you have to hand-write this?

No. And after this page, you can evaluate the automations honestly, because you know what they generate.

Caddy collapses Steps 3 and 4 into a two-line config with automatic HTTPS. Nginx Proxy Manager puts a web UI over exactly what you just wrote. Traefik watches Docker and builds the routes itself from container labels.

And if you want the whole pattern managed, this is literally what Coolify does: it runs Traefik as the front desk, and every time you deploy an app and type a domain, it writes the routing rule and gets the certificate. The reverse proxy from this page is running under every Coolify server, including mine. You now know exactly what it is doing and where to look when it misbehaves.

That trade is the honest pitch for my Self Hosting 2.0 course: we use Coolify so the proxy work is automatic, but you learn what is underneath, which is what makes a 2 AM debugging session short.

Questions people actually ask

What is a reverse proxy in simple terms?

One program that answers every web request to your server and passes each one to the right app behind it. It reads the domain the visitor asked for and forwards the request to the matching app on an internal port. One server, one IP, many apps.

What is the difference between a forward proxy and a reverse proxy?

A forward proxy works for clients and hides who is asking: many users share one outgoing middleman. A reverse proxy works for servers and hides what is serving: many apps share one entrance. Same trick, opposite direction.

What is the difference between a reverse proxy and a load balancer?

A reverse proxy routes different requests to different apps. A load balancer spreads identical requests across copies of one app. nginx does both. For a handful of different self-hosted apps on one box, you want the reverse proxy behavior.

Can I route by path instead of subdomain?

Yes, a location /kuma/ block per app works. In my experience it breaks more often: many self-hosted apps generate links and redirects that assume they live at a domain's root, so they escape their prefix. Subdomains give each app its own root and its own cookies, and DNS records cost nothing. I default to subdomains.

Do I need a wildcard certificate for multiple subdomains?

No. One certbot command with three -d flags issued one certificate covering all three demo apps. A wildcard only becomes worth it when subdomains come and go often, and it requires the DNS challenge, which needs API access to your DNS provider. I walk that whole issuance, plus how the challenges actually verify you and how to prove auto-renewal fires, in the free SSL certificate guide.

Does a reverse proxy slow down my apps?

Measured on the demo server: 1.5 ms median direct, 1.5 ms mean through nginx plus HTTPS on a reused connection. The hop costs about a tenth of a millisecond. The TLS handshake costs 64 ms once per connection, and that is HTTPS's price, not the proxy's.

What is a reverse proxy vs an API gateway?

An API gateway is a reverse proxy with extra opinions: authentication, rate limits, request transformation, usage plans. Hosting apps? You want a reverse proxy. Selling an API and need keys and quotas? That is when gateway features earn their complexity.

What about apps that are not websites, like game servers or databases?

The setup on this page routes HTTP by Host header. Raw TCP services (Postgres, a Minecraft server) have no Host header to read, so they cannot share port 443 this way. nginx can still forward raw TCP with its stream module, but each service needs its own port. For databases my honest advice is: do not expose them at all, reach them over SSH or a VPN.

Do I still need nginx if I use Coolify or Nginx Proxy Manager?

Coolify runs Traefik as its reverse proxy and configures it for you. You do not need to write server blocks. You do want to understand this page anyway: when a deploy 404s or a certificate refuses to issue, the thing you are debugging is the reverse proxy layer.


  • Updated August 2026
  • Ubuntu 24.04.4
  • nginx 1.24.0
  • Docker 29.6.2
  • certbot 2.9.0
  • Time ~30 minutes
  • Difficulty Beginner-friendly
Last verified: August 11, 2026. Built and probed on a fresh Ubuntu 24.04.4 DigitalOcean droplet: nginx 1.24.0, Docker 29.6.2, certbot 2.9.0, three live containers behind one proxy.

Want the whole self-hosting path, in order?

This page is one layer of it. In Self Hosting 2.0 I take an empty VPS to a full stack you own: Coolify with its managed reverse proxy, real apps, backups, email, and security in the order it should actually happen. 34 lessons, nothing skipped between them.

Hasan Aboul Hasan giving a thumbs up

One entrance. Many rooms.

Hasan Aboul Hasan builds open-source tools and teaches solo developers how to build, host, and sell AI-powered products. Founder of LearnWithHasan.com, creator of SimplerLLM and PyRunner.

Vibe Engineering Blocks — free guide
Free guide

Get the free Vibe Engineering Blocks guide

The exact building blocks I use to ship real products with AI — yours as a free PDF.

Free PDF · double opt-in · unsubscribe anytime.

Have a question? Ask it in the community — it's tagged #guide and linked back here. Reading is open to everyone; posting needs a free account.

Loading questions…