CodeGym /Kursy /Docker for Spring /Property sources Spring Boot

Property sources Spring Boot

Docker for Spring
Poziom 11, Lekcja 1
Dostępny

1. Order of property sources

If you have ever changed server.port in application.yml, started the service, and seen that it stubbornly launches on a different port — congratulations: you have already encountered the fact that Spring Boot configuration lives not in one file, but in several “layers”. In a Docker environment, this shows up even faster, because some values come from “outside” the container, and the developer starts treating the wrong cause: edits the file, rebuilds the image, blames Docker, then Spring, then themselves (that is a normal path, do not be shy).

What matters here is not the specific syntax of -e, -D, or --..., but the foundation: Spring Boot always assembles final values from multiple sources, and those sources have a priority order. As soon as you understand this order, configuration stops being mysticism and becomes an engineering task: “where exactly is the key defined, and which source overrode it?”

Property sources and Environment

In Spring Boot, the word property is just a pair of key=value. Familiar examples: server.port=8080, spring.profiles.active=standalone, app.mode=postgres. And a property source is a specific place from which such pairs can come: a configuration file, environment variables, startup arguments, JVM system properties, and so on. What matters here is not the number of sources (there are many), but the mechanism: Boot puts them all into one “piggy bank”, but not according to the principle “what was read first is true”, but according to the principle of priority.

Inside the application, the role of the “chief accountant of configuration” is played by the Environment object. Almost all Spring code (and your code too) ultimately asks it: “what is the value for key X?” And the interesting part is this: Environment returns the final value already, without explaining “who exactly won”. It does not care whether the value came from a file, an env var, or a startup argument — it returns the winner.

Here is the minimal logic fragment that is convenient to keep in any temporary diagnostic probe. What matters here is not a new endpoint as such, but the fact that we ask Environment for the final value.

String mode(Environment env) {
    return env.getProperty("app.mode", "standalone");
}

Here standalone is just the default for our educational app.mode, if the key is not defined anywhere. By changing the file, env vars, or startup arguments, you will still get the answer from here: from the final Environment.

2. The “layered cake” model

To avoid drowning in details, it is convenient to imagine Spring Boot configuration as a layered cake. At the bottom is the base configuration from files (for example, application.yml inside the jar). On top of it, container environment variables can be added. Higher still — JVM system properties. And at the very top — application arguments (--key=value). When the same key appears in several layers, the upper layer wins. And it does not win “the whole file”, but the specific key.

It is convenient to capture this idea with a diagram:

flowchart TB
    %% Bottom layer: base values and "defaults"; top layer: the highest-priority overrides.
    A["Code (default in @Value / getProperty)"] --> B["Config data files (application.yml)"]
    B --> C["Env vars (environment variables)"]
    C --> D["JVM system properties (-Dkey=value)"]
    D --> E["Application args (--key=value)"]

    %% Important: the conflict is resolved at the KEY level, not at the "whole file" level.
    note["The same key can exist in several layers. The highest source where it is defined wins."]
    E --> note

Pay attention to the bottom layer. When you write ${app.mode:standalone} or getProperty("app.mode", "standalone"), you are defining a default in code that is used only if the key comes from nowhere. This is not a property source in the usual sense, but for a beginner’s mental model it is useful: “even if I pass nothing, the application does not have to crash — it can have sensible defaults.”

4. A simplified priority ladder

In real Spring Boot, there are more sources than we need for an everyday container workflow, and that is normal. But for a container scenario, it is enough to remember the working ladder that covers 95% of cases in a Java developer’s daily life:

Level (weak → strong) What it is Example key What it usually stores
Weakest Default in code ${app.mode:standalone} “Let it at least work without configuration”
Base application.yml (config data) server.port: 8080 “Project defaults”
Stronger Env vars SERVER_PORT=8081 Environment overrides (container, dev machine)
Even stronger JVM system properties -Dserver.port=8082 JVM / process startup parameters
Strongest (in our simplified model) Application args --server.port=8083 Explicit application startup arguments

This table is not meant to make you memorize every form and nuance. The goal is simpler: if a value is “not the same as in application.yml”, then somewhere higher on the ladder it has been overridden. And the closer you are to Docker, the more often the upper levels participate in real life.

To avoid mixing precedence with profile mechanics and infrastructure modes, we will continue using the simple educational key app.mode. Let us imagine a very typical situation. Inside application.yml in the jar, we have a default port and default mode set:

# Base project values: they work "out of the box" if nothing is overridden at startup.
server:
  # Default port for local startup and for a container without additional settings.
  port: 8080

app:
  # Default application mode; later we will override it from above.
  mode: standalone

This is an excellent baseline: the service starts locally, in a container, anywhere. But as soon as at some point you start the container with a port override (or a profile), application.yml stops being the “last instance”. It remains the base, but not the winner.

5. Override by key

One of the most deceptive things in configuration is the feeling that if “source X won”, it won everything. In reality, Spring Boot chooses a winner for each key separately. This is very convenient: you can override exactly one value without rewriting the whole config. But it is also a source of confusion: you edit application.yml, expecting that “everything will become like there”, but only what is not shadowed from above changes.

Let us extend any temporary runtime probe so that it shows a couple of values at once. What matters here is not a new controller, but the fact that you can see two keys at the same time and immediately understand what won for each one.

// If you already have a diagnostic GET /api/runtime endpoint, it is convenient to extend it like this:
@Value("${server.port}")
private String port;

@Value("${app.mode:standalone}")
private String mode;

@GetMapping("/api/runtime")
String runtime() {
    return mode + " @ " + port;
}

It is convenient to attach such a method to your diagnostic GET /api/runtime endpoint. The key idea is what matters here: we look at two keys at once and see the winner for each one.

Now imagine three launches (you can do this locally or in a container — the principle is the same). I will specifically show the commands using java -jar, because in a container that is exactly how we start, even if the image was built with multi-stage.

Launch #1: we do not override anything, we take the base from application.yml.

# Base launch: take values from application.yml (and defaults from code if there is no key).
java -jar app.jar

Then the call:

GET /api/runtime

will return something like:

standalone @ 8080

Launch #2: we override only app.mode through a startup argument (yes, that is already the “top layer”).

# Override exactly one key via application args: it will shadow the file.
java -jar app.jar --app.mode=postgres

Then:

standalone @ 8080   (will not happen)
postgres @ 8080     (will happen)

Notice that the port did not change, because we did not touch server.port. And that is a normal model: configuration does not “switch as a whole”; it is assembled by key.

Launch #3: we create a small conflict: we set app.mode both as a system property and as an application arg.

# The conflict is intentional: the same key is set both via -D and via --args.
# In the simplified priority ladder, --app.mode is "higher" and will win.
java -Dapp.mode=fromSystem -jar app.jar --app.mode=fromArg

The result will be:

fromArg @ 8080

And this is the moment to accept as a rule of the game: if you define the same key in several places, the more prioritised source will override the less prioritised one. No “both values will be considered”, no “they will merge”, no “a random one will be chosen”. There will simply be a winner. And the loser will remain in history as the reason for your future questions of the form “why is it being ignored?”

6. Practice: conflicts and debugging

Rebuilding the image and configuration conflicts

When you are in the container world, the first reaction to strange behaviour is often: “Probably the image is old. We need to rebuild it.” That is sometimes true (if you changed the code), but with configuration conflicts, rebuilding is like trying to cure a fever by replacing the thermometer: the action exists, but the point is small. The order of property sources does not change from rebuilding, and if a value is overridden from above, it stays overridden.

The most common scenario looks like this. Inside the jar there is application.yml, you change server.port: 8080 there, rebuild the image, start the container — and the port is still “not the one”. And then the guessing begins, although you only need to ask one question: “Am I passing server.port somewhere else?” In Docker reality this is easy: ENV in a Dockerfile, -e in docker run, environment variables from your IDE or terminal, startup arguments in ENTRYPOINT/CMD. All of this can override the file, and rebuilding has nothing to do with it.

So a useful discipline sounds like this: if only the startup config changed — do not rebuild the image, look for the winning source; if the code/artifact changed — rebuild the image. That is exactly the boundary we want to reinforce in module 3.

Debugging: “find where the key is defined”

When configuration “does not match expectations”, it is very tempting to jump into the most visible layer — usually application.yml — and fix things there. But it is better to start differently: choose one specific key (for example, server.port) and conduct a small investigation: where can it actually be defined? At this stage, it is important not to expand the problem into “the whole configuration is broken”, because it almost never is; usually only 1–2 keys are in dispute.

The practical path looks like this. First, you make sure that you see the final value. That is why we made /api/runtime: it is a quick answer that shows “what actually happened”. Then you check the base layer: what is written in application.yml. After that, you remember the priority ladder and honestly ask yourself: “Am I overriding this through env vars, through -D, or through --...?” If the application runs in a container, you add another important layer: “what exactly did I pass to the container at startup?”

When it comes to a container, it is very helpful that we studied in the early days of the course about Docker CLI. The docker inspect command can show with which environment variables the container was started. And this is not some “rare DevOps command”, but a simple flashlight in the dark. If you see something like Env containing SERVER_PORT=8081, then the question “why not 8080 from the file” is answered by itself.

To reinforce the idea, here is another small diagram to mentally rely on when configuration panic begins:

flowchart TD
    A["Symptom: the value is wrong"] --> B["Fix the key (for example, server.port)"]
    B --> C["Check the final result (Environment / /api/runtime / logs)"]
    C --> D["Look for the key in application.yml"]
    D --> E["Check whether the key is overridden from above: env vars → -D → --args"]
    E --> F["If container: check docker inspect / startup command"]
    F --> G["Remove duplicates / leave one main source"]

The key idea at the end: your goal is not to “guess the correct source”, but to make sure that the key has one clear owner. Otherwise, configuration turns into a sport of “who can shout louder” — and usually the louder one is the one that was set last and from above.

7. Typical mistakes

Mistake #1: treating application.yml as absolute truth.
The application.yml file is the base layer, great for project defaults, but it cannot be “absolute truth”, because Spring Boot is specifically designed so that it can be overridden at startup. In Docker scenarios this is especially noticeable: the container environment often overrides values from the jar, and that is expected behaviour, not a bug.

Mistake #2: fixing a configuration problem by rebuilding the image.
If you changed only startup values, rebuilding the image usually changes nothing except your mood and waiting time. A configuration conflict is solved not by docker build, but by finding the winning source and removing duplicates. Rebuilding makes sense when the code or artifact changes, not when you are arguing with SERVER_PORT.

Mistake #3: setting the same key in three places “just in case”.
Sometimes it seems logical: “let it be in application.yml, and in an env var, and also in the startup arguments — just in case.” In practice this leads to the fact that a week later nobody (including you) remembers who really affects the result. The right habit is to keep the default in application.yml, and override it surgically and deliberately in one chosen way.

Mistake #4: thinking of configuration as a “whole-mode switch”.
Beginners often expect that “if I enabled some mode”, then the whole application will live by one file and the other layers will disappear. But Spring Boot overrides values by key, so you can get a “hybrid”: one key taken from the file, another from an env var, a third from startup arguments. This is not an anomaly, it is normal mechanics that you need to keep in mind.

Mistake #5: trying to analyse configuration “by location” instead of “by key”.
A very common picture: a developer stares for a long time only at the Dockerfile or only at application.yml and cannot understand why the value is not changing. Configuration in Boot is resolved by key, so the investigation should also go by key: “where is server.port defined?”, “where is spring.profiles.active defined?” As soon as you switch to this lens, the chaos drops sharply.

1
Zadanie
Docker for Spring, poziom 11, lekcja 1
Niedostępne
Wartość z `application.yml` i nadpisywanie przez env var
Wartość z `application.yml` i nadpisywanie przez env var
1
Zadanie
Docker for Spring, poziom 11, lekcja 1
Niedostępne
Nadpisywanie tylko jednego klucza
Nadpisywanie tylko jednego klucza
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION