Description
Adding a secrets field to railpack.json causes all environment variables not listed in secrets to be completely unavailable to build steps. This contradicts the documentation which states that secrets and environment variables are treated separately:
Build secrets and environment variables are treated separately. The main differences being:
- Environment variables are saved in the final image and should not contain sensitive information. Since they are in the final image, providers can add variables that will be available to the app at runtime.
- Secrets are never logged or saved in the build logs. They are also only available at build time and not saved to the final image.
Expected behavior
The secrets field should only control which secret names are part of the build plan for cache invalidation purposes. It should not affect the availability of other environment variables passed via --env.
Actual behavior
The secrets field acts as an exclusive whitelist — only the env vars named in secrets are mounted via llb.AddSecret in BuildKit. All other env vars passed via --env are silently dropped from every build step.
Reproduction
Given a railpack.json without a secrets field:
{
"steps": {
"build": {
"commands": ["...", "./check-env.sh"]
}
},
"deploy": { "startCommand": "node main.js" }
}
railpack build \
--env DATABASE_URL=postgres://localhost/mydb \
--env SENTRY_AUTH_TOKEN=sntrx_abc \
--env VITE_APP_TITLE=MyApp \
--env MY_SECRET=s3cret .
Build step output — all 4 vars available:
DATABASE_URL=postgres://localhost/mydb
SENTRY_AUTH_TOKEN=sntrx_abc
VITE_APP_TITLE=MyApp
MY_SECRET=s3cret
Now add "secrets": ["VITE_APP_TITLE"] to the same config:
{
"secrets": ["VITE_APP_TITLE"],
"steps": {
"build": {
"commands": ["...", "./check-env.sh"]
}
},
"deploy": { "startCommand": "node main.js" }
}
Same build command, output:
DATABASE_URL=NOT SET
SENTRY_AUTH_TOKEN=NOT SET
VITE_APP_TITLE=MyApp
MY_SECRET=NOT SET
DATABASE_URL, SENTRY_AUTH_TOKEN, and MY_SECRET are silently gone. The spread syntax "secrets": ["...", "VITE_APP_TITLE"] also doesn't help.
Root cause
In core/core.go, GetConfig merges three config sources:
mergedConfig := c.Merge(optionsConfig, envConfig, fileConfig)
GenerateConfigFromEnvironment (core.go:234) adds all --env var names to envConfig.Secrets
- The file config's
Secrets slice replaces (not extends) the env config's via MergeStructs, which treats slices as last-non-nil-wins (internal/utils/merge.go:42-45)
- The resulting
plan.Secrets only contains names from the config file
- In
buildkit/build_llb/build_graph.go:261-263, only secrets listed in plan.Secrets get mounted as env vars via llb.AddSecret — so the dropped vars are never available to any build command
No existing tests cover this scenario — the tests for Config.Merge confirm the replacement behavior, but TestGenerateContext bypasses the merge entirely by assigning ctx.Config directly.
Description
Adding a
secretsfield torailpack.jsoncauses all environment variables not listed insecretsto be completely unavailable to build steps. This contradicts the documentation which states that secrets and environment variables are treated separately:Expected behavior
The
secretsfield should only control which secret names are part of the build plan for cache invalidation purposes. It should not affect the availability of other environment variables passed via--env.Actual behavior
The
secretsfield acts as an exclusive whitelist — only the env vars named insecretsare mounted viallb.AddSecretin BuildKit. All other env vars passed via--envare silently dropped from every build step.Reproduction
Given a
railpack.jsonwithout asecretsfield:{ "steps": { "build": { "commands": ["...", "./check-env.sh"] } }, "deploy": { "startCommand": "node main.js" } }railpack build \ --env DATABASE_URL=postgres://localhost/mydb \ --env SENTRY_AUTH_TOKEN=sntrx_abc \ --env VITE_APP_TITLE=MyApp \ --env MY_SECRET=s3cret .Build step output — all 4 vars available:
Now add
"secrets": ["VITE_APP_TITLE"]to the same config:{ "secrets": ["VITE_APP_TITLE"], "steps": { "build": { "commands": ["...", "./check-env.sh"] } }, "deploy": { "startCommand": "node main.js" } }Same build command, output:
DATABASE_URL,SENTRY_AUTH_TOKEN, andMY_SECRETare silently gone. The spread syntax"secrets": ["...", "VITE_APP_TITLE"]also doesn't help.Root cause
In
core/core.go,GetConfigmerges three config sources:GenerateConfigFromEnvironment(core.go:234) adds all--envvar names toenvConfig.SecretsSecretsslice replaces (not extends) the env config's viaMergeStructs, which treats slices as last-non-nil-wins (internal/utils/merge.go:42-45)plan.Secretsonly contains names from the config filebuildkit/build_llb/build_graph.go:261-263, only secrets listed inplan.Secretsget mounted as env vars viallb.AddSecret— so the dropped vars are never available to any build commandNo existing tests cover this scenario — the tests for
Config.Mergeconfirm the replacement behavior, butTestGenerateContextbypasses the merge entirely by assigningctx.Configdirectly.