Skip to content
ExponaBLOG
JAVA, SPRING FRAMEWORK & SPRING BOOT · DAY 026 / 100

Spring Boot configuration and profiles, explained simply

application.yml, profiles and environment variables — without secrets in git.

Expona Admin6 min read

Introduction

Configuration management is where many Spring Boot projects stumble. You hardcode a database URL in one environment, a Slack webhook in another, and suddenly your team is wrestling with secrets in git, misconfigured deployments, and no clear path to production. Spring Boot configuration and profiles, explained simply, gives you a clean way to manage settings across dev, test, and prod without committing secrets or maintaining separate codebases.

In this post, we'll walk through application.yml, profile-specific files, environment variable overrides, and how to keep secrets out of your repository. This is the pattern we've used across e-commerce, SaaS, and CRM systems—and it scales from a single jar to containerized deployments on AWS or Kubernetes.

One file, many profiles — Use profile-specific files for dev, test and prod differences

Spring Boot's profile system lets you define separate configurations for each environment without duplicating your entire settings file. The idea is simple: you maintain a base application.yml, then override specific properties in application-dev.yml, application-prod.yml, and so on.

Here's a real example. You're building a notification service. In dev, you want to log messages to console. In prod, you send them to a third-party API. Your base config looks like this:

java

# application.yml
spring:
  application:
    name: notification-service
  datasource:
    url: jdbc:postgresql://localhost:5432/notifications
    username: postgres
    password: password

notification:
  provider: console

Then create environment-specific files in the same directory:

java

# application-dev.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/notifications_dev

notification:
  provider: console
  log-level: debug
java

# application-prod.yml
spring:
  datasource:
    url: jdbc:postgresql://prod-db.internal:5432/notifications

notification:
  provider: aws-sns
  batch-size: 100

When you activate the dev profile, Spring loads application.yml first, then merges in application-dev.yml, overriding matching keys. Properties not redefined in the profile file keep their base values. This eliminates duplication and makes diffs clear: you only see what's different.

In your code, you can inject these config values into a component:

java

@Configuration
@ConfigurationProperties(prefix = "notification")
public class NotificationConfig {
    private String provider;
    private String logLevel = "info";
    private Integer batchSize = 10;
    
    // getters and setters
}

@Service
public class NotificationService {
    private final NotificationConfig config;
    
    public NotificationService(NotificationConfig config) {
        this.config = config;
    }
    
    public void send(Message msg) {
        if ("console".equals(config.getProvider())) {
            System.out.println("Message: " + msg.getBody());
        } else if ("aws-sns".equals(config.getProvider())) {
            // call AWS SNS
        }
    }
}

This approach keeps your code environment-agnostic. The same jar can run in dev or prod—the profile changes the behavior.

Override with environment — SPRING_DATASOURCE_URL beats the yml value — perfect for containers

YAML files are version-controlled, but environment variables are runtime—set by your platform, Kubernetes ConfigMap, or Docker Compose. Spring Boot automatically maps environment variables to application properties, so you don't need to touch a file to change a setting.

The mapping rule is simple: convert the property path to SCREAMING_SNAKE_CASE with dots becoming underscores. For instance:

YAML PropertyEnvironment Variable
spring.datasource.urlSPRING_DATASOURCE_URL
spring.datasource.usernameSPRING_DATASOURCE_USERNAME
notification.providerNOTIFICATION_PROVIDER
server.portSERVER_PORT

In practice, here's how we deployed a service to AWS ECS:

java

# Base application.yml (committed to git)
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp
    username: postgres
    password: local-dev-password
  jpa:
    hibernate:
      ddl-auto: update

# Set at runtime (ECS task definition env vars)
SPRING_DATASOURCE_URL=jdbc:postgresql://prod-rds.internal:5432/myapp
SPRING_DATASOURCE_USERNAME=app_user
SPRING_DATASOURCE_PASSWORD=
SPRING_JPA_HIBERNATE_DDL_AUTO=validate

When the container starts, Spring reads the YAML, then applies the environment variables on top. The password from ECS secrets overrides the dev password in the file, the RDS endpoint overrides localhost, and ddl-auto: validate prevents accidental schema changes in prod.

This pattern is essential for containers and infrastructure-as-code. Your Dockerfile and jar are immutable; configuration lives in the platform layer. Kubernetes does the same with ConfigMaps and Secrets.

Activate a profile — Set it per environment, never hard-code it

You activate a profile by setting spring.profiles.active. Never hardcode this in your application.yml or as a constant in code—always let the deployment layer decide.

Common activation methods:

  • Command line: java -jar app.jar --spring.profiles.active=prod
  • Environment variable: SPRING_PROFILES_ACTIVE=prod java -jar app.jar
  • Docker Compose: Set it in the services environment block
  • Kubernetes: Define it in the Deployment env or ConfigMap
  • IDE for local dev: Set it in your run configuration or environment variables

In a real deployment pipeline, here's how we manage it:

java

# Docker Compose (local testing)
services:
  app:
    image: myapp:latest
    environment:
      SPRING_PROFILES_ACTIVE: dev
      SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/myapp

# Kubernetes (prod)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-prod
spec:
  template:
    spec:
      containers:
      - name: myapp
        image: myapp:v1.2.3
        env:
        - name: SPRING_PROFILES_ACTIVE
          value: prod
        - name: SPRING_DATASOURCE_URL
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: url

The key principle: your jar doesn't know or care which environment it's in. The operator running the container decides by passing an environment variable. This keeps your code clean and your deployment process transparent.

Secrets stay outside — Inject secrets from a vault or the platform, not from committed files

This is non-negotiable. Never commit a real password, API key, or token to git—even if the repository is private. Use a secrets management system:

  • AWS: Secrets Manager or Parameter Store
  • Kubernetes: Secrets resource
  • GCP: Secret Manager
  • HashiCorp: Vault
  • Docker Compose (dev only): .env file excluded from git via .gitignore

Your application.yml can have placeholder or dummy values for documentation, but the real values come from the platform at runtime:

java

# application.yml (safe to commit)
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp
    username: postgres
    password: change-me-at-runtime

aws:
  sns:
    access-key: YOUR_AWS_ACCESS_KEY_ID_HERE
    secret-key: YOUR_AWS_SECRET_ACCESS_KEY_HERE

slack:
  webhook-url: https://hooks.slack.com/services/YOUR/WEBHOOK/URL

Then, at deployment time, inject the real values:

java

# Docker run
docker run -e SPRING_DATASOURCE_PASSWORD=real-password \
           -e AWS_SNS_ACCESS_KEY=AKIA... \
           -e AWS_SNS_SECRET_KEY=abcd... \
           -e SLACK_WEBHOOK_URL=https://... \
           myapp:latest

# Kubernetes
kubectl create secret generic app-secrets \
  --from-literal=db-password=real-password \
  --from-literal=aws-access-key=AKIA... \
  --from-literal=slack-webhook=https://...

# Then reference in Deployment
env:
- name: SPRING_DATASOURCE_PASSWORD
  valueFrom:
    secretKeyRef:
      name: app-secrets
      key: db-password

Your build process never sees secrets. They're injected only when the container runs, and they're logged only if you explicitly configure logging. This is how we handle sensitive data across all our production systems.

Common mistakes

Hardcoding the profile. Setting spring.profiles.active=prod inside application.yml defeats the purpose. Your dev jar becomes production-ready by accident, or you have to rebuild jars per environment. Always pass the profile from outside.

Committing secrets in any form. Including them in profile files, .properties files, or even commented-out in YAML is a security risk. Once it's in git history, it's compromised. Use a vault.

Not documenting placeholder values. When developers clone the repo and run the app locally, they need to know which env vars to set. Document them in a README or provide a .env.example file.

Forgetting environment variable precedence. It's YAML file, then profile file, then env vars. If you set a value both in application.yml and as an env var, the env var wins. Know this when debugging why a setting isn't changing.

Testing with prod profile locally. It's tempting to validate your prod config by activating it locally, but you risk pointing at real databases or APIs. Test with dev or a dedicated test profile instead.

Key takeaways

  • Use application.yml for shared settings and profile-specific files like application-prod.yml for environment overrides. This eliminates duplication and makes changes explicit.
  • Environment variables override YAML values at runtime. Perfect for containers and cloud platforms where you can't modify files.
  • Activate profiles via SPRING_PROFILES_ACTIVE or command-line flags—never hardcode them. Let your deployment layer choose the environment.
  • Keep secrets out of version control. Inject them from a vault, Kubernetes Secrets, or AWS Secrets Manager at runtime.
  • Document placeholder values and expected environment variables so your team knows what to set where.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles