Skip to main content

Dev vs Prod: Why You Can't Store Secrets in Files on Production Servers

Β· 9 min read
Mahmut Salman
Software Developer

"Why do we use application.properties for secrets in development but environment variables in production?" Because file-based secrets are fine for your local machine (only you have access), but dangerous on production servers (many people have access, files can be compromised). Let's understand why this distinction matters and how to properly manage secrets across environments.

The Two Worlds: Development vs Production​

Development (Your Local Machine) βœ…β€‹

application.properties (on your computer)
β”œβ”€β”€ jwt.secret=MySecretKeyForJWTTokensThisNeedsToBeVeryLongForHS256Algorithm
β”œβ”€β”€ database.password=localpass123
└── spring.mail.password=gmailAppPassword123

Characteristics:

  • βœ… Stored in files on your computer
  • βœ… Only you can see it
  • βœ… Added to .gitignore (doesn't upload to GitHub)
  • βœ… Easy to manage and modify
  • βœ… Safe because your computer is private

Production (Live Server / Cloud) πŸ”΄β€‹

❌ NO application.properties with secrets!

βœ… Environment Variables / Secrets Manager:
export JWT_SECRET=prod-secret-key-xyz789
export DATABASE_PASSWORD=prod-password-secure
export MAIL_PASSWORD=prod-gmail-password

Characteristics:

  • βœ… Set at server startup, not in files
  • βœ… Only active in memory when app runs
  • βœ… Can be rotated/changed without redeploying code
  • βœ… Managed by DevOps/infrastructure team
  • βœ… Not visible in your codebase

Why the Difference?​

Problem 1: Server Access​

Development (Your Laptop):

Your Computer
↓
Only YOU have access
↓
application.properties with secrets = SAFE βœ…

Production (Cloud Server):

Production Server
↓
Multiple people have access:
- DevOps engineers
- System administrators
- Security team
- Contractors
- Former employees (if not revoked)
↓
application.properties with secrets = DANGEROUS ❌

Problem 2: Server Compromise​

If a hacker compromises the server:

With files:

# Hacker gets server access
ssh hacker@your-server

# Reads your secrets file
cat /app/application.properties

# Game over! All secrets exposed:
jwt.secret=MySecretKeyForJWTTokens...
database.password=localpass123
mail.password=gmailAppPassword123

With environment variables:

# Hacker gets server access
ssh hacker@your-server

# Tries to read secrets file
cat /app/application.properties
# ❌ No secrets in file!

# Tries to read environment variables
env | grep SECRET
# ⚠️ May see variables IF process is running
# But can rotate keys immediately without redeploying

Problem 3: Secret Rotation​

With files:

1. Update application.properties with new secret
2. Rebuild application
3. Create new Docker image
4. Deploy new image to production
5. Restart all servers
⏱️ Time: Hours, Risk: High

With environment variables:

1. Update environment variable on server
2. Restart application
⏱️ Time: Minutes, Risk: Low

Problem 4: Git History​

With files (even if .gitignored later):

# Developer accidentally commits secrets
git add application.properties
git commit -m "Add config"
git push

# Later adds to .gitignore
echo "application.properties" >> .gitignore
git commit -m "Add gitignore"

# ❌ Secrets STILL in Git history!
git log --all -- application.properties # Shows old commit with secrets

With environment variables:

βœ… Never in Git
βœ… Never in commit history
βœ… Never uploaded anywhere

How Spring Boot Handles Both​

Property Resolution Order​

Spring Boot looks for configuration values in this order:

1. Environment variables (highest priority)
2. System properties
3. application.properties file
4. Default values (lowest priority)

Example:

@Value("${jwt.secret}")
private String secretKey;

Spring looks for:

1. Environment variable: JWT_SECRET
2. System property: -Djwt.secret=...
3. application.properties: jwt.secret=...
4. Not found β†’ Error

Naming Convention​

Property in application.properties:

jwt.secret=value
database.password=value
spring.mail.password=value

Equivalent environment variable:

JWT_SECRET=value            # jwt.secret
DATABASE_PASSWORD=value # database.password
SPRING_MAIL_PASSWORD=value # spring.mail.password

Rule: Replace . with _ and make uppercase.


Development Setup​

Step 1: Create application.properties​

# application.properties (development only)

# Server Configuration
server.port=8082

# Database Configuration
spring.datasource.url=jdbc:h2:file:./data/ecommerce
spring.datasource.username=sa
spring.datasource.password=localpass123

# JWT Configuration
jwt.secret=MySecretKeyForJWTTokensThisNeedsToBeVeryLongForHS256Algorithm
jwt.expiration=86400000

# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com
spring.mail.password=gmailAppPassword123

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Step 2: Add to .gitignore​

# .gitignore

# Secrets
application.properties
application-dev.properties
application-local.properties

# But allow example files
!application.properties.example

Step 3: Create application.properties.example​

# application.properties.example
# Copy this to application.properties and fill in real values

# Server Configuration
server.port=8082

# Database Configuration
spring.datasource.url=jdbc:h2:file:./data/ecommerce
spring.datasource.username=sa
spring.datasource.password=YOUR_DATABASE_PASSWORD

# JWT Configuration
jwt.secret=YOUR_JWT_SECRET_KEY_AT_LEAST_256_BITS
jwt.expiration=86400000

# Email Configuration
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=YOUR_EMAIL@gmail.com
spring.mail.password=YOUR_GMAIL_APP_PASSWORD

# JPA Configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Step 4: Share with Team​

# βœ… Commit example file (no secrets)
git add application.properties.example
git commit -m "Add configuration example"
git push

# ❌ Never commit actual file (has secrets)
# application.properties is .gitignored

Production Setup​

Option 1: Environment Variables (Simple)​

On server:

# Set environment variables
export JWT_SECRET="prod-secret-key-xyz789-very-long-and-secure"
export DATABASE_PASSWORD="prod-database-password-secure-123"
export SPRING_MAIL_PASSWORD="prod-gmail-app-password"

# Start application
java -jar app.jar

Or in Docker:

# Dockerfile
FROM openjdk:17-jdk-slim
COPY target/app.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
# Run with environment variables
docker run -d \
-e JWT_SECRET="prod-secret-xyz789" \
-e DATABASE_PASSWORD="prod-db-pass" \
-e SPRING_MAIL_PASSWORD="prod-mail-pass" \
-p 8080:8080 \
myapp:latest

Install dependency:

<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-secrets-manager-config</artifactId>
</dependency>

Configure Spring Boot:

# application.properties (production)
spring.config.import=aws-secretsmanager:/my-app/prod/secrets

Secrets stored in AWS:

{
"jwt.secret": "prod-secret-xyz789",
"database.password": "prod-db-password",
"spring.mail.password": "prod-mail-password"
}

Spring Boot automatically fetches secrets at startup! βœ…

Option 3: Kubernetes Secrets​

Create secret:

# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
jwt-secret: cHJvZC1zZWNyZXQteHl6Nzg5 # base64 encoded
db-password: cHJvZC1kYi1wYXNzd29yZA==

Use in deployment:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: app-secrets
key: jwt-secret
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password

Real-World Example: Complete Flow​

Development​

Your laptop:

# Clone repository
git clone https://github.com/yourcompany/myapp.git
cd myapp

# Copy example config
cp application.properties.example application.properties

# Edit with your dev secrets
nano application.properties
# application.properties (your local file, .gitignored)
jwt.secret=dev-secret-key-12345-for-local-testing
database.password=devpass123
spring.mail.password=dev-gmail-app-password
# Run application
mvn spring-boot:run

# βœ… Works with secrets from file

Staging (Pre-Production)​

Staging server:

# Set environment variables
export JWT_SECRET="staging-secret-xyz789"
export DATABASE_PASSWORD="staging-db-password"
export SPRING_MAIL_PASSWORD="staging-mail-password"

# Deploy application
java -jar app.jar

# βœ… Works with secrets from environment variables

Production​

Production server (AWS ECS example):

{
"family": "myapp-production",
"containerDefinitions": [
{
"name": "myapp",
"image": "myapp:1.0.0",
"secrets": [
{
"name": "JWT_SECRET",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/jwt-secret"
},
{
"name": "DATABASE_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/db-password"
}
]
}
]
}

Application code (same everywhere):

@Component
public class JwtService {

@Value("${jwt.secret}") // Works in dev, staging, AND production!
private String secretKey;

// Spring automatically reads from:
// - Development: application.properties file
// - Staging: Environment variables
// - Production: AWS Secrets Manager
}

Security Best Practices​

βœ… DO: Development​

  1. Use application.properties for local development

    jwt.secret=dev-secret-key-12345
  2. Add to .gitignore

    application.properties
  3. Provide example file

    # application.properties.example
    jwt.secret=YOUR_SECRET_HERE
  4. Use weak secrets for development (it's okay, it's local!)

    jwt.secret=dev-key-123
    database.password=devpass

βœ… DO: Production​

  1. Use environment variables

    export JWT_SECRET="strong-prod-secret-xyz789"
  2. Or use secrets manager

    aws secretsmanager create-secret --name prod/jwt-secret
  3. Rotate secrets regularly

    # Update secret without redeploying code
    aws secretsmanager update-secret --secret-id prod/jwt-secret --secret-string "new-secret"
  4. Use strong secrets for production

    # At least 32 characters, random, unique
    jwt.secret=vM8f3Kp2Lq9Nn1Rr7Ss5Tt3Uu6Vv4Ww8Xx2Yy0Zz

❌ DON'T: Ever​

  1. Commit secrets to Git

    # ❌ NEVER DO THIS
    git add application.properties # Contains secrets!
    git commit -m "Add config"
  2. Hardcode secrets in code

    // ❌ NEVER DO THIS
    private String secretKey = "MySecretKey123";
  3. Share secrets in Slack/Email

    ❌ "Hey team, the JWT secret is: abc123..."
  4. Use the same secrets in dev and prod

    # ❌ NEVER DO THIS
    # Dev and prod using same secret = dangerous!

Comparison Table​

AspectDevelopmentProduction
Storageapplication.properties fileEnvironment variables / Secrets Manager
LocationYour laptopCloud server
AccessOnly youMultiple people
Git.gitignoredNever in Git
Secret StrengthWeak (dev-key-123)Strong (random, 32+ chars)
RotationRarely neededRegular rotation required
Compromise RiskLow (only your machine)High (many attack vectors)
ManagementYou manuallyDevOps team / Infrastructure
RecoveryEasy (just recreate file)Requires secret rotation process

Migration Checklist: Dev to Prod​

Before Deployment​

  • Remove application.properties from production build
  • Set all secrets as environment variables on production server
  • Test application can read environment variables
  • Verify no secrets in Git history
  • Generate strong production secrets (32+ characters)
  • Document which environment variables are required

During Deployment​

  • Set environment variables on server
  • Deploy application
  • Verify application starts successfully
  • Test all features requiring secrets (JWT, database, email)
  • Monitor logs for configuration errors

After Deployment​

  • Rotate any secrets that were accidentally exposed
  • Set up secret rotation schedule (e.g., every 90 days)
  • Document secret management process for team
  • Set up alerts for failed authentication (wrong secrets)
  • Review access logs for unauthorized access attempts

Troubleshooting​

Problem 1: Application Can't Find Secret​

Error:

Error creating bean with name 'jwtService': Unsatisfied dependency expressed through field 'secretKey'

Solution:

# Check environment variable is set
echo $JWT_SECRET

# If not set:
export JWT_SECRET="your-secret-key"

# Restart application
java -jar app.jar

Problem 2: Wrong Secret Used​

Error:

JWT verification failed: Invalid signature

Solution:

# Check which secret Spring Boot is using
java -jar app.jar --debug

# Look for: "Property 'jwt.secret' from 'EnvironmentVariable'"

# Verify environment variable value
echo $JWT_SECRET

Problem 3: Secret Visible in Logs​

Problem:

// ❌ WRONG
logger.info("JWT Secret: {}", secretKey); // Exposes secret in logs!

Solution:

// βœ… CORRECT
logger.info("JWT Secret configured: {}", secretKey != null ? "Yes" : "No");

Summary​

The Key Difference​

EnvironmentSecret StorageWhy
Developmentapplication.properties fileOnly you have access to your computer
ProductionEnvironment variables / Secrets ManagerMany people have access to server

Why It Matters​

Development:

  • βœ… Files are convenient
  • βœ… Only you can access
  • βœ… Easy to modify

Production:

  • ❌ Files are dangerous (many people have access)
  • βœ… Environment variables are secure
  • βœ… Can rotate without redeploying
  • βœ… Not in Git history

What You Should Do Now​

For Development:

  1. Create application.properties with dev secrets
  2. Add application.properties to .gitignore
  3. Create application.properties.example without secrets
  4. Commit example file to Git

For Production (when ready to deploy):

  1. Set secrets as environment variables on server
  2. Remove application.properties from production build
  3. Test application can read environment variables
  4. Use strong, random secrets (32+ characters)

The Bottom Line​

Development: File-based secrets = βœ… Safe (only you have access)

Production: File-based secrets = ❌ Dangerous (many people + attackers can access)

Solution: Use environment variables or secrets managers in production! πŸ”’

Now you understand why we store secrets differently in development vs production - it's all about minimizing the attack surface and who has access to your secrets! 🎯