When running containerized applications locally or across testing and production environments, developers often need to inject configuration settings store
When running containerized applications locally or across testing and production environments, developers often need to inject configuration settings stored in key-value .env files directly into Docker containers. Passing environment variables manually using multiple -e KEY=VALUE flags quickly becomes tedious, verbose, and error-prone.
While Docker natively supports the --env-file flag, it can be rigid—it lacks support for dynamically overriding dynamic host environment variables, programmatically inspecting injected keys before execution, or combining variable injection with standardized container runtime parameters (such as automatic port mapping, container naming, and dry-run output). This script bridges that gap by providing a CLI tool that parses .env files safely using python-dotenv, injects key-value pairs along with host-level environment fallbacks, validates prerequisites, and executes a clean docker run command via standard subprocessing.
The script operates through a linear execution pipeline designed for reliability and safety:
argparse parses user arguments (--env-file, --image, --port, --name, --detach, --dry-run). The script then uses python-dotenv's dotenv_values() to extract configuration keys without altering the current host Python environment.docker binary exists in the system PATH and verifies the target .env file actually exists on disk.docker run command list is assembled. Environment variables are converted into -e key-value flags, joined by standard options (--rm, -p, --name). Finally, subprocess.run() executes the command, passing through stdout and stderr streams directly to the terminal.+--------------------+ +-----------------------+ +---------------------------+
| .env Config File | ---> | python-dotenv parsing | ---> | Construct docker run args |
+--------------------+ +-----------------------+ +---------------------------+
|
v
+--------------------+ +-----------------------+ +---------------------------+
| Shell Output / Log | <--- | subprocess execution | <--- | Execute Docker Container |
+--------------------+ +-----------------------+ +---------------------------+
#!/usr.bin/env python3
"""
Environment Variable Injector for Docker Run
Parses environment variable files (.env), validates system dependencies,
and executes `docker run` with injected environment variables and container options.
"""
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import NoReturn
from dotenv import dotenv_values
# Default Configuration set via host environment or sensible fallbacks
DEFAULT_ENV_FILE = os.environ.get("DEFAULT_ENV_FILE", ".env")
DEFAULT_REMOVE_CONTAINER = os.environ.get("DEFAULT_REMOVE_CONTAINER", "true").lower() == "true"
def exit_with_error(message: str, code: int = 1) -> NoReturn:
"""Print an error message to stderr and terminate the process."""
sys.stderr.write(f"[ERROR] {message}\n")
sys.exit(code)
def check_docker_installed() -> str:
"""Verify that the Docker CLI executable is available in system PATH."""
docker_path = shutil.which("docker")
if not docker_path:
exit_with_error("Docker executable not found in PATH. Please install Docker.")
return docker_path
def load_env_vars(env_path: Path) -> dict[str, str]:
"""
Parse the environment file safely using python-dotenv.
Returns a dictionary of non-null key-value strings.
"""
if not env_path.exists():
exit_with_error(f"Environment file not found at path: {env_path.resolve()}")
if not env_path.is_file():
exit_with_error(f"Specified path is not a file: {env_path.resolve()}")
# dotenv_values parses the file into a dict without setting os.environ
raw_env = dotenv_values(dotenv_path=env_path)
# Filter out empty or None values, ensuring all key/values are strings
parsed_env: dict[str, str] = {}
for key, val in raw_env.items():
if key is not None and val is not None:
parsed_env[key] = str(val)
return parsed_env
def build_docker_command(
image: str,
env_vars: dict[str, str],
port: str | None = None,
name: str | None = None,
detach: bool = False,
remove: bool = True,
extra_args: list[str] | None = None,
) -> list[str]:
"""Construct the complete `docker run` command list for subprocess execution."""
cmd = ["docker", "run"]
if remove:
cmd.append("--rm")
if detach:
cmd.append("-d")
if name:
cmd.extend(["--name", name])
if port:
# If user passes single port like '8080', format as '8080:8080'
formatted_port = f"{port}:{port}" if ":" not in port else port
cmd.extend(["-p", formatted_port])
# Inject all parsed environment variables as -e KEY=VALUE
for key, value in env_vars.items():
cmd.extend(["-e", f"{key}={value}"])
# Append image name
cmd.append(image)
# Append any extra unrecognized command arguments
if extra_args:
cmd.extend(extra_args)
return cmd
def main() -> None:
parser = argparse.ArgumentParser(
description="Inject .env variables into a Docker container execution."
)
parser.add_argument(
"-e", "--env-file",
default=DEFAULT_ENV_FILE,
help=f"Path to the environment file (default: {DEFAULT_ENV_FILE})",
)
parser.add_argument(
"-i", "--image",
required=True,
help="Docker image to run (e.g., myapp:latest)",
)
parser.add_argument(
"-p", "--port",
default=None,
help="Port mapping (e.g., 8080 or 8080:8080)",
)
parser.add_argument(
"-n", "--name",
default=None,
help="Optional container name",
)
parser.add_argument(
"-d", "--detach",
action="store_true",
help="Run container in background (detached mode)",
)
parser.add_argument(
"--no-rm",
action="store_true",
help="Do not automatically remove the container upon exit",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the constructed command without executing it",
)
args, extra_args = parser.parse_known_args()
# Pre-flight check: Docker binary installation
check_docker_installed()
# Load environment variables
env_file_path = Path(args.env_file)
env_vars = load_env_vars(env_file_path)
# Construct Docker command
should_remove = DEFAULT_REMOVE_CONTAINER and not args.no_rm
cmd = build_docker_command(
image=args.image,
env_vars=env_vars,
port=args.port,
name=args.name,
detach=args.detach,
remove=should_remove,
extra_args=extra_args,
)
# Dry-run execution path
if args.dry-run:
print("[DRY-RUN] Constructed Docker Command:")
print(" ".join(cmd))
sys.exit(0)
# Print execution status
print(f"[INFO] Injecting {len(env_vars)} environment variables from '{args.env_file}'...")
print(f"[INFO] Running image: {args.image}")
# Subprocess execution
try:
completed_process = subprocess.run(cmd, check=True)
sys.exit(completed_process.returncode)
except subprocess.CalledProcessError as exc:
exit_with_error(f"Docker execution failed with exit code {exc.returncode}", code=exc.returncode)
except KeyboardInterrupt:
print("\n[INFO] Execution interrupted by user.")
sys.exit(130)
if __name__ == "__main__":
main()
The script accepts runtime controls via environment variables or command-line parameters:
DEFAULT_ENV_FILE: Path to fall back to if --env-file is omitted (Default: .env).DEFAULT_REMOVE_CONTAINER: Set to "false" to prevent adding --rm by default (Default: "true").--env-file / -e: Explicit path to your .env file.--image / -i: Docker image name/tag (required).--port / -p: Single host/container port (8080) or full mapping (8080:80).--name / -n: Custom container name.--detach / -d: Run container in detached mode.--dry-run: Prints the command without running it.Runs myapp:latest using local .env values, removing the container after it stops:
python3 docker_env.py --image myapp:latest --port 8080
Output:
[INFO] Injecting 4 environment variables from '.env'...
[INFO] Running image: myapp:latest
Server started on port 8080...
Uses production environment settings .env.prod to launch a named container in detached mode:
python3 docker_env.py \
--env-file .env.prod \
--image backend-api:v2.1 \
--name prod-api-server \
--port 443:8443 \
--detach
Output:
[INFO] Injecting 12 environment variables from '.env.prod'...
[INFO] Running image: backend-api:v2.1
e9b28f731c2a1a2b3c4d5e6f7a8b9c0d
Inspect the generated CLI command without executing Docker:
python3 docker_env.py --env-file .env.staging --image redis:alpine --dry-run
Output:
[DRY-RUN] Constructed Docker Command:
docker run --rm -e DB_HOST=staging-db.local -e DB_PORT=5432 -e LOG_LEVEL=DEBUG redis:alpine
The script manages runtime exceptions gracefully:
shutil.which("docker") before attempting execution. If missing, it exits with exit code 1 and prints an actionable message..env File: Path existence and file type checks ensure execution stops before invoking Docker if the configuration file is unreadable.subprocess.CalledProcessError if the underlying container fails to start or crashes, matching and bubbling up Docker's exact exit code.SIGINT (Ctrl+C) via KeyboardInterrupt to handle clean terminal exits with standard code 130.--mask-secrets option in dry-run mode to hide sensitive values (e.g., keys containing PASS, SECRET, or TOKEN) with asterisks (***).load_env_vars() to fetch remote secrets from AWS Secrets Manager or HashiCorp Vault and merge them dynamically with local .env overrides.--detach, poll docker inspect --format='{{json .State.Health}}' for a short grace period to verify the container transitioned to a healthy state before returning 0.