22 KiB
Managing Environment Variables in Production
Audience: DevOps Engineers, System Administrators Last Updated: November 2025 Applies To: MapleFile Backend
Table of Contents
- Overview
- Architecture
- Updating Environment Variables
- Updating Secrets
- Common Scenarios
- Verification and Rollback
- Troubleshooting
- Best Practices
Overview
What Are Environment Variables?
Environment variables configure your application's behavior without changing code:
- Database connections (Cassandra, Redis)
- External APIs (Mailgun, AWS S3)
- Application settings (CORS origins, JWT duration, log level)
- Feature flags (leader election, etc.)
Where Configuration Lives
All configuration is managed from the MANAGER node (<MANAGER_IP>):
Manager Node (<MANAGER_IP>)
└── ~/stacks/
├── maplefile-stack.yml # Stack definition with environment variables
├── maplefile-caddy-config/
│ └── Caddyfile # Caddy reverse proxy config
└── secrets/ (managed separately by Docker Swarm)
├── maplefile_jwt_secret
├── maplefile_mailgun_api_key
├── redis_password
├── spaces_access_key
└── spaces_secret_key
⚠️ NEVER edit configuration on worker nodes! Workers receive configuration from the manager via Docker Swarm.
Architecture
How Configuration Works
MapleFile backend uses two types of configuration:
1. Environment Variables (in maplefile-stack.yml)
Non-sensitive configuration defined directly in the stack file:
services:
backend:
environment:
# Application
- APP_ENVIRONMENT=production
- SERVER_PORT=8000
# Database
- DATABASE_HOSTS=cassandra-1,cassandra-2,cassandra-3
- DATABASE_KEYSPACE=maplefile
# Mailgun
- MAILGUN_DOMAIN=mg.example.com
- MAILGUN_FROM_EMAIL=no-reply@mg.example.com
# CORS
- SECURITY_ALLOWED_ORIGINS=https://maplefile.com
2. Docker Secrets (sensitive credentials)
Sensitive values managed by Docker Swarm and mounted into containers:
services:
backend:
secrets:
- maplefile_jwt_secret
- redis_password
- maplefile_mailgun_api_key
- spaces_access_key
- spaces_secret_key
command:
- |
# Secrets are read from /run/secrets/ and exported
export JWT_SECRET=$(cat /run/secrets/maplefile_jwt_secret)
export CACHE_PASSWORD=$(cat /run/secrets/redis_password)
export MAILGUN_API_KEY=$(cat /run/secrets/maplefile_mailgun_api_key)
exec /app/maplefile-backend daemon
Configuration Flow
Manager Node
├── maplefile-stack.yml (environment variables)
└── Docker Swarm Secrets (sensitive values)
↓
Docker Stack Deploy
↓
Docker Swarm Manager
↓
Worker-8 (Backend)
Environment Variables vs Secrets
| Type | Use For | Location | Example |
|---|---|---|---|
| Environment Variables | Non-sensitive config | maplefile-stack.yml |
APP_ENVIRONMENT=production |
| Secrets | Sensitive credentials | Docker Swarm secrets | API keys, passwords, JWT secret |
Why separate?
- Environment variables: Visible in
docker inspect(OK for non-sensitive data) - Secrets: Encrypted by Docker Swarm, only accessible inside containers (required for credentials)
Updating Environment Variables
Environment variables are defined directly in the maplefile-stack.yml file.
Step 1: SSH to Manager Node
ssh dockeradmin@<MANAGER_IP>
Step 2: Backup Current Stack File
Always backup before making changes:
cd ~/stacks
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d-%H%M%S)
# Verify backup created
ls -la maplefile-stack.yml.backup-*
Step 3: Edit Stack File
# Open editor
nano ~/stacks/maplefile-stack.yml
Find the environment: section and make your changes:
services:
backend:
environment:
# Change log level from INFO to DEBUG
- LOG_LEVEL=debug
# Update CORS origins
- SECURITY_ALLOWED_ORIGINS=https://maplefile.com,https://www.maplefile.com
Save changes:
- Press
Ctrl+Oto save - Press
Enterto confirm - Press
Ctrl+Xto exit
Step 4: Verify Changes
# Check what you changed
cat ~/stacks/maplefile-stack.yml | grep -A5 "environment:"
# Or search for specific variable
cat ~/stacks/maplefile-stack.yml | grep LOG_LEVEL
Step 5: Redeploy the Stack
cd ~/stacks
# Redeploy stack (picks up new environment variables)
docker stack deploy -c maplefile-stack.yml maplefile
Expected output:
Updating service maplefile_backend (id: xyz123...)
Updating service maplefile_backend-caddy (id: abc456...)
Note: Docker Swarm will perform a rolling update with zero downtime.
Step 6: Monitor Deployment
# Watch service update progress
docker service ps maplefile_backend
# Check logs for errors
docker service logs maplefile_backend --tail 50
# Verify service is healthy
docker service ls | grep maplefile
Expected healthy state:
NAME REPLICAS IMAGE
maplefile_backend 1/1 registry.digitalocean.com/ssp/maplefile-backend:prod
maplefile_backend-caddy 1/1 caddy:2.9.1-alpine
Updating Secrets
When to Update Secrets
- API keys rotated (Mailgun, AWS S3)
- Passwords changed (Redis)
- Security incident (compromised credentials)
- JWT secret rotation (security best practice)
Understanding Docker Secrets
Docker secrets are IMMUTABLE. Once created, they cannot be changed. To update a secret:
- Remove the stack completely
- Delete the old secret
- Create new secret with updated value
- Redeploy stack
Step 1: SSH to Manager
ssh dockeradmin@<MANAGER_IP>
Step 2: List Current Secrets
docker secret ls
You should see:
NAME CREATED
maplefile_jwt_secret 8 hours ago
maplefile_mailgun_api_key 8 hours ago
redis_password 10 days ago
spaces_access_key 9 days ago
spaces_secret_key 9 days ago
Step 3: Remove Stack
Must remove stack first to release secrets:
docker stack rm maplefile
# Wait for stack to fully shutdown
sleep 20
# Verify stack removed
docker stack ls | grep maplefile
# Should return nothing
Step 4: Remove Old Secret
# Remove the secret you want to update
docker secret rm maplefile_mailgun_api_key
# Verify removed
docker secret ls | grep mailgun
# Should return nothing
Step 5: Create New Secret
Method 1: Using echo (recommended):
# Create new secret from command line
echo "key-NEW_MAILGUN_API_KEY_HERE" | docker secret create maplefile_mailgun_api_key -
# Verify created
docker secret ls | grep mailgun
Method 2: Using file:
# Create temporary file
echo "key-NEW_MAILGUN_API_KEY_HERE" > /tmp/mailgun_key.txt
# Create secret from file
docker secret create maplefile_mailgun_api_key /tmp/mailgun_key.txt
# Remove temporary file (important!)
rm /tmp/mailgun_key.txt
# Verify created
docker secret ls | grep mailgun
⚠️ Important:
- No quotes around the value
- No trailing newlines or spaces
- Exact format required by the application
Step 6: Redeploy Stack
cd ~/stacks
# Deploy stack with new secret
docker stack deploy -c maplefile-stack.yml maplefile
# Watch services start
docker service ls
Step 7: Verify Secret Updated
# Check service logs for successful startup
docker service logs maplefile_backend --tail 50
# Look for successful initialization
docker service logs maplefile_backend --tail 100 | grep -i "connected\|started"
# Test the service
curl -I https://maplefile.ca/health
# Should return: HTTP/2 200
Common Scenarios
Scenario 1: Update Mailgun API Key
Problem: Email sending fails with 401 Forbidden
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Remove stack
docker stack rm maplefile
sleep 20
# Remove old secret
docker secret rm maplefile_mailgun_api_key
# Create new secret (get key from Mailgun dashboard)
echo "key-YOUR_NEW_MAILGUN_API_KEY" | docker secret create maplefile_mailgun_api_key -
# Redeploy
cd ~/stacks
docker stack deploy -c maplefile-stack.yml maplefile
# Monitor
docker service logs -f maplefile_backend --tail 20
# Test email sending from app
Scenario 2: Update Mailgun Domain
Problem: Need to change from mg.example.com to maplefile.ca
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Backup stack file
cd ~/stacks
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d)
# Edit stack file
nano maplefile-stack.yml
# Find and update:
# - MAILGUN_DOMAIN=maplefile.ca
# - MAILGUN_FROM_EMAIL=noreply@maplefile.ca
# - MAILGUN_BACKEND_DOMAIN=maplefile.ca
# Save and redeploy
docker stack deploy -c maplefile-stack.yml maplefile
# Monitor
docker service logs maplefile_backend --tail 50 | grep -i mailgun
Scenario 3: Update CORS Origins
Problem: Frontend domain changed or new domain added
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Backup
cd ~/stacks
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d)
# Edit
nano maplefile-stack.yml
# Find and update:
# - SECURITY_ALLOWED_ORIGINS=https://maplefile.com,https://www.maplefile.com,https://new-domain.com
# Save and redeploy
docker stack deploy -c maplefile-stack.yml maplefile
# Test from browser (check for CORS errors in console)
Scenario 4: Change JWT Secret (Security Incident)
Problem: JWT secret potentially compromised
⚠️ WARNING: This will invalidate ALL user sessions!
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Generate new secure secret (64 characters)
NEW_SECRET=$(openssl rand -base64 48)
echo "New JWT secret generated (not shown for security)"
# Remove stack
docker stack rm maplefile
sleep 20
# Remove old secret
docker secret rm maplefile_jwt_secret
# Create new secret
echo "$NEW_SECRET" | docker secret create maplefile_jwt_secret -
# Redeploy
cd ~/stacks
docker stack deploy -c maplefile-stack.yml maplefile
# Monitor startup
docker service logs maplefile_backend --tail 50
# ⚠️ All users will need to log in again!
Scenario 5: Enable Debug Logging
Problem: Need detailed logs for troubleshooting
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Backup
cd ~/stacks
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d)
# Edit
nano maplefile-stack.yml
# Find and change:
# - LOG_LEVEL=debug # Was: info
# Save and redeploy
docker stack deploy -c maplefile-stack.yml maplefile
# Watch detailed logs
docker service logs -f maplefile_backend --tail 100
# ⚠️ Remember to set back to info when done!
Scenario 6: Update AWS S3 Credentials
Problem: S3 access keys rotated (DigitalOcean Spaces)
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Remove stack
docker stack rm maplefile
sleep 20
# Remove old secrets
docker secret rm spaces_access_key
docker secret rm spaces_secret_key
# Create new secrets (get from DigitalOcean Spaces dashboard)
echo "YOUR_NEW_ACCESS_KEY" | docker secret create spaces_access_key -
echo "YOUR_NEW_SECRET_KEY" | docker secret create spaces_secret_key -
# Verify created
docker secret ls | grep spaces
# Redeploy
cd ~/stacks
docker stack deploy -c maplefile-stack.yml maplefile
# Test S3 access
docker service logs maplefile_backend --tail 50 | grep -i "s3\|storage"
Scenario 7: Update Database Hosts
Problem: Cassandra node hostname changed
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Backup
cd ~/stacks
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d)
# Edit
nano maplefile-stack.yml
# Find and update:
# - DATABASE_HOSTS=cassandra-1,cassandra-2,cassandra-3,cassandra-4
# Save and redeploy
docker stack deploy -c maplefile-stack.yml maplefile
# Monitor connection
docker service logs maplefile_backend --tail 100 | grep -i cassandra
Scenario 8: Update Redis Password
Problem: Redis password changed
Solution:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Remove stack
docker stack rm maplefile
sleep 20
# Remove old secret
docker secret rm redis_password
# Create new secret
echo "NEW_REDIS_PASSWORD_HERE" | docker secret create redis_password -
# Redeploy
cd ~/stacks
docker stack deploy -c maplefile-stack.yml maplefile
# Monitor Redis connection
docker service logs maplefile_backend --tail 50 | grep -i redis
Verification and Rollback
Verify Changes Applied
Check service updated:
# Check service update time
docker service ps maplefile_backend --format "table {{.Name}}\t{{.Image}}\t{{.CurrentState}}"
# Recent "Running" state means it restarted
Check environment inside container:
# Get container ID
CONTAINER_ID=$(docker ps -q -f name=maplefile_backend)
# Check environment variable
docker exec $CONTAINER_ID env | grep LOG_LEVEL
# Should show: LOG_LEVEL=debug
# DON'T print secrets to terminal!
# Instead, check if they exist:
docker exec $CONTAINER_ID sh -c 'test -f /run/secrets/maplefile_jwt_secret && echo "JWT secret exists" || echo "JWT secret missing"'
Check application logs:
# Look for initialization messages
docker service logs maplefile_backend --tail 100 | grep -i "connected\|initialized"
# Check for errors
docker service logs maplefile_backend --tail 100 | grep -i "error\|fatal\|panic"
Rollback Configuration
If something goes wrong:
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
cd ~/stacks
# List backups
ls -la maplefile-stack.yml.backup-*
# Restore from backup
cp maplefile-stack.yml.backup-YYYYMMDD-HHMMSS maplefile-stack.yml
# Redeploy with old config
docker stack deploy -c maplefile-stack.yml maplefile
# Verify rollback successful
docker service logs maplefile_backend --tail 50
Rollback Secrets
To rollback a secret:
# Remove stack
docker stack rm maplefile
sleep 20
# Remove new secret
docker secret rm maplefile_mailgun_api_key
# Recreate old secret (you need to have saved the old value!)
echo "OLD_SECRET_VALUE" | docker secret create maplefile_mailgun_api_key -
# Redeploy
docker stack deploy -c maplefile-stack.yml maplefile
⚠️ Important: This is why you should always backup secret values before changing them!
Rollback Service (Docker Swarm)
If service is failing after update:
# Docker Swarm can rollback to previous image version
docker service rollback maplefile_backend
# Watch rollback
docker service ps maplefile_backend
Troubleshooting
Problem: Changes Not Applied
Symptom: Updated stack file but service still uses old values
Diagnosis:
# Check when stack was last deployed
docker stack ps maplefile --format "table {{.Name}}\t{{.CurrentState}}"
# Check service definition
docker service inspect maplefile_backend --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}' | jq
Solution:
# Force redeploy by removing and recreating
docker stack rm maplefile
sleep 20
docker stack deploy -c maplefile-stack.yml maplefile
Problem: Service Won't Start After Update
Symptom: Service stuck in "Starting" or "Failed" state
Diagnosis:
# Check service status
docker service ps maplefile_backend --no-trunc
# Check logs for startup errors
docker service logs maplefile_backend --tail 100
Common causes:
-
Invalid environment value:
# Check for syntax errors in stack file cat ~/stacks/maplefile-stack.yml | grep -A50 "environment:" -
Missing required variable:
# Check logs for "missing" or "required" docker service logs maplefile_backend | grep -i "missing\|required" -
Secret not found:
# Verify secret exists docker secret ls | grep maplefile # If missing, recreate it echo "SECRET_VALUE" | docker secret create maplefile_jwt_secret -
Solution:
- Fix the invalid value
- Redeploy
- If still failing, rollback to backup
Problem: Secret Not Updating
Symptom: Created new secret but service still uses old value
Cause: Stack still references old secret
Solution:
# Must remove stack completely first
docker stack rm maplefile
sleep 20
# Verify stack removed
docker stack ls
docker ps | grep maplefile # Should return nothing
# Now redeploy
docker stack deploy -c maplefile-stack.yml maplefile
Problem: Can't Remove Secret - "In Use"
Symptom: Error response from daemon: secret is in use by service
Cause: Service is still using the secret
Solution:
# Must remove stack first
docker stack rm maplefile
sleep 20
# Now you can remove secret
docker secret rm maplefile_jwt_secret
# Recreate and redeploy
echo "NEW_SECRET" | docker secret create maplefile_jwt_secret -
docker stack deploy -c maplefile-stack.yml maplefile
Problem: YAML Syntax Error
Symptom: error parsing YAML file
Diagnosis:
# Check YAML syntax
cat ~/stacks/maplefile-stack.yml
# Common issues:
# - Inconsistent indentation (use spaces, not tabs)
# - Missing colons
# - Incorrect nesting
Solution:
# Restore from backup
cp maplefile-stack.yml.backup-LATEST maplefile-stack.yml
# Try again with correct YAML syntax
Best Practices
1. Always Backup Before Changes
# Good practice - timestamped backups
cp maplefile-stack.yml maplefile-stack.yml.backup-$(date +%Y%m%d-%H%M%S)
# Keep backups organized
mkdir -p ~/stacks/backups/$(date +%Y%m%d)
cp maplefile-stack.yml ~/stacks/backups/$(date +%Y%m%d)/
2. Document Secret Values Before Changing
# Save old secret value to temporary secure location
# (NOT in version control!)
docker secret inspect maplefile_mailgun_api_key --format '{{.ID}}' > /tmp/old_secret_id.txt
# Or write it down securely before removing
3. Test in Development First
For major changes:
# If you have a dev environment, test there first
# Then apply same changes to production
4. Use Strong Secrets
# Generate secure random secrets
openssl rand -base64 48 # JWT secret (64 chars)
openssl rand -hex 32 # API tokens (64 chars)
# Don't use:
# - Weak passwords (password123)
# - Default values (secret)
# - Short strings (abc)
5. Rotate Secrets Regularly
Security schedule:
| Secret | Rotation Frequency | Priority |
|---|---|---|
| JWT Secret | Every 6 months | High |
| API Keys (Mailgun, S3) | When provider requires | Medium |
| Redis Password | Yearly | Medium |
6. Monitor After Changes
# After deploying changes, monitor for at least 5 minutes
docker service logs -f maplefile_backend --tail 50
# Check for:
# - Successful startup messages
# - No error messages
# - Expected functionality (test key features)
7. Keep Stack Files in Version Control
# Initialize git if not already done
cd ~/stacks
git init
# Add stack files (but NOT secrets!)
git add maplefile-stack.yml
git add maplefile-caddy-config/Caddyfile
# Commit
git commit -m "Update Mailgun domain configuration"
Add to .gitignore:
# Create .gitignore
cat > ~/stacks/.gitignore << 'EOF'
# Never commit backups
*.backup-*
# Never commit secrets
secrets/
# Never commit temporary files
*.tmp
*.log
EOF
8. Use Comments in Stack File
services:
backend:
environment:
# Updated 2025-11-14: Changed to EU region for better performance
- MAILGUN_API_BASE=https://api.eu.mailgun.net/v3
Quick Reference
Essential Commands
# SSH to manager
ssh dockeradmin@<MANAGER_IP>
# Edit stack file
nano ~/stacks/maplefile-stack.yml
# Redeploy stack (for environment variable changes)
docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile
# Update secret (requires removing stack first)
docker stack rm maplefile
sleep 20
docker secret rm maplefile_mailgun_api_key
echo "NEW_KEY" | docker secret create maplefile_mailgun_api_key -
docker stack deploy -c ~/stacks/maplefile-stack.yml maplefile
# Watch logs
docker service logs -f maplefile_backend --tail 50
# Check service health
docker service ls | grep maplefile
# Rollback service
docker service rollback maplefile_backend
File Locations
| Item | Location |
|---|---|
| Stack Definition | ~/stacks/maplefile-stack.yml |
| Caddy Config | ~/stacks/maplefile-caddy-config/Caddyfile |
| Secrets | Managed by Docker Swarm (use docker secret commands) |
| Backups | ~/stacks/*.backup-* |
Docker Secrets
| Secret Name | Purpose |
|---|---|
maplefile_jwt_secret |
JWT token signing |
maplefile_mailgun_api_key |
Mailgun email API |
redis_password |
Redis cache authentication |
spaces_access_key |
DigitalOcean Spaces access key |
spaces_secret_key |
DigitalOcean Spaces secret key |
Related Documentation
Questions?
- Check service logs:
docker service logs maplefile_backend - Review stack file:
cat ~/stacks/maplefile-stack.yml - List secrets:
docker secret ls
Last Updated: November 2025