Overview
Load Env provides comprehensive support for loading secrets from files, which is essential for Docker Swarm secrets, Kubernetes secrets mounted as files, and other containerized environments. All secret functions are async and support the same type safety as environment variable functions.
What Are Docker Secrets?
Docker secrets are a secure way to store sensitive data like passwords, API keys, and certificates. In Docker Swarm and Kubernetes:
Secrets are stored encrypted at rest
They’re mounted as files in containers at runtime
Default location: /run/secrets/<secret-name>
Basic Usage
Secret functions work identically to env functions, but read from files:
import { secretString } from "@axel/load-env"
// Reads from /run/secrets/DATABASE_PASSWORD
const password = await secretString ( "DATABASE_PASSWORD" )
How Secret Loading Works
When you call a secret function, the library:
Check for environment variable
First checks if process.env[key] contains a file path // If DATABASE_PASSWORD=/custom/path/to/secret is set
const password = await secretString ( "DATABASE_PASSWORD" )
// Reads from /custom/path/to/secret
Fall back to default path
If no environment variable exists, uses /run/secrets/<key> // If DATABASE_PASSWORD is not set in environment
const password = await secretString ( "DATABASE_PASSWORD" )
// Reads from /run/secrets/DATABASE_PASSWORD
Read and validate file
Reads the file content, trims whitespace, and validates the value
This dual behavior allows flexibility: use standard Docker secrets paths by default, or override with custom paths via environment variables.
Available Secret Functions
All secret functions mirror their env* counterparts:
secretString
Read a required string secret:
import { secretString } from "@axel/load-env"
export const API_KEY = await secretString ( "API_KEY" )
export const JWT_SECRET = await secretString ( "JWT_SECRET" , "dev-secret" )
secretInt and secretFloat
Read numeric secrets:
import { secretInt , secretFloat } from "@axel/load-env"
export const MAX_RETRIES = await secretInt ( "MAX_RETRIES" , 3 )
export const THRESHOLD = await secretFloat ( "THRESHOLD" )
secretBool
Read boolean secrets:
import { secretBool } from "@axel/load-env"
export const FEATURE_ENABLED = await secretBool ( "FEATURE_ENABLED" , false )
secretUrl
Read URL secrets:
import { secretUrl } from "@axel/load-env"
export const DATABASE_URL = await secretUrl ( "DATABASE_URL" )
export const API_ENDPOINT = await secretUrl (
"API_ENDPOINT" ,
new URL ( "https://api.example.com" ),
)
secretUuid
Read UUID secrets:
import { secretUuid } from "@axel/load-env"
export const CLIENT_ID = await secretUuid ( "CLIENT_ID" )
export const TENANT_ID = await secretUuid ( "TENANT_ID" )
secretDate
Read date secrets:
import { secretDate } from "@axel/load-env"
export const LICENSE_EXPIRY = await secretDate ( "LICENSE_EXPIRY" )
secretEnum
Read enumerated secrets:
import { secretEnum } from "@axel/load-env"
export const ENVIRONMENT = await secretEnum (
"ENVIRONMENT" ,
[ "development" , "staging" , "production" ] as const ,
"development" ,
)
secretStrings
Read comma-separated lists from secrets:
import { secretStrings } from "@axel/load-env"
export const ALLOWED_IPS = await secretStrings ( "ALLOWED_IPS" )
export const ADMIN_EMAILS = await secretStrings ( "ADMIN_EMAILS" , [
"[email protected] " ,
])
Optional Secrets
Use maybeSecret* functions for optional secrets that return undefined if missing:
maybeSecretString
import { maybeSecretString } from "@axel/load-env"
const optionalKey = await maybeSecretString ( "OPTIONAL_API_KEY" )
if ( optionalKey ) {
console . log ( "Using API key from secret" )
}
All maybeSecret Functions
import {
maybeSecretBool ,
maybeSecretInt ,
maybeSecretFloat ,
maybeSecretUrl ,
maybeSecretUuid ,
maybeSecretDate ,
maybeSecretEnum ,
maybeSecretStrings ,
} from "@axel/load-env"
const debugMode = await maybeSecretBool ( "DEBUG_MODE" )
const port = await maybeSecretInt ( "CUSTOM_PORT" )
const webhookUrl = await maybeSecretUrl ( "WEBHOOK_URL" )
Error Handling
Secret functions provide detailed error messages:
Missing Secret
try {
const key = await secretString ( "API_KEY" )
} catch ( error ) {
// Error: $API_KEY is missing
console . error ( error . message )
console . error ( error . cause ) // { error, key, path }
}
File Access Error
// API_KEY=/custom/path/secret (but file doesn't exist)
try {
const key = await secretString ( "API_KEY" )
} catch ( error ) {
// Error: Couldn't access secret at "/custom/path/secret"
console . error ( error . cause ) // { error, key, path: "/custom/path/secret" }
}
Empty Secret File
try {
const key = await secretString ( "API_KEY" )
} catch ( error ) {
// Error: The secret at "/run/secrets/API_KEY" is empty
console . error ( error . cause ) // { content: "", key, path }
}
Type Validation Error
// Secret file contains "not-a-number"
try {
const port = await secretInt ( "PORT" )
} catch ( error ) {
// TypeError: $PORT is not a number
console . error ( error . cause ) // "not-a-number"
}
Implementation Details
View source implementation
The secret() utility function handles file reading: export async function secret ( key : string ) : Promise < string > {
const envPath = process . env [ key ]?. trim ()
const path = envPath || `/run/secrets/ ${ key } `
const accessed = await access ( path ). catch (( error : unknown ) => {
if ( ! envPath )
return new Error ( `$ ${ key } is missing` , {
cause: { error , key , path: envPath },
})
return new Error ( `Couldn't access secret at " ${ path } "` , {
cause: { error , key , path },
})
})
if ( accessed instanceof Error ) throw accessed
const content = await readFile ( path , "utf8" )
. then ( content => content . trim ())
. catch (( error : unknown ) => {
return new Error ( `Couldn't read secret at " ${ path } "` , {
cause: { error , key , path },
})
})
if ( content instanceof Error ) throw content
if ( ! content ) {
if ( ! envPath )
throw new Error ( `$ ${ key } is missing` , {
cause: { content , key , path: envPath },
})
throw new Error ( `The secret at " ${ path } " is empty` , {
cause: { content , key , path },
})
}
return content
}
Docker Compose Example
Here’s how to use secrets with Docker Compose:
version : "3.8"
services :
app :
image : myapp:latest
secrets :
- db_password
- api_key
environment :
NODE_ENV : production
secrets :
db_password :
file : ./secrets/db_password.txt
api_key :
file : ./secrets/api_key.txt
In your application:
import { loadEnv , secretString } from "@axel/load-env"
// Load environment variables first
await loadEnv ()
// Then load secrets
const dbPassword = await secretString ( "db_password" )
const apiKey = await secretString ( "api_key" )
// Use in your application
await connectDatabase ({
password: dbPassword ,
})
Kubernetes Example
With Kubernetes secrets mounted as files:
apiVersion : v1
kind : Secret
metadata :
name : app-secrets
type : Opaque
stringData :
DATABASE_PASSWORD : "super-secret-password"
API_KEY : "api-key-value"
---
apiVersion : apps/v1
kind : Deployment
metadata :
name : myapp
spec :
template :
spec :
containers :
- name : app
image : myapp:latest
volumeMounts :
- name : secrets
mountPath : /run/secrets
readOnly : true
volumes :
- name : secrets
secret :
secretName : app-secrets
In your application:
import { secretString } from "@axel/load-env"
// Reads from /run/secrets/DATABASE_PASSWORD
const dbPassword = await secretString ( "DATABASE_PASSWORD" )
// Reads from /run/secrets/API_KEY
const apiKey = await secretString ( "API_KEY" )
Custom Secret Paths
Override the default path using environment variables:
// Set custom path via environment
process . env . DB_PASSWORD = "/etc/secrets/database/password"
// Reads from /etc/secrets/database/password
const password = await secretString ( "DB_PASSWORD" )
This is useful for:
Custom secret mount paths
Development environments
Testing with mock secrets
Integration with non-standard secret managers
Mixing Secrets and Environment Variables
You can use both together in your application:
import { loadEnv , envString , envInt , secretString } from "@axel/load-env"
// Load .env files
await loadEnv ()
// Use environment variables for non-sensitive config
export const NODE_ENV = envString ( "NODE_ENV" )
export const PORT = envInt ( "PORT" , 3000 )
// Use secrets for sensitive data
export const DATABASE_PASSWORD = await secretString ( "DATABASE_PASSWORD" )
export const JWT_SECRET = await secretString ( "JWT_SECRET" )
Use environment variables for configuration and secrets for sensitive data. This separation makes it clear what needs to be protected.
Best Practices
Never Commit Secrets
Ensure secret files are gitignored:
# Ignore secret files
secrets/
* .secret
* .key
Use Fallbacks for Development
import { secretString } from "@axel/load-env"
// Provide dev fallback for local development
export const API_KEY = await secretString (
"API_KEY" ,
process . env . NODE_ENV === "development" ? "dev-key" : undefined ,
)
Validate Critical Secrets Early
Load and validate all secrets at startup:
import { secretString , secretUrl } from "@axel/load-env"
async function loadSecrets () {
try {
const dbPassword = await secretString ( "DATABASE_PASSWORD" )
const apiUrl = await secretUrl ( "API_URL" )
const jwtSecret = await secretString ( "JWT_SECRET" )
return { dbPassword , apiUrl , jwtSecret }
} catch ( error ) {
console . error ( "Failed to load required secrets" )
throw error
}
}
// Fail fast if secrets are missing
const secrets = await loadSecrets ()
Handle Optional Secrets Gracefully
import { maybeSecretString , secretString } from "@axel/load-env"
// Required secret
const apiKey = await secretString ( "API_KEY" )
// Optional secret with fallback behavior
const webhookUrl = await maybeSecretString ( "WEBHOOK_URL" )
if ( webhookUrl ) {
console . log ( "Webhooks enabled" )
enableWebhooks ( webhookUrl )
} else {
console . log ( "Webhooks disabled" )
}
Next Steps
Type Safety Learn how TypeScript types are enforced and validated throughout the library