CodeGym /Kursy /Docker for Spring /Docker breakages by symptoms

Docker breakages by symptoms

Docker for Spring
Poziom 22, Lekcja 2
Dostępny

1. The symptom matters more than the first guess

When a container "does not work", the beginner developer's brain (and, honestly, sometimes an experienced one too) does two things: it panics and invents an explanation. The problem is that the explanation usually appears before the facts, which means we start treating a made-up disease. Today's goal is to learn how to recognize several typical breakages by their first symptoms and choose the cheapest check that confirms or disproves a specific hypothesis.

In troubleshooting, it is useful to think like a doctor in an emergency room, not like a philosopher: we have a complaint (the symptom), we have minimal diagnostics (tests), and only then do we have a diagnosis. If you have "the container is alive, but the API does not open", that is not yet "Spring is broken" — it is just an observation. And from observations like that, we will now assemble four very common scenarios: a broken port, a broken host, a broken env var, and a broken mount.

The vocabulary for today is already here, and so are the basic tools. Now it is useful to run them through four breakages that repeat most often: broken port, broken host, broken env var, and broken mount. What matters here is not the list of horror stories, but the habit of taking the cheapest first check for the symptom.

For orientation, keep a small chain in mind that we will repeat almost like a mantra:

flowchart TD
  S[Symptom] --> H[One hypothesis]
  H --> C[One confirming check]
  C --> F[One change]
  F --> R[Repeat the original check]

This is not "bureaucracy". It is a way not to drown in chaotic edits, after which it is no longer clear what exactly helped.

2. Broken port: the container is alive, HTTP is unavailable

Port problems are a classic, roughly like "it does not compile because I forgot the semicolon", only in the world of containers. What makes them especially tricky is that the application may start cleanly, the logs will look nice, health may be green, and yet you will still get Connection refused or a timeout when you request it from your machine. The key here is not to guess, but to break the situation down into levels: which port Spring Boot listens on inside the container, which port is published on the host, and whether these two worlds match.

Two ports, three realities

In Docker/Compose there are almost always at least two realities:

1) The port inside the container, where the Java process lives (for example, server.port=8080).

2) The port on your host machine, through which you connect from the outside (for example, localhost:8080).

The relationship between them is defined by port publishing. In Compose this is ports, in docker run it is -p.

It is very useful to see this once as a small table:

What it is Where it exists Example
Application port (Spring Boot) inside the container 8080
Container port (network namespace) inside Docker 8080/tcp
Host port (your laptop) outside 8081

Yes, it can be the same number, but it does not have to be. And this is where the "magical" feeling begins, although in reality it is all quite mechanical.

Typical broken port symptom

Suppose you started the app service and it looks alive. You run:

curl -i http://localhost:8080/actuator/health

...and you get a connection error. The first desire is to blame Spring Boot. The second is to blame Docker. The third is to blame life. But instead, let us make a simple thinking pause: "What port is actually published?".

Cheap check #1: what Docker actually published

The shortest and most honest check is to look at the container port mapping. For Compose, docker compose ps is often enough, but we are sticking to today's line: "we check facts through what was actually applied".

If you have the container name (or you are working through docker compose and know the service), then the basic variant looks like this:

docker compose ps
# Look at the actual port mapping for the app service container
docker inspect --format '{{json .NetworkSettings.Ports}}' $(docker compose ps -q app)

If the output looks like there is no required mapping in Ports, then that is the explanation for "why it does not open from the outside": you simply did not tell Docker to publish the port.

Cheap check #2: which port Spring Boot actually started on

At startup, Spring Boot usually writes the port in the logs (Tomcat/Netty). For a typical Tomcat instance it looks roughly like this:

Tomcat started on port(s): 8080 (http) with context path ''

And here is the important point: if the logs show port 8081, but you publish 8080:8080, then from the outside you are honestly sending requests into "nothing". The application is listening somewhere else.

Mini-breakage: ports mixed up in Compose

Here is an example of configuration that looks plausible, but in reality creates the symptom "the container is alive, but the API does not open where you expect it":

services:
  app:
    ports:
      # Publish host:8080 -> container:8081 (while the application inside may listen on 8080)
      - "8080:8081"

Here you publish host:8080 → container:8081. If the application inside the container listens on 8080, then requests to localhost:8080 will go to port 8081, where nobody is listening.

In such a scenario, the first fix should be minimal and targeted: either change the application port, or change the port mapping. For example, if the application inside the container listens on 8080, it is more logical to fix the mapping:

services:
  app:
    ports:
      - "8080:8080"

Mini-breakage: the port was changed via env var, but publishing was forgotten

An even more realistic story: you enabled an environment variable so the service listens on a different port, and forgot that Docker is still publishing the old one.

services:
  app:
    ports:
      # Publishing 8080 of the container from the outside...
      - "8080:8080"
    environment:
      # ...but the application inside the container starts listening on 8081
      SERVER_PORT: "8081"

Now the application inside the container listens on 8081, while from the outside you publish container port 8080, where nobody is listening. The result is the same symptom.

And again, the "one change" rule helps you avoid making things worse: either return SERVER_PORT back to 8080, or change the publication to 8080:8081. Do not change both "just in case", otherwise you will not understand what exactly was the cause.

An important clarification about 0.0.0.0 and 127.0.0.1

In Spring Boot, the default is fairly container-friendly (usually the application listens on all interfaces). But if you set server.address=127.0.0.1 somewhere, then inside the container it means: "I listen only on loopback inside the container". From the outside this often feels like "the port is published, but it still does not open".

This is a rarer situation, but for troubleshooting it is useful to remember: a published port is not yet a guarantee that the application listens "outside" its network space.

3. Broken host: we did not connect to a dependency

After the port stops being a mystery, the next popularity hit comes to the front: "the application starts, but does not connect to PostgreSQL/Redis/RabbitMQ". And here a psychological trap often appears: Spring Boot fails in the logs, so "the application is broken". In practice, very often it was not the application that broke, but the address through which it looks for a dependency — that is, the host (or service name) in the configuration.

In a Compose environment, everything becomes especially sensitive to correct names, because inside the Compose network you should address dependencies by the service name, not by localhost. If you keep that as a hard rule, half of network problems stop existing.

What a broken host symptom looks like

The symptom usually sounds like this: the app container starts and almost immediately begins complaining about a connection. For example, for PostgreSQL you will see something like "connection refused" or "unknown host". Even without the exact text of the error, it is important to catch the shape: the application is trying to reach something over the network and cannot.

A common error shape in the logs looks roughly like this (heavily shortened so we do not drown in details):

org.postgresql.util.PSQLException: Connection to localhost:5432 refused

If you see localhost in a containerized application, that is almost always a reason to stop and ask: "Did we forget that localhost inside a container is the container itself?".

Cheap check: what host value actually made it into the container

Instead of editing YAML "by eye", do a fact check: what configuration value is actually inside the container.

If app is staying up stably in running, the cheapest check is to see what value actually made it inside the container. If the container is already Exited or goes into Restarting, exec will not help here: first look at docker compose config and the startup logs of app, and only go inside after the service is at least alive.

docker compose exec app printenv | grep SPRING_DATASOURCE_URL
docker compose exec app printenv | grep SPRING_DATA_REDIS_HOST

If SPRING_DATASOURCE_URL contains localhost, then the problem is almost certainly that. Inside Compose you need the service name, for example postgres.

Mini-breakage: typo in the service name

The most annoying broken-host variant is when you understood everything correctly, but made a typo:

services:
  app:
    environment:
      # Typo in hostname: DNS inside the Compose network will not find a service with this name
      SPRING_DATASOURCE_URL: jdbc:postgresql://postres:5432/catalog

postres instead of postgres — and that is it, DNS in the Compose network honestly says: "there is no such name". The application looks "broken", even though it is just following the wrong instruction.

Here it is especially important not to rebuild the image. We are not changing the code and we are not changing the Dockerfile. We are fixing a configuration value and restarting the container, because this is exactly the "same image, different runtime config" case we reinforced in the externalized configuration module.

Mini-breakage: you use the host port inside the Compose network

Another common confusion: you publish PostgreSQL to the outside at, say, 5433:5432 and then try to connect to 5433 from the application.

Inside the Compose network, that is unnecessary. The application should go to the service's internal port, usually 5432, and to the host postgres. An example of a correct value:

services:
  app:
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog

The port published to the outside is needed when you want to connect to the database from the host (for example, from an IDE, DBeaver, or psql). But inside Compose it is a different world.

Quick logic: who can see whom

If you are unsure, it is useful to mentally say the phrase: "The application lives inside the Compose network. There, services have DNS. There is no point in going to the host and back."

This is exactly why we so insistently reinforced the rule "service names instead of localhost".

4. Broken env var: the wrong operating mode

Configuration errors are the trickiest, because they often do not look like a "nice crash". Sometimes the application starts, but behaves strangely. Sometimes it crashes at startup, but the error is easy to mistake for a "code problem". Sometimes everything looks normal, but you launched the wrong profile and got the wrong runtime mode. And all of this is one class of breakage: env var / args / profiles are set differently than you think.

In our project this is especially important, because it intentionally supports multiple modes (standalone, postgres, cache, messaging) and must switch through external configuration. The cost of a typo here is quite high: you may be sure that postgres is running, while in reality you remained in standalone.

Typical broken env var symptom

The symptom can take different forms, but most often it is one of two stories.

The first story is that the application does not start because the value is invalid by type. For example, SERVER_PORT=abc and Spring Boot honestly cannot turn abc into a number. That looks like a startup/config error.

The second story is that the application starts, but "not quite right". For example, you expected postgres, but in fact a profile with a typo was activated, and the needed settings were not picked up.

Cheap check: which env vars are actually inside the container

Here again, a simple fact check wins.

If app lives long enough, the quickest check is to look at the actual env vars from inside. If the container dies too early, do not get stuck on exec: here docker compose config and the startup logs are faster, because that is usually where you can see the profile or value that Boot tripped over.

docker compose exec app printenv | grep SPRING_PROFILES_ACTIVE
docker compose exec app printenv | grep SERVER_PORT

If you saw SPRING_PROFILES_ACTIVE=postgers, then congratulations: you just found the root cause without a single code change.

Mini-breakage: a typo in SPRING_PROFILES_ACTIVE

services:
  app:
    environment:
      SPRING_PROFILES_ACTIVE: postgers,cache

Spring Boot is not required to complain that the profile does not exist. It simply considers this profile active. And then the interesting part begins: your application-postgres.yml will not apply (because the profile has a different name), your @Profile("postgres") beans will not activate, and behavior changes.

A very useful practical signal is the startup logs. Usually Spring writes the active profiles. Roughly like this:

The following 2 profiles are active: postgers,cache

If you see a misspelled word in the logs, this is not a "small thing", this is almost a ready-made root cause.

Mini-breakage: the env var was named incorrectly

Another common situation is when you are sure you passed the variable, but the application behaves as if it does not exist. The reason may be banal: the variable name does not match what Spring Boot expects.

For example, do not do this:

services:
  app:
    environment:
      SPRING_DATASOURCEURL: jdbc:postgresql://postgres:5432/catalog

There is a missing underscore: it should be SPRING_DATASOURCE_URL. Spring Boot will simply not see this setting (meaning: it will not recognize it as the needed property), and then you will get a "strange" default datasource or a failure somewhere else.

You check this the same way: printenv inside the container shows that the variable exists, and then you match its name against the expected format.

Mini-breakage: the value exists, but in quotes/of the wrong kind

Sometimes everything is correct, but the YAML value looks like you are creating a trap for yourself. In most cases YAML handles strings just fine, but when you pass numbers, it is better to be explicitly careful, especially for beginners.

services:
  app:
    environment:
      SPRING_DATA_REDIS_PORT: "6379"

That is okay, because Spring itself knows how to parse a string into an int. But if you accidentally do this:

services:
  app:
    environment:
      SPRING_DATA_REDIS_PORT: "six-three-seven-nine"

then this is a pure configuration breakage. And it is not fixed by rebuilding the image — only by correcting the value.

Why env var often gets "masked" as other problems

Because env var is the cause, and "cannot connect to the database" is the consequence. You may have made a mistake in the profile, and then the application picked up the wrong datasource URL. Outwardly this looks like a network problem, but at the root cause it is a config problem.

We will reinforce this thought even more strongly in the next lecture, where we will talk about Compose errors that mask themselves as application errors. For now, just remember: before you treat the network, make sure the application is actually configured for the right mode.

5. Broken mount: the path does not match

Mounts (bind mounts and volumes) are where a beginner especially easily starts to feel that "Docker lives in a parallel universe". Because outside you see a folder on disk, inside the container you see "nothing", and the application writes "No such file or directory" or creates a file, but it does not appear on the host. In reality, as with ports, this is not magic: it is just a mismatch of paths or expectations.

In our Container-Ready Catalog Service, the mount is especially important for the file export scenario: the application writes CSV files to the directory defined by APP_EXPORT_DIR. If APP_EXPORT_DIR and the mount target do not match, you get strange effects and a lot of unnecessary emotion.

Typical broken mount symptom

There are two very characteristic forms.

The first form is that the export "seems to have succeeded", but there is no file on the host. That means the application may be writing into the container's writable layer instead of the mounted folder.

The second form is that the application crashes when trying to write a file. Then you usually see NoSuchFileException or a similar error: the directory inside the container does not exist, or it is not the right one.

Cheap check #1: which mounts are actually applied

The formatted inspect we already introduced is perfect here:

docker inspect --format '{{json .Mounts}}' $(docker compose ps -q app)

You need three things: Type, Source, and Destination. If Destination does not match where the application writes, then you have almost certainly found the problem.

Cheap check #2: which APP_EXPORT_DIR is actually inside the container

Because the mount itself may be correct, but the application is writing somewhere else:

docker compose exec app printenv | grep APP_EXPORT_DIR

And now we simply match two facts: APP_EXPORT_DIR and the mount Destination.

Mini-breakage: the mount and APP_EXPORT_DIR point to different places

Here is an example that looks "almost right", but in reality guarantees pain:

services:
  app:
    volumes:
      # Mount host folder to /app/export...
      - ./data/exports:/app/export
    environment:
      # ...but the application expects to write to /app/exports (a different path!)
      APP_EXPORT_DIR: /app/exports

Pay attention: /app/export and /app/exports are two different paths. One character s at the end — and you get a situation where everything looks nice on the host, but the application writes to an unmounted place (or vice versa).

The fix should again be targeted. For example, make them the same path:

services:
  app:
    volumes:
      - ./data/exports:/app/exports
    environment:
      APP_EXPORT_DIR: /app/exports

Mini-breakage: you look in the wrong place and think the mount is broken

Sometimes the mount works, but you check the wrong path inside the container. Here the simplest "by eye" check helps us (and yes, this is that rare case where exec is absolutely appropriate, because we already have a concrete hypothesis).

docker compose exec app ls -la /app/exports

If the directory is empty, that does not yet mean the mount is broken. It means either the export has not been triggered yet, or the application writes to a different path, or it writes with a different file name. Therefore, the check must be tied to a specific expected artifact: "after the POST /api/catalog/exports request, the file should appear."

But it is important not to turn diagnostics into a "journey through the file system at random". We look exactly at the path that should be the source of truth.

Nuance: the mount may be of the wrong type

Even without going deep into storage topics, it is useful to distinguish two kinds: bind mount and volume. In docker inspect this is visible in the Type field. For an export scenario, we usually need a bind mount, because the result must be visible on the host.

If you expected to see the file on your disk but connected a named volume, the file will live in Docker's managed storage, and you will not "just see" it on the host. That is not a Docker error — it is a different storage strategy.

6. Cheat sheet: 4 breakages and checks

What matters now is not to memorize everything "by heart", but to learn how to quickly choose the starting check. So I would keep a short table nearby in the exact format "symptom → check → what should be true".

Breakage First symptom Cheapest check What is usually fixed
Broken port the container is alive, HTTP is unavailable from the host inspect ports or compose ps ports: or SERVER_PORT
Broken host the application does not connect to DB/Redis/Rabbit printenv for the needed URL/host host to service name, typo correction
Broken env var "the wrong mode" or a config startup crash printenv + active profile logs variable name/value, SPRING_PROFILES_ACTIVE
Broken mount the file does not appear on the host or there is a write error inspect .Mounts + APP_EXPORT_DIR destination path, APP_EXPORT_DIR matching, mount type

Notice: there is no "rebuild the image" anywhere here. That is not because rebuilding is evil. It is because in these four scenarios the runtime wiring usually breaks, not the build. Rebuilding in such a place is almost always an expensive way to fix nothing, while feeling busy.

7. Typical diagnostic mistakes

Mistake #1: treating "the API does not open" by changing the controller code.
This is a very human reaction: you know how to change Java code and want to do something useful. But if the container is alive and curl does not go through, then with very high probability this is a port mapping, a listening address, or an incorrect port inside the application. Logs and inspect are cheaper than any code change.

Mistake #2: trying to fix broken host by rebuilding the image.
When SPRING_DATASOURCE_URL says jdbc:postgresql://postres:5432/..., no docker build will turn postres into postgres. This is not a "broken image", this is incorrect runtime configuration. Fix it by editing the env var/Compose YAML and restarting.

Mistake #3: changing the port, profile, and host all at once, and then guessing what helped.
This is usually what I call "diagnostics by shamanism": everything jumps, the drum rattles, sometimes it even starts working — but nobody knows why. The one-change rule may seem tedious, but it saves hours and nerves.

Mistake #4: confusing "variables for substitution in YAML" with env vars inside the container.
In Compose there are two layers: value substitution when building the configuration and the container environment at runtime. If you do not check the actual env vars through printenv or inspect, you risk arguing with reality based on how you "thought it should be".

Mistake #5: checking a mount "on the host" without checking the destination inside the container.
The host folder may exist, but if the Destination in the container is different, the application will write past it. And vice versa: a file may be created in the container, but you are looking in the wrong host folder. inspect .Mounts here is like a passport: it says what is actually connected, without assumptions.

1
Zadanie
Docker for Spring, poziom 22, lekcja 2
Niedostępne
Broken port według pierwszego symptomu
Broken port według pierwszego symptomu
1
Zadanie
Docker for Spring, poziom 22, lekcja 2
Niedostępne
Uszkodzony mount i niezgodnosc sciezki ze zmiennej srodowiskowej
Uszkodzony mount i niezgodnosc sciezki ze zmiennej srodowiskowej
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION