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:
# 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:
# application-dev.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/notifications_dev
notification:
provider: console
log-level: debug
# 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:
@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 Property | Environment Variable |
|---|---|
| spring.datasource.url | SPRING_DATASOURCE_URL |
| spring.datasource.username | SPRING_DATASOURCE_USERNAME |
| notification.provider | NOTIFICATION_PROVIDER |
| server.port | SERVER_PORT |
In practice, here's how we deployed a service to AWS ECS:
# 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:
# 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:
# 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:
# 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.ymlfor shared settings and profile-specific files likeapplication-prod.ymlfor 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_ACTIVEor 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.






Comments
Loading comments…