Single-binary / distroless deployments: document `load()` or ship a glibc build

Open
#1,071 6 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@theoephraim is already working on this.

Since Sep 7, 2026.

Assessment

This issue has not been assessed yet.

Description

I'm deploying a Bun app with Elysiajs and compiled with bun build --compile on
gcr.io/distroless/base-debian13:nonroot
(glibc, no shell, no node_modules). I wanted varlock to validate the env
inside the container at boot, and ran into three things.

1. The ghcr binary is musl-only

ghcr.io/dmno-dev/varlock:1.18.0
ships a binary dynamically linked against musl and libstdc++:

$ ldd /usr/local/bin/varlock
	/lib/ld-musl-x86_64.so.1
	libstdc++.so.6 => /usr/lib/libstdc++.so.6
	libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1
	libgcc_s.so.1 => /usr/lib/libgcc_s.so.1

Copied into distroless base-debian13 it fails before starting:

exec /varlock: no such file or directory

On a bare alpine:3.20 it fails on libstdc++.so.6 / libgcc_s.so.1.
So it only runs on Alpine with libstdc++ installed. The Docker integration
guide presents COPY --from=varlock /usr/local/bin/varlock as usable in
distroless images without saying that.

2. The documented paths for compiled executables need the CLI as a process

Both varlock/auto-load and varlock run -- shell out to the CLI
(auto-load walks up the tree looking for node_modules/.bin/varlock).
Since the ghcr binary can't run here, my first attempt was to compile the
CLI from node_modules for glibc in the builder stage and use it as the
entrypoint:

FROM oven/bun:1.4.2-slim AS builder
# ... bun ci, COPY src, .env.schema ...
RUN bun build --compile --minify --target bun-linux-x64 src/index.ts --outfile dist/server \
 && bun build --compile --minify --target bun-linux-x64 node_modules/varlock/bin/cli.js --outfile dist/varlock

FROM gcr.io/distroless/base-debian13:nonroot
WORKDIR /app
COPY --from=builder /app/dist/server /app/dist/varlock ./
COPY --from=builder /app/.env.schema ./.env.schema
ENTRYPOINT ["/app/varlock", "run", "--"]
CMD ["/app/server"]

This works: validation runs at boot, missing vars are reported and the
container exits 1. But every bun build --compile embeds the whole Bun
runtime, so the image goes from 158 MB to 278 MB (server 82 MB + varlock
82 MB) just to validate a .env.schema.

3. What I ended up doing

The varlock package exports load(), which is what auto-load does
minus the execSync. Using it as the entry of the compiled binary gives a
single process and a single binary:

// src/index.ts, the bun build --compile entry
import { load } from "varlock";

try {
	await load();
} catch (error) {
	const formatted =
		error instanceof Error && "getFormattedOutput" in error
			? (error as { getFormattedOutput: () => string }).getFormattedOutput()
			: String(error);
	process.stderr.write(`${formatted}\n`);
	process.exit(1);
}

await import("@/modules/app"); // the Elysia app, which imports { ENV } from "varlock/env"
FROM gcr.io/distroless/base-debian13:nonroot
WORKDIR /app
COPY --from=builder /app/dist/server ./server
COPY --from=builder /app/.env.schema ./.env.schema
ENV NODE_ENV=production
ENTRYPOINT ["/app/server"]

Image is back to 158 MB, the server is PID 1, ENV is typed and coerced
from @generateTsTypes, and a bad env prints the usual varlock report and
exits 1. For bun run dev / bun test I keep preload = ["varlock/auto-load"]
in bunfig.toml and point those scripts at the app module, not the entry.

The catch is that load() appears in index.d.mts but not in the docs,
so I don't know whether it's meant to be stable.

Would you consider documenting load() as the recommended path for
single-binary / distroless deployments, and either a glibc or static build
of the CLI, or a note in the Docker guide that the ghcr binary is
Alpine-only?

varlock 1.18.0, Bun 1.4.2, linux/amd64.

Dominant language
TypeScript
Stars
4.9k
Forks
140
Avg merge
2d 3h
Merged PRs (30d)
35

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dmno-dev/varlock

All issues in dmno-dev/varlock

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.