07. Port binding
1. The rule: export the service by binding a port
The factor is one sentence: export services via port binding. A twelve-factor app is completely self-contained. It brings its own webserver, binds to a port, and listens there for requests. That bound port is how it exports its service to the outside world. It does not rely on a webserver being injected into its runtime by the environment around it.
Self-containment is the whole point. The app’s only contract with its execution environment is “give me a port to bind.” Everything above that, parsing HTTP, routing requests, holding open connections, is code the app carries itself. As the series introduction put it, a twelve-factor process exports its service by binding to a port. The rest of this post is about what that inverts, and why a single bound port turned out to matter far more than a port number should.
2. The inversion: the app embeds the webserver
The factor inverts who owns the webserver. In the older model, the app is a module loaded inside a webserver the environment provides: PHP running as a module inside Apache HTTPD, or a Java application packaged as a WAR and deployed into a Tomcat or JBoss container that is already running. The webserver is infrastructure. It exists before your app does, it is administered separately, and your code runs as a guest inside it.
Twelve-factor turns that around. The app embeds the webserver as a library, pulled in through ordinary dependency declaration, and binds the port itself. The server stops being infrastructure the environment hands you and becomes part of the app, in user space, versioned and deployed with the rest of your code. This is the other half of Dependencies: the webserver is a declared dependency like any other, not ambient infrastructure you assume is present. You do not deploy your app into a server; you deploy a server that is your app.
In practice, the app runs the same way everywhere. There is no app-server to install, configure and version-match in each environment. The run step starts your process, your process binds a port, and that is the same act on a laptop and in production. The execution environment shrinks to one job: hand the process a port.
3. From a bound port to a public URL
A bound port becomes a public service through a routing layer. On its own, a bound port is just a process listening on an address, and it goes public in two steps. In local development you hit it directly: the app binds, say, port 8080, and you open http://localhost:8080/ in a browser. In deployment, a routing layer sits in front and maps a public hostname onto the port-bound processes, so a request to https://api.example.com/ lands on your process listening on its port. The app exports HTTP through the port; the routing layer publishes that service under a name.
None of this is specific to HTTP. Nearly any request-response protocol works the same way: a process binds a port and awaits requests on it. The original factor pointed at XMPP served by ejabberd and at the Redis wire protocol; gRPC is the modern example, a service binding a port and speaking HTTP/2 frames instead of JSON over HTTP/1.1. Port binding is protocol-agnostic. The contract is “bind a port and listen,” not “serve HTTP.”
The composability payoff comes next: because a port-bound app is reachable at a URL, one app can become the backing service of another. You take the producer’s URL, the address of its bound port behind whatever hostname the routing layer gave it, and hand it to the consumer as a resource handle in the consumer’s config. At that point the producer is just another attached resource, indistinguishable from a database or a cache. This is where Factor 7 meets Backing services: port binding is what makes an app addressable, and Factor 4 is what consuming that address looks like from the other side. Binding a port is how a service publishes itself; a config handle is how the next service consumes it.
4. Port binding won, and containers are why
The embedded-server model went from a contrarian 2011 recommendation to today’s default. That approach is no longer the unusual choice; it is how almost everything ships. Spring Boot bundles an embedded Tomcat, so a Java service is a plain executable JAR that binds a port with no external app-server in sight. Node’s http server is in the standard library. Go’s net/http is a few lines to a listening server. The default posture across modern stacks is a self-contained process that owns its own listener.
Containers settled it. A container is exactly a self-contained process that binds a port. EXPOSE in a Dockerfile documents the port; the platform maps traffic to the port the process actually binds. A Kubernetes Service and ingress, or a service mesh, routes requests to it. That is Factor 7 at platform scale. The stateless process from Processes is the same process that binds the port here; statelessness makes it safe to run many copies and port binding makes each copy addressable. The 2011 argument against loading applications into a shared app-server container reads today less like a recommendation and more like a description of how the industry decided to build.
5. “The app is the server” doesn’t mean “no infrastructure”
The clean line, the app is the server, understates the routing layer it waves at. “Map a public hostname to the port-bound processes” is one clause hiding a lot of real work. The reverse proxy, ingress, or service mesh in front of your process terminates TLS, balances load across your replicas, and routes by path and host. That is infrastructure your app is deliberately not doing, and it does not disappear because the app brought its own webserver. Port binding moves the webserver into the app; it does not move the whole network edge in with it. “The app is the server” means the app owns the HTTP listener, not that nothing sits in front of it.
The sharper tension is serverless, and it splits in two. Serverless container platforms still require your app to bind a port. Google Cloud Run and Knative are the clean case: they scale to zero and bill per request, yet stay fully port-binding in how they run. AWS Fargate fits looser, no servers to manage and still a bound port, but billed per second for a running task rather than scaled to zero. All three sit inside Factor 7. Function-as-a-service is the real departure. A Lambda-style function binds no port at all. The platform owns the listener and invokes your handler once per event, so what you provide is a function, not a server. That inverts Factor 7’s contract outright: the execution environment no longer hands you a port to bind, it holds the socket and calls you. FaaS keeps that statelessness while dropping port binding, a fair reminder that the twelve factors are separable and a platform can honor some while trading others away.
Set that exception beside the win, though, and the win is the larger fact. A self-contained process that binds a port is an addressable, composable unit: something with its own listener, reachable at a URL, that another app can attach to as a backing service. That property is most of what made services composable in the first place, and it is why the container and microservice model works the way it does. FaaS chose a different contract for its own reasons; port binding is still the one that turns a process into a service you can name, reach, and build on. Bring your own server, bind a port, and the app becomes something the rest of the system can point at.