A tiny, dependency-free, statically linked binary that resolves Docker/Kubernetes-style
*_FILE secret references into plain environment variables and then execs your
actual application — designed to run in scratch or distroless images that have
no shell.
DB_PASSWORD_FILE=/run/secrets/db_password
│
▼
fileenv reads the file
│
▼
DB_PASSWORD=<file contents> → exec("myapp", ...)
Docker Swarm and Kubernetes both mount secrets as files (typically under
/run/secrets), but many applications only know how to read configuration from
environment variables. The common workaround is a shell-based docker-entrypoint.sh
with a file_env() function (used by the official postgres, mysql, wordpress
images, among others). That approach requires a shell — which distroless/scratch
images intentionally don't have.
fileenv implements the same idea as a single static binary with no runtime
dependencies, so it also works in minimal, shell-less images.
- Single static binary, no libc dependency (
CGO_ENABLED=0) - No shell, no package manager, no attack surface beyond the binary itself
- Reads any environment variable ending in
_FILE, resolves the referenced file, sets the same variable name without the suffix, and removes the_FILEvariable - Replaces itself with the target process via
exec(not fork) — the target process keeps PID 1, so signals likeSIGTERMfromdocker stop/docker service updatereach it directly, with no zombie-process risk and no need for a separate init system (e.g. tini) - Trims a trailing newline from file contents (common with
echo-generated secrets) - ~2 MB binary, builds in seconds, multi-arch images published on every release
fileenviterates over its own environment.- For every variable
FOO_FILE=/path/to/file, it reads/path/to/file, setsFOO=<contents of the file>(trailing\n/\r\nstripped), and removesFOO_FILE. - It looks up the command given as arguments (via
PATHif not an absolute path). - It calls
exec(), replacing itself with that command — the target process inherits the now-fully-populated environment.
If a file cannot be read, fileenv exits with a non-zero status and an error
message on stderr; it never silently continues with a missing secret.
Copy the binary out of the published image into your own multi-stage build —
this is the pattern used by tools like secrets-init or tini-static:
FROM ghcr.io/skriptfabrik/fileenv:1 AS fileenv
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=fileenv /usr/local/bin/fileenv /usr/local/bin/fileenv
COPY myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/fileenv"]
CMD ["--", "/usr/local/bin/myapp"]Images are published to both:
- GitHub Container Registry:
ghcr.io/skriptfabrik/fileenv - Docker Hub:
skriptfabrik/fileenv
Available tags: <major>, <major>.<minor>, <major>.<minor>.<patch>, and
latest (tracks the most recent non-prerelease release). Pin to at least the
major tag (:1) in production.
Every release includes
prebuilt binaries for the most common platforms, plus a checksums.txt:
| OS | Architecture | Archive |
|---|---|---|
| Linux | amd64 | fileenv_<version>_linux_amd64.tar.gz |
| Linux | arm64 | fileenv_<version>_linux_arm64.tar.gz |
| Linux | armv7 | fileenv_<version>_linux_armv7.tar.gz |
| macOS | amd64 | fileenv_<version>_darwin_amd64.tar.gz |
| macOS | arm64 (Apple Sil.) | fileenv_<version>_darwin_arm64.tar.gz |
| Windows | amd64 | fileenv_<version>_windows_amd64.zip |
curl -LO https://github.com/skriptfabrik/fileenv/releases/latest/download/fileenv_<version>_linux_amd64.tar.gz
curl -LO https://github.com/skriptfabrik/fileenv/releases/latest/download/checksums.txt
sha256sum --ignore-missing -c checksums.txt
tar -xzf fileenv_<version>_linux_amd64.tar.gzgo install github.com/skriptfabrik/fileenv@latestfileenv [--] <command> [args...]The -- is optional and only improves readability in Dockerfiles / Compose
files; fileenv myapp --serve and fileenv -- myapp --serve behave identically.
fileenv --version # prints the build version and exitsservices:
app:
image: myapp
entrypoint: ["fileenv", "--"]
command: ["node", "server.js"]
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
volumes:
- ./secrets/db_password:/run/secrets/db_password:roservices:
app:
image: myapp
entrypoint: ["fileenv", "--"]
command: ["node", "index.js"]
secrets:
- db_password
environment:
DB_PASSWORD_FILE: /run/secrets/db_password
secrets:
db_password:
external: trueSwarm mounts secrets at /run/secrets/<secret_name>; point the _FILE variable
at that path explicitly (the secret name and the desired environment variable
name don't have to match).
env:
- name: DB_PASSWORD_FILE
value: /etc/secrets/db-password
volumeMounts:
- name: db-password
mountPath: /etc/secrets
readOnly: truefileenv itself is configured via a small number of FILEENV_* environment
variables. These are never treated as _FILE candidates themselves, and are
left in the environment when the target process is exec'd.
| Variable | Purpose |
|---|---|
FILEENV_SUFFIX |
Suffix to look for instead of the default _FILE (e.g. __FILE). |
FILEENV_EXCLUDE |
Comma-separated list of variable names to skip. |
FILEENV_INCLUDE |
Comma-separated list of variable names to resolve; all others are skipped. |
FILEENV_EXCLUDE and FILEENV_INCLUDE are mutually exclusive — setting both
makes fileenv exit with an error before resolving anything. If neither is
set, fileenv resolves every variable ending in the configured suffix, as
before.
This is useful when a base image already sets an unrelated _FILE-suffixed
variable that isn't a secret reference — for example SSL_CERT_FILE, the
Go/OpenSSL convention for the system CA bundle path. Without excluding it,
fileenv would read the (often large) CA bundle into an SSL_CERT environment
variable, which can even exceed the kernel's environment size limit and make
the subsequent exec fail with "argument list too long":
ENV FILEENV_EXCLUDE=SSL_CERT_FILE- The suffix is
_FILEby default; override it withFILEENV_SUFFIX(see Configuration). - If both
FOOandFOO_FILEare set,FOO_FILEwins and overwritesFOO. - The original suffixed variable (e.g.
FOO_FILE) is removed from the environment once resolved — the target process only ever sees the resolvedFOO, not the file path. - Empty variable names (i.e. a variable literally named
_FILE) are ignored. - File contents are used as-is except for a trimmed trailing
\r\n/\n— no further parsing, quoting, or templating is applied. fileenvnever logs variable names or values.
- No shell, no dynamic linking, no third-party Go dependencies (stdlib only) — minimal supply-chain and attack surface.
- Runs as whatever user the container is configured with; use the
:nonrootdistroless variant (as shown above) unless you have a specific reason not to. - Secrets only ever exist in the target process's environment, not on disk beyond the original secret file, and not in any log output.
Requirements: Go 1.27+, Docker with Buildx (for multi-arch image builds).
git clone https://github.com/skriptfabrik/fileenv.git
cd fileenv
go build -o fileenv .
go vet ./...
./fileenv --versionLocal multi-arch image build (no push):
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t fileenv:dev ..
├── main.go # the entire implementation
├── go.mod
├── Dockerfile # multi-arch, distroless runtime
└── .github/workflows/
├── ci.yml # build & vet on push/PR
└── release.yml # binaries + Docker images on GitHub Release
Releases are fully automated via GitHub Actions and triggered by publishing a
GitHub Release with a semantic-versioning tag (vX.Y.Z, e.g. v1.2.0):
- Create and publish a release for tag
v1.2.0on GitHub (this can be a new tag or an existing one). .github/workflows/release.ymlthen, in parallel:- builds static binaries for Linux (amd64, arm64, armv7), macOS (amd64,
arm64) and Windows (amd64), packages them as
.tar.gz/.zip, generateschecksums.txt, and uploads everything to the release, and - builds a multi-arch (
linux/amd64,linux/arm64,linux/arm/v7) Docker image and pushes it to bothghcr.io/skriptfabrik/fileenvandskriptfabrik/fileenvon Docker Hub, tagged1.2.0,1.2,1, andlatest.
- builds static binaries for Linux (amd64, arm64, armv7), macOS (amd64,
arm64) and Windows (amd64), packages them as
Required repository secrets for the Docker Hub push (GHCR authenticates with
the built-in GITHUB_TOKEN, no extra secret needed):
| Secret | Description |
|---|---|
DOCKERHUB_USERNAME |
Docker Hub username or org |
DOCKERHUB_TOKEN |
Docker Hub access token (Account Settings → Security → New Access Token) |
Issues and pull requests are welcome. Given the intentionally small scope of this tool, please open an issue to discuss larger changes before submitting a PR.