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:
- “I want to reach my stuff from my phone when I am out.” This is a private access problem. Tailscale, WireGuard, or a plain VPN solves it in about ten minutes, and you never touch DNS, certificates, or port forwarding. Genuinely, go do that.
- “I want other people to use my app.” This is a public access problem. No VPN solves it, because the whole point is that the other person should not have to install anything. This post is about that one.
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:
- iOS needs the CA installed as a configuration profile and separately switched on under Settings > General > About > Certificate Trust Settings. Installing the profile alone does nothing. On top of that, since iOS 13 Apple rejects server certificates that lack a
subjectAltNameDNS entry (a Common Name is not enough), that lack theserverAuthextended key usage, or that are valid for more than 398 days. Most hand-rolledopensslone-liners violate at least one of those, which is why the exact same certificate works fine in Chrome on your desktop. - Android 7 and later ignores user-installed CAs for app traffic by default. An app has to opt in with a network security config. So the browser may accept your certificate while the native app talking to the same host keeps failing, which reads like a random bug and is not one.
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:
- Up to 5 certificates per exact same set of identifiers every 7 days
- Up to 5 authorization failures per identifier per account every hour
- Up to 50 certificates per registered domain every 7 days
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:
- 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 defaultbridgenetwork 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. - 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. - Use
host.docker.internalif your platform supports it. Convenient, least portable.
Two more things that produce a confusing 502 or a broken page rather than a clean error:
- Scheme mismatch. If the app is plain HTTP internally, the proxy target must be
http://, nothttps://, even though the public side is HTTPS. TLS terminates at the proxy. - WebSockets. Anything with a live UI needs the upgrade headers passed through. In Nginx that is
proxy_set_header Upgrade $http_upgrade;andproxy_set_header Connection "upgrade";withproxy_http_version 1.1. Without them the page loads and then quietly does nothing.
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:
- Split DNS. Run a local resolver (Technitium, Pi-hole, AdGuard Home, your router) and give it an internal A record for the same hostname pointing at the LAN IP. Inside the network the name resolves locally; outside it resolves publicly. Same hostname, same certificate, both work.
- Enable NAT loopback / hairpin NAT on the router if it supports it.
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:
- 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.
- 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.
- 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:
- We host the app for you. InstaPods does not put HTTPS in front of hardware sitting in your house. If the requirement is “this specific box in my basement, reachable publicly,” a VPS plus a tunnel is your answer, not us. We priced the small VPS plans if you need one for that.
- We are not a VPN replacement. For private access to your own machines from a hotel, Tailscale is the better tool and costs nothing at your scale.
- We are not a Docker host. Pods run from a preset (static, Node, Python, PHP) or from a pre-baked 1-click app image. If your app only exists as a multi-service compose stack, that is a port, not a click.
| Home server + reverse proxy | VPS + Nginx Proxy Manager | Tunnel (Cloudflare, ngrok) | InstaPods pod | |
|---|---|---|---|---|
| Public HTTPS URL | You build it | You build it | Included | Included |
| Certificate management | certbot, yours to renew | certbot, yours to renew | Handled by provider | Handled, auto-renewed |
| Reverse proxy config | You write it | You write it | Provider’s edge | None to write |
| Needs port forwarding | Yes | No | No | No |
| Survives CGNAT | No | Yes | Yes | Yes |
| Works if your house loses power | No | Yes | No | Yes |
| Runs any Docker compose stack | Yes | Yes | Yes | No |
| Typical cost | Hardware plus power | A few dollars a month plus your time | Free tier available | From $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
- How to deploy your app without configuring Nginx - the reverse proxy config you are avoiding, and what replaces it.
- Self-hosted apps directory - every app you can deploy as a 1-click pod.
- Self-hosting Vaultwarden - the HTTPS-required case in detail.
- The real cost of self-hosting in 2026 - hardware, power, and time, counted honestly.