CodeGym /Courses /Docker for Spring /Ports in Docker: the service does not open

Ports in Docker: the service does not open

Docker for Spring
Level 2, Lesson 4
Available

1. Symptom: the container is running, but HTTP “does not open”

If the container is running, but HTTP from the host does not open, the problem is most often in ports and addresses, not in “broken Docker”. The container may look alive: it is listed in docker ps, the processes inside are in place, and when you try to open the service from the host you get Connection refused or endless waiting. A beginner at this point usually suspects everything — from “Docker is broken” to “my laptop is cursed”. But in practice the error is usually much more prosaic: you are looking in the wrong place — the wrong port, the wrong address, or you forgot to “show” the port to the outside world altogether.

It is important to fix the main rule of the day: the container can be alive, while the service can still be unavailable from your machine. And that is not a contradiction. The container is a separate network “world”, while access from the host is a separate setting that must be configured explicitly.

That is exactly why ports should be examined separately. The same symptom appears with a ready-made httpd and with your own backend service: the image changes, but the questions stay the same — is the port published, which host port are you connecting to, and on which address does the process actually listen.

2. Two different ports: “inside the container” and “on your machine”

When we say “the service listens on port 80” or “Spring Boot listens on 8080”, it is important to clarify which world this happens in. Docker creates two worlds: your computer’s world (host) and the container world. Port numbers in them may match, but these are different doors in different buildings. Simply saying “8080” without specifying where is already enough to create an adventure for yourself.

Let’s look at this as a simple diagram. The user (you) opens localhost:8080 on the host machine. Docker can forward this request into the container, for example to port 80, where the web server listens. If there is no forwarding, the request simply will not reach the container, even if everything works perfectly inside.

flowchart LR
    %% Diagram idea: on the left you (host), in the middle port publishing, on the right the container and process
    U["You: browser / curl
on the host machine"] -->|"localhost:8080"| P["Port publishing
docker run -p"] P -->|"container:80"| C["Container"] C --> S["Process inside the container
(for example, httpd)"]

The key idea: the container port is the port of a process inside the container. The host port is the port on your machine that you access through localhost:.... And the docker run -p command is the “translator” between them.

3. docker run -p and the HOST:CONTAINER format

The -p flag in docker run is exactly port publishing. It tells Docker: “make whatever happens on the container port available from the outside on a port of my machine.” It is important to think of it not as “just another flag”, but as part of a network contract: without it your service will almost always remain “internal”.

The most basic form looks like this:

Entry in docker run How to read it in plain English
-p 8080:80 “Open host port 8080 and route traffic into the container to port 80”

Let’s look at a minimal example with the ready-made httpd web server (Apache). It is convenient because it can immediately respond over HTTP and does not require any code from us.

# Start the container in the background (-d) and publish the port: 8080 on the host -> 80 in the container
docker run -d --name web8080 -p 8080:80 httpd:2.4

# Check from the host machine that HTTP is really available on the published port
curl -I localhost:8080

# Expect response headers and code 200, which means port forwarding works
# HTTP/1.1 200 OK

A useful “magic without magic” happens here. Inside the httpd container, port 80 is being listened on. Outside, you connect to localhost:8080. And Docker makes the bridge: 8080 on the host → 80 in the container.

If you accidentally mix up the order and write -p 80:8080, Docker will honestly forward host port 80 to container port 8080... but nothing is listening on 8080 in the container. As a result, the container will be “alive”, but HTTP will be “dead” for you. That is why we keep repeating so insistently: on the left — host, on the right — container.

Another important point: if the host port is already in use (for example, you chose 8080, but another service is already running locally there), Docker will not be able to “attach” to that same port. In this case docker run usually ends with an error like “port is already allocated”. This is not a Docker bug, but normal physics: two different processes on one host port do not coexist without special tricks.

Checking the publication: docker ps and docker inspect

When a service “does not open”, it is very easy to switch into action-movie hero mode: restart everything, dig through configs, and generally move fast but without a plan. In practice, it is usually enough to first look at the facts, and Docker gives you two convenient sources for this: docker ps and docker inspect. The first is a quick overview, the second is the source of truth in a structured form.

Let’s start with docker ps. A container has a PORTS column. It exists specifically so that you can immediately see whether the port is published externally or not.

# Look at the container list and the PORTS column (that is where the whole truth about port forwarding lives)
docker ps

# CONTAINER ID   NAMES     STATUS         PORTS
# ...            web8080   Up ...         0.0.0.0:8080->80/tcp

The entry 0.0.0.0:8080->80/tcp is read like this: “on the host machine, port 8080 is open (on all interfaces), and it is forwarded into the container to port 80 over TCP.” Even if you do not yet fully understand 0.0.0.0, the main point is already visible: 8080->80.

Now let’s look at docker inspect — this is a more detailed “profile” of the container. A full inspect is huge, so we take only the needed part with formatted output: port mappings.

# Extract only the section with port forwarding from inspect (in JSON), so we do not drown in the full output
docker inspect --format '{{json .NetworkSettings.Ports}}' web8080

# Here you can see: container 80/tcp is published as HostPort 8080 on HostIp 0.0.0.0
# {"80/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

This is the same fact, but in JSON form. Here it is convenient to see two fields: HostIp and HostPort. We will come back to HostIp when we discuss 127.0.0.1 and 0.0.0.0.

Now it is useful to see the contrast: the container is running, the service exists inside, but the port is not published outward. This is exactly the “container is alive, but does not open” scenario.

# Start httpd WITHOUT publishing the port (-p is missing) — from outside it will be unavailable
docker run -d --name web-no-port httpd:2.4

# PORTS will show only the internal container port, without an arrow to the host
docker ps
# ... web-no-port  Up ...   80/tcp

# There is nothing to check on the host: we did not publish anything, so the connection will be refused
curl -I localhost:8080
# curl: (7) Failed to connect to localhost port 8080: Connection refused

Pay attention to the detail: in PORTS for web-no-port, you will see something like 80/tcp, but without the arrow -> to the host. That is the hint: “port 80 exists inside the container”, but it is not published. In other words, Docker knows that 80/tcp exists inside, but it has not shown it to the outside world.

And yes, this is another important rule: docker ps shows that the container is “Up”, but this is not a promise that your service is reachable from the host. It only confirms that the container’s main process is alive. Availability is a separate story.

4. localhost and other addresses

In the Docker context, addresses quickly start to look like spells from a beginner network wizard book: localhost, 127.0.0.1, 0.0.0.0. And very often the “does not open” problem is not the port, but exactly the wrong interpretation of the address. Here it is important not to “learn magic”, but simply to understand the meaning: where we listen and where we connect from.

First, a short table that is useful to keep in mind:

Address How to think about it
localhost “this same machine”, usually a synonym for 127.0.0.1 when checking in a browser/curl
127.0.0.1 loopback interface: “local only on this machine”
0.0.0.0 “listen on all interfaces” (this is not the address you usually put in a URL)

The main trap: you often see 0.0.0.0 in docker ps or docker inspect, but you should not try to open http://0.0.0.0:8080 in the browser as the “correct address”. In most local scenarios you open http://localhost:8080 or http://127.0.0.1:8080.

But for availability from the host machine, that is still not enough. Two things must happen at the same time:

- Docker publishes the required port outward via -p;
- the process inside the container listens not only on its own loopback 127.0.0.1, but on the container’s accessible interface, usually 0.0.0.0.

And here it is easy to confuse two different localities. 127.0.0.1 on the host and 127.0.0.1 inside the container are not the same address. If the application inside the container is bound only to its own loopback, publishing the port alone will not save you: the request will reach the container, but not the process.

Now let’s look at an important practical option for publishing a port: you can “open it to everyone” (by default), or “only locally”. This is especially useful when you do not want to expose the service on the local network.

# Publish the port so that it listens only on loopback: available only from this machine
docker run -d --name web8090 -p 127.0.0.1:8090:80 httpd:2.4

# Check that Docker really bound the publication to 127.0.0.1, not all interfaces
docker inspect --format '{{json .NetworkSettings.Ports}}' web8090
# {"80/tcp":[{"HostIp":"127.0.0.1","HostPort":"8090"}]}

# Local check will succeed
curl -I localhost:8090
# HTTP/1.1 200 OK

Locally everything works. But if you try to open this port not from this machine, for example from a phone on the same network (or from a colleague’s laptop), nothing will work. And that is expected: you explicitly asked Docker to listen only on 127.0.0.1, that is, only on loopback.

And one more confusion that often comes up specifically in containers: localhost inside the container is “the container itself”, while localhost on the host machine is “your computer”. These are two different “I am at home”s. Here we are interested in access from the host machine, because this is exactly the boundary where local checks most often break.

5. Three common failures and diagnostics with commands

Instead of memorising dozens of flags, it is much more useful to keep a few characteristic scenarios in mind. Each has its own symptoms, and each is diagnosed with literally a couple of commands. It is like medicine, only without a white coat: first we measure the pressure (ps/inspect), then we read the tests (logs), and only then do we draw conclusions.

Scenario 1: the port is not published at all (you forgot -p).
The container is “Up”, in docker ps you see 80/tcp (or another port), but there is no arrow -> to the host. From the host, any curl localhost:... to the expected port will be useless. The fix is not “tuning curl”, but restarting the container with the correct port publication.

# Start without -p: Docker does not create an "entrance door" from the host into the container
docker run -d --name web-no-port httpd:2.4

# In PORTS there will again be only the internal container port (without publication)
docker ps
# ... web-no-port  Up ...  80/tcp

Scenario 2: the port is published, but you are checking the wrong host port.
This is the most frustrating failure, because it usually looks like “everything is broken”, when in fact you simply typed “8091” instead of “8090”. At such a moment it helps not to argue with reality, but to look at docker ps or inspect again and put exactly the numbers shown there into curl.

# Publish 8080 on the host -> 80 in the container
docker run -d --name web8080 -p 8080:80 httpd:2.4

# Checking error: we connect to a different port that was not published
curl -I localhost:8090
# curl: (7) Failed to connect ...

# Correct check: use exactly the host port specified in -p
curl -I localhost:8080
# HTTP/1.1 200 OK

Scenario 3: you “narrowed” publication to 127.0.0.1, and then expect the service to be available “from outside”.
From the host (curl localhost:...) everything will work, and that is even sneaky: it feels like “the network is fine”. But as soon as the check comes from a different machine, it suddenly “does not open”. In such a situation, docker inspect explains the reason very quickly: HostIp will be 127.0.0.1. And that is not an error, but your explicit setting.

# Restrict publication to loopback — access will be only from the current machine
docker run -d --name web-local -p 127.0.0.1:8090:80 httpd:2.4

# HostIp will show 127.0.0.1 — that is exactly why you cannot reach it "from the network"
docker inspect --format '{{json .NetworkSettings.Ports}}' web-local
# {"80/tcp":[{"HostIp":"127.0.0.1","HostPort":"8090"}]}

If you look at these three stories and think, “this is too simple to be true,” then congratulations: you are on the right path. Most early Docker problems are solved by exactly these simple checks. The brain, after several years of development, is just used to the idea that “if it does not work, then the problem is complicated”. Docker, at the first steps, is often more honest.

6. Mini diagnostic algorithm

It is very tempting to have “one command that fixes everything”. But proper engineering begins where, instead of magic, there is a short, repeatable order of actions. It should not be long — otherwise you just will not use it. Let it be like this: five minutes for facts, and only then — for hypotheses.

Below is a simple flow that you can keep in mind. It relies only on the commands we already have today.

flowchart TD
    %% Top-down diagnostics: is the container alive? is the port published? is the host port correct?
    A["Service does not open from host"] --> B["docker ps: container Up"]
    B -->|no| C["Container is not running: check logs/exit code"]
    B -->|yes| D["docker ps: is there PORTS with an arrow ->"]
    D -->|no| E["No publication: restart with -p"]
    D -->|yes| F["docker inspect: which HostPort?"]
    F --> G["curl localhost:HostPort"]
    G -->|does not work| H["docker logs (are there startup errors?)"]
    G -->|works| I["OK: availability confirmed"]

The point of this algorithm is that it answers several different questions in sequence. First: “is the container alive at all?” Then: “is the port published at all?” And only then: “am I checking the host port correctly?” If everything looks correct at these steps, but there is still no response, one more common hypothesis remains: the process inside the container listens only on its own loopback. Then the problem is no longer in -p, but in the process’s bind address itself. This saves a lot of time and protects you from diagnosis “on emotions”.

7. Typical mistakes when publishing ports

Mistake #1: assuming that “container port” and “computer port” are the same thing.
This is a classic. A thought sounds in your head: “The service is on port 80, so I open localhost:80.” But if you published -p 8080:80, then from the host your entry point is 8080. In such situations, a simple habit helps: always look at the HOST->CONTAINER arrow in docker ps and literally read it out loud, like a school kid in class.

Mistake #2: seeing 80/tcp in docker ps and thinking that the “port is open externally”.
The entry 80/tcp without an arrow is, in essence, an “internal container port”. It may exist, but be inaccessible from outside. Docker is not mocking you here, it is simply honestly showing two different things: “there is such a port inside the container” and “it is published”. Do not confuse them.

Mistake #3: trying to open http://0.0.0.0:8080 as the “correct address”.
0.0.0.0 is not “the service address for the browser”, but “listen on all interfaces” (what is written as HostIp in inspect). To check from the same machine, use localhost or 127.0.0.1. If you really want to check it “properly”, you can use the real IP of your computer, but that is a different story.

Mistake #4: checking the wrong host port (the human factor, which beats even good brains).
It usually looks like this: “everything is configured, but it does not open.” Then it turns out that you started it on 8080, but are checking 8090. This is fixed not by knowing Docker, but by discipline: docker ps → copy the port → curl. You can even not understand networking — just be honest with the numbers.

Mistake #5: accidentally restricting publication to 127.0.0.1 and expecting access “from everywhere”.
This often comes up when someone sees an example with 127.0.0.1:... and copies it “just in case”. For local development, this is normal and even useful. But if you are testing access not from the same machine (or in more complex environments), it will look like “Docker broke the network”. In reality, Docker simply fulfilled the request: “only locally”. In inspect this is immediately visible by HostIp.

1
Task
Docker for Spring, level 2, lesson 4
Locked
HTTP Service on an Exposed Host Port
HTTP Service on an Exposed Host Port
1
Task
Docker for Spring, level 2, lesson 4
Locked
Local publication on 127.0.0.1
Local publication on 127.0.0.1
1
Survey/quiz
Docker Basics, level 2, lesson 4
Unavailable
Docker Basics
Image and container commands
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION