Installing Private Dependencies When Your Build Has No Secrets

·6 min read·Updated
dockerbuildsecretsdeploy-pathshow-to

If your project depends on a private npm package, an internal Python library, a private git submodule, or a base image from a private registry, the platform build will fail — and it will fail in a way that looks like a network problem. The build passes no build arguments and no registry credentials, so there is no way to hand a token to docker build here. That's deliberate rather than an oversight: a build-time secret ends up in image layers more often than not. You have three real options — vendor the dependency, fetch it at runtime, or build the image somewhere you do have credentials and push the finished result. For anything ongoing, the third is the right answer.

What the build actually gives you

The platform builds your image from the root Dockerfile in whatever you pushed. What it hands the builder is deliberately small:

  • No build arguments. An ARG resolves to its default, or to empty if you didn't give it one — which is the most common way an AI-written Dockerfile fails here.
  • No registry credentials. A FROM that points at a private registry can't authenticate, so the base image must be publicly pullable.
  • No BuildKit secret mounts. RUN --mount=type=secret is BuildKit syntax, and it isn't available on this builder.
  • No build stage selection. There's no --target, so your final stage is the last one in the file.
  • Your app's environment variables aren't there either. Env vars are set on the running container, not on the build.

So every credential-shaped approach is out at once. Knowing that up front saves an afternoon of trying variations on the same idea.

Option 1 — vendor the dependency

The simplest fix is to stop making it a dependency. Copy the source into your repository, under vendor/ or lib/, and import it from there.

It sounds crude and it's often correct for a small internal library that changes twice a year. What you're trading is version management — updates become a manual copy, and nothing warns you that upstream moved.

Worth checking first whether the "private" package is genuinely private or just unpublished. Plenty of internal libraries contain nothing sensitive and would be entirely fine as a public package, which turns this problem into no problem at all.

Option 2 — fetch it at runtime

If the thing you need at build time is data rather than code — a config file, a model list, a set of assets — you may not need it at build time at all. Fetch it when the app starts, using a token from an environment variable, which is a real secret store that never touches an image layer.

Two cautions before you reach for this:

Don't fetch code this way. An app that downloads and executes code at startup is a supply-chain problem and a startup-time failure mode at once.

Your app now fails to start when the source is down. Containers are recreated on every deploy and every configuration change, so a start-time dependency gets exercised far more often than you'd think. Cache what you fetch on a volume and fall back to the cached copy.

Option 3 — build it yourself and push the image

This is the answer that scales, and it's a first-class deploy path rather than a workaround.

On your own machine or in CI, you have every credential mechanism Docker offers — --build-arg, --secret, a mounted .npmrc, an authenticated docker login for a private base image. Build there, then push the finished image to your private registry on the platform and deploy from it.

docker build --secret id=npmrc,src=$HOME/.npmrc -t <your-registry-path>:latest .
docker push <your-registry-path>:latest

What you gain beyond solving the credential problem:

  • The secret never leaves your build environment. With a BuildKit secret mount it isn't in a layer at all.
  • Private base images work, because you're authenticated where the pull happens.
  • CI becomes the build, which is where most teams want it anyway.

The one thing to watch is architecture. An image built on an Apple Silicon Mac is arm64, and an arm64 image won't run here — build with --platform linux/amd64 or let x86 CI do it. This is the trade for moving the build off the platform: you become responsible for producing the right kind of image.

Git submodules don't come with a push

A specific case that surprises people, because nothing errors — it just builds without the code.

When you push to a deploy branch, what arrives is your repository's tree. A submodule is stored as a pointer, not as content, so the directory arrives empty and your build fails at COPY or at import time with a message about a missing file rather than a missing submodule.

Your options are the same three as above, and for submodules the honest one is usually vendoring: replace the submodule with real files in your tree, or move the build to CI where you can git submodule update --init with credentials before building.

What not to do

Don't commit the token. Even in a private repo it's now in git history forever, and it will be in the image too. If one has already been committed, rotate it rather than removing it.

Don't ENV TOKEN=... in the Dockerfile. ENV values are baked into image metadata and readable by anyone who can pull the image, without ever running it.

Don't COPY a credentials file and delete it later. Deleting in a later layer doesn't remove it from the earlier one. The file is still in the image, and this is the single most common way build secrets leak.

Don't make a private repository public to get around this. It's the one solution that can't be undone.

Choosing

Your situation Approach
A small internal library that rarely changes Vendor it
A package that isn't actually sensitive Publish it publicly
Config or assets behind a token Fetch at runtime, cache on a volume
A private base image Build and push the image yourself
A private registry dependency, ongoing Build in CI, push the image
A git submodule Vendor it, or build in CI

FAQ

Can I pass a build argument or secret to the build?

No. No build args, no registry credentials and no BuildKit secret mounts are available, so an ARG gets its default value or nothing.

How do I install a private npm or pip package?

Build the image where you have credentials — locally or in CI — and push the finished image, rather than trying to authenticate during the platform build.

Can I use a private base image in FROM?

Not in a platform build; there's nothing to authenticate the pull with. Build against it elsewhere and push the result.

Do my environment variables exist during the build?

No. They're set on the running container. Anything needed at build time has to be in the repository.

Do git submodules get deployed?

No. A push carries a pointer, not the submodule's contents, so the directory arrives empty and the build fails on a missing file.

Is it safe to put a token in an ARG if I never print it?

No — even where build args exist, they're recorded in image history. Runtime environment variables are the right place for secrets.

Wrapping up

The absence of build secrets pushes you toward a distinction worth keeping anyway: things your image needs in order to exist, and things your app needs in order to run. The second belongs in environment variables, where rotating a value is an edit and a restart. The first belongs in your repository, or in an image you built somewhere you're already authenticated. If you're reaching for a build-time credential, that's usually a sign the dependency wants to be vendored or the build wants to move to CI — and both are better outcomes than a token you'd otherwise be typing into a Dockerfile.