Skip to content

How ports work

How Warpforge assigns service ports, what a service's `port:` value means, and how to get the port into your app.

Updated View as Markdown

Warpforge assigns the port, it does not discover it

Nothing in Warpforge detects which port a service actually bound. The flow runs the other way: Warpforge picks a port — the first free one in the project’s range, or exactly the pinned one — passes it to the process as the PORT environment variable, then probes that port with a TCP connect and marks the service Running when something answers.

This means the app has to cooperate. If it reads PORT, everything works. If it doesn’t, Warpforge warns on the service row once the service is up but nothing answers on its port, and names the ports the app printed in its logs that actually answer (for example “Listening on 8787 and 9501, not 4000”). If none of them answers, it says nothing answers on the allocated port. Agents see the same warning in list_runtime.

Pinning a port

When the project declares a port range in its config (ports.range), a service’s port: is the exact port it binds:

ports:
  range: "4200-4299"

services:
  app:
    command: bun run dev
    port: 4210

If 4210 is already taken, the service fails with an explanation instead of quietly moving — that is the point of pinning, since a pinned number is the one you put in .env files, Postman collections, and manifests. If 4210 sits outside the declared range, the service fails too, with both ways out named in the error: move the port inside the range, or set portFallback: auto on that service to let it shift to a free one.

Without a declared range, the number is only a marker that the service needs a port at all. Warpforge picks the first free one in the project’s range and passes it to the app as PORT.

Getting the port into the app

Four cases, most common first.

1. The app reads PORT. Nothing to do — this is the default path. Many Node, Go, and Python frameworks pick the port up from the environment automatically.

2. The app wants the port as a flag. The command runs through sh -c, so the shell expands $PORT before your command sees it:

ports:
  range: "4200-4299"

services:
  app:
    command: bun run dev -- --port $PORT
    port: 4210

3. One service needs another service’s port. Interpolate ${<service>.port} inside env values or the command itself:

services:
  db:
    command: docker compose up postgres
    port: 4215

  app:
    command: bun run dev
    port: 4210
    env:
      DATABASE_URL: postgres://localhost:${db.port}/app

${<service>.port} is substituted in env values, command, and healthcheck.url. It resolves only once that service has a port, so list it in dependsOn. Otherwise the dependent fails at start rather than receiving the literal placeholder.

4. The app reads a different variable. Some apps take HTTP_PORT, SERVER_PORT, or similar instead of PORT. Set that variable from the service’s own port:

services:
  search:
    command: go run ./cmd/server
    port: 4220
    env:
      HTTP_PORT: "${search.port}"

If the app loads a .env file at startup, make sure that file doesn’t set the same variable, or check that the loader leaves already-set variables alone (most do, e.g. dotenv/godotenv without override).

A worked example that combines all four cases with port-forwards and dependsOn is in Projects and their runtime.

The sharp edge

If an app ignores PORT and has its port hardcoded, Warpforge never finds out. It reports the port it chose, probes that port, and gets no answer. After the service’s readyTimeout (five minutes by default) it is marked failed with did not become ready within 5m: nothing accepted connections on port …, while the app happily serves on its own port. The diagnostic signal: a service that failed this way, and whose logs say it started, almost always means the app is not reading PORT. Fix the app (case 1, 2, or 4 above).

Worse case, and it deserves its own warning: the port probe is not the only way to become Running. A log line that contains readyPattern, or a common “server started” message, marks the service Running too, whichever comes first. An app that ignores PORT is then marked Running while listening somewhere else entirely, and Warpforge displays a port number that nothing is bound to. If a service refuses to respond at its reported address, check what port the app actually bound in its logs.

Conflicts and precedence

A project’s range is resolved strongest first:

  1. A machine-local override (stored in your local registry, set from the UI when resolving a conflict)
  2. An explicit ports.range from the committed config
  3. A previously assigned range, kept for the project
  4. A fresh scan from port 4000 upward for a free 100-port block

Two projects declaring the same range is a conflict: one of them refuses to start services until you set a local override on that machine. Committed config is never edited to resolve it — a machine-local problem stays machine-local.

The full schema of the port-related keys lives in the configuration reference; where a project’s range shows up in the UI is described in Projects and their runtime.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close