icated directory (e.g., ~/.dctx/<context-name>/config.json). This ensures credentials are physically separated.
2. Shell Wrapper Pattern: A binary cannot modify the environment variables of its parent shell. The tool must output the target path, and a shell function must capture this output to set DOCKER_CONFIG. This avoids eval-based injection risks.
3. Credential Helper Preservation: When docker login generates a new config.json, it may include credsStore or credHelpers. The context manager must merge these fields from the source config to the target context to prevent breaking Docker Desktop integration.
4. Strict Validation: Context names must be validated against a regex pattern to prevent path traversal and ensure filesystem compatibility.
Implementation: TypeScript CLI
The CLI handles context creation, switching, and metadata management. It interacts with the filesystem and invokes docker login when necessary.
import { execSync } from 'child_process';
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
const BASE_DIR = join(process.env.HOME || '', '.dctx');
const NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
interface DockerConfig {
auths?: Record<string, { auth: string }>;
credsStore?: string;
credHelpers?: Record<string, string>;
}
function validateName(name: string): void {
if (!NAME_REGEX.test(name)) {
throw new Error(`Invalid context name: "${name}". Must match ${NAME_REGEX.source}`);
}
}
function getContextPath(name: string): string {
return join(BASE_DIR, name);
}
function mergeCredentials(source: DockerConfig, target: DockerConfig): void {
// Preserve credential helpers to maintain OS keychain integration
if (source.credsStore) {
target.credsStore = source.credsStore;
}
if (source.credHelpers) {
target.credHelpers = { ...source.credHelpers };
}
}
export async function addContext(name: string, username: string): Promise<void> {
validateName(name);
const ctxPath = getContextPath(name);
mkdirSync(ctxPath, { recursive: true });
// Temporarily point Docker to the new context for login
const originalConfig = process.env.DOCKER_CONFIG;
process.env.DOCKER_CONFIG = ctxPath;
try {
// Execute docker login; this writes to the temp config
execSync(`docker login --username ${username}`, { stdio: 'inherit' });
} finally {
process.env.DOCKER_CONFIG = originalConfig;
}
// Read the generated config and merge helpers
const configPath = join(ctxPath, 'config.json');
if (existsSync(configPath)) {
const rawConfig = JSON.parse(readFileSync(configPath, 'utf-8'));
const mergedConfig: DockerConfig = { ...rawConfig };
// If the user had helpers in their default config, preserve them
const defaultConfigPath = join(process.env.HOME || '', '.docker', 'config.json');
if (existsSync(defaultConfigPath)) {
const defaultConfig = JSON.parse(readFileSync(defaultConfigPath, 'utf-8'));
mergeCredentials(defaultConfig, mergedConfig);
}
writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2));
}
console.log(`Context "${name}" created successfully.`);
}
export function switchContext(name: string): string {
validateName(name);
const ctxPath = getContextPath(name);
if (!existsSync(join(ctxPath, 'config.json'))) {
throw new Error(`Context "${name}" does not exist. Run 'dctx add ${name}' first.`);
}
return ctxPath;
}
export function listContexts(): string[] {
if (!existsSync(BASE_DIR)) return [];
return readdirSync(BASE_DIR).filter(name =>
NAME_REGEX.test(name) && existsSync(join(BASE_DIR, name, 'config.json'))
);
}
Implementation: Shell Wrapper
The shell wrapper captures the output of dctx switch and assigns it to DOCKER_CONFIG. This pattern ensures that the environment variable is updated in the current shell session without executing arbitrary code.
# dctx.sh
# Place this in your shell profile or source it via dctx init
_dctx_wrapper() {
local action="$1"
shift
# Execute the binary and capture stdout
local result
result=$(dctx-bin "$action" "$@" 2>&1)
local exit_code=$?
if [[ $exit_code -ne 0 ]]; then
echo "$result" >&2
return 1
fi
# For 'switch' action, the binary outputs the config path
if [[ "$action" == "switch" ]]; then
if [[ -n "$result" ]]; then
export DOCKER_CONFIG="$result"
echo "Switched to context. DOCKER_CONFIG=$result"
fi
else
# For other actions, print output directly
echo "$result"
fi
}
# Alias dctx to the wrapper
alias dctx=_dctx_wrapper
Rationale
- Type Safety: Using TypeScript ensures the
config.json structure is handled correctly, reducing runtime errors when parsing Docker configurations.
- Explicit Assignment: The shell wrapper uses
export DOCKER_CONFIG="$result" rather than eval. This prevents command injection if the binary output is compromised or malformed.
- Helper Merging: The
mergeCredentials function ensures that switching contexts does not strip credsStore. Without this, Docker would fall back to prompting for passwords, breaking the user experience.
- Validation: The regex
^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$ prevents directory traversal attacks (e.g., ../etc) and ensures names are compatible with all filesystems.
Pitfall Guide
When implementing or using directory-based context management, avoid these common mistakes:
-
Credential Helper Erasure
- Explanation: Copying
config.json without preserving credsStore or credHelpers breaks Docker Desktop integration. The client will attempt to store credentials in plain text or prompt for passwords.
- Fix: Always merge credential helper fields from the source configuration into the target context during creation or update.
-
Shell Injection via Eval
- Explanation: Using
eval $(dctx switch work) executes the binary's output as shell code. If the output contains malicious payloads or unexpected characters, this can lead to arbitrary code execution.
- Fix: Capture output in a variable and assign it directly to the environment variable. Never
eval output from untrusted or user-controlled sources.
-
Path Traversal in Context Names
- Explanation: Allowing arbitrary strings for context names enables attacks like
dctx add ../../etc, which could overwrite system files or read sensitive data.
- Fix: Enforce strict regex validation on context names before any filesystem operations. Restrict characters to alphanumeric, dots, hyphens, and underscores.
-
Stale Authentication Tokens
- Explanation: Contexts store tokens statically. If a token expires or is revoked, the context will fail until refreshed. Users may assume contexts auto-refresh.
- Fix: Provide a
dctx refresh command that re-runs docker login for the active context, or document that users must re-authenticate when tokens expire.
-
Daemon vs. Client Confusion
- Explanation:
DOCKER_CONFIG affects only the Docker client. The Docker daemon does not use this variable. Users may expect daemon behavior to change, which it does not.
- Fix: Clarify in documentation that context switching is client-side only. The daemon continues to manage containers and images independently of client credentials.
-
CI/CD Environment Conflicts
- Explanation: In CI pipelines,
DOCKER_CONFIG may be set globally or conflict with default paths, causing authentication failures.
- Fix: In CI/CD, explicitly set
DOCKER_CONFIG to a temporary directory and populate it with service account credentials. Avoid relying on interactive context managers in automated environments.
-
Multiple Shell Sessions
- Explanation: Changing the context in one terminal session does not affect other open sessions. Each shell maintains its own environment.
- Fix: This is expected behavior for isolation. Educate users that context switches are session-scoped. Use
dctx whoami to verify the active context in each terminal.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Solo Developer, 2 Accounts | Directory Contexts | Simple, native support, zero overhead. | Free |
| Enterprise, SSO Integration | Docker Credential Helpers | Centralized auth, policy enforcement. | Infrastructure cost |
| CI/CD Pipeline | Explicit Env Var | Deterministic, no state management. | Free |
| High-Security Environment | Ephemeral Tokens + Contexts | Limits blast radius of credential leaks. | Operational overhead |
Configuration Template
Shell Integration (~/.zshrc or ~/.bashrc):
# Initialize dctx shell wrapper
eval "$(dctx init zsh)"
# Optional: Prompt indicator for current context
_dctx_prompt() {
local ctx_name
ctx_name=$(basename "$DOCKER_CONFIG" 2>/dev/null)
if [[ -n "$ctx_name" ]]; then
echo " [docker:$ctx_name]"
fi
}
# Add to your PS1 or prompt configuration
# PS1="%n@%m$(_dctx_prompt) %~ $ "
Directory Structure:
~/.dctx/
βββ work/
β βββ config.json
βββ personal/
β βββ config.json
βββ ci-service/
βββ config.json
Quick Start Guide
- Install the Tool: Deploy the
dctx binary to your system path.
- Source Shell Integration: Add
eval "$(dctx init zsh)" to your shell profile and reload.
- Create a Context: Run
dctx add work -u your-work-username and authenticate via the prompted docker login.
- Switch Context: Run
dctx switch work. The shell will update DOCKER_CONFIG automatically.
- Verify: Run
dctx whoami to confirm the active context and username. Push images with confidence.