Files
dokploy/packages/server/src/utils/builders/railpack.ts
T
Souvik Kumar 679dbf98cb fix: run Railpack install with sudo during remote deploy
Deploy-time Railpack install ran without sudo, so deploys to a remote
server with a non-root user (passwordless sudo) failed because the
install script could not write to /usr/local/bin ("A terminal is
required to authenticate").

Detect whether sudo is needed (root vs. passwordless-sudo user) and run
the install through it, matching the server-setup script.

Fixes #5007
2026-08-09 20:01:06 +05:30

123 lines
3.4 KiB
TypeScript

import { createHash } from "node:crypto";
import { nanoid } from "nanoid";
import { quote } from "shell-quote";
import {
parseEnvironmentKeyValuePair,
prepareEnvironmentVariables,
prepareEnvironmentVariablesForShell,
} from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory";
import type { ApplicationNested } from ".";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");
for (const env of envVariables.sort()) {
hash.update(env);
}
return hash.digest("hex");
};
export const getRailpackCommand = (application: ApplicationNested) => {
const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariablesForShell(
env,
application.environment.project.env,
application.environment.env,
);
// Prepare command
const prepareArgs = [
"prepare",
buildAppDirectory,
"--plan-out",
`${buildAppDirectory}/railpack-plan.json`,
"--info-out",
`${buildAppDirectory}/railpack-info.json`,
];
for (const env of envVariables) {
prepareArgs.push("--env", env);
}
// Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
const cacheKey = cleanCache ? nanoid(10) : undefined;
// Build command.
// Use a unique builder name per build so concurrent deployments don't race
// on a shared "builder-containerd" instance (create/use/rm collisions).
const builderName = `railpack-${appName}-${nanoid(6)}`;
const buildArgs = [
"buildx",
"build",
"--builder",
builderName,
"--build-arg",
`secrets-hash=${secretsHash}`,
...(cacheKey ? ["--build-arg", `cache-key=${cacheKey}`] : []),
"--build-arg",
`BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v${application.railpackVersion}`,
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
`type=docker,name=${appName}`,
];
// Add secrets properly formatted
// Use prepareEnvironmentVariables (without ForShell) to get raw values for parsing
const rawEnvVariables = prepareEnvironmentVariables(
env,
application.environment.project.env,
application.environment.env,
);
const exportEnvs = [];
for (const pair of rawEnvVariables) {
const [key, value] = parseEnvironmentKeyValuePair(pair);
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}=${quote([value])}`);
}
}
buildArgs.push(buildAppDirectory);
const bashCommand = `
# Ensure we have a builder with containerd (isolated per build)
export RAILPACK_VERSION=${application.railpackVersion}
# use sudo for non-root so the install can write to /usr/local/bin
if [ "$(id -u)" -eq 0 ]; then
SUDO_CMD=""
elif sudo -n true 2>/dev/null; then
SUDO_CMD="sudo"
else
SUDO_CMD=""
fi
$SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
docker buildx create --name ${builderName} --driver docker-container || true
echo "Preparing Railpack build plan..." ;
railpack ${prepareArgs.join(" ")} || {
echo "❌ Railpack prepare failed" ;
docker buildx rm ${builderName} || true
exit 1;
}
echo "✅ Railpack prepare completed." ;
echo "Building with Railpack frontend..." ;
# Export environment variables for secrets
${exportEnvs.join("\n")}
docker ${buildArgs.join(" ")} || {
echo "❌ Railpack build failed" ;
docker buildx rm ${builderName} || true
exit 1;
}
echo "✅ Railpack build completed." ;
docker buildx rm ${builderName} || true
`;
return bashCommand;
};