How Ports and URLs Work — HTTPS, Raw TCP, and Custom Subdomains
Every port you expose picks one of three protocols, and the protocol decides what you get back. HTTPS gives you a hostname with a certificate issued automatically, reachable on 443 like any website. TCP and UDP give you a host and a port number — and that public port is not the container port you chose, which is the single most common surprise. You can also replace the generated hostname with a custom subdomain of your own choosing, subject to a few rules, which is the difference between a1234-9f2c.… and my-bot.… in front of your users.
Three protocols, two shapes of endpoint
| Protocol | What you get | Reachable at |
|---|---|---|
| HTTPS | Hostname + automatic certificate | https://<host> on 443 |
| TCP | Host + assigned public port | <host>:<port> |
| UDP | Host + assigned public port | <host>:<port> |
HTTPS is for anything speaking HTTP. Bind your app's own port — 8080, 3000, whatever it listens on — and TLS is terminated at the edge and forwarded to that port inside the container. Your app speaks plain HTTP internally and never handles a certificate. That's the path a FastAPI service or any web app wants.
TCP and UDP are for everything else: an MQTT broker, a database, a game protocol, anything with its own wire format. There's no TLS termination and no hostname routing — it's a straight L4 mapping.
No port at all is a valid and common choice. A bot or worker that only makes outbound connections needs nothing exposed, which is why bots need no special configuration.
The public port isn't your container port
This catches people every time. When you bind container port 1883 as TCP, clients do not connect to port 1883 — the platform assigns a public port, and the panel shows you the actual host and port to use.
So the mapping is: your container listens on the port you configured; the outside world reaches it on a different, assigned one. Always take the connection details from the Ports panel rather than assuming they match what you set.
HTTPS bindings don't have this problem, because everything arrives on 443 and is routed by hostname instead.
Your generated hostname
Without a custom subdomain, an app gets a hostname built from its ID plus a short hash — something like a1234-9f2c.<platform-domain>. It's stable, unguessable-ish, and fine for an API nobody types by hand.
Your app can read its own public URLs from the DF_URLS environment variable, which beats hard-coding them — that's how you avoid a staging app handing out production links, and it's what a Telegram Mini App or an OAuth redirect needs.
Choosing a custom subdomain
You can set your own subdomain per port binding, giving you my-bot.<platform-domain> instead of the generated name. The rules, so you don't discover them by trial and error:
- Start with a letter, then letters, digits, hyphens, or underscores. At least two characters.
- It's lowercased automatically, so don't rely on capitalisation.
- Unsafe characters aren't silently cleaned — if stripping them would change what you typed, the name is rejected rather than quietly altered.
- It must be globally unique. Subdomains are first-come, first-served across the whole platform, so a short generic name may already be taken.
- One per binding. Two bindings on the same app can't share a subdomain.
- You can't imitate a generated name. The
a<digits>-<4 characters>pattern is reserved, so you can't claim something that looks like another app's automatic hostname.
Note this is a subdomain of the platform's domain, not a custom domain of your own — pointing api.yourcompany.com at your app isn't available today. If you need that, this isn't the platform feature to wait for.
Ports count against a quota
Exposed ports are one of the quotas on your Limits page, alongside CPU, RAM, disk, and app count. It's generous for normal use, but worth knowing for two reasons:
- A multi-port app spends more of it. An app with an HTTP interface, an admin port, and a metrics endpoint uses three.
- Removing an unused binding frees one — and removes an exposed surface, which is the cheapest security control there is.
Changing a binding restarts the app
Ports are part of the container's shape, so adding, removing, or editing one recreates the container. That means a short gap, and it means port changes batch well with other configuration edits rather than being done one at a time.
It also means a binding you add doesn't take effect until that restart completes — if you've just added a port and the endpoint isn't answering yet, that's usually why.
FAQ
Why is my TCP service on a different port than I configured?
The container port and the public port are separate. The platform assigns the public one; check the Ports panel for the host and port your clients should use.
Do I need a certificate for HTTPS?
No. An HTTPS binding gets one issued and renewed automatically. Your app serves plain HTTP inside the container.
Can I use my own domain name?
Not currently. You can choose a custom subdomain of the platform domain, but pointing your own domain at an app isn't available.
Why was my subdomain rejected?
Either the format is wrong (it must start with a letter and contain only letters, digits, hyphens or underscores), it's already taken by someone else, or it resembles the reserved pattern used for generated hostnames.
How does my app find its own URL?
Read the DF_URLS environment variable, which contains the app's HTTPS URLs. Don't hard-code them.
Does adding a port restart my app?
Yes. Port bindings are part of the container's configuration, so changing them recreates it.
Wrapping up
The model is small once it's written down: pick HTTPS and get a hostname with a certificate, pick TCP or UDP and get a host and an assigned port, or pick nothing and expose nothing. The two things worth remembering are that the public port for raw protocols isn't the one you typed, and that a custom subdomain is free, first-come, and takes about ten seconds — which is a reasonable trade for a URL your users can actually read.