CASCADING OVERRIDES - nself-org/cli GitHub Wiki
Understand how nself's environment variable system works and how to manage configuration across development, staging, and production.
nself uses a cascading override system where configuration files layer on top of each other. Each file overrides the previous one, allowing for:
- Shared defaults (committed to git)
- Local customization (on your machine only)
- Environment-specific overrides (staging vs production)
- Secrets management (never in git)
Configuration is loaded in this exact order. Later files override earlier files:
.env.dev (1. Base - Committed to git)
โ
.env.local (2. Local - On your machine only)
โ
.env.staging (3. Staging - On staging server only)
โ
.env.prod (4. Production - On prod server only)
โ
.env.secrets (5. Secrets - Ultra-sensitive, server only)
โ
.env.computed (6. Computed - Auto-generated, never edit)
Purpose: Base configuration used by all developers.
Properties:
- Committed to git
- Shared by entire team
- Contains safe default values
- No secrets or sensitive data
Example:
PROJECT_NAME=my-app
ENV=dev
BASE_DOMAIN=localhost
POSTGRES_DB=myapp_dev
HASURA_GRAPHQL_ENABLE_CONSOLE=truePurpose: Your personal machine customizations.
Properties:
- Gitignored (never committed)
- Only on your machine
- Overrides .env.dev
- Can contain personal secrets
Example:
# You want to use a different Postgres password
POSTGRES_PASSWORD=my-super-secret-local-password
# You want to enable Redis locally
REDIS_ENABLED=trueResult: Your machine uses your password, all other values from .env.dev.
Purpose: Configuration specific to the staging environment.
Properties:
- On staging server only (not on developer machines)
- Overrides .env.dev and .env.local
- Staging-specific values (staging database, staging API keys)
Example:
# Staging database with more data
POSTGRES_PASSWORD=staging-db-password
# Staging Hasura admin secret
HASURA_GRAPHQL_ADMIN_SECRET=staging-secret-123
# Enable monitoring on staging
MONITORING_ENABLED=trueAccess: Senior developers via SSH to staging server.
Purpose: Production-specific configuration.
Properties:
- On production server only
- Overrides everything above
- Production URLs, API keys, feature flags
Example:
# Production database
POSTGRES_PASSWORD=prod-database-password
# Production domain
BASE_DOMAIN=myapp.com
# Production Hasura secret
HASURA_GRAPHQL_ADMIN_SECRET=prod-secret-xyzAccess: Lead developers only via SSH to production.
Purpose: Ultra-sensitive credentials, generated on server.
Properties:
- Generated on server at setup time
- Never in git (ever)
- Only on production server
- Encrypted if stored at rest
Example:
# Master encryption key
ENCRYPTION_KEY=long-random-string-from-server
# Database root password
POSTGRES_ROOT_PASSWORD=ultra-secret-generated-key
# JWT signing key
JWT_SECRET=another-generated-secretAccess: Lead developers only, synced via secure SSH.
Purpose: Computed values generated during nself build.
Properties:
- Auto-generated by nself (never edit)
- Contains derived values
- Based on other environment variables
Example:
# Generated from POSTGRES credentials
DATABASE_URL=postgresql://postgres:password@postgres:5432/myapp_db
# Generated from PROJECT_NAME
DOCKER_NETWORK=my-app_network
# Generated service hosts
POSTGRES_HOST=postgres
HASURA_HOST=hasuraWhen nself starts, it loads variables in order:
#!/bin/bash
# Load .env.dev first (shared defaults)
if [ -f .env.dev ]; then
source .env.dev
fi
# Load .env.local second (your overrides)
if [ -f .env.local ]; then
source .env.local
fi
# Load environment-specific (staging, prod, etc)
if [ -f .env.${ENV} ]; then
source .env.${ENV}
fi
# Load production secrets last (if they exist)
if [ -f .env.secrets ]; then
source .env.secrets
fi
# Auto-generate computed values
source .env.computedStarting configuration:
File: .env.dev (committed to git)
PROJECT_NAME=my-app
ENV=dev
BASE_DOMAIN=localhost
POSTGRES_DB=myapp_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=change-me
HASURA_GRAPHQL_ADMIN_SECRET=change-me
REDIS_ENABLED=false
MONITORING_ENABLED=falseFile: .env.local (on developer's machine, gitignored)
# Developer wants local Redis for caching
REDIS_ENABLED=true
# Developer uses different password
POSTGRES_PASSWORD=my-local-password-456
HASURA_GRAPHQL_ADMIN_SECRET=my-local-secret-789When you run nself start on your local machine:
| Variable | Source | Value |
|---|---|---|
PROJECT_NAME |
.env.dev | my-app |
ENV |
.env.dev | dev |
BASE_DOMAIN |
.env.dev | localhost |
POSTGRES_DB |
.env.dev | myapp_db |
POSTGRES_USER |
.env.dev | postgres |
POSTGRES_PASSWORD |
.env.local (override) | my-local-password-456 |
HASURA_GRAPHQL_ADMIN_SECRET |
.env.local (override) | my-local-secret-789 |
REDIS_ENABLED |
.env.local (override) | true |
MONITORING_ENABLED |
.env.dev | false |
The staging server doesn't have .env.local. It loads:
File: .env.dev (from git)
PROJECT_NAME=my-app
POSTGRES_PASSWORD=change-me
# ...File: .env.staging (only on staging server)
POSTGRES_PASSWORD=staging-db-password-xyz
BASE_DOMAIN=staging.myapp.com
MONITORING_ENABLED=trueResult on staging:
| Variable | Source | Value |
|---|---|---|
POSTGRES_PASSWORD |
.env.staging | staging-db-password-xyz |
BASE_DOMAIN |
.env.staging | staging.myapp.com |
MONITORING_ENABLED |
.env.staging | true |
| Other vars | .env.dev | (defaults) |
nself supports role-based environment access:
Access: .env.dev + .env.local only
Can develop locally but cannot see staging/production secrets.
nself env switch local # Works (they have .env.local)
nself sync pull staging # Fails (no SSH access to staging)Access: .env.dev, .env.local, + .env.staging
Can test on staging server without accessing production.
nself env switch local # Works
nself sync pull staging # Works (SSH access to staging)
nself sync pull prod # Fails (no SSH access to prod)Access: All files including .env.secrets
Full access to production for emergencies and critical tasks.
nself env switch local # Works
nself sync pull staging # Works
nself sync pull prod # Works (SSH access to prod)
nself sync pull secrets # Works (access to .env.secrets)Senior developers can pull staging configuration to their local machine:
# Pull .env.staging from staging server via SSH
nself sync pull staging
# This creates .env.staging locally (gitignored)
# Now you can test staging config locallyLead developers only:
# Pull .env.prod from production server
nself sync pull prod
# Pull .env.secrets (master credentials)
nself sync pull secretsUpdate configuration on remote:
# Sync your .env changes to staging
nself sync push staging
# Sync to production (Lead Dev only)
nself sync push prodYour .gitignore should include:
# Local overrides
.env.local
# Environment-specific (don't commit staging/prod)
.env.staging
.env.prod
# Secrets (NEVER commit)
.env.secrets
# Computed values (auto-generated)
.env.computedOnly .env.dev should be committed to git.
# Good - only base config in git
git add .env.dev
git commit -m "feat: update base configuration"
# BAD - never do this
git add .env.secrets # โ NEVER!
git add .env.prod # โ NEVER!# Edit .env.dev for shared changes
vim .env.dev
git add .env.dev
git commit -m "update base config"
git push
# Edit .env.local for personal changes
vim .env.local # Not committed
# No git commands needed# Pull staging config
nself sync pull staging
# Switch to staging config for testing
nself env switch staging
# Run tests
nself build
nself start
# Switch back to local
nself env switch local# Lead dev only
nself sync pull prod
# Make emergency change
vim .env.prod
# Apply immediately
nself build
nself restart
# Push change to remote
nself sync push prodAs a developer:
# Add to .env.dev (shared)
echo "NEW_VAR=value" >> .env.dev
git add .env.dev
git commit -m "add NEW_VAR to configuration"
git push
# Tell Sr Dev and Lead Dev they need to add to their environment filesAs Sr Dev:
# Add staging value
echo "NEW_VAR=staging-value" >> .env.staging
nself sync push stagingAs Lead Dev:
# Add production value
echo "NEW_VAR=prod-value" >> .env.prod
nself sync push prodProblem: You set a value in .env.local but it's not being used.
Solution: Check the cascade order. Later files override earlier ones.
# Check which file has the value
grep "POSTGRES_PASSWORD" .env.dev
grep "POSTGRES_PASSWORD" .env.local
# The .env.local value should be usedProblem: Changed .env.staging on staging server but not seeing changes locally.
Solution: Pull the latest config:
nself sync pull stagingProblem: .env.secrets doesn't exist on production server.
Solution: Generate it:
# On production server
nself config generate-secrets
# This creates .env.secrets with generated valuesProblem: Local dev uses port 8080, but staging also tries to use 8080.
Solution: Use different ports in each environment:
# .env.dev
NGINX_PORT=8080
# .env.staging
NGINX_PORT=8081
# .env.prod
NGINX_PORT=443 # HTTPS only- Keep .env.dev minimal - Only shared defaults
- Never commit .env.local - Add to .gitignore
- Never commit secrets - Use .env.secrets on servers
-
Use meaningful defaults -
change-mefor passwords - Document required variables - List in README
- Rotate secrets regularly - Especially in production
-
Audit changes - Use
nself audit logs config
- Build Configuration - How build uses env files
- Secrets Management - Secure secret handling
- Deployment Guide - Deploying across environments
Key Takeaway: The cascading environment system lets you have one source of truth (git) while allowing safe local development and secure production secrets. Understand the order and you'll never have configuration issues.