Short answer: to put a self-hosted app on the internet with HTTPS you need four things, in this order: a domain name with a DNS record pointing at a public IP, a path from the internet to that IP (a forwarded port, a VPS, or a tunnel), a reverse proxy listening on 443 that forwards to your app’s port, and a certificate from a public CA like Let’s Encrypt. Self-signed certificates will not work for other people. If you only need access for yourself, skip all of this and use a VPN.

That last sentence is the fork in the road, and getting it wrong is why so many of these setups fail halfway. There are two completely different problems wearing the same clothes:

The rest of this is the list of walls people actually hit, each with the short version of how to get through it. It is written for someone who already has the app running, because that is almost always where people are stuck. Nobody is confused about docker compose up. They are confused about one specific layer above it.

Why does my self-signed certificate work on my laptop but not on my phone?

Because you told your laptop to trust that certificate and you cannot meaningfully do the same on iOS and Android. A self-signed certificate is trusted by exactly the machines you have manually taught to trust it. Every visitor’s browser will show a full-page security warning, so a self-signed certificate is never the answer for a public app.

Even for your own devices it is harder than it looks:

This bites hardest with apps that hard-require HTTPS. Vaultwarden is the classic case: its web vault uses the Web Crypto API, which browsers only expose in a secure context, so it will simply refuse to work over plain HTTP from anything other than localhost.

The fix is to stop generating your own certificate and get a real one from a public CA. Which brings us to the next wall.

How do I get a real Let’s Encrypt certificate if I cannot open port 80?

Use the DNS-01 challenge instead of HTTP-01. HTTP-01 proves control by serving a file on port 80, so it needs an inbound port open. DNS-01 proves control by putting a TXT record in your zone, so it works behind CGNAT, behind a closed firewall, and on a machine with no inbound path at all. It is also the only way to get a wildcard certificate.

Caddy, Traefik, acme.sh and certbot all support it through DNS provider plugins. You give the proxy an API token for your DNS provider and it writes the _acme-challenge TXT record itself.

Watch the rate limits while you are debugging, because this is where people lock themselves out for a week. Let’s Encrypt’s published limits are:

Five retries of the same hostname is nothing when you are guessing at a config, so use --dry-run or the staging endpoint until the challenge actually passes, then switch to production for the real certificate.

Why does my reverse proxy return 502 when the app is clearly running?

Nine times out of ten: the address the proxy is dialing means something different inside the proxy’s own container than it does on your host.

If Nginx Proxy Manager is itself running in Docker and you point it at 127.0.0.1:8080, it dials port 8080 inside the NPM container, where nothing is listening. Same for localhost. The three ways out:

  1. Put both on the same user-defined Docker network and use the service name as the hostname, e.g. http://navidrome:4533. This is the clean answer. Note that name resolution only works on user-defined networks - the default bridge network has no built-in DNS, which is exactly why “it works in my compose file but not from NPM” happens when the two are in different stacks.
  2. Use the host’s LAN IP with the published port, e.g. http://192.168.1.50:8080. Works, but you have now published that port to your whole network.
  3. Use host.docker.internal if your platform supports it. Convenient, least portable.

Two more things that produce a confusing 502 or a broken page rather than a clean error:

Why do my apps log the Docker gateway IP instead of the real visitor IP?

Because the connection your app sees genuinely comes from the proxy, not from the visitor. Two layers can do this, and people usually fix one and not the other.

Layer one, the proxy. The reverse proxy has to forward the original address in a header (X-Forwarded-For, X-Real-IP, X-Forwarded-Proto) and the app has to be configured to trust it. Most apps ignore those headers by default, correctly, since anyone could send them. Look for a TRUSTED_PROXIES, trusted_proxy_ips or equivalent setting.

Layer two, Docker itself. Docker’s userland proxy rewrites the source address on published ports, so a container can see 172.17.0.1 regardless of what any HTTP header says. That is not something a proxy header fixes. Putting the proxy and the app on a shared user-defined network and not publishing the app’s port at all avoids the whole problem.

There is a security corollary here worth knowing: Docker writes its own iptables rules and bypasses UFW. A port you published with -p 8080:8080 is reachable from the internet even if ufw status says that port is denied. If you are hardening a box, check iptables -L DOCKER rather than trusting the UFW output.

Why does my domain work from outside but not from inside my own network?

This one feels like haunted hardware. Your domain resolves to your public IP. From the outside that is correct. From inside your LAN, the request goes to your router’s WAN address and many routers will not loop it back to an internal host. That is NAT hairpinning, and plenty of consumer routers do not do it.

Two fixes:

If you are running a local DNS server in front of a reverse proxy and the name resolves on the desktop but not on your phone, check that the phone is actually using your resolver. Mobile devices ignore your DHCP-supplied DNS more often than you would expect: private DNS / DNS-over-HTTPS is on by default in a lot of configurations and silently routes around your resolver entirely.

Do I need to open ports on my router, and what if port forwarding does nothing?

For a public app on a home connection, yes, you need inbound 80 and 443 reaching the host, unless you use a tunnel. If you forwarded the ports and nothing happens, check these in order:

  1. Are you behind CGNAT? Compare the WAN IP shown in your router’s status page against what a “what is my IP” service reports. If they differ, your ISP is sharing that public address across many customers and you do not control it. No amount of port forwarding will help. Your options are asking the ISP for a static or public IPv4 (often a small monthly fee), using IPv6 if both ends have it, or renting a cheap VPS and tunnelling out to it.
  2. Is your ISP blocking 80/443? Many residential plans do, and some terms of service prohibit running public servers at all. Worth reading before you build on it.
  3. Is your IP dynamic? If so you need dynamic DNS, or the domain points at a stranger’s connection after the next lease renewal.

None of this makes home hosting unsafe or wrong. Plenty of people run public services from home perfectly well. It is just genuinely more moving parts than the tutorials admit, and every one of those parts is a place the whole thing can be down while you are asleep.

What if you skip the layer entirely?

Everything above exists because your app is running somewhere that does not have a public address, a certificate, or a proxy in front of it. Hosting the app somewhere that already has all three removes the problem rather than solving it.

That is what InstaPods does. You deploy an app to a pod and it comes up on a public HTTPS URL immediately, with the certificate issued and the proxy route configured by the platform. There is no Nginx config to write, no certbot to run, no port to forward. If you want your own domain, you add it in the dashboard, point a CNAME at the pod, and a Let’s Encrypt certificate is issued and auto-renewed for it - up to 5 custom domains per pod. Plans start at $3/mo (Launch: 1 vCPU, 512MB RAM, 10GB disk), and you still get full SSH into a real Linux server, so it is your instance and your data, not a tab in someone’s SaaS.

Being straight about the boundaries, since this audience will check:

Home server + reverse proxyVPS + Nginx Proxy ManagerTunnel (Cloudflare, ngrok)InstaPods pod
Public HTTPS URLYou build itYou build itIncludedIncluded
Certificate managementcertbot, yours to renewcertbot, yours to renewHandled by providerHandled, auto-renewed
Reverse proxy configYou write itYou write itProvider’s edgeNone to write
Needs port forwardingYesNoNoNo
Survives CGNATNoYesYesYes
Works if your house loses powerNoYesNoYes
Runs any Docker compose stackYesYesYesNo
Typical costHardware plus powerA few dollars a month plus your timeFree tier availableFrom $3/mo

The honest read on that table: a home server plus a reverse proxy is the most capable option and the most work, a tunnel is the fastest way to get something reachable, and a managed pod is the option where the certificate and the proxy are simply not your problem. Pick by which of those you actually want to own.

If the app you are trying to expose is one of the usual suspects, it is probably already a 1-click deploy: Vaultwarden, Uptime Kuma, n8n, Memos. Those come up on an HTTPS URL that works on an iPhone on cellular data on the first try, which is the actual finish line most of these threads are looking for.

Where to go next