Docker’s networking model isolates your containers from your host system. Containers exist in their own networking namespaces and have a separate port range.
Docker doesn’t automatically expose ports outside of containers. To access a container over your host’s network, you must expose and publish the target container port yourself. This allows you to then direct traffic to the container from your host’s networking interfaces.
Dealing with Docker ports can become confusing due to subtleties in the terminology. In this article, you’ll learn what it means to expose a port and how it differs from the separate publish (also known as bind) operation. We’ll show some detailed examples for both exposing and publishing container ports.
We will cover:
Exposing a Docker port means advertising it as actively used by the containerized workload. Docker shows information about the ports that containers expose, allowing you to decide which ports you’ll bind to your host.
It’s important to realize that exposing a port does not automatically publish it to your host. In Docker, exposed ports are purely metadata. They document the ports that containerized apps listen on.
For example, a Dockerfile for a web server could include the following instruction:
EXPOSE 80
This means the app running in the container listens on port 80, so the user should bind that port to their host in order to access the server from outside the container. However, the instruction does not result in a port being automatically bound when the container starts.
Exposing ports can appear redundant to some Docker newcomers, as you can publish ports without exposing them first. However, expose remains an important documentation mechanism that makes your containers easier to manage and reason about.
Exposed port listings instantly tell you which ports should be bound to a container while also revealing any host ports that have been unnecessarily assigned to containers that won’t use them. It’s also possible to automatically publish all exposed ports when you start a container, which saves time when deploying new app instances.
Exposing a port vs publishing a port
As we’ve outlined above, exposing a port is a passive action that merely indicates a container is capable of listening to that port. External communications to the port must be separately enabled by publishing it to your host. This process maps a host port number to the port inside the container.
Users—and other tutorials—frequently conflate these processes. Publishing a port is often described as “exposing” it because it makes the port accessible via your host’s networking stack. However, this terminology is inaccurate because it overlooks the possibility of either exposing a Docker port without publishing it or publishing a port that is not exposed.
- Expose a port: Set metadata that indicates an app or service in the container will listen on that port.
- Publish/bind a port: Allocate a host port to a container port, allowing you to communicate with the container from your host’s networking interface.
Now let’s study some examples of exposing and publishing Docker ports.
There are two main ways to expose a port in Docker—either within a Dockerfile, using the EXPOSE
instruction shown above, or by setting the --expose
flag when you start a container with docker run
.
To expose a port using a Dockerfile, you should use the EXPOSE
instruction for each port that you want to advertise.
EXPOSE 80
By default, the port will be exposed with TCP. You can expose UDP by explicitly requesting it:
EXPOSE 80/udp
To expose a port with both TCP and UDP, repeat the EXPOSE
instruction twice, once for each mode:
EXPOSE 80/tcp
EXPOSE 80/udp
Docker will automatically expose all the listed ports when you start new containers from a Docker image that uses EXPOSE
instructions in its Dockerfile.
On occasion, you might want to manually expose a port when starting a container. You can do this by passing the --expose
flag to your docker run
command.
$ docker run my-container:latest --expose 80
You can repeat the flag to expose multiple ports. Similarly to the EXPOSE
instruction shown above, you can also request TCP and/or UDP exposure using the <port>/tcp
and <port>/udp
syntax. When only a port number is indicated, TCP is selected.
In practice, --expose
is infrequently used. The ports a container can listen on normally match those defined in the image’s Dockerfile. Setting additional port exposures when a container’s started isn’t generally helpful, as they’ll have no effect if the application doesn’t actually use those ports.
Here’s a quick demo that shows you how to expose ports, and check their values.
Exposing ports with Dockerfile EXPOSE
The following is a simple Dockerfile that defines an image with an exposed port:
FROM alpine:latest
EXPOSE 80
CMD ["sleep", "300"]
Build your image, then start a container from it:
$ docker build -t demo-image:latest .
$ docker run -d --name demo demo-image:latest
Using Docker run expose
If you don’t have control of the image’s Dockerfile, you can manually expose ports when starting a container instead:
$ docker run -d --name demo-run --expose 443 demo-image:latest
Viewing the exposed ports
To see the ports that are exposed for a container, use the docker ps
command:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
2fdfd038a368 demo-image:latest "sleep 300" 2 seconds ago Up Less than a second 80/tcp, 443/tcp demo-run
dd50cd05e515 demo-image:latest "sleep 300" About a minute ago Up About a minute 80/tcp demo
The containers running on your host will be displayed. The ports each container has exposed are shown in the PORTS
column of the output.
As we’ve seen above, you can expose multiple ports for a container by repeating the EXPOSE
Dockerfile instruction or --expose
flag for docker run
.
In cases where both EXPOSE
and --expose
apply to a single container, all the referenced ports will be exposed. You can see this in the demo output from docker ps above, where the demo-run
container exposes both port 80 (from its image’s Dockerfile) and port 443 (from the docker run
command line).
Exposed ports need to be published to your host before external traffic can reach your container. You need to map the container’s ports to host port numbers. This is achieved using the -p
flag for docker run
:
$ docker run -d --name nginx -p 80:80 nginx:latest
This example maps your host’s port 80 to port 80 inside the container. You can then reach the containerized app using your host’s network interface:
$ curl http://localhost:80
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
Published ports show in the PORTS
column of the docker ps
command’s output. Unlike exposed ports—which only show the port number used by the container, such as 80/tcp
—published ports show the mapping between the host port and the container port:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
cafc1f042132 nginx:latest "/docker-entrypoint.…" About an hour ago Up About an hour 0.0.0.0:80->80/tcp, :::80->80/tcp nginx
Remapping published ports
The port you assign from your host doesn’t have to be the same one that the container listens to. In this example, you use host port 8080 to connect to the container port 80:
$ docker run -d --name nginx -p 8080:80 nginx:latest
$ curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
Publishing ports to a specific host interface (e.g. localhost)
The port publishing examples shown above bind the container port to all the networking interfaces on your host. This makes the container accessible to the other devices on your network.
This can be a security risk for services that should only be exposed to neighbors running directly on your host. Fortunately, you can publish ports to specific host interfaces using host_address:host_port:container_port
syntax:
$ docker run -d --name nginx -p 127.0.0.1:8080:80 nginx:latest
In this example, the port 8080 on the Docker host 127.0.0.1
(i.e. localhost) is mapped to port 80 in the container. Traffic that arrives at the port from a different IP address—such as your host’s public network interface—won’t reach the container.
Automatically publishing exposed ports
So far, our port expose and publish demos have been completely independent of each other. We exposed ports and published them using two manual steps. You can publish any port—whether it’s exposed or not—or you can choose not to publish already exposed ports.
However, as exposing a port signals that the container uses it, in most cases, you will want to publish all the ports that a container exposes. You can achieve this when starting a container with docker run by setting the -P
(--publish-all
) flag:
$ docker run -d -P --name demo demo-image:latest
This avoids having to check which ports the image exposes, then publish them manually with explicit -p flags. The “publish all” option randomly maps a host port to each of the exposed container ports.
The container’s docker ps
output will confirm which port mappings have been assigned:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d66f376e3860 demo-image:latest "sleep 300" 2 seconds ago Up 1 second 0.0.0.0:32768->80/tcp, :::32768->80/tcp demo
The container port 80, defined in the EXPOSE
instruction in our demo Dockerfile, has been randomly assigned the host port 32768
due to starting the container with the -P flag included.
This article has walked you through how to expose and publish ports for your Docker containers. To summarize, an exposed port is metadata that indicates the port number is used by the application within the container. Exposed ports are then published to create a binding to your host that makes the container accessible.
Publishing a binding is a runtime operation when containers are started, whereas exposed ports are defined by image authors within Dockerfiles. Understanding this distinction will help you correctly manage ports across your Docker containers and images.
We encourage you also to explore how Spacelift offers full flexibility when it comes to customizing your workflow. You have the possibility of bringing your own Docker image and using it as a runner to speed up the deployments that leverage third party tools. Spacelift’s official runner image can be found here.
The Most Flexible CI/CD Automation Tool
Spacelift is an alternative to using homegrown solutions on top of a generic CI. It helps overcome common state management issues and adds several must-have capabilities for infrastructure management.