What Are Environment Variables: A Guide For Beginners
Environment variables enable configuring applications without changing code. They detach external data from app logic, which can remain quite mystifying to budding developers (and even some seasoned ones).
Through this hands-on guide, we will lift the veil around environment variables so you can understand what they entail, why they matter, and how to leverage environment variables confidently.
Grab your favorite beverage (and maybe some cookies) cause we’re about to get into it. Let’s unpack environmental variable concepts from the ground up.
What Are Environment Variables?
Environment variables are dynamic named values that can affect how running processes behave on a computer. Some key properties of environment variables are:
- Named: Have descriptive variable names like APP_MODE and DB_URL.
- External: Values are set outside the app code via files, command lines, and systems.
- Dynamic: Can update variables without restarting apps.
- Configured: Code relies on variables but doesn’t define them.
- Decoupled: No need to alter code configurations once variables are set.
Here’s an analogy. Imagine you’re following a chocolate chip cookie recipe. The recipe might say:
- Add 1 cup of sugar
- Add 1 stick of softened butter
- Add 2 eggs
Instead of those hard-coded values, you could use environment variables instead:
- Add $SUGAR cup of sugar
- Add $BUTTER sticks of softened butter
- Add $EGGS eggs
Before making the cookies, you’d set those environment variable names to values of your choosing:
SUGAR=1
BUTTER=1
EGGS=2
So, when following the recipe, your ingredients would resolve to:
- Add 1 cup of sugar
- Add 1 stick of softened butter
- Add 2 eggs
This allows you to configure the cookie recipe without changing the recipe code.
The same concept applies to computing and development. Environment variables allow you to alter the environment in which a process runs without changing the underlying code. Here are a few common examples:
- Setting the environment to “development” or “production”
- Configuring API keys for external services
- Passing in secret keys or credentials
- Toggling certain features on and off
Environment variables provide great flexibility. You can deploy the same code to multiple environments without changing the code itself. But let’s understand further why they are valuable.
Why Are Environment Variables Valuable?
Consider environment variables like application knobs used to dial-in preferences. We will explore excellent use cases shortly.
Let’s solidify intuition on why environment variables matter!
Reason #1: They Separate Application Code From Configurations
Hard-coding configurations and credentials directly into your code can cause all sorts of problems:
- Accidental commits to source control
- Rebuilding and redeploying code just to change a value
- Configuration issues when promoting across environments
It also leads to messy code:
import os
# Hard-coded configuration
DB_USER = ‘appuser’
DB_PASS = ‘password123’
DB_HOST = ‘localhost’
DB_NAME = ‘myappdb’
def connect_to_db():
print(f”Connecting to {DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}”)
connect_to_db()
This entangles business logic with configuration details. Tight coupling makes maintenance arduous over time:
- Changes require modifying the source code
- Risk of leaking secrets into source control
Using environment variables reduces these issues. For instance, you can set the DB_USER and DB_NAME environment variables.
# .env file
DB_USER=appuser
DB_PASS=password123
DB_HOST=localhost
DB_NAME=myappdb
The application code can access the environment variables whenever required, keeping the code clean and simple.
import os
# Load config from environment
DB_USER = os.environ[‘DB_USER’]
DB_PASS = os.environ[‘DB_PASS’]
DB_HOST = os.environ[‘DB_HOST’]
DB_NAME = os.environ[‘DB_NAME’]
def connect_to_db():
print(f”Connecting to {DB_USER}:{DB_PASS}@{DB_HOST}/{DB_NAME}”)
connect_to_db()
Environment variables cleanly separate configuration from code, keeping sensitive values abstracted into the environment.
You can deploy the same code from development to production without changing a thing. The environment variables can differ between environments without impacting the code at all.
Reason #2: They Simplify Configuring Applications
Environment variables simplify tweaking configurations without touching code:
# .env file:
DEBUG=true
Here’s how we could use it within the script file:
# Script content:
import os
DEBUG = os.environ.get(‘DEBUG’) == ‘true’
if DEBUG:
print(“In DEBUG mode”)
Toggling debug mode requires only updating the .env file—no code changes, rebuilding, or redeploying are needed. “Env vars” for short, also help deploy across environments seamlessly:
import os
# Retrieve environment variable to determine the current environment (production or staging)
current_env = os.getenv(‘APP_ENV’, ‘staging’) # Default to ‘staging’ if not set
# Production API key
PROD_API_KEY = os.environ[‘PROD_API_KEY’]
# Staging API key
STG_API_KEY = os.environ[‘STG_API_KEY’]
# Logic that sets api_key based on the current environment
if current_env == ‘production’:
api_key = PROD_API_KEY
else:
api_key = STG_API_KEY
# Initialize API client with the appropriate API key
api = ApiClient(api_key)
The same code can use separate API keys for production vs staging without any changes.
And lastly, they enable feature toggles without new deployments:
NEW_FEATURE = os.environ[‘NEW_FEATURE’] == ‘true’
if NEW_FEATURE:
enableNewFeature()
Changing the NEW_FEATURE var activates functionality instantly within our code. The interface for updating configurations depends on the systems:
- Cloud platforms like Heroku use web dashboards
- Servers use OS command tools
- Local dev can use .env files
Environment variables are beneficial when creating applications, allowing users to configure elements per their requirements.
Reason #3: They Help Manage Secrets And Credentials
Checking secrets like API keys, passwords, and private keys directly into source code raises substantial security risks:
# Avoid exposing secrets in code!
STRIPE_KEY = ‘sk_live_1234abc’
DB_PASSWORD = ‘password123’
stripe.api_key = STRIPE_KEY
db.connect(DB_PASSWORD)
Those credentials are now exposed if this code gets committed into a public GitHub repository!
Environment variables prevent leakage by externalizing secrets:
import os
STRIPE_KEY = os.environ.get(‘STRIPE_KEY’)
DB_PASS = os.environ.get(‘DB_PASS’)
stripe.api_key = STRIPE_KEY
db.connect(DB_PASS)
The actual secret values get set in a local .env File.
# .env file
STRIPE_KEY=sk_live_1234abc
DB_PASS=password123
Don’t forget to .gitignore the .env file to keep secrets out of source control. This involves defining the .env file in a .gitignore file in any repo root, which tells git to ignore the file during commit creation.
This separates secret definitions from application code, loading them securely from protected environments during runtime. The risk of accidentally exposing credentials reduces dramatically.
Reason #4: They Promote Consistency
Imagine having different configuration files for development, QA, and production environments:
# Development
DB_HOST = ‘localhost’
DB_NAME = ‘appdb_dev’
# Production
DB_HOST = ‘db.myapp.com’
DB_NAME = ‘appdb_prod’
This discrepancy introduces subtle bugs that are hard to catch. Code that works flawlessly in development might suddenly break production due to mismatched configurations.
Environment variables solve this by centralizing configuration in one place:
DB_HOST=db.myapp.com
DB_NAME=appdb_prod
Now, the same variables get used consistently across all environments. You no longer have to worry about random or incorrect settings kicking in.
The application code simply references the variables:
import os
db_host = os.environ[‘DB_HOST’]
db_name = os.environ[‘DB_NAME’]
db.connect(db_host, db_name)
Whether the app runs locally or on a production server, it always uses the correct database host and name.
This uniformity reduces bugs, improves predictability, and makes the app more robust overall. Developers can have confidence that the code will behave identically in every environment.
Get Content Delivered Straight to Your Inbox
Subscribe to our blog and receive great content just like this delivered straight to your inbox.
How Can You Define Environment Variables
Environment variables can be defined in several places, allowing flexibility in setting and accessing them across processes and systems.
1. Operating System Environment Variables
Most operating systems provide built-in mechanisms for defining global variables. This makes the variables accessible system-wide to all users, applications, etc.
On Linux/Unix systems, variables can be defined in shell startup scripts.
For example, ~/.bashrc can be used to set user-level variables, while /etc/environment is for system-wide variables that all users can access.
Variables can also be set inline before executing commands using the export command or directly through the env command in bash:
# In ~/.bashrc
export DB_URL=localhost
export APP_PORT=3000
# In /etc/environment
DB_HOST=localhost
DB_NAME=mydatabase
Variables can also be set inline before executing commands:
export TOKEN=abcdef
python app.py
Defining variables at the OS level makes them globally available, which is quite helpful when you want to run the app without depending on internal values.
You can also reference defined variables in scripts or command-line arguments.
python app.py –db-name $DB_NAME –db-host $DB_HOST –batch-size $BATCH_SIZE
2. Defining Environment Variables In Application Code
In addition to OS-level variables, environment variables can be defined and accessed directly within the application code while running.
The os.environ dictionary in Python contains all currently defined environment variables. We can set new ones by simply adding key-value pairs:
Environment variables can also be defined and accessed directly within the application code. In Python, the os.environ dictionary contains all defined environment variables:
import os
os.environ[“API_KEY”] = “123456”
api_key = os.environ.get(“API_KEY”)
So, the os.environ dictionary allows for the dynamic setting and retrieving of environment variables from within Python code.
Most languages come bundled with their libraries, offering access to environment variables during runtime.
You can also use frameworks like Express, Django, and Laravel to have deeper integrations, such as auto-loading .env files containing environment variables.
3. Creating Local Configuration Files For Environment Variables
In addition to system-level variables, environment variables can be loaded from an application’s local configuration files. This keeps configuration details separate from code, even for local development and testing.
Some popular approaches:
.env Files
The .env file format convention popularized by Node.js provides a convenient way to specify environment variables in a key-value format:
# .env
DB_URL=localhost
API_KEY=123456
Web frameworks like Django and Laravel automatically load variables defined in .env files into the application environment. For other languages like Python, libraries such as python-dotenv handle importing .env files:
from dotenv import load_dotenv
load_dotenv() # Loads .env variables
print(os.environ[‘DB_URL’]) # localhost
The benefit of using .env files is they keep configuration clean and separate without making changes to code.
JSON Configuration Files
For more complex configuration needs involving multiple environment variables, using JSON or YAML files helps organize variables together:
// config.json
{
“api_url”: “https://api.example.com”,
“api_key”: “123456”,
“port”: 3000
}
Application code can then quickly load this JSON data as a dictionary to access configured variables:
import json
config = json.load(‘config.json’)
api_url = config[‘api_url’]
api_key = config[‘api_key’]
port = config[‘port’] # 3000
This prevents messy dotenv files when dealing with multiple app configurations.
How Do You Access Environment Variables In Different Programming Languages?
However we choose to define environment variables, our applications need a consistent way of looking up values during runtime.
While various ways exist to define environment variables, application code needs a standard way to access them at runtime, regardless of language. Here is an overview of techniques to access env variables across popular languages:
Python
Python provides the os.environ dictionary to access defined environment variables:
import os
db = os.environ.get(‘DB_NAME’)
print(db)
We can get a variable using os.environ.get(), which returns None if undefined. Or access directly via os.environ(), which will raise KeyError if it is not present.
Additional methods like os.getenv() and os.environ.get() allow specifying default values if unset.
JavaScript (Node.js)
In Node.js JavaScript code, environment variables are available on the global process.env object:
// Get env var
const db = process.env.DB_NAME;
console.log(db);
If undefined, process.env will contain undefined. We can also supply defaults like:
const db = process.env.DB_NAME || ‘defaultdb’;
Ruby
Ruby applications access environment variables through the ENV hash:
# Access variable
db = ENV[‘DB_NAME’]
puts db
We can also pass a default value if the desired key does not exist:
db = ENV.fetch(‘DB_NAME’, ‘defaultdb’)
PHP
PHP provides global methods getenv(), $_ENV and $_SERVER to access environment variables:
// Get env var
$db_name = getenv(‘DB_NAME’);
// Or access $_ENV or $_SERVER arrays
$db_name = $_ENV[‘DB_NAME’];
Depending on the variable source, they may be available in different globals.
Java
In Java, the System.getenv() method returns env variables which can be accessed:
String dbName = System.getenv(“DB_NAME”);
This allows access to variables defined at a system level globally in Java.
For now, some best practices around environment variable hygiene.
Environment Variable Security Guide
When it comes to managing environment variables securely, we should keep several best practices in mind.
Never Store Sensitive Information In Code
First and foremost, never store sensitive information like passwords, API keys, or tokens directly in your code.
It may be tempting to just hardcode a database password or an encryption key into your source code for quick access, but resist that urge!
If you accidentally commit that code to a public repository on GitHub, you’re essentially broadcasting your secrets to the entire world. Imagine if a hacker got ahold of your production database credentials just because they were sitting in plain text in your codebase. Scary thought, right?
Instead, always use environment variables to store any sort of sensitive configuration. Keep your secrets in a secure place like a .env file or a secrets management tool, and reference them in your code via environment variables. For example, instead of doing something like this in your Python code:
db_password = “supers3cr3tpassw0rd”
You’d store that password in an environment variable like this:
# .env file
DB_PASSWORD=supers3cr3tpassw0rd
And then access it in your code like:
import os
db_password = os.environ.get(‘DB_PASSWORD’)
This way, your secrets are still safe even if your source code gets compromised. Environment variables act as a secure abstraction layer.
Use Environment-Specific Variables
Another practice is using different environment variables for each application environment, such as development, staging, and production.
You don’t want to accidentally connect to your production database while developing locally just because you forgot to update a config variable! Namespace your environment variables for each environment:
# Dev
DEV_API_KEY=abc123
DEV_DB_URL=localhost
# Production
PROD_API_KEY=xyz789
PROD_DB_URL=proddb.amazonaws.com
Then, reference the appropriate variables in your code depending on the current environment. Many frameworks like Rails provide environment-specific config files for this purpose.
Keep Secrets Out Of Version Control
It’s also crucial to keep your .env and config files containing secrets out of version control. Add .env to your .gitignore so you don’t accidentally commit it to your repository.
You can use git-secrets to scan for sensitive info before each commit. For extra security, encrypt your secrets file before storing it. Tools like Ansible Vault and BlackBox can help with this.
Secure Secrets On Production Servers
When managing environment variables on your production servers, avoid setting them using command line arguments, which can be inspected through the process table.
Instead, use your operating system or container orchestration platform’s environment management tools. For example, you can use Kubernetes Secrets to store and expose secrets securely to your application pods.
Use Strong Encryption Algorithms
Use robust and modern encryption algorithms when encrypting your secrets, whether in transit or at rest. Avoid deprecated algorithms like DES or MD5, which have known vulnerabilities. Instead, opt for industry-standard algorithms like AES-256 for symmetric encryption and RSA-2048 or ECDSA for asymmetric encryption.
Rotate Secrets Regularly
Rotate your secrets regularly, especially if you suspect they may have been compromised. Treat secrets like you would a password — update them every few months. A secrets management tool like Hashicorp Vault or AWS Secrets Manager can help automate this process.
Be Careful With Logging And Error Reporting
Be careful about logging and error reporting. Make sure not to log any environment variables that contain sensitive values. If you’re using a third-party error tracking tool, configure it to sanitize sensitive data. The last thing you want is for your secrets to appear in a stack trace on an exception reporting dashboard!
When To Avoid Environment Variables?
There are several cases where environment variables should be avoided:
Managing Complex Configuration
Using environment variables to manage configuration for complex software systems can become messy and error-prone. As the number of configuration parameters grows, you end up with long environment variable names that can unintentionally collide. There is also no easy way to organize related configuration values together.
Instead of environment variables, consider using configuration files in a format like JSON or YAML. These allow you to:
- Group related configuration parameters together in a nested structure.
- Avoid naming collisions by encapsulating config in scopes and namespaces.
- Define custom data types instead of just strings.
- Quickly view and modify configurations using a text editor.
Storing Sensitive Information
While environment variables seem easy to inject external configurations like API keys, database passwords, etc., this can cause security issues.
The problem is environment variables are accessible globally in a process. So, if an exploit exists in part of your application, it could compromise secrets stored in environment variables.
A more secure approach is using a secret management service that handles encryption and access control. These services allow storing of sensitive data externally and provide SDKs for retrieving application values.
So, consider using a dedicated secrets management solution rather than environment variables for credentials and private keys. This reduces the risk of accidentally exposing sensitive data through exploits or unintended logging.
Working With Multiple Environments
Managing environment variables can become tedious as applications grow and get deployed across multiple environments (dev, staging, staging, prod). You may have fragmented configuration data spread across various bash scripts, deployment tools, etc.
A configuration management solution helps consolidate all environment-specific settings into a centralized place. This could be files in a repository, a dedicated configuration server, or integrated with your CI/CD pipelines.
If the goal is to avoid duplicating environment variables, a single source of truth for configurations makes more sense.
Sharing Configuration Across Teams
Since environment variables are sourced locally per process, sharing and synchronizing configuration data across different teams working on the same application or suite of services becomes very difficult.
Each team may maintain its copy of configuration values in different bash scripts, deployment manifests, etc. This decentralized configuration leads to the following:
Rather than this fragmented approach, having a centralized configuration solution allows teams to manage configuration from a single platform or repository.
Build Your Apps With Environment Variables For The Long-Term
As your application grows, consider how you may need more advanced ways to manage its configuration settings.
What seems straightforward now could get more complicated later on. You’ll likely need better ways to control access, share team settings, organize everything clearly, and update configurations smoothly.
Don’t back yourself into a corner by just using environment variables from the start. You want to plan how to handle configurations as your needs expand.
While environment variables are great for handling environment-focused data like login credentials, database names, local IPs, etc, you want to create a system that follows sound principles like security, shareability, organization, and the ability to adapt to changes quickly.
The alternatives we discussed, like using a dedicated configuration file or service, have valuable features that align with those principles. That will help you to keep moving quickly without getting slowed down.
Get Content Delivered Straight to Your Inbox
Subscribe to our blog and receive great content just like this delivered straight to your inbox.