Environment variables are the duct tape of modern application configuration, but they’re also a surprisingly powerful, albeit often misunderstood, tool for managing secrets.
Let’s see how this plays out in a simple Node.js app. Imagine a basic Express server that needs to connect to a PostgreSQL database.
// server.js
const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = process.env.PORT || 3000;
// Database connection details
const dbConfig = {
user: process.env.DB_USER || 'default_user',
host: process.env.DB_HOST || 'localhost',
database: process.env.DB_NAME || 'default_db',
password: process.env.DB_PASSWORD, // This is the secret!
port: parseInt(process.env.DB_PORT || '5432', 10),
};
const pool = new Pool(dbConfig);
app.get('/', async (req, res) => {
try {
const client = await pool.connect();
const result = await client.query('SELECT NOW()');
client.release();
res.send(`Database time: ${result.rows[0].now}`);
} catch (err) {
console.error('Database query failed:', err);
res.status(500).send('Error connecting to or querying database.');
}
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Here, process.env.DB_PASSWORD is where the magic (and potential pain) happens. The application expects to find the database password in an environment variable named DB_PASSWORD. If it’s not there, the Pool constructor will likely throw an error or fail to connect.
The mental model for this is straightforward: your application reads configuration from its environment. This environment is a collection of key-value pairs that exist outside the application’s codebase. For Node.js, process.env is the direct interface to this. It’s like a global dictionary where the operating system or the process manager puts all the settings.
When you deploy this app, you’d typically set these variables. On Linux, it might look like this in your shell:
export DB_USER="myuser"
export DB_HOST="db.example.com"
export DB_NAME="myappdb"
export DB_PASSWORD="supersecretpassword123"
export DB_PORT="5432"
node server.js
Or, if you’re using a process manager like pm2:
{
"apps": [
{
"name": "my-app",
"script": "server.js",
"env": {
"NODE_ENV": "production",
"DB_USER": "myuser",
"DB_HOST": "db.example.com",
"DB_NAME": "myappdb",
"DB_PASSWORD": "supersecretpassword123",
"DB_PORT": "5432"
}
}
]
}
The beauty is that the server.js code remains ignorant of how these variables are set. It just reads them. This decouples configuration from code, a fundamental principle of Twelve-Factor App methodology. You can run the exact same code in development (with DB_PASSWORD=localhost_dev_pass), staging, and production, each with different credentials.
When you’re running this locally, you might use a .env file and a library like dotenv to load these variables into process.env before your application starts.
# .env file
DB_USER=myuser
DB_HOST=localhost
DB_NAME=dev_db
DB_PASSWORD=dev_secret_pass
DB_PORT=5432
// server.js (with dotenv)
require('dotenv').config(); // Load .env file into process.env
// ... rest of the server.js code
Running node server.js would then pick up these values.
The real trick to managing secrets with environment variables is understanding that they are not inherently secure. They are simply strings passed from the environment to the process. The security comes from how you control access to that environment. If your server’s filesystem is compromised, or if an attacker can inspect the running processes on the host, they can often see these environment variables. This is why solutions like Kubernetes Secrets, AWS Secrets Manager, or HashiCorp Vault are used in production. These systems provide a more robust way to inject secrets into the environment of your application’s container or pod without exposing them directly in plain text configuration files or command lines that might be logged. The Node.js application itself doesn’t change; it still just reads process.env.DB_PASSWORD. The external system handles fetching that secret and making it available.
The next hurdle in configuration often involves handling complex structures or conditionally loaded settings based on the environment.