Injecting Secrets into a DevContainer Safely

A secret baked into an image layer or committed to .devcontainer/ is recoverable forever. This page injects secrets at runtime instead — via remoteEnv pulling from the host or Codespaces secret store — so credentials never touch an image layer or git history.

Prerequisites

You need a secret store and a devcontainer you can add remoteEnv to.

  • A host env var or Codespaces/host secret store holding the secret.
  • A devcontainer config you can edit.
  • No secrets currently committed (rotate any that are).

Secret prerequisitesYou need a store, a remoteEnv reference, runtime injection, and an image check.Secret storehost/CodespacesremoteEnvreference localEnvRuntimeinjected on attachVerifynot in image

Step-by-Step Implementation

  1. Store the secret outside the repo (host env or secret store).
export NPM_TOKEN=...   # or set it in the Codespaces secret store
  1. Reference it via remoteEnv — never a literal value.
{
  "remoteEnv": { "NPM_TOKEN": "${localEnv:NPM_TOKEN}" },
  "remoteUser": "vscode"
}
  1. Consume it at runtime, e.g. in a hook.
{ "postCreateCommand": "npm ci" }
  1. Verify the secret is not baked into the image.
docker history --no-trunc <image> | grep -i NPM_TOKEN || echo "clean"

Runtime injection modelSecrets live in a store and are injected per session via remoteEnv, never baked in.Secret storeoutside the reporemoteEnvruntime referencePer-sessioninjected on attachImage stays cleanno baked value

Common Pitfalls

Secret leaks come from baking values into images, configs, or Dockerfile ENV.

Secret triageA triage path from a baked secret to safe runtime injection.Is the secret a literal in image/config?YESMove it to remoteEnv + storeReferenced via localEnv from a store?YESPrefer short-lived tokensNo baked secrets

SymptomRoot CauseRemediation
Token found in image layersBaked via Dockerfile ENV/ARGInject via remoteEnv at runtime; rotate
Secret committed to .devcontainerLiteral value in configReference ${localEnv:...} from a store
Secret leaks in CI logsEchoed by a commandMask it; don't print secret env
Long-lived token over-exposedStatic broad-scope credentialUse short-lived scoped tokens

Conclusion

Keep secrets out of everything that persists. Store them in the host or Codespaces secret store and inject at runtime with remoteEnv referencing ${localEnv:…}, so no image layer or committed file ever holds the value. Prefer short-lived, scoped tokens to bound any leak.

Never vs alwaysNever bake secrets; always inject them at runtime from a store.NeverDockerfile ENV secretCommitted literalLong-lived broad tokenAlwaysSecret storeremoteEnv referenceShort-lived scoped token

FAQ

Why not use a Dockerfile ARG/ENV for a token? Because it persists in the image's build history and layers, recoverable with docker history even after you remove it. Inject secrets at runtime via remoteEnv from a store instead, so nothing sensitive is ever written into an image.

How does remoteEnv keep the secret out of the repo? remoteEnv references an environment variable (for example ${localEnv:NPM_TOKEN}) resolved at attach time from your host or the Codespaces secret store. The config stores only the name, never the value, so the repo stays clean.

What if a secret was already committed? Rotate it immediately — assume it is compromised — then remove it from history and switch to runtime injection. A committed secret remains in git history until rewritten, so rotation is the real fix; scrubbing history is secondary.