diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts index af4794dbc2..82d2e260ac 100644 --- a/apps/dokploy/__test__/server/server-components.test.ts +++ b/apps/dokploy/__test__/server/server-components.test.ts @@ -28,6 +28,7 @@ import { parseComponentVersions, type UpdatableComponent, } from "@dokploy/server/setup/server-components"; +import { RAILPACK_INSTALL_SCRIPT_URLS } from "@dokploy/server/setup/server-setup"; import { buildTraefikCreateCommand, buildTraefikCreateWithImage, @@ -251,10 +252,10 @@ describe("buildComponentUpdateScript", () => { const script = buildComponentUpdateScript(["nixpacks", "railpack"]); expect(script).toContain( - 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"', + "for nixpacks_installer_url in https://nixpacks.com/install.sh; do", ); expect(script).toContain( - 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"', + `for railpack_installer_url in ${RAILPACK_INSTALL_SCRIPT_URLS.join(" ")}; do`, ); expect(script).toContain('bash -n "$nixpacks_installer"'); expect(script).not.toContain('bash -c "$(curl'); @@ -1590,6 +1591,46 @@ describe("buildComponentUpdateScript run order", { timeout: 60_000 }, () => { }, SPAWN_TEST_TIMEOUT_MS, ); + it( + "installs Railpack from the GitHub copy of the install script when railpack.com fails", + () => { + const run = runComponentScript(["railpack"], { + env: { FAKE_CURL_FAILS_FOR: "railpack.com" }, + }); + + expect(run.status).toBe(0); + expect( + run.calls.some((call) => + call.startsWith( + "curl -fsSL https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh -o ", + ), + ), + ).toBe(true); + expect(run.stderr).toContain( + "The Railpack install script could not be downloaded or checked from https://railpack.com/install.sh.", + ); + expect(run.stdout).toContain( + `Railpack version ${RAILPACK_VERSION} installed`, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "fails the update when neither copy of the Railpack install script can be downloaded", + () => { + const run = runComponentScript(["railpack"], { + env: { FAKE_CURL_FAILS_FOR: "install.sh" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "the Railpack install script could not be downloaded or checked; Railpack was not changed.", + ); + expect(run.stdout).not.toContain("Railpack version"); + }, + SPAWN_TEST_TIMEOUT_MS, + ); it( "fails the update when the RClone download fails", () => { diff --git a/apps/dokploy/__test__/server/server-setup.test.ts b/apps/dokploy/__test__/server/server-setup.test.ts index e3b7ebdc72..8659cbde1b 100644 --- a/apps/dokploy/__test__/server/server-setup.test.ts +++ b/apps/dokploy/__test__/server/server-setup.test.ts @@ -12,6 +12,7 @@ import path from "node:path"; import { createTraefikInstance, defaultCommand, + RAILPACK_INSTALL_SCRIPT_URLS, reportDockerVersion, } from "@dokploy/server"; import { @@ -129,7 +130,8 @@ const setupStep = (script: string, marker: string) => { // Runs one step as the setup script runs it: under set -e, with a PATH that holds only // the fake curl and the tools the step needs, so nixpacks, railpack and pack are absent. -const runSetupStep = (step: string, curlMode: "ok" | "fail") => { +// failsFor fails only the downloads whose URL contains it. +const runSetupStep = (step: string, curlMode: "ok" | "fail", failsFor = "") => { const dir = mkdtempSync(path.join(tmpdir(), "dokploy-setup-step-")); try { const binDir = path.join(dir, "bin"); @@ -142,6 +144,11 @@ const runSetupStep = (step: string, curlMode: "ok" | "fail") => { curl, `#!/bin/sh if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi +if [ -n "$FAKE_CURL_FAILS_FOR" ]; then + case "$*" in + *"$FAKE_CURL_FAILS_FOR"*) exit 22 ;; + esac +fi out="" while [ "$#" -gt 0 ]; do if [ "$1" = "-o" ]; then out="$2"; fi @@ -165,6 +172,7 @@ printf '%s\\n' 'echo fake installer "$NIXPACKS_VERSION$RAILPACK_VERSION"' > "$ou ...process.env, PATH: `${dir}:${binDir}`, FAKE_CURL_MODE: curlMode, + FAKE_CURL_FAILS_FOR: failsFor, }, }); } finally { @@ -192,14 +200,20 @@ describe("install scripts in the server setup", () => { const script = defaultCommand(isBuildServer); expect(script).toContain( - 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"', + "for nixpacks_installer_url in https://nixpacks.com/install.sh; do", + ); + expect(script).toContain( + 'curl -fsSL "$nixpacks_installer_url" -o "$nixpacks_installer"', ); expect(script).toContain('bash -n "$nixpacks_installer"'); expect(script).toContain( `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash "$nixpacks_installer"`, ); expect(script).toContain( - 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"', + `for railpack_installer_url in ${RAILPACK_INSTALL_SCRIPT_URLS.join(" ")}; do`, + ); + expect(script).toContain( + 'curl -fsSL "$railpack_installer_url" -o "$railpack_installer"', ); expect(script).toContain('bash -n "$railpack_installer"'); expect(script).toContain( @@ -248,6 +262,37 @@ describe("install scripts in the server setup", () => { ); }); + it("installs Railpack from the GitHub copy of the install script when railpack.com fails", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Railpack"'), + "ok", + "railpack.com", + ); + + expect(result.status).toBe(0); + expect(result.stderr).toContain( + "The Railpack install script could not be downloaded or checked from https://railpack.com/install.sh.", + ); + expect(result.stdout).toContain(`fake installer ${RAILPACK_VERSION}`); + expect(result.stdout).toContain( + `Railpack version ${RAILPACK_VERSION} installed`, + ); + }); + + it("stops the setup when neither copy of the Railpack install script can be downloaded", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Railpack"'), + "ok", + "install.sh", + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "could not be downloaded or checked from https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh.", + ); + expect(result.stdout).not.toContain("Railpack version"); + }); + it("stops the setup when the RClone download fails", () => { const result = runSetupStep( setupStep(defaultCommand(true), 'Installing RClone. "'), diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts index 31b6990157..37b4484af9 100644 --- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts +++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts @@ -1085,6 +1085,13 @@ describe("install-agenthits.sh toolchain downloads", () => { }; const FAKE_INSTALL_CURL = `#!/bin/sh if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi +case "$FAKE_CURL_MODE" in + fail-for:*) + case "$*" in + *"\${FAKE_CURL_MODE#fail-for:}"*) exit 22 ;; + esac + ;; +esac out="" while [ "$#" -gt 0 ]; do if [ "$1" = "-o" ]; then out="$2"; fi @@ -1093,7 +1100,11 @@ done printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out" `; - const runHelper = (args: string[], curlMode: "ok" | "fail") => { + // "fail-for:" fails only the downloads whose URL contains . + const runHelper = ( + args: string[], + curlMode: "ok" | "fail" | `fail-for:${string}`, + ) => { const dir = mkdtempSync(path.join(tmpdir(), "dokploy-install-script-")); try { const binDir = path.join(dir, "bin"); @@ -1131,7 +1142,7 @@ printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out" 'run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION"', ); expect(installer).toContain( - 'run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION"', + 'run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION" \\\n\t\t\thttps://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh', ); expect(installer).not.toContain('bash -c "$(curl'); }); @@ -1152,6 +1163,41 @@ printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out" ); }); + it("downloads the install script from the fallback URL when the first one fails", () => { + const result = runHelper( + [ + "https://railpack.com/install.sh", + "Railpack", + "NIXPACKS_VERSION=29.4.0", + "https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh", + ], + "fail-for:railpack.com", + ); + + expect(result.status).toBe(0); + expect(result.stderr).toContain( + "The Railpack install script could not be downloaded or checked from https://railpack.com/install.sh.", + ); + expect(result.stdout).toContain("fake nixpacks 29.4.0"); + }); + + it("stops the install when the fallback URL fails too", () => { + const result = runHelper( + [ + "https://railpack.com/install.sh", + "Railpack", + "NIXPACKS_VERSION=29.4.0", + "https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh", + ], + "fail-for:install.sh", + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "the Railpack install script could not be downloaded or checked; Railpack was not installed.", + ); + }); + it("runs the downloaded install script with the pinned version", () => { const result = runHelper( [ diff --git a/install-agenthits.sh b/install-agenthits.sh index a460a9d900..046dcdba3f 100755 --- a/install-agenthits.sh +++ b/install-agenthits.sh @@ -395,9 +395,17 @@ run_install_script() { local url="$1" local label="$2" local assignment="$3" - local script="" + local fallback_url="${4:-}" + local script="" candidate="" downloaded=0 script="$(mktemp)" - if ! curl -fsSL "$url" -o "$script" || [ ! -s "$script" ] || ! bash -n "$script"; then + for candidate in "$url" ${fallback_url:+"$fallback_url"}; do + if curl -fsSL "$candidate" -o "$script" && [ -s "$script" ] && bash -n "$script"; then + downloaded=1 + break + fi + echo "The $label install script could not be downloaded or checked from $candidate." >&2 + done + if [ "$downloaded" != 1 ]; then rm -f "$script" echo "Error: the $label install script could not be downloaded or checked; $label was not installed." >&2 exit 1 @@ -430,7 +438,10 @@ install_toolchain() { run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION" fi if ! tool_version_matches "$RAILPACK_VERSION" railpack --version; then - run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION" + # railpack.com only redirects through Cloudflare to this GitHub file, and some + # networks get a 525 from Cloudflare there. + run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION" \ + https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh fi if ! tool_version_matches "$BUILDPACKS_VERSION" pack --version; then curl -sSL "https://github.com/buildpacks/pack/releases/download/v${BUILDPACKS_VERSION}/pack-v${BUILDPACKS_VERSION}-linux${pack_suffix}.tgz" | tar -C /usr/local/bin/ --no-same-owner -xz pack diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts index c96688df69..9dca2c9b64 100644 --- a/packages/server/src/setup/server-components.ts +++ b/packages/server/src/setup/server-components.ts @@ -1,5 +1,10 @@ import { PINNED_VERSIONS } from "./component-versions"; -import { rcloneInstallCommand } from "./server-setup"; +import { + installScriptDownload, + NIXPACKS_INSTALL_SCRIPT_URLS, + RAILPACK_INSTALL_SCRIPT_URLS, + rcloneInstallCommand, +} from "./server-setup"; import { runSshCommand } from "./ssh-command"; import { buildTraefikCreateWithImage, @@ -552,12 +557,12 @@ fi case "nixpacks": return ` echo "Updating Nixpacks to ${PINNED_VERSIONS.nixpacks}" -nixpacks_installer="$(mktemp)" -if ! curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer" || [ ! -s "$nixpacks_installer" ] || ! bash -n "$nixpacks_installer"; then - rm -f "$nixpacks_installer" - echo "Error: the Nixpacks install script could not be downloaded or checked; Nixpacks was not changed." >&2 - exit 1 -fi +${installScriptDownload({ + label: "Nixpacks", + name: "nixpacks", + urls: NIXPACKS_INSTALL_SCRIPT_URLS, + outcome: "Nixpacks was not changed.", +})} if ! $SUDO_CMD env NIXPACKS_VERSION=${PINNED_VERSIONS.nixpacks} bash "$nixpacks_installer"; then rm -f "$nixpacks_installer" echo "Error: the Nixpacks install script failed." >&2 @@ -569,12 +574,12 @@ echo "Nixpacks version ${PINNED_VERSIONS.nixpacks} installed ✅" case "railpack": return ` echo "Updating Railpack to ${PINNED_VERSIONS.railpack}" -railpack_installer="$(mktemp)" -if ! curl -fsSL https://railpack.com/install.sh -o "$railpack_installer" || [ ! -s "$railpack_installer" ] || ! bash -n "$railpack_installer"; then - rm -f "$railpack_installer" - echo "Error: the Railpack install script could not be downloaded or checked; Railpack was not changed." >&2 - exit 1 -fi +${installScriptDownload({ + label: "Railpack", + name: "railpack", + urls: RAILPACK_INSTALL_SCRIPT_URLS, + outcome: "Railpack was not changed.", +})} if ! $SUDO_CMD env RAILPACK_VERSION=${PINNED_VERSIONS.railpack} bash "$railpack_installer"; then rm -f "$railpack_installer" echo "Error: the Railpack install script failed." >&2 diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts index d03a6c2e20..2100a98b62 100644 --- a/packages/server/src/setup/server-setup.ts +++ b/packages/server/src/setup/server-setup.ts @@ -792,27 +792,58 @@ ${buildTraefikTlsMigrationStep()} return command; }; +export const NIXPACKS_INSTALL_SCRIPT_URLS = ["https://nixpacks.com/install.sh"]; + +// railpack.com only redirects through Cloudflare to the script on GitHub, and some +// networks get a 525 from Cloudflare there, so the GitHub copy is the fallback. +export const RAILPACK_INSTALL_SCRIPT_URLS = [ + "https://railpack.com/install.sh", + "https://raw.githubusercontent.com/railwayapp/railpack/refs/heads/main/install.sh", +]; + // A failed download must fail the setup. Piping curl into bash, or running it inside // $(...), runs nothing when the download fails and still reports success. +export const installScriptDownload = ({ + label, + name, + urls, + outcome, +}: { + label: string; + name: string; + urls: string[]; + outcome: string; +}) => ` +${name}_installer="$(mktemp)" +${name}_installer_ok=0 +for ${name}_installer_url in ${urls.join(" ")}; do + if curl -fsSL "$${name}_installer_url" -o "$${name}_installer" && [ -s "$${name}_installer" ] && bash -n "$${name}_installer"; then + ${name}_installer_ok=1 + break + fi + echo "The ${label} install script could not be downloaded or checked from $${name}_installer_url." >&2 +done +if [ "$${name}_installer_ok" != 1 ]; then + rm -f "$${name}_installer" + echo "Error: the ${label} install script could not be downloaded or checked; ${outcome}" >&2 + exit 1 +fi +`; + const installScriptRun = ({ label, - url, + urls, name, variable, version, }: { label: string; - url: string; + urls: string[]; name: string; variable: string; version: string; }) => ` - ${name}_installer="$(mktemp)" - if ! curl -fsSL ${url} -o "$${name}_installer" || [ ! -s "$${name}_installer" ] || ! bash -n "$${name}_installer"; then - rm -f "$${name}_installer" - echo "Error: the ${label} install script could not be downloaded or checked; ${label} was not installed." >&2 - exit 1 - fi +${installScriptDownload({ label, name, urls, outcome: `${label} was not installed.` })} if ! $SUDO_CMD env ${variable}=${version} bash "$${name}_installer"; then rm -f "$${name}_installer" echo "Error: the ${label} install script failed." >&2 @@ -828,7 +859,7 @@ const installNixpacks = () => ` export NIXPACKS_VERSION=${NIXPACKS_VERSION} ${installScriptRun({ label: "Nixpacks", - url: "https://nixpacks.com/install.sh", + urls: NIXPACKS_INSTALL_SCRIPT_URLS, name: "nixpacks", variable: "NIXPACKS_VERSION", version: NIXPACKS_VERSION, @@ -844,7 +875,7 @@ const installRailpack = () => ` export RAILPACK_VERSION=${RAILPACK_VERSION} ${installScriptRun({ label: "Railpack", - url: "https://railpack.com/install.sh", + urls: RAILPACK_INSTALL_SCRIPT_URLS, name: "railpack", variable: "RAILPACK_VERSION", version: RAILPACK_VERSION,