One property, many places
The problem. The config file says the pool size is 20, the app uses 10, and nobody knows why. A Spring Boot app can get the same setting from a dozen places; knowing which one wins turns a guessing game into one curl.
What you need to know already: Actuator and /actuator/env (21.1), env vars in systemd units and drop-ins (2.3, 2.21), env vars and Secrets in pods (15.29, 15.33), the JVM's -D flags and jcmd (20.2, 20.3).
A Spring Boot app reads configuration from many property sources and, for each property, uses the value from the highest-priority source that has it. The real order, highest first (Spring Boot reference, "Externalized Configuration"):
1. Devtools global settings (dev machines only)
2. @TestPropertySource / test properties tests only
3. Command line arguments --server.port=9090
4. SPRING_APPLICATION_JSON inline JSON in an env var or -D
5. ServletConfig / ServletContext init params
6. JNDI attributes
7. Java System properties -Dserver.port=9090
8. OS environment variables SERVER_PORT=9090
9. random.* values
10. Config data files, most specific first:
application-{profile}.properties OUTSIDE the jar (./config/, ./, spring.config.location)
application.properties OUTSIDE the jar
application-{profile}.properties INSIDE the jar (classpath)
application.properties INSIDE the jar
11. @PropertySource on @Configuration classes
12. Default properties (SpringApplication.setDefaultProperties)
Things worth noticing:
- Command line beats everything you will meet in production.
- -D system properties beat environment variables - often forgotten.
- Environment variables beat every file. That is what makes Kubernetes work: the image carries
application.propertieswith defaults, and the Deployment overrides per environment withenv:- no new image, no file mount. - A file outside the jar beats the same file inside (the jar is the zip file the app ships as, 20.1), and a profile-specific file beats the plain one at the same location (profile = a named set of settings such as
prod, below). -Dname=value= a Java system property, passed on the java command line (or inJAVA_TOOL_OPTIONS, 17.6). JNDI, ServletConfig and devtools are old or dev-only; you can ignore them.
Relaxed binding: from env var to property
Relaxed binding = Boot accepting several spellings of the same property. Environment variable names cannot contain dots or dashes, so Boot maps them: replace dots with underscores, remove dashes, uppercase:
spring.datasource.password SPRING_DATASOURCE_PASSWORD
spring.datasource.hikari.maximum-pool-size SPRING_DATASOURCE_HIKARI_MAXIMUMPOOLSIZE
server.port SERVER_PORT
spring.config.additional-location SPRING_CONFIG_ADDITIONALLOCATION
my.list[0] MY_LIST_0_
The Notion question - where does SPRING_DATASOURCE_PASSWORD sit? - OS environment, position 8: it beats every properties/YAML file (inside or outside the jar) and loses to -D system properties and command-line arguments. In Kubernetes it usually comes from a Secret via valueFrom.secretKeyRef.
A common trap: SPRING_CONFIG_ADDITIONAL_LOCATION (with the underscore in the middle) is not spring.config.additional-location - the dash is removed, not turned into an underscore. The variable is silently ignored.
Where the files are looked for
Without any configuration, Boot searches:
optional:classpath:/ optional:classpath:/config/
optional:file:./ optional:file:./config/ optional:file:./config/*/
./ is the working directory of the process - WorkingDirectory=/opt/app for our services, not the directory the jar is in. Two properties change the search:
spring.config.location=... REPLACES the defaults
spring.config.additional-location=... ADDS to them (and wins over them)
Locations are comma-separated and can be files or directories (directories end in /). optional: means "fine if missing"; without it a missing location is a startup failure:
***************************
APPLICATION FAILED TO START
***************************
Description:
Config data resource 'file [/etc/orders/app.conf]' via location 'file:/etc/orders/app.conf' does not exist
Action:
Check that the value 'file:/etc/orders/app.conf' is correct, or prefix it with 'optional:'
The file extension decides the parser, so app.conf needs a hint:
file:/etc/orders/app.conf -> IllegalStateException: File extension of config file location
'file:/etc/orders/app.conf' is not known to any PropertySourceLoader...
file:/etc/orders/app.conf[.properties] -> read as a .properties file
The orders service on this box is started with SPRING_CONFIG_ADDITIONALLOCATION=optional:file:/etc/orders/app.conf[.properties] from a systemd drop-in - that is how /etc/orders/app.conf reaches it.
Placeholders
A placeholder ${name:default} inside a value means "the value of property name, or default if nothing sets it":
spring.datasource.hikari.maximum-pool-size=${db.pool.max:10}
The value of db.pool.max, or 10. Resolution happens across all sources, so an env var DB_POOL_MAX=30 changes the pool size even though nobody set the Hikari property directly. (HikariCP - "Hikari" - is the database connection pool Spring Boot uses: a fixed set of open database connections the app borrows and returns, lesson 21.15.)
Profiles
spring.profiles.active=prod in a file, or SPRING_PROFILES_ACTIVE=prod
application-prod.properties loaded on top of application.properties
Do not bake the environment into the image as a profile file per environment unless you have to - environment variables per Deployment are simpler to reason about.
Debugging "why does it have that value?"
/actuator/env/<property>- every source that defines it, in precedence order, with an origin (file and line, env var name). The first one listed with a value wins. Values are masked; origins are not.systemctl show -p Environment <unit>orkubectl exec ... -- env- what the process actually got.jcmd PID VM.system_properties/VM.command_line- the -D and the arguments.- For bound values:
/actuator/configpropsor the effective metric (hikaricp_connections_maxis the pool size Hikari really uses, 21.11).
What you can now do
- Name the precedence order that matters: command line >
-D> env vars > files outside > inside the jar. - Translate a property name into its env var (relaxed binding) and spot the dash trap.
- Find which source won with
/actuator/env/<name>.