Hosting Something Only You Can Reach
Plenty of things you'd want to run are for an audience of one: a dashboard over your own data, an admin tool, a script with a web form on it, something an AI tool built you last weekend. The instinct is to look for a "private" or "internal only" setting. There isn't one — every port binding here is reachable from the internet the moment your app starts, with no IP allowlist and nothing in front of it. So the question isn't how to hide it, it's which of three real approaches fits: don't open a port at all, open one and authenticate properly, or keep the sensitive part off the network entirely. The first is the most under-used and often the best.
Option 1: don't bind a port
An app with no port binding has no inbound surface. Nothing to find, nothing to scan, nothing to authenticate — the attack surface is genuinely zero because there's no listener.
You still need a way to interact with it, and the good news is you can borrow one: make a Discord or Telegram bot your interface. Your app connects outward to the chat platform, receives your commands there, and replies.
What that buys you is authentication you didn't have to build. The chat platform already knows who you are, and it does the hard parts — sessions, password resets, two-factor — better than a weekend project will. Your app only has to check one thing:
if message.author.id != OWNER_ID:
return
For an internal tool with a handful of operations — "show me today's numbers", "reprocess that order", "what's in the queue" — a bot is often a nicer interface than a web page, because it's already on your phone and it keeps a history of what you asked.
Two cautions. In a shared server, anyone can press your buttons, so authorise the person on every action rather than assuming. And a chat platform is a third party — don't pipe genuinely sensitive data through it.
Option 2: bind a port and authenticate
Sometimes you need a real web interface. Fine — but the protection has to be in your app, because the edge terminates TLS and routes, and does nothing else.
For a single user, this is simpler than a full auth system:
- A long random token, checked on every request, stored in a cookie after a one-time setup. Compare it in constant time.
- Or delegate to a provider — sign in with GitHub or Google and allow exactly one account ID. Fewer things for you to get wrong.
- Rate limit the login path regardless. Credential stuffing arrives at anything that looks like a login form.
Set the cookie Secure, HttpOnly and a sensible SameSite, and remember that session state has to survive restarts — a signing secret generated at startup logs you out every time you deploy.
Option 3: keep it off the network
If the sensitive thing is an operation rather than an interface, you may not need an interface at all.
The shell in the panel runs commands inside your container, authenticated by your account. A maintenance script you run occasionally doesn't need a web form — it needs to be a script you can invoke when you're already logged into the panel.
That's the smallest possible attack surface: no endpoint, no session, no token to leak. It's worth asking whether the thing you're about to build a UI for is really a thing you run four times a year.
What doesn't work
Worth naming, because each of these gets used as a substitute for authentication:
An unguessable hostname. It's a URL. It ends up in browser history, screenshots, referrer headers, and support tickets. Treat it as public knowledge and it will never disappoint you.
A secret path. Same reasoning, plus it appears in your own logs.
CORS. It's a browser policy, not access control. curl ignores it entirely, and so does anything else that isn't a browser.
EXPOSE in a Dockerfile. It's image metadata and publishes nothing — the port binding is what makes an app reachable, and a missing EXPOSE won't protect a port you did bind.
Checking the client IP. With a proxy in front, that means trusting a forwarded header, which the client controls. There's no IP allowlist at the edge to do it properly.
Reduce what's exposed at all
Whichever option you pick, two habits shrink the problem:
- Unbind ports you aren't using. A database you only ever reach from your own app doesn't need a public endpoint — that binding is pure exposure. This is the single highest-value five-second change available.
- Keep a kill switch. An environment variable your app checks, so you can disable a surface with a restart rather than a redeploy. It's the fastest recovery you can give yourself.
Choosing
| What you're building | Approach |
|---|---|
| A few operations you trigger occasionally | Bot interface, no port |
| A dashboard you'll actually browse | Port + real authentication |
| Something you run four times a year | The panel shell, no interface |
| A database only your app uses | No port binding |
| Anything genuinely sensitive | Reconsider whether it needs to be reachable at all |
FAQ
Can I make an app private or internal-only?
No. Every port binding is public. Either don't bind one, or authenticate in your application.
What's the simplest secure interface for a personal tool?
A Discord or Telegram bot. Your app makes only outbound connections, and the chat platform handles identity — you just check the user ID.
Is an unguessable URL enough?
No. URLs leak through history, screenshots and referrers. Treat the hostname as public.
Does CORS stop other people calling my API?
No. It's enforced by browsers only; anything else ignores it.
Can I restrict access by IP address?
Not at the edge. Checking a forwarded header in your app means trusting something the client can set.
What's the fastest thing I can do right now?
Remove port bindings you don't use. Anything not exposed can't be attacked.
Wrapping up
The absence of a private-networking option sounds like a gap and mostly changes where the boundary lives: in your application rather than in the platform. That's more honest than it sounds — a firewall rule you forget is a firewall rule that eventually doesn't apply, whereas a check in your request handler is visible in the code you maintain. Start by asking whether you need a listener at all, because a bot with no ports is both the smallest surface and, for a personal tool, frequently the nicest thing to use.