You’ve seen this tool: it only reads from a config file, so containerizing it means writing wrapper scripts just to inject values you already have in your environment. Or it only reads environment variables, so local development means polluting your shell with settings that belong in a project-specific file. These problems have the same root cause - the tool made a configuration decision that doesn’t match your deployment context.
Well-designed tools don’t force that choice. They support CLI flags for immediacy, environment variables for deployment, and config files for persistence - with a clear precedence rule that makes all three work together predictably. Here’s how to build that.
The Three Pillars of Configuration
Effective configuration typically comes in three forms, each serving distinct use cases:
CLI Flags: Immediate and Explicit
Command-line flags offer the most explicit form of configuration. When you run dprs --port 8080 --debug, there’s no ambiguity about what’s happening. CLI flags excel in:
- Ad-hoc usage: Quick one-off commands with specific parameters
- Script automation: Shell scripts where explicit parameters improve readability
- Testing and debugging: Temporarily overriding default behavior
- Documentation: Self-documenting commands that show exactly what’s configured
The immediacy of CLI flags makes them perfect for exploratory work and situations where you need to see exactly what configuration is being applied.
Environment Variables: Context-Aware Configuration
Environment variables bridge the gap between ephemeral CLI flags and persistent config files. They shine in:
- Containerized environments: Docker, Kubernetes, and cloud platforms make environment variables the natural configuration mechanism
- CI/CD pipelines: Build and deployment systems expect environment-based configuration
- Secret management: Sensitive values like API keys and credentials are safer in environment variables than committed config files
- Multi-environment workflows: Different values for development, staging, and production
Modern deployment infrastructure is built around environment variables, making them essential for tools that need to work in contemporary DevOps workflows.
Config Files: Persistent and Comprehensive
Configuration files provide the most comprehensive and maintainable approach for complex setups:
- Version control: Config files can be committed and tracked alongside code
- Complex configurations: Nested structures, arrays, and detailed settings that would be unwieldy as flags
- Team collaboration: Shared configurations that ensure consistent behavior across a team
- Documentation: Config files serve as living documentation of how a tool is configured
Whether YAML, TOML, JSON, or another format, config files offer the structure needed for sophisticated configuration requirements.
Once you implement all three with a clear precedence chain, you can drop your tool into a Kubernetes deployment, a CI pipeline, and a local dev environment without changing a line of code - each context supplies configuration the way it naturally does.
Why All Three Matter
The power isn’t in choosing one method - it’s in supporting all three with clear precedence rules. Users should be able to:
- Set sensible defaults in a config file
- Override for an environment using environment variables
- Fine-tune specific invocations with CLI flags
This precedence chain (config file → environment variables → CLI flags) gives users maximum flexibility while maintaining predictable behavior.
Reference Implementations
If you want to see this pattern in working code, two open source projects demonstrate it across different language ecosystems:
dprs - A Docker container management TUI built in Rust. If you’re working in the Rust ecosystem and want to see how configuration layering integrates with CLI argument parsing libraries like clap, this is a useful reference.
Implementation Patterns
Supporting multiple configuration methods requires thoughtful design:
Precedence is Critical
Users must understand which configuration takes priority. The typical precedence from lowest to highest is:
- Hardcoded defaults (in code)
- Config file values
- Environment variables
- CLI flags
This order makes intuitive sense: more specific and immediate configuration methods override more general ones.
Discoverability Matters
Each configuration method should be discoverable:
- CLI flags should appear in
--helpoutput - Environment variables should be documented and ideally follow a consistent naming pattern
- Config file locations and formats should be clearly specified
- Error messages should guide users when configuration is missing or invalid
Validation Should Be Consistent
Whether a value comes from a CLI flag, environment variable, or config file, validation should be identical. A port number is invalid if it’s out of range regardless of how it was specified.
Real-World Scenarios
Consider these common scenarios and how multiple configuration methods solve them:
Local Development: A developer uses config files for their standard setup, but occasionally needs to override the port with a CLI flag when testing multiple instances.
CI/CD Pipeline: The same tool reads from a committed config file for most settings, but uses environment variables for secrets and environment-specific values like API endpoints.
Container Deployment: In Kubernetes, environment variables configure the tool for different namespaces and environments, while a mounted ConfigMap provides the detailed configuration that rarely changes.
Team Onboarding: New team members clone the repository, and the committed config file gives them a working setup immediately. As they learn, they can override settings temporarily with flags before eventually customizing their own config file.
The Cost of Rigidity
Tools that only support one configuration method create unnecessary friction:
- CLI-only tools require unwieldy scripts full of long command invocations
- Environment-only tools force users to pollute their shell environment or create wrapper scripts
- Config-file-only tools make ad-hoc usage painful and complicate containerization
For most tools, adding environment variable support on top of existing CLI flags is a few dozen lines of code - typically one lookup per parameter before the CLI default is applied. The engineering cost is modest. The friction you avoid for users deploying into containers or CI environments is not.
Start Simple, Grow Thoughtfully
You don’t need to implement every configuration method on day one. A pragmatic approach:
- Start with CLI flags for core parameters
- Add config file support when configuration grows complex
- Layer in environment variable support when users deploy to different environments
- Maintain clear documentation as you add methods
The key is having a plan for supporting all three as your tool matures.
Where This Pattern Ends
The three-layer approach works well for single-process tools and libraries. If your system spans multiple services that need shared configuration - a microservices fleet, for example - you’ll eventually need a config server, a secrets manager like Vault, or a distributed key-value store. That’s a different problem with different trade-offs, and a different article.
For a single tool or library, the precedence chain described here covers the overwhelming majority of real deployment scenarios. Start with it. You can layer additional complexity on top later if your actual usage demands it - but most tools never will.
Want to see this pattern in working code? dprs is open source at github.com/durableprogramming. If your team is working through a configuration design decision and wants a second opinion, reach out.

