process.env.dev

Environment Variable Tips & Tricks

Practical techniques for working with environment variables across Docker, CI/CD pipelines, Kubernetes, and local development.

Docker: Passing Environment Variables to Containers

Docker provides several mechanisms for injecting environment variables into containers. Understanding when to use each one is key to keeping your images portable and your secrets secure.

The --env (or -e) flag sets a single variable when running a container. This is convenient for quick tests but becomes unwieldy with many variables:

bash
docker run -e DATABASE_URL=postgres://localhost/mydb -e LOG_LEVEL=debug myapp

For multiple variables, use --env-file to load from a file. The file uses a simpleKEY=VALUE format with one variable per line. Unlike shell dotenv files, Docker env files do not support variable interpolation or quoting:

bash
# app.env
DATABASE_URL=postgres://db:5432/production
REDIS_URL=redis://cache:6379
LOG_LEVEL=info

docker run --env-file app.env myapp

In Docker Compose, the environment key sets variables directly in your service definition, while env_file loads from an external file. You can also reference host environment variables using the ${VARIABLE} syntax. Compose resolves these from the host shell and from any.env file in the project root:

yaml
# docker-compose.yml
services:
  api:
    image: myapp
    env_file:
      - .env
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}

Be careful with build-time variables (ARG in Dockerfile) versus runtime variables (ENV). Build arguments are baked into the image layer and visible to anyone who inspects the image. Never pass secrets as build arguments. Use multi-stage builds to ensure that secrets needed during build do not persist in the final image.

CI/CD: Managing Secrets in Pipelines

Every major CI/CD platform provides a secrets management mechanism. The common pattern is: store secrets in the platform's encrypted storage, then expose them as environment variables during job execution.

GitHub Actions

Store secrets in repository or organization settings under Settings > Secrets and variables > Actions. Reference them in workflow files using the ${{ secrets.SECRET_NAME }} syntax. GitHub automatically masks secret values in log output:

yaml
# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      API_KEY: ${{ secrets.API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - run: npm run deploy

Use environment-scoped secrets for different deployment targets. Create GitHub Environments (e.g., "production", "staging") with their own secret sets and protection rules like required reviewers.

GitLab CI

GitLab CI variables are configured in Settings > CI/CD > Variables. Mark sensitive values as "Masked" to prevent them from appearing in logs, and "Protected" to limit them to protected branches. Variables are automatically available as environment variables in all jobs:

yaml
# .gitlab-ci.yml
deploy:
  script:
    - echo "Deploying with $DATABASE_URL"
  environment:
    name: production
  only:
    - main

Jenkins

Jenkins uses the Credentials plugin to store secrets. Bind them to environment variables in your pipeline using the withCredentials block or the credentials() helper in declarative pipelines:

text
pipeline {
  agent any
  environment {
    DATABASE_URL = credentials('database-url')
  }
  stages {
    stage('Deploy') {
      steps {
        sh 'npm run deploy'
      }
    }
  }
}

Kubernetes: ConfigMaps and Secrets

Kubernetes separates non-sensitive configuration (ConfigMaps) from sensitive data (Secrets). Both can be injected as environment variables or mounted as files in your pods.

ConfigMaps hold non-sensitive key-value pairs. Create them from literal values or files, then reference them in your pod spec:

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  FEATURE_NEW_UI: "true"

Secrets store sensitive data encoded in base64. While base64 is not encryption, Kubernetes can be configured to encrypt secrets at rest using an EncryptionConfiguration. Use kubectl create secret to avoid base64 encoding manually:

bash
kubectl create secret generic app-secrets \
  --from-literal=DATABASE_URL='postgres://prod:5432/mydb' \
  --from-literal=API_KEY='sk-live-abc123'

The envFrom field in a container spec loads all entries from a ConfigMap or Secret as environment variables at once, avoiding the need to list each variable individually:

yaml
spec:
  containers:
    - name: api
      envFrom:
        - configMapRef:
            name: app-config
        - secretRef:
            name: app-secrets

For production clusters, consider using the External Secrets Operator or Sealed Secrets to sync secrets from external stores like AWS Secrets Manager or HashiCorp Vault into Kubernetes Secrets, keeping your GitOps manifests free of actual secret values.

Local Development: dotenv, direnv, and More

For local development, .env files are the most popular approach. The dotenvlibrary (available for Node.js, Python, Ruby, and others) loads variables from a .env file into the process environment at startup.

A more powerful alternative is direnv, a shell extension that automatically loads and unloads environment variables when you enter or leave a project directory. It uses a .envrc file (which can contain shell logic) and requires explicit approval before loading:

bash
# .envrc
export DATABASE_URL=postgres://localhost:5432/myapp_dev
export LOG_LEVEL=debug
export API_KEY=$(cat ~/.secrets/api-key)

The advantage of direnv is that it works at the shell level, so every tool and command you run in that directory has access to the variables — not just your application. It also supports dotenvfiles directly with dotenv in the .envrc, giving you the best of both worlds.

Another helpful tool is envsubst, included with GNU gettext. It replaces environment variable references in template files, which is useful for generating configuration files from templates:

bash
envsubst < config.template.json > config.json

Multi-Stage Builds: Build-Time vs Runtime Variables

A common source of confusion is the difference between build-time and runtime environment variables, especially in containerized deployments and static site generators.

Build-time variables are available during the build process. In Docker, these areARG values. In frontend frameworks like Next.js or Vite, these are variables embedded into the JavaScript bundle at build time (e.g., NEXT_PUBLIC_* or VITE_*). Once the build is complete, changing these values requires rebuilding.

Runtime variables are read when the application starts. Server-side code reads them fromprocess.env (Node.js), os.environ (Python), or the system environment. These can be changed without rebuilding — just restart the process with new values.

In Docker multi-stage builds, ensure that secrets used in early stages (e.g., to install private packages) do not persist in the final image. Use --mount=type=secret with BuildKit for secrets that should never be written to an image layer:

dockerfile
# syntax=docker/dockerfile:1
FROM node:22 AS builder
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
RUN npm run build

FROM node:22-slim
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Debugging: Inspecting Environment Variables Safely

When debugging environment variable issues, you often need to verify which variables are set and what values they hold. Do this carefully — never log or print secrets in production.

On Linux or macOS, printenv lists all environment variables. Use printenv VARIABLE_NAMEto check a specific one. The env command also works:

bash
# List all env vars (careful in production!)
printenv

# Check a specific variable
printenv DATABASE_URL

# Check if a variable is set (without revealing its value)
[ -n "$DATABASE_URL" ] && echo "DATABASE_URL is set" || echo "DATABASE_URL is NOT set"

In your application code, log the presence of variables without their values. A simple startup check that reports which required variables are present (or missing) without exposing secrets is invaluable for debugging deployment issues:

javascript
const required = ['DATABASE_URL', 'API_KEY', 'REDIS_URL'];
for (const name of required) {
  const status = process.env[name] ? 'SET' : 'MISSING';
  console.log(`  ${name}: ${status}`);
}

In Kubernetes, use kubectl exec to inspect the environment inside a running pod. In Docker,docker inspect shows the configured environment variables for a container (note: this reveals secret values, so restrict access to the Docker socket).

When filing bug reports or asking for help, always redact secrets before sharing environment details. Replace actual values with placeholders like [REDACTED] or ***. Most CI/CD platforms do this automatically in logs, but local debugging output will not.