I self-host a handful of side projects on a single server: a couple of web apps, an email service, a messaging gateway, and the panel that deploys them all. The goal of the setup is simple: one front door, nothing else reachable, and a clean line between what's public and what's only mine.
The idea
Every request goes through the proxy first. The origin server is never addressed directly. Some routes are public on purpose (REST APIs, provider webhooks, unsubscribe links), and everything else (dashboards, API docs) sits behind identity. A firewall makes the whole thing airtight: the origin only accepts traffic coming from the proxy's network, so there's no way to skip the access layer by hitting the server directly.
The layers
Internet
|
+----------+-----------+
| |
via proxied domain direct origin IP
| |
v X dropped
+-------------------------------------------+
| Edge proxy |
| DNS proxy + Zero Trust Access |
| |
| PUBLIC / bypass PRIVATE / allow |
| -> everyone -> my identity |
| . service REST API . dashboards |
| . gateway API . API docs |
| . webhook callbacks (SSO required) |
| . unsubscribe links |
+-------------------------------------------+
|
v
+-------------------------------------------+
| Origin firewall |
| ports 80/443 only from proxy ranges |
| everything else -> dropped |
+-------------------------------------------+
|
v
+-------------------------------------------+
| Origin server (PaaS) |
| reverse proxy + containers: |
| messaging gateway, email service, |
| web apps, deploy panel |
+-------------------------------------------+
|
downstream services
(email provider, client devices)
Why it holds
The trick is the firewall, not the access layer alone. Without it, anyone who knows the origin address could talk to the reverse proxy directly and skip identity entirely. By only allowing the proxy's IP ranges in, the access policies become the single source of truth: public routes stay public, private routes demand a login, and there is no back door.
The public/private split is declared per route, so adding a new service is just a matter of deciding which side it belongs to. No service ever has to implement its own auth gate for the admin surface, and the webhooks that need to be open stay open without leaking anything else.
That's the whole model: one entry point, an explicit public surface, and an origin that refuses to talk to anyone but the proxy.