Spring Boot
Environment Variables
Environment variables are the standard mechanism for injecting secrets and environment-specific configuration into Spring Boot applications at runtime — without modifying code or config files. Spring Boot's relaxed binding maps OS environment variables to property keys automatically, making env vars a first-class configuration source for Docker, Kubernetes, and CI/CD pipelines.
How Spring Boot Reads Environment Variables
Spring Boot automatically maps OS environment variables into its property Environment using relaxed binding. You do not need any extra configuration — set the env var and Spring Boot finds it.
The mapping rule: take the Spring property key, uppercase it, and replace dots and hyphens with underscores. Spring Boot normalizes both sides before matching, so the lookup is case-insensitive and separator-insensitive.
Environment variables sit at priority 4 in Spring Boot's property source hierarchy — higher than application.yml and profile files, but lower than command-line -- arguments and JVM -D system properties. This means application.yml provides safe defaults, and env vars override them per deployment.
Shell
# Relaxed binding — all three forms resolve to the same property:
#
# Property key (in application.yml): spring.datasource.url
# Environment variable: SPRING_DATASOURCE_URL
# JVM system property (-D): spring.datasource.url
#
# Mapping rule: uppercase + replace . and - with _
# Common examples:
# server.port → SERVER_PORT
# spring.datasource.url → SPRING_DATASOURCE_URL
# spring.datasource.username → SPRING_DATASOURCE_USERNAME
# spring.profiles.active → SPRING_PROFILES_ACTIVE
# spring.jpa.show-sql → SPRING_JPA_SHOW_SQL
# app.jwt.secret → APP_JWT_SECRET
# app.allowed-origins[0] → APP_ALLOWED_ORIGINS_0
# Set and verify on Linux/macOS:
export SPRING_PROFILES_ACTIVE=prod
export SERVER_PORT=8080
export SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydb
export SPRING_DATASOURCE_PASSWORD=supersecret
# Verify the mapping is working (Spring Boot logs at DEBUG):
java -jar myapp.jar --logging.level.org.springframework.boot.context.config=DEBUGInjecting Environment Variables with @Value
@Value works identically whether the property comes from application.yml or an environment variable — Spring Boot resolves both through the same Environment abstraction. You can reference the canonical property key or the env var name directly.
Java
@Component
public class AppConfig {
// Resolved via relaxed binding — works whether set in yml or as env var:
@Value("${spring.datasource.url}")
private String dbUrl;
// Reference the env var name directly (also works):
@Value("${SPRING_DATASOURCE_URL}")
private String dbUrlDirect;
// With a default fallback — uses the default if neither yml nor env var is set:
@Value("${server.port:8080}")
private int serverPort;
// Secrets injected from env vars — never hardcode these:
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.stripe.api-key:${STRIPE_API_KEY:}}")
private String stripeApiKey; // checks app.stripe.api-key, then STRIPE_API_KEY, then ""
}Injecting Environment Variables with @ConfigurationProperties
@ConfigurationProperties is the preferred approach for groups of related configuration. Relaxed binding applies here too — the same env var naming convention maps into nested @ConfigurationProperties classes automatically.
Java
// @ConfigurationProperties class:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
private final Jwt jwt = new Jwt();
private final Database database = new Database();
public static class Jwt {
@NotBlank
private String secret; // set via APP_JWT_SECRET
private long expiration = 86400; // set via APP_JWT_EXPIRATION
// getters + setters
}
public static class Database {
private int maxPoolSize = 10; // set via APP_DATABASE_MAX_POOL_SIZE
// getters + setters
}
// getters + setters
}
# Corresponding environment variables:
# APP_NAME → app.name
# APP_JWT_SECRET → app.jwt.secret
# APP_JWT_EXPIRATION → app.jwt.expiration
# APP_DATABASE_MAX_POOL_SIZE → app.database.max-pool-size
export APP_NAME=MyApplication
export APP_JWT_SECRET=my-super-secret-signing-key-32chars
export APP_JWT_EXPIRATION=3600
export APP_DATABASE_MAX_POOL_SIZE=20Reading Environment Variables Programmatically
Inject the Spring Environment bean to read properties and check active profiles at runtime. For raw OS-level env vars that bypass Spring entirely, use System.getenv().
Java
@Component
@RequiredArgsConstructor
public class EnvironmentInspector {
private final Environment environment;
public void inspect() {
// Read any property (resolves yml, env vars, args — full hierarchy):
String dbUrl = environment.getProperty("spring.datasource.url");
String port = environment.getProperty("server.port", "8080"); // with default
// Type-safe retrieval:
Integer maxPool = environment.getProperty("app.database.max-pool-size", Integer.class, 10);
// Check active profiles:
String[] active = environment.getActiveProfiles();
boolean isProd = environment.acceptsProfiles(Profiles.of("prod"));
// Check if a property is set at all:
boolean hasSecret = environment.containsProperty("app.jwt.secret");
}
}
@Component
public class RawEnvReader {
public void readRaw() {
// Bypass Spring entirely — read OS env vars directly:
String dbPassword = System.getenv("DB_PASSWORD");
String home = System.getenv("HOME");
// Read all env vars:
Map<String, String> allEnv = System.getenv();
allEnv.forEach((k, v) -> log.debug("ENV {}={}", k, v));
// JVM system properties (-D flags):
String profile = System.getProperty("spring.profiles.active");
}
}Environment Variables in Docker
Docker is the most common context for env var injection. Pass vars with -e flags, load from a file with --env-file, or define them in Docker Compose. Never bake secrets into the image.
yaml
# ── docker run — inline env vars ──────────────────────────────────────
docker run \
-e SPRING_PROFILES_ACTIVE=prod \
-e SERVER_PORT=8080 \
-e SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydb \
-e SPRING_DATASOURCE_USERNAME=appuser \
-e SPRING_DATASOURCE_PASSWORD=supersecret \
-e APP_JWT_SECRET=my-signing-key \
myapp:latest
# ── --env-file — load from a file (keep secrets out of shell history) ──
# .env.prod:
SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=8080
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/mydb
SPRING_DATASOURCE_USERNAME=appuser
SPRING_DATASOURCE_PASSWORD=supersecret
APP_JWT_SECRET=my-signing-key
docker run --env-file .env.prod myapp:latest
# ── Docker Compose ─────────────────────────────────────────────────────
services:
app:
image: myapp:latest
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
SERVER_PORT: 8080
SPRING_DATASOURCE_URL: jdbc:mysql://db:3306/mydb
SPRING_DATASOURCE_USERNAME: appuser
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} # from host env or .env file
APP_JWT_SECRET: ${JWT_SECRET}
env_file:
- .env.prod # supplement with a file for longer lists
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: mydb
MYSQL_USER: appuser
MYSQL_PASSWORD: ${DB_PASSWORD}Environment Variables in Kubernetes
Kubernetes separates non-sensitive config (ConfigMap) from sensitive config (Secret). Both are injected into the pod as environment variables. This is the production-standard pattern for secrets management in containerised Spring Boot apps.
yaml
# ── ConfigMap — non-sensitive configuration ───────────────────────────
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
data:
SPRING_PROFILES_ACTIVE: "prod"
SERVER_PORT: "8080"
SPRING_DATASOURCE_URL: "jdbc:mysql://mysql-service:3306/mydb"
APP_MAIL_FROM: "noreply@myapp.com"
SPRING_JPA_SHOW_SQL: "false"
---
# ── Secret — sensitive values (base64-encoded) ────────────────────────
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
type: Opaque
stringData: # stringData auto-encodes to base64
SPRING_DATASOURCE_USERNAME: "appuser"
SPRING_DATASOURCE_PASSWORD: "supersecret"
APP_JWT_SECRET: "my-32-char-signing-key-here!!"
---
# ── Deployment — inject both into pods ────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
template:
spec:
containers:
- name: myapp
image: myapp:latest
envFrom:
- configMapRef:
name: myapp-config # all ConfigMap keys become env vars
- secretRef:
name: myapp-secrets # all Secret keys become env vars
# Or inject individual keys:
env:
- name: APP_JWT_SECRET
valueFrom:
secretKeyRef:
name: myapp-secrets
key: APP_JWT_SECRET
- name: POD_NAME # inject pod metadata as env var
valueFrom:
fieldRef:
fieldPath: metadata.nameSecrets Management Best Practices
Environment variables are a significant improvement over hardcoded values, but they are still visible in process listings, logs, and container inspect output. For production, layer env vars with a dedicated secrets manager.
yaml
# ── What NOT to do ────────────────────────────────────────────────────
# Never hardcode secrets in application.yml:
app:
jwt:
secret: my-hardcoded-secret # committed to git — NEVER do this
# Never log environment variables:
log.info("DB password: {}", System.getenv("DB_PASSWORD")) # leaks secrets
# Never print all env vars at startup:
System.getenv().forEach((k, v) -> log.info("{} = {}", k, v))
# ── What to do ────────────────────────────────────────────────────────
# 1. Use placeholders in application.yml — no defaults for secrets:
spring:
datasource:
password: ${SPRING_DATASOURCE_PASSWORD} # fails loudly if not set
app:
jwt:
secret: ${APP_JWT_SECRET} # no default — required
# 2. For production, use a secrets manager and inject at startup:
# - AWS Secrets Manager + Spring Cloud AWS
# - HashiCorp Vault + Spring Cloud Vault
# - Azure Key Vault + Azure Spring Apps
# - GCP Secret Manager + Spring Cloud GCP
# Spring Cloud Vault example (vault injects properties into Environment):
spring:
cloud:
vault:
host: vault.internal
port: 8200
scheme: https
authentication: KUBERNETES
kubernetes:
role: myapp
config:
import: vault://secret/myapp # loads vault secrets as Spring properties
# 3. Rotate secrets without redeploying:
# Update the Secret in K8s or the entry in your secrets manager,
# then trigger a rolling restart:
kubectl rollout restart deployment/myapp
# 4. Audit secret access — most secrets managers provide access logs.
# Env vars on the host do not.