Environment Variables and Externalized Config

Never Commit Secrets

Passwords and API keys must not live in Git. Put them in environment variables (or a secret manager) and reference them, or let Spring bind them automatically. Spring's relaxed binding maps an environment variable to a property: uppercase, dots become underscores, dashes are removed.

PropertyEnvironment variable
spring.datasource.passwordSPRING_DATASOURCE_PASSWORD
shopease.payment.key-secretSHOPEASE_PAYMENT_KEYSECRET
server.portSERVER_PORT

Who Wins? The Precedence Order

Spring Boot reads many sources. When a key appears in several, the higher source wins (simplified list, highest first):

  1. Command-line arguments: --server.port=9090
  2. SPRING_APPLICATION_JSON
  3. OS environment variables: SERVER_PORT=9090
  4. Profile files outside the jar, then inside: application-prod.yml
  5. application.yml / application.properties outside the jar, then inside
  6. Defaults in code (@Value("${x:default}"))

Example

A plain-Java model of that lookup, including the environment-variable name conversion:

import java.util.*;

public class Main {
    static String toEnvName(String key) {
        return key.replace(".", "_").replace("-", "").toUpperCase();
    }

    public static void main(String[] args) {
        Map<String, Map<String, String>> sources = new LinkedHashMap<>();   // highest priority first
        sources.put("command line", Map.of("server.port", "9090"));
        sources.put("environment", Map.of("SERVER_PORT", "8081", "SHOPEASE_PAYMENT_KEYSECRET", "rzp_live_secret"));
        sources.put("application-prod.yml", Map.of("spring.datasource.url", "jdbc:mysql://db:3306/shopease",
                "shopease.payment.timeout", "3s"));
        sources.put("application.yml", Map.of("server.port", "8080", "shopease.payment.timeout", "5s",
                "spring.application.name", "shopease"));

        for (String key : List.of("server.port", "shopease.payment.key-secret", "shopease.payment.timeout",
                "spring.datasource.url", "spring.application.name", "shopease.payment.max-retries")) {
            String found = "(not set, use default)";
            for (var source : sources.entrySet()) {
                String name = source.getKey().equals("environment") ? toEnvName(key) : key;
                String value = source.getValue().get(name);
                if (value != null) { found = value + "  <- " + source.getKey() + (name.equals(key) ? "" : " (" + name + ")"); break; }
            }
            System.out.printf("%-30s = %s%n", key, found);
        }
    }
}

Output

server.port                    = 9090  <- command line
shopease.payment.key-secret    = rzp_live_secret  <- environment (SHOPEASE_PAYMENT_KEYSECRET)
shopease.payment.timeout       = 3s  <- application-prod.yml
spring.datasource.url          = jdbc:mysql://db:3306/shopease  <- application-prod.yml
spring.application.name        = shopease  <- application.yml
shopease.payment.max-retries   = (not set, use default)

.env Files, Config Trees and Secret Managers

  • Locally, keep secrets in a .env file that is in .gitignore; IntelliJ run configurations and Docker Compose (env_file) can load it.
  • spring.config.import=optional:file:.env[.properties] imports a properties-style file directly.
  • In Kubernetes, secrets mounted as files are read with spring.config.import=configtree:/run/secrets/.
  • Cloud secret managers (AWS Secrets Manager, GCP Secret Manager, Vault) have Spring Cloud integrations.

Exercise: Production must listen on port 9000 without changing any file inside the jar. Give two ways.

Show answer
# 1. command-line argument
java -jar shopease.jar --server.port=9000
# 2. environment variable
SERVER_PORT=9000 java -jar shopease.jar