By WebNest Studio Engineering Team
Spring Boot Tutorial
Production Configuration Best Practices for Spring Boot
Moving a Spring Boot application from a laptop to production means changing how configuration is managed: secrets can no longer live in a properties file committed to source control, logging needs to be less noisy, and the application needs to be observable and resilient to restarts. Spring Boot's externalized configuration model is what makes all of this possible without code changes.
Externalizing Configuration
Spring Boot resolves configuration from multiple sources in a defined precedence order, roughly: command-line arguments, environment variables, profile-specific files, then the base application.properties/yml, with higher-precedence sources overriding lower ones. In production, the recommended practice is to keep application.properties committed with safe defaults, and inject anything environment-specific or secret (database credentials, API keys) through environment variables or a secrets manager at deploy time.
Using Environment Variables
Spring Boot's relaxed binding automatically maps environment variables to the equivalent property — SPRING_DATASOURCE_URL maps to spring.datasource.url, and SERVER_PORT maps to server.port. The rule is: upper-case the name, replace dots with underscores and drop dashes, so app.api-key becomes APP_APIKEY. This lets container orchestration platforms and CI/CD pipelines inject configuration without the application needing any awareness of where it's actually deployed. Because the environment variable already outranks every properties file, you do not need a line like spring.datasource.url=${SPRING_DATASOURCE_URL} at all.
Seeing the Precedence in Action
The order is easiest to remember after watching one property change hands. Below, app.greeting is defined in four places, and each run adds one more source. Actuator's /actuator/env/app.greeting reports both the winning value and where it came from, which is the quickest way to answer "why is production using this value?":
- Only
application.properties: the base file wins. - Add
SPRING_PROFILES_ACTIVE=prod:application-prod.propertiesoverrides the base file. - Add the environment variable
APP_GREETING: the source becomessystemEnvironment, beating both files. - Add
--app.greeting=...on the command line:commandLineArgsbeats everything above.
Defaults and Required Values
A placeholder can carry a fallback: app.pool=${DB_POOL_SIZE:10} uses the environment variable when it is set and 10 otherwise. That suits tuning values with a sensible default. Secrets are different: they must not have a default, and a missing one should stop the application at startup rather than fail later on the first request. Be aware that a missing variable does not always produce a clear message. When SPRING_DATASOURCE_URL was not set, the literal text ${SPRING_DATASOURCE_URL} reached the connection pool, and startup failed with 'url' must start with "jdbc". Binding required settings to a @ConfigurationProperties class with @Validated and @NotBlank gives a much clearer error.
Secrets from Files (Kubernetes and Docker Secrets)
Environment variables can leak: they show up in process listings, crash reports and the /actuator/env endpoint if someone exposes it. Kubernetes and Docker can instead mount each secret as a file. Spring Boot reads such a folder with spring.config.import=optional:configtree:/run/secrets/: every file becomes a property named after the file, with the file's content as the value. A file called app.api-key becomes the property app.api-key, and Actuator lists its source as "Config tree". The optional: prefix lets the application still start locally where the folder does not exist.
Production-Oriented Settings
A handful of practical adjustments distinguish a production configuration from a development one: setting spring.profiles.active=prod, disabling Swagger UI or restricting it, raising log levels to WARN/INFO for verbose libraries, enabling connection pool tuning (HikariCP settings like maximum-pool-size), and restricting exposed Actuator endpoints. Graceful shutdown no longer needs a setting: since Spring Boot 3.4 the embedded server stops taking new requests on shutdown and waits for active ones, up to spring.lifecycle.timeout-per-shutdown-phase (30 seconds by default).
- Never commit real secrets to application.properties in version control — use environment variables, a vault, or a cloud secrets manager instead.
- Set explicit connection pool sizes rather than relying on defaults tuned for development-scale traffic.
- Expose only the Actuator endpoints actually needed operationally, and secure them.
- Configure appropriate log levels and, ideally, structured (JSON) log output for aggregation tools.
- Set spring.jpa.open-in-view=false. It is on by default, Spring Boot logs a WARN about it at startup, and it keeps a database connection open for the whole web request.
Examples
A production profile for an app with a database (tested on Spring Boot 4.1.1)
# application-prod.properties
# No datasource URL/username/password lines: the SPRING_DATASOURCE_* environment
# variables bind to them directly.
spring.datasource.hikari.maximum-pool-size=20
spring.jpa.open-in-view=false
spring.lifecycle.timeout-per-shutdown-phase=30s
management.endpoints.web.exposure.include=health,metrics
management.endpoint.health.show-details=never
logging.level.root=WARN
logging.level.com.webnest.app=INFO
# Launched with:
# SPRING_PROFILES_ACTIVE=prod \
# SPRING_DATASOURCE_URL=jdbc:postgresql://db.internal:5432/webnest \
# SPRING_DATASOURCE_USERNAME=app_user \
# SPRING_DATASOURCE_PASSWORD=**** \
# java -jar app.jar
INFO ... : The following 1 profile is active: "prod"
INFO ... : Started App in 4.987 seconds (process running for 5.497)
(With root at WARN, the Tomcat and HikariCP startup lines are no longer printed;
only your own INFO lines and warnings remain.)
GET /actuator/health -> {"groups":["liveness","readiness"],"status":"UP"}
GET /actuator/metrics/hikaricp.connections.max -> ... "measurements":[{"statistic":"VALUE","value":20.0}] ...
GET /actuator/env -> 404 (not exposed)
On shutdown, with a 4-second request still running (these lines are INFO,
so they appear only with root, or org.springframework.boot, at INFO):
INFO ... GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
INFO ... GracefulShutdown : Graceful shutdown complete
INFO ... HikariDataSource : HikariPool-1 - Shutdown initiated...
(the running request still returned 200; new connections were refused)
Which source wins? Checked with /actuator/env/app.greeting
# application.properties
app.greeting=from application.properties
app.pool=${DB_POOL_SIZE:10}
spring.config.import=optional:configtree:./secrets/
# application-prod.properties
app.greeting=from application-prod.properties
# ./secrets/app.api-key (a file whose content is the secret)
from-a-mounted-secret
Run 1: java -jar app.jar
app.greeting = from application.properties <- application.properties
app.pool = 10 <- default from the placeholder
app.api-key = from-a-mounted-secret <- Config tree '...\secrets'
Run 2: SPRING_PROFILES_ACTIVE=prod
app.greeting = from application-prod.properties
Run 3: + APP_GREETING="from env var" DB_POOL_SIZE=25
app.greeting = from env var <- systemEnvironment
app.pool = 25
Run 4: + --app.greeting="from command line"
app.greeting = from command line <- commandLineArgs
What a missing environment variable looks like
# application-prod.properties (the pattern to avoid)
spring.datasource.url=${SPRING_DATASOURCE_URL}
# Started with SPRING_PROFILES_ACTIVE=prod but no SPRING_DATASOURCE_URL
ERROR ... o.s.boot.SpringApplication : Application run failed
...
Caused by: java.lang.IllegalArgumentException: 'url' must start with "jdbc"
(The unresolved text "${SPRING_DATASOURCE_URL}" was used as the URL.
Removing the line and relying on the environment variable directly, or
validating required settings with @ConfigurationProperties, avoids this.)
Common Mistakes
- Committing real database passwords or API keys directly into application-prod.properties in version control.
- Leaving development-tuned defaults (small connection pools, verbose DEBUG logging, exposed management endpoints) unchanged when deploying to production.
- Not testing the "prod" profile locally before deployment, discovering missing or misspelled environment variable names only after a failed rollout.
- Writing spring.datasource.url=${SPRING_DATASOURCE_URL}: the line is unnecessary, and when the variable is missing the error message is confusing.
- Giving a secret a placeholder default such as ${DB_PASSWORD:changeme}, so a forgotten variable silently runs production with a known password.
- Exposing /actuator/env in production. It shows where every value came from and, if values are not masked, the values themselves.
- Setting logging.level.root=WARN and then searching the logs for "Tomcat started" during an incident. Keep your own packages at INFO.
Exercise
Using the lesson's A production profile for an app with a database (tested on Spring Boot 4.1.1) example, create a small implementation or design sketch that demonstrates Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties.. Add a test case or scenario that would catch Committing real database passwords or API keys directly into application-prod.properties in version control., and describe the expected result.
Show hints
- Start with Externalizing Configuration and compare your approach with A production profile for an app with a database (tested on Spring Boot 4.1.1). Identify the decision or behavior the lesson recommends.
- Walk through your scenario step by step. Confirm that it follows Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties. and that it would catch the failure described by Committing real database passwords or API keys directly into application-prod.properties in version control..
Quick quiz
1. Which choice is a mistake to avoid in Production Configuration Best Practices for Spring Boot?
- A. Leaving development-tuned defaults (small connection pools, verbose DEBUG logging, exposed management endpoints) unchanged when deploying to production.
- B. Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties.
- C. Committing real database passwords or API keys directly into application-prod.properties in version control.
Show answer
C. Avoid this approach: Committing real database passwords or API keys directly into application-prod.properties in version control. The lesson recommends: Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties.
Interview questions
A teammate is about to make this mistake: Committing real database passwords or API keys directly into application-prod.properties in version control. How would you diagnose and correct it using the lesson's guidance?
Show answer
Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties. Apply this in Externalizing Configuration; the example A production profile for an app with a database (tested on Spring Boot 4.1.1) shows one way to do it. Avoid Committing real database passwords or API keys directly into application-prod.properties in version control. and explain how your implementation or design prevents that failure.
Key Points to Remember
- Spring Boot resolves configuration from multiple sources with a defined precedence — command-line arguments beat environment variables, which beat profile files, which beat application.properties.
- Environment variables bind by relaxed rules (SPRING_DATASOURCE_URL -> spring.datasource.url), so no placeholder line is needed.
- ${NAME:default} gives tuning values a fallback; required secrets should have no default and be validated at startup.
- spring.config.import=optional:configtree:/run/secrets/ reads mounted secret files as properties.
- Secrets belong in environment variables, mounted files or a secrets manager, never committed to application.properties in source control.
- Production configuration should tune connection pools, restrict Actuator exposure, and reduce log verbosity compared to development defaults.
- Graceful shutdown is on by default; spring.lifecycle.timeout-per-shutdown-phase sets how long active requests get to finish.
Practice in the playground
Write and run your own code for this topic, or start from the exercise or an example. Runs plain Java 17. Frameworks such as Spring, and databases, are not available here. Your code for each topic is kept in this browser.
Run the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
These examples need a JDK project with their libraries on your computer, so they can be copied but not run here. Codelab runs Python, FastAPI backends with SQLite, plain Java 17 and HTML/CSS/JavaScript.
Written by
WebNest Studio Engineering Team
Practical lessons from developers working with Java, Python, React, databases and production systems.
Led by Vansh Duggal, Founder, WebNest Studio · Full-stack & software engineering