Published ports
The platform claims a published port across every node of the cluster it's published on. Two applications on one cluster publishing the same number would break each other, so the platform tracks every one and guarantees that two applications are never given the same wire on the same cluster.
A wire is a (protocol, port) pair on one cluster. 1194/udp and 1194/tcp are
different wires and can be held by different applications at once, and so are
1194/udp on two different clusters.
Ask the platform to pick
Put 0 on the published side and the platform assigns a free port:
services:
openvpn:
image: someones/openvpn:latest
ports:
# `0` on the published side means "platform, pick a free one and tell me
# which". Read it back from GET /cli/applications/{slug}/endpoints once the
# revision is running; it does not change on redeploy.
- "0:1194/udp"
You don't have to discover what's free, and you can't lose a race. The platform makes the assignment under a database constraint, so if two deploys pick the same instant, one is redelivered and lands on the next port instead.
Read back what you were given from
/cli/applications/{slug}/endpoints, where the port arrives with
"assigned": true.
Naming one yourself does nothing
You can write a number on the published side, but you won't get it. It's
dropped at ingest, and the platform reads the entry as 0:—published, on a port the platform
picked—with a warning on the revision saying so:
services:
openvpn:
image: someones/openvpn:latest
ports:
# A number you name is dropped with a warning: this is read as "0:1194/udp",
# published on a port the platform picked. Read it back from /endpoints.
- "1194:1194/udp"
service "openvpn": dropped the published port 1194 you chose for 1194/udp—the platform assigns every published port. It's published on one the platform picked. Read that back from /cli/applications/{slug}/endpoints.
The upload isn't refused, because the part the platform can honour is still there: this service wants 1194/udp reachable from outside. Only the number goes.
The platform claims a published port across a whole cluster, so a number you pick is one the platform can't know is free and can't promise you. Assigning every port removes that class of failure entirely: no collision with another tenant, and no well-known port somebody else has already squatted.
Warnings are listed on the revision's page in the control panel, beside the spec the platform actually deployed.
How stable the address is
A reservation is keyed on the cluster, the application, the compose service, the container port and the protocol. Redeploying onto the same cluster resolves to the same reservation, so the port you were given on the first deploy is the port you have on the hundredth. That's the case you are in almost always, and it's what makes it reasonable to write the address into something you hand to a client.
It's not a promise for all time, and a consumer shouldn't treat it as one. Two things can move it:
- The platform re-places the application onto a different cluster—because its own is draining, unreachable, or gone. The platform offers it the number it already holds, so the move is usually invisible. But if that number is taken on the destination, the platform gives it a different one, and the deploy still succeeds.
- The stack is torn down. Stopping and deleting are one teardown, and a teardown hands every wire back straight away. Starting again allocates afresh, so a stopped application doesn't keep its address—the platform may give it the same number back, because assignment takes the lowest free port, and the one just released usually is that port. But that's a coincidence rather than a promise.
So: re-read /endpoints after a deploy, rather than once at the
first one, and treat the address you get as current rather than permanent. If you
have issued something durable with an address baked into it—a Virtual Private Network (VPN) profile on
somebody's phone names a literal port—you need a way to re-issue it, and stopping
the stack is enough to require it.
You are told when it moves
You don't have to poll to find out. When an address changes or goes away, the platform raises a notification against the organization:
| Kind | Raised when |
|---|---|
published_port_released |
The stack came down, so its ports went back into the pool. Starting again allocates new ones |
published_port_changed |
The application was re-placed onto another cluster that already had its number taken |
A machine consumer reads them the same way a person does:
GET /cli/notifications
An organization token sees the organization's notifications, because the account behind it's a member (Authentication).
The notification is the signal, not the payload. It tells you an address moved;
/endpoints remains the single source of truth for what it moved
to. Re-read that on seeing one rather than parsing the title, which is written
for a person and will change wording.
A port you named yourself in compose never raises one—you changed it, so there is nothing to tell you.
The rules, in one table
compose ports: entry |
Result |
|---|---|
"0:1194/udp" |
Platform assigns a free UDP port on the target cluster, stable across redeploys there |
"1194:1194/udp" |
Read as "0:1194/udp"—the platform drops the number with a warning, and assigns a free one |
"1194/udp" |
Not published. Reachable from sibling services on the stack network only |
| Two services asking to publish the same container port | Each is assigned its own wire; a wire belongs to one service |
| Assigned range exhausted | The revision fails, naming the range for the operator to widen |
Assigned ports come from 30000–32767 by default—Swarm's own range for automatic assignment,
so an assigned port is one an operator already expects to be open. The range is
therefore the whole of what may be published; a revision that exhausts it fails,
naming the range for the operator to widen.
Web traffic doesn't need any of this
A service exposing 80, 8080, 3000, 8000 or 5000 is detected as HTTP and given a hostname at the platform's edge, with TLS terminated for it. It needs no published port at all, and publishing one doesn't replace the hostname—you get both.
When your port isn't one of those, say so
Those five are a guess, and the guess is all the platform had: whether a port speaks HTTP isn't a property of the number. A service listening on 8473 got no hostname, no certificate and no router, and there was no way to correct it.
x-someones.http is that way. An x--prefixed key is the compose spec's own
extension field, so the file still runs under docker compose up unchanged and every
other tool ignores it:
services:
lab:
image: ghcr.io/acme/lab
x-someones:
http: 8473
| Declares | which container port speaks HTTP |
| Publishes | nothing—the edge reaches it over the shared ingress network |
Needs a ports: entry |
no. Add one only if you also want a raw wire; both is fine |
| Beats the guess | yes. A service declaring 8473 while also exposing 80 is routed to 8473 |
| Bad value | a 400 at ingest, not a warning—an ignored, mistyped port would leave you exactly as unrouted as before |
Read the result back from /endpoints, or off the application's page
in the control panel, which answers from the same resolver.