Docker Port Mapping by example

Port Mapping

So far the code that we’ve run inside of containers has been writing some text to standard output. Now it’s time to run something a bit more complex. So let’s run a basic web application. Here’s the web application we’re going to run. It’s an application written in Go.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", HelloServer)
    http.ListenAndServe(":8080", nil)
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}

There is not much to this webapp. The important part to know is that this is a basic web app. It’s going to run on port 8080 and serve up some text. I have this webapp compiled into a binary called webapp, and it is in the same directory as my Dockerfile.

FROM scratch
COPY webapp /
EXPOSE 8080
CMD ["/webapp"]

Ok, so let’s build this image, then we can startup a container based off this image.

(base) shravan-web-example# docker build -t "webapp" .
Sending build context to Docker daemon  6.638MB
Step 1/4 : FROM scratch
 --->
Step 2/4 : COPY webapp /
 ---> 8f68dcd2a2be
Step 3/4 : EXPOSE 8080
 ---> Running in 8f8b871d96d5
Removing intermediate container 8f8b871d96d5
 ---> ae552d4df668
Step 4/4 : CMD ["/webapp"]
 ---> Running in 868d06ed9522
Removing intermediate container 868d06ed9522
 ---> 6ab358b2f0f6
Successfully built 6ab358b2f0f6
Successfully tagged webapp:latest
(base) shravan-web-example#

Great, so the image built successfully. And we can see it here in the list of images.

(base) shravan-web-example# docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
webapp                            latest              6ab358b2f0f6        30 seconds ago      6.63MB

Let’s run this container with the docker run command. So notice it’s just hanging here.

(base) shravan-web-example# docker run webapp

This is expected, because the code is running inside of the container in an infinite loop. It’s just running forever, serving up this website until we stop it. So the web application is running indefinitely and waiting for connections. And while it’s running, we’re unable to interact with our bash prompt. Now really this is no different than if we ran this outside of Docker. It’s not specific to Docker.

Detach mode

However, we can solve this. Docker allows us to run the process in the background with the -D flag. So for this flag, D stands for detach, as in Docker detaches from standard I/O and instead just prints the container ID, and then it’s going to return us to our prompt. So this allows us to run containers in the background.

(base) shravan-~# docker run -d webapp
93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e
(base) shravan-~#

So if I list off the containers, you can see based on the status here that that container is up and running. At this point, the container is running.

(base) shravan-~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
93803a8295ef        webapp              "/webapp"           30 seconds ago      Up 30 seconds       8080/tcp            tender_shannon
(base) shravan-~#

However, it’s really not clear how we interact with this web app.

By default, Docker containers run inside of their own network, called the bridge network.

I will cover networking in another post, but for now, the thing to know is that this web app is running inside of the container, and it’s accessible via the container’s IP address on port 8080.

Docker inspect to get the ip of a container

It’s possible to fetch the IP address using the docker inspect command and then passing in the container ID or name. Running the docker inspect <container-id> will return an ungodly amounts of information about the container, shown here, out of this find the “NetworkSettings” section, where you will find the IPAddress field.

(base) shravan-~# docker inspect 93803a8295ef
[
    {
        "Id": "93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e",
        "Created": "2020-04-20T20:14:11.0986813Z",
        "Path": "/webapp",
        "Args": [],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 4663,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2020-04-20T20:14:11.3836497Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:6ab358b2f0f627cf06a4404f267d577fba5c7c24b7ec2f4d19c2e26746cd8d3e",
        "ResolvConfPath": "/var/lib/docker/containers/93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e/hostname",
        "HostsPath": "/var/lib/docker/containers/93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e/hosts",
        "LogPath": "/var/lib/docker/containers/93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e/93803a8295efa46b12225b5483c8fb1096f1b4463ec4f11e314207d13db43a6e-json.log",
        "Name": "/tender_shannon",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "default",
            "PortBindings": {},
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "Capabilities": null,
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": false,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/fc0caff8a460eebe0b39b4c6fd161a8bac57c1c2a0c803cfe9c4e50d41c92a77-init/diff:/var/lib/docker/overlay2/034c6cfbcac4ecb5b7eb718c05d163e25c6c03faad1004d758fdc3efa107a8cb/diff",
                "MergedDir": "/var/lib/docker/overlay2/fc0caff8a460eebe0b39b4c6fd161a8bac57c1c2a0c803cfe9c4e50d41c92a77/merged",
                "UpperDir": "/var/lib/docker/overlay2/fc0caff8a460eebe0b39b4c6fd161a8bac57c1c2a0c803cfe9c4e50d41c92a77/diff",
                "WorkDir": "/var/lib/docker/overlay2/fc0caff8a460eebe0b39b4c6fd161a8bac57c1c2a0c803cfe9c4e50d41c92a77/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [],
        "Config": {
            "Hostname": "93803a8295ef",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "8080/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
            ],
            "Cmd": [
                "/webapp"
            ],
            "Image": "webapp",
            "Volumes": null,
            "WorkingDir": "",
            "Entrypoint": null,
            "OnBuild": null,
            "Labels": {}
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "d1a7504802cb959b5c814e29305f5b3d38b96d4f405f7a47c2d23ba48037b04f",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {
                "8080/tcp": null
            },
            "SandboxKey": "/var/run/docker/netns/d1a7504802cb",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "bf59251961b1eb843855744b2e1693d3442633ee62f45355c13d3cb1ef268497",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "3011d8e256e139ef6d6f5339468d963806f8ce6ee8dfe415406fffc8b5bd3b3c",
                    "EndpointID": "bf59251961b1eb843855744b2e1693d3442633ee62f45355c13d3cb1ef268497",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]

Notice here, the IP address field, you can see that it’s set with the container’s IP. However, this is a rather cumbersome way of dealing with the webapp.

Note: Because each container has its own IP address, all of them can run on port 8080 inside of their own container, and then we can interact with them via their IP address.

Publish all -P and publish -p flags to map ports

There is a better way which does not require us to get the IP address of the container. And that would be to bind the port in the container to a port on the host.

Bind the port in the container to a port on the host.

Docker allows you to do this dynamically or explicitly. The Dockerfile specifies that the container exposes port 8080. By default, the expose instruction really doesn’t do anything to the host. Docker lets you bind the container port to a host port with the publish or publish all flags.

Publish all will dynamically map the exposed ports of the container to open ports on the host.

The -P flag is the short form of publish all. Let’s run this command: docker run -d -P webapp

So this is going to run the container in detached mode, which means it’s gonna run in the background. It will also dynamically bind the exposed port 8080 to a port on the host. So we can use the docker ps command to see which port. In this case, the port is 32769.

(base) shravan-~# docker run -d -P webapp
94faeb7972782e5387c127367ce8e328d78784c2eb1788e7ee8d96c5b7610cae
(base) shravan-~# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                     NAMES
94faeb797278        webapp              "/webapp"           11 seconds ago      Up 10 seconds       0.0.0.0:32769->8080/tcp   bold_murdock
(base) shravan-~#

We can validate this by accessing the webapp using http://localhost:32769/shravan as shown: docker_port_mapping

So now if we start up another container, again using the publish all flag to dynamically map the ports, it’s going to choose a different port automatically.

All right, perfect. So using publish all, you can dynamically map the ports that are exposed from the container to the host. Now, you can also use the publish flag, which will allow you to map specific ports. So in this case, we’re going to use a -p flag, which is the short form of publish, and it allows you to specify the port on the host that you want to use, and then a colon, and then the port on the container.

For example, if I want to map port 3000 on the host to port 8080 inside of the container. I would run this docker run -d -p 3000:8080 webapp

(base) shravan-docker# docker run -d -p 3000:8080 webapp
e142e347cd81cf534a0b13892bc8008bcf89682d94bfebb466660dcba3db7f0a
(base) shravan-docker# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                    NAMES
e142e347cd81        webapp              "/webapp"           10 seconds ago      Up 10 seconds       0.0.0.0:3000->8080/tcp   amazing_babbage
(base) shravan-docker#

Summary

Docker allows you to map ports from containers to the hosts running the containers. The mapping can be done dynamically with the publish all flag or explicitly with the publish flag. And by default, the expose instruction in the Docker file doesn’t actually perform any port mapping. It’s up to you to determine how you want to publish the ports.

Tags:

Updated: