fix(railpack): interpolate build-time env variables by sourcing build-time .env

Railpack builds forwarded build-time variables inline as `env 'KEY=VALUE'`,
which single-quotes each value and prevents shell interpolation. References
like BETTER_AUTH_URL=$COOLIFY_URL reached the build as the literal string
"$COOLIFY_URL" instead of the resolved URL, breaking builds that validate
their env (e.g. SvelteKit/better-auth).

Wrap the railpack `docker buildx build` invocation with the same
wrap_build_command_with_env_export() helper used by the Dockerfile and
Nixpacks build paths, sourcing the build-time .env file (which writes
COOLIFY_* first and double-quotes normal vars to allow $VAR expansion).
Only buildpack control variables (NIXPACKS_/RAILPACK_), excluded from that
file and never needing interpolation, remain inline. Secret flags are
unchanged and read the interpolated values from the exported environment.

Fixes #10736
This commit is contained in:
Aditya Tripathi 2026-06-24 20:11:46 +00:00
parent 3f0a0f7a0d
commit 623bf89543
No known key found for this signature in database
GPG key ID: D7007FB45C05CEF5
2 changed files with 50 additions and 4 deletions

View file

@ -2667,12 +2667,22 @@ private function railpack_build_command(string $imageName, Collection $variables
$cacheArgs .= ' --build-arg secrets-hash='.$this->generate_secrets_hash($variables); $cacheArgs .= ' --build-arg secrets-hash='.$this->generate_secrets_hash($variables);
} }
$environmentPrefix = $this->railpack_build_environment_prefix($variables); // Build-time variables reach the build through the sourced build-time .env file
// (written by save_buildtime_environment_variables), which interpolates shell-style
// references such as BETTER_AUTH_URL=$COOLIFY_URL. Passing them inline via `env`
// would forward the literal `$COOLIFY_URL` because each value is single-quoted and
// `env` does not interpolate its own assignments. Only buildpack control variables
// (NIXPACKS_/RAILPACK_) — which are excluded from the build-time .env file and never
// need interpolation — are still passed inline.
$controlVariables = $variables->filter(
fn ($value, $key) => str($key)->startsWith(EnvironmentVariable::BUILDPACK_CONTROL_VARIABLE_PREFIXES)
);
$environmentPrefix = $this->railpack_build_environment_prefix($controlVariables);
$secretFlags = $this->railpack_build_secret_flags($variables); $secretFlags = $this->railpack_build_secret_flags($variables);
$frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version'); $frontendImage = 'ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version');
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true' $buildxBuildCommand = "{$environmentPrefix}docker buildx build --builder coolify-railpack"
." && {$environmentPrefix}docker buildx build --builder coolify-railpack"
." {$this->addHosts} --network host" ." {$this->addHosts} --network host"
." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\"" ." --build-arg BUILDKIT_SYNTAX=\"{$frontendImage}\""
." {$cacheArgs}" ." {$cacheArgs}"
@ -2682,6 +2692,9 @@ private function railpack_build_command(string $imageName, Collection $variables
.' --load' .' --load'
." -t {$imageName}" ." -t {$imageName}"
." {$this->workdir}"; ." {$this->workdir}";
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
.' && '.$this->wrap_build_command_with_env_export($buildxBuildCommand);
} }
private function decode_railpack_config(string $config, string $source): array private function decode_railpack_config(string $config, string $source): array

View file

@ -236,10 +236,15 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
], ],
); );
// Build-time variables are interpolated by sourcing the build-time .env file before
// the build, so user/Coolify variables must NOT be forwarded inline as literals.
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
expect($command)->toContain("env 'RAILPACK_NODE_VERSION=22'"); expect($command)->toContain("env 'RAILPACK_NODE_VERSION=22'");
expect($command)->toContain("'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'"); expect($command)->toContain("'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'");
expect($command)->toContain("'RAILPACK_DEPLOY_APT_PACKAGES=curl wget'"); expect($command)->toContain("'RAILPACK_DEPLOY_APT_PACKAGES=curl wget'");
expect($command)->toContain("'SECRET_JSON={\"token\":\"abc\"}'"); // SECRET_JSON is not a buildpack control variable, so it is provided via the sourced
// build-time .env file (which supports $VAR interpolation) rather than inline `env`.
expect($command)->not->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
expect($command)->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'"); expect($command)->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'");
expect($command)->toContain("--secret 'id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD'"); expect($command)->toContain("--secret 'id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD'");
expect($command)->toContain("--secret 'id=RAILPACK_DEPLOY_APT_PACKAGES,env=RAILPACK_DEPLOY_APT_PACKAGES'"); expect($command)->toContain("--secret 'id=RAILPACK_DEPLOY_APT_PACKAGES,env=RAILPACK_DEPLOY_APT_PACKAGES'");
@ -247,3 +252,31 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
expect($command)->toContain(' --build-arg secrets-hash='); expect($command)->toContain(' --build-arg secrets-hash=');
expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version').'"'); expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version').'"');
}); });
it('interpolates build-time variable references for railpack by sourcing the build-time env file', function () {
[$job, $reflection] = makeRailpackDeploymentJob([
'uuid' => 'application-uuid',
]);
// Mirrors the issue: BETTER_AUTH_URL=$COOLIFY_URL must be interpolated at build time.
$command = invokeRailpackMethod(
$job,
$reflection,
'railpack_build_command',
[
'coollabsio/coolify:test',
collect([
'BETTER_AUTH_URL' => '$COOLIFY_URL',
'COOLIFY_URL' => 'https://sapere-10.bobman.dev',
]),
],
);
// The literal `$COOLIFY_URL` must NOT be forwarded inline; it is resolved by the shell
// after sourcing the build-time .env file, then read through the build secret.
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
expect($command)->not->toContain("'BETTER_AUTH_URL=\$COOLIFY_URL'");
expect($command)->not->toContain("env 'BETTER_AUTH_URL");
expect($command)->toContain("--secret 'id=BETTER_AUTH_URL,env=BETTER_AUTH_URL'");
expect($command)->toContain("--secret 'id=COOLIFY_URL,env=COOLIFY_URL'");
});