Spring BootEnvironment Variables
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=DEBUG

Injecting 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=20

Reading 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.name

Secrets 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.