A lightweight container initialization utility that reads secrets mounted as files (such as Docker secrets in /run/secrets/) and exposes them as container environment variables.
The image is packaged as a minimal scratch container containing root filesystem assets with zero external runtime dependencies. It provides first-class support for S6 Overlay (see s6-rootfs) via s6-rc service definitions, while maintaining full compatibility with generic container runtimes and init systems through a configurable export path and a standalone environment loader.
- Automatic Name Normalization: Converts secret file basenames to uppercase identifiers by default to align with POSIX environment variable conventions.
- S6 Overlay Integration: Includes ready-to-use
s6-rcservice definitions (init-docker-secrets) targeting/var/run/s6/container_environment. - Generic Init Compatibility: Can export secrets to any directory via
SECRETS_EXPORT_PATHand load them into a subshell or service using the bundled/usr/local/lib/load-envhelper. - Collision Protection: Detects and skips secrets that would overwrite existing export targets or collision candidates, logging warnings to
stderr. - Symlink & Hidden File Handling: Dereferences symbolic links (
find -L) and automatically ignores hidden files (.*). - Minimal Footprint: Packaged from
scratchwith zero third-party dependencies.
- Ingestion: The script scans
SECRETS_PATH(default:/run/secrets/) for regular files and symlinks, ignoring hidden dotfiles. - Name Resolution: Each secret filename is either converted to uppercase (default) or preserved as-is, depending on the
NORMALIZE_SECRET_NAMESconfiguration. - Collision Check & Export: The file is copied to
${SECRETS_EXPORT_PATH}/${EXPORT_NAME}. If a file with the target name already exists or was already processed during the run, the duplicate is skipped and a warning is logged tostderr. - Environment Exposure:
- In S6 Overlay environments, services running under
with-contenvautomatically inherit the exported variables from/var/run/s6/container_environment/. - In standard Linux containers,
/usr/local/lib/load-envvalidates each secret name as a legal shell identifier (^[a-zA-Z_][a-zA-Z0-9_]*$) and exports it into the calling shell.
- In S6 Overlay environments, services running under
Secrets are copied to ${SECRETS_EXPORT_PATH} rather than symlinked for several operational reasons:
- Mount Decoupling & Resilience: Copying creates an independent, immutable snapshot in the target directory (typically
tmpfs). If the source secrets volume is unmounted, cleared, or modified after container initialization, exported environment variables remain intact without producing broken (dangling) symlinks. - Permission & Least-Privilege Isolation: Mounted secret files (e.g., in Docker Swarm or Kubernetes) often carry restrictive permissions such as
0400 root:root, or reside in a restricted0700directory. Symlinks enforce the target file's access permissions and directory traversal rules. If a container service drops privileges to run as a non-root user, resolving a symlink to a root-only target results inPermission deniederrors. Copying during root initialization creates standard, readable target files for supervised services. - Container Environment Immutability: Process environments in Docker containers are fixed at the time processes are spawned. Dynamic secret rotation cannot update the environment of active running processes without a container restart or deliberate application reload mechanism, making symlink-based live file updates unnecessary for static container environments.
The utility is configured via container environment variables:
| Variable | Default | Description |
|---|---|---|
SECRETS_PATH |
/run/secrets/ |
Source directory containing secret files or symbolic links. |
SECRETS_EXPORT_PATH |
/var/run/s6/container_environment/ |
Destination directory where processed secrets are written as individual files. Do not override when using S6 Overlay. |
NORMALIZE_SECRET_NAMES |
1 |
Set to 1 to convert secret names to uppercase (e.g., db_password -> DB_PASSWORD). Set to 0 to retain original file casing. |
When using S6 Overlay, copy the root filesystem layers into your container build.
Warning
Do not override SECRETS_EXPORT_PATH when using S6 Overlay.
S6 Overlay and with-contenv strictly rely on /var/run/s6/container_environment/ to propagate environment variables to supervised services. Overriding this path will break the S6 supervisor configuration and prevent services from inheriting the exported secrets.
# ---------------------
# Build root filesystem
# ---------------------
FROM scratch AS rootfs
# Copy base filesystem files
COPY ["./rootfs", "/"]
# Install S6 Overlay
COPY --from=ghcr.io/n0rthernl1ghts/s6-rootfs:3.2.0.2 ["/", "/"]
# Install init-docker-secrets service
COPY --from=ghcr.io/n0rthernl1ghts/docker-env-secrets:latest ["/", "/"]
# ---------------------
# Build final image
# ---------------------
FROM alpine:latest
COPY --from=rootfs ["/", "/"]
# Service configuration...The image registers init-docker-secrets as a oneshot service inside the default user bundle (/etc/s6-overlay/s6-rc.d/user/contents.d/init-docker-secrets). Any custom S6 services that require secrets at initialization should declare a dependency on init-docker-secrets.
To access the environment variables in a service run script, execute using with-contenv:
#!/command/with-contenv bash
exec your-service --your-flagsAlternatively, load variables using s6-envdir:
s6-envdir /var/run/s6/container_environment your-service --your-flagsNote
When executing under with-contenv, the environment variable S6_KEEP_ENV must be set to 0. If S6_KEEP_ENV=1, the existing container environment is preserved without reloading updated files from /var/run/s6/container_environment. If S6_KEEP_ENV=0 cannot be set, use the generic loader method described below:
source /usr/local/lib/load-env /var/run/s6/container_environmentFor containers not utilizing S6 Overlay, install the binaries and adjust the export directory.
# ---------------------
# Build root filesystem
# Note: busybox is used only as a build stage and is not included in the final image
# ---------------------
FROM busybox AS rootfs
# Copy base filesystem files
COPY ["./rootfs", "/rootfs/"]
# Install init-docker-secrets service
COPY --from=ghcr.io/n0rthernl1ghts/docker-env-secrets:latest ["/", "/rootfs/"]
# Remove S6 Overlay specific service files
RUN set -eux \
&& rm -rfv "/rootfs/etc/s6-overlay/"
# Or this to remove only init-docker-secrets files
# RUN set -eux \
# && rm -rfv "/rootfs/etc/s6-overlay/s6-rc.d/init-docker-secrets" \
# && rm -rfv "/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/init-docker-secrets"
# ---------------------
# Build final image
# ---------------------
FROM alpine:latest
COPY --from=rootfs ["/rootfs/", "/"]
ENV SECRETS_EXPORT_PATH=/run/secrets_normalized
ENV NORMALIZE_SECRET_NAMES=1-
Execute the initialization script in your entrypoint or startup routine to ingest secrets:
# Optional: specify SECRETS_EXPORT_PATH if not defined in Dockerfile # export SECRETS_EXPORT_PATH=/run/secrets_normalized /usr/local/bin/init-docker-secrets
-
Source the secrets into the current shell process using
/usr/local/lib/load-env:source /usr/local/lib/load-env /run/secrets_normalized exec your-service --your-flags
The load-env helper validates each filename to ensure it conforms to standard shell variable naming conventions (^[a-zA-Z_][a-zA-Z0-9_]*$) before exporting.
Automated tests and validation checks are included in the repository.
./tests/run-tests.shExecute the end-to-end container test suite (requires Docker and Docker Compose):
./tests/run-integration-tests.sh# Check syntax with ShellCheck
shellcheck src/*.sh tests/*.sh
# Verify formatting with shfmt (4-space indentation)
shfmt -i 4 -d src/*.sh tests/*.shdocker build -t docker-env-secrets:local .This project embraces AI-assisted development tools to enhance development velocity, expand test coverage, and refine documentation:
- Scope of Use: AI tooling is utilized for regression test generation, edge-case exploration, and documentation formatting.
- Human Verification: Every line of code, shell script, and container configuration is manually reviewed, verified, and audited by human maintainers. CODING is an art!
- Multi-Tier Quality Gates:
- Static Analysis & Linting: Strict adherence to POSIX/Bash guidelines validated via
shellcheckandshfmt. - Functional & Unit Tests: Automated test suite (
./tests/run-tests.sh) validating path handling, secret parsing, collision guards, and shell exports. - Container Integration Tests: Full end-to-end integration test suite (
./tests/run-integration-tests.sh) asserting real-world container behavior across S6 Overlay, generic init environments, and edge cases via Docker Compose.
- Static Analysis & Linting: Strict adherence to POSIX/Bash guidelines validated via
- Accountability: The human maintainers retain complete ownership, maintenance commitment, and security accountability for all committed logic.
- Agent Guidelines: For agents and automated tools contributing to this repository, operational directives and strict coding standards are codified in AGENTS.md.
This project is licensed under the MIT License. See the LICENSE file for details.