From d048fec5b16fd902d3711bd367b0e9267adeb783 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 25 Mar 2026 16:59:24 +0100 Subject: [PATCH 1/7] feat(build): fetch railpack versions dynamically from GitHub releases Replace the hardcoded RAILPACK_VERSIONS array with a tRPC query that fetches releases from the GitHub API. Versions are cached for 24 hours and only fetched when the Railpack build type is selected. Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/application/build/show.tsx | 60 +++++++------------ .../dokploy/server/api/routers/application.ts | 15 +++++ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 32aee23d37..de6a449431 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -29,29 +29,6 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; -// Railpack versions from https://github.com/railwayapp/railpack/releases -export const RAILPACK_VERSIONS = [ - "0.15.4", - "0.15.3", - "0.15.2", - "0.15.1", - "0.15.0", - "0.14.0", - "0.13.0", - "0.12.0", - "0.11.0", - "0.10.0", - "0.9.2", - "0.9.1", - "0.9.0", - "0.8.0", - "0.7.0", - "0.6.0", - "0.5.0", - "0.4.0", - "0.3.0", - "0.2.2", -] as const; export enum BuildType { dockerfile = "dockerfile", @@ -169,7 +146,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { { applicationId }, { enabled: !!applicationId }, ); - const form = useForm({ defaultValues: { buildType: BuildType.nixpacks, @@ -178,6 +154,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { }); const buildType = form.watch("buildType"); + + const { data: railpackVersions, isLoading: isLoadingRailpackVersions } = + api.application.getRailpackVersions.useQuery(undefined, { + enabled: buildType === BuildType.railpack, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }); const railpackVersion = form.watch("railpackVersion"); const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false); @@ -191,16 +173,16 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { }; form.reset(resetData(typedData)); + } + }, [data, form]); - // Check if railpack version is manual (not in the predefined list) - if ( - data.railpackVersion && - !RAILPACK_VERSIONS.includes(data.railpackVersion as any) - ) { + useEffect(() => { + if (data?.railpackVersion && railpackVersions) { + if (!railpackVersions.includes(data.railpackVersion)) { setIsManualRailpackVersion(true); } } - }, [data, form]); + }, [railpackVersions, data?.railpackVersion]); // Hide builder section when Docker provider is selected if (data?.sourceType === "docker") { @@ -227,7 +209,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.static ? data.isStaticSpa : null, railpackVersion: data.buildType === BuildType.railpack - ? data.railpackVersion || "0.15.4" + ? data.railpackVersion || railpackVersions?.[0] || "" : null, }) .then(async () => { @@ -465,7 +447,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { size="sm" onClick={() => { setIsManualRailpackVersion(false); - field.onChange("0.15.4"); + field.onChange(railpackVersions?.[0] ?? ""); }} > Use predefined versions @@ -481,10 +463,14 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { field.onChange(value); } }} - value={field.value ?? "0.15.4"} + value={field.value ?? railpackVersions?.[0] ?? ""} > - - + + {isLoadingRailpackVersions ? ( + Loading versions... + ) : ( + + )} @@ -492,10 +478,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { ✏️ Manual (Custom Version) - {RAILPACK_VERSIONS.map((version) => ( + {railpackVersions?.map((version) => ( v{version} - {version === "0.15.4" && ( + {version === railpackVersions?.[0] && ( { + const res = await fetch( + "https://api.github.com/repos/railwayapp/railpack/releases", + { headers: { Accept: "application/vnd.github+json" } }, + ); + if (!res.ok) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch Railpack versions from GitHub", + }); + } + const releases = (await res.json()) as Array<{ tag_name: string }>; + return releases.map((r) => r.tag_name.replace(/^v/, "")); + }), }); From c56adea0ac948b5cda7e6e1dd1d9fb3b2fdcbedc Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 25 Mar 2026 17:15:31 +0100 Subject: [PATCH 2/7] fix(build): remove unused railpackVersion watch Co-Authored-By: Claude Sonnet 4.6 --- apps/dokploy/components/dashboard/application/build/show.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index de6a449431..077f1d9f3b 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -160,7 +160,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { enabled: buildType === BuildType.railpack, staleTime: 1000 * 60 * 60 * 24, // 24 hours }); - const railpackVersion = form.watch("railpackVersion"); const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false); useEffect(() => { From eb5601ce07a30deea93cb37cc83b3b74e6a24075 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 25 Mar 2026 17:41:19 +0100 Subject: [PATCH 3/7] fix(build): correctly default railpack version to latest on first selection - Change Zod schema default from hardcoded "0.15.4" to null so Zod does not silently override the form field before the fetched versions arrive - Set form value to latest fetched version when no version is selected - Strip v prefix from manually entered versions before saving - Fix form reset loop by splitting useEffect into separate concerns Co-Authored-By: Claude Sonnet 4.6 --- .../dashboard/application/build/show.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 077f1d9f3b..fdbb8364a2 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -68,7 +68,7 @@ const mySchema = z.discriminatedUnion("buildType", [ }), z.object({ buildType: z.literal(BuildType.railpack), - railpackVersion: z.string().nullable().default("0.15.4"), + railpackVersion: z.string().nullable().default(null), }), z.object({ buildType: z.literal(BuildType.static), @@ -176,12 +176,18 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { }, [data, form]); useEffect(() => { - if (data?.railpackVersion && railpackVersions) { - if (!railpackVersions.includes(data.railpackVersion)) { + if (railpackVersions?.length) { + if ( + data?.railpackVersion && + !railpackVersions.includes(data.railpackVersion) + ) { setIsManualRailpackVersion(true); } + if (!form.getValues("railpackVersion")) { + form.setValue("railpackVersion", railpackVersions[0]); + } } - }, [railpackVersions, data?.railpackVersion]); + }, [railpackVersions, data?.railpackVersion, form]); // Hide builder section when Docker provider is selected if (data?.sourceType === "docker") { @@ -208,7 +214,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.static ? data.isStaticSpa : null, railpackVersion: data.buildType === BuildType.railpack - ? data.railpackVersion || railpackVersions?.[0] || "" + ? (data.railpackVersion || railpackVersions?.[0] || "").replace(/^v/, "") : null, }) .then(async () => { From fc2a072fed295a788dc78a73262e5e0b5d646ed4 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 15 Apr 2026 18:20:57 +0200 Subject: [PATCH 4/7] feat: enforce railpack version requirement in build form Add validation to ensure that a railpack version is provided when the build type is set to railpack. Update the submission logic to normalize the railpack version and disable the submit button if the version is missing. This enhances user experience by preventing submission with invalid data. --- .../dashboard/application/build/show.tsx | 24 +++++++++++++++++-- packages/server/src/db/schema/application.ts | 12 +++++++++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index fdbb8364a2..9634f8fb3d 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -195,6 +195,18 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { } const onSubmit = async (data: AddTemplate) => { + const normalizedRailpackVersion = + data.buildType === BuildType.railpack + ? (data.railpackVersion || railpackVersions?.[0] || "") + .replace(/^v/, "") + .trim() + : null; + + if (data.buildType === BuildType.railpack && !normalizedRailpackVersion) { + toast.error("Railpack version is required"); + return; + } + await mutateAsync({ applicationId, buildType: data.buildType, @@ -214,7 +226,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { data.buildType === BuildType.static ? data.isStaticSpa : null, railpackVersion: data.buildType === BuildType.railpack - ? (data.railpackVersion || railpackVersions?.[0] || "").replace(/^v/, "") + ? normalizedRailpackVersion : null, }) .then(async () => { @@ -519,7 +531,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { )}
-
diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts index a7067f63f3..c13b9aa702 100644 --- a/packages/server/src/db/schema/application.ts +++ b/packages/server/src/db/schema/application.ts @@ -428,9 +428,19 @@ export const apiSaveBuildType = createSchema dockerBuildStage: true, herokuVersion: true, railpackVersion: true, + publishDirectory: true, + isStaticSpa: true, }) .required() - .merge(createSchema.pick({ publishDirectory: true, isStaticSpa: true })); + .superRefine((data, ctx) => { + if (data.buildType === "railpack" && !data.railpackVersion?.trim()) { + ctx.addIssue({ + code: "custom", + path: ["railpackVersion"], + message: "Railpack version is required", + }); + } + }); export const apiSaveGithubProvider = createSchema .pick({ From 9b8d1673fe6e4d8100c0350e1b19ec969e44e0b4 Mon Sep 17 00:00:00 2001 From: Michal Date: Wed, 15 Apr 2026 18:44:48 +0200 Subject: [PATCH 5/7] feat: handle railpack version loading errors in build form Made-with: Cursor --- .../dashboard/application/build/show.tsx | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 9634f8fb3d..a3b2271516 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -155,11 +155,14 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { const buildType = form.watch("buildType"); - const { data: railpackVersions, isLoading: isLoadingRailpackVersions } = - api.application.getRailpackVersions.useQuery(undefined, { - enabled: buildType === BuildType.railpack, - staleTime: 1000 * 60 * 60 * 24, // 24 hours - }); + const { + data: railpackVersions, + isLoading: isLoadingRailpackVersions, + isError: isErrorRailpackVersions, + } = api.application.getRailpackVersions.useQuery(undefined, { + enabled: buildType === BuildType.railpack, + staleTime: 1000 * 60 * 60 * 24, // 24 hours + }); const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false); useEffect(() => { @@ -189,6 +192,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { } }, [railpackVersions, data?.railpackVersion, form]); + useEffect(() => { + if (buildType === BuildType.railpack && isErrorRailpackVersions) { + setIsManualRailpackVersion(true); + } + }, [buildType, isErrorRailpackVersions]); + // Hide builder section when Docker provider is selected if (data?.sourceType === "docker") { return null; @@ -211,7 +220,9 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { applicationId, buildType: data.buildType, publishDirectory: - data.buildType === BuildType.nixpacks ? data.publishDirectory : null, + data.buildType === BuildType.nixpacks + ? (data.publishDirectory ?? null) + : null, dockerfile: data.buildType === BuildType.dockerfile ? data.dockerfile : null, dockerContextPath: @@ -524,6 +535,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { View releases + {isErrorRailpackVersions && !isManualRailpackVersion && ( +

+ Failed to load Railpack versions. Switch to manual mode + to set a custom version. +

+ )} )} From e64d1f2778ae06e845c864ee62230dac8e63f1cc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 04:55:25 +0000 Subject: [PATCH 6/7] [autofix.ci] apply automated fixes --- .../components/dashboard/application/build/show.tsx | 9 +++++---- apps/dokploy/server/api/routers/application.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index a3b2271516..83f52153f1 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -29,7 +29,6 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; - export enum BuildType { dockerfile = "dockerfile", heroku_buildpacks = "heroku_buildpacks", @@ -495,7 +494,9 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { > {isLoadingRailpackVersions ? ( - Loading versions... + + Loading versions... + ) : ( )} @@ -537,8 +538,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { {isErrorRailpackVersions && !isManualRailpackVersion && (

- Failed to load Railpack versions. Switch to manual mode - to set a custom version. + Failed to load Railpack versions. Switch to manual + mode to set a custom version.

)} diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index c22c7de7f2..a0f5433fa2 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -1117,7 +1117,7 @@ export const applicationRouter = createTRPCRouter({ const releases = (await res.json()) as Array<{ tag_name: string }>; return releases.map((r) => r.tag_name.replace(/^v/, "")); }), - + readLogs: protectedProcedure .input( apiFindOneApplication.extend({ From b092ea0d6d93cc67bf135ea54be0abfa960be407 Mon Sep 17 00:00:00 2001 From: Michal Date: Mon, 6 Jul 2026 23:28:36 +0200 Subject: [PATCH 7/7] fix: cache railpack versions --- .../dashboard/application/build/show.tsx | 9 ++++--- .../dokploy/server/api/routers/application.ts | 25 +++++++++++++++++-- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 83f52153f1..bb40c16baa 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -138,6 +138,9 @@ const resetData = (data: ApplicationData): AddTemplate => { } }; +const normalizeRailpackVersion = (version: string) => + version.trim().replace(/^v/i, ""); + export const ShowBuildChooseForm = ({ applicationId }: Props) => { const { mutateAsync, isPending } = api.application.saveBuildType.useMutation(); @@ -205,9 +208,9 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { const onSubmit = async (data: AddTemplate) => { const normalizedRailpackVersion = data.buildType === BuildType.railpack - ? (data.railpackVersion || railpackVersions?.[0] || "") - .replace(/^v/, "") - .trim() + ? normalizeRailpackVersion( + data.railpackVersion || railpackVersions?.[0] || "", + ) : null; if (data.buildType === BuildType.railpack && !normalizedRailpackVersion) { diff --git a/apps/dokploy/server/api/routers/application.ts b/apps/dokploy/server/api/routers/application.ts index a0f5433fa2..16d428638f 100644 --- a/apps/dokploy/server/api/routers/application.ts +++ b/apps/dokploy/server/api/routers/application.ts @@ -77,6 +77,11 @@ import { } from "@/server/queues/queueSetup"; import { cancelDeployment, deploy } from "@/server/utils/deploy"; +const RAILPACK_VERSIONS_CACHE_TTL = 1000 * 60 * 60 * 24; +let railpackVersionsCache: + | { versions: string[]; fetchedAt: number } + | undefined; + export const applicationRouter = createTRPCRouter({ create: protectedProcedure .input(apiCreateApplication) @@ -1104,9 +1109,23 @@ export const applicationRouter = createTRPCRouter({ }), getRailpackVersions: protectedProcedure.query(async () => { + if ( + railpackVersionsCache && + Date.now() - railpackVersionsCache.fetchedAt < RAILPACK_VERSIONS_CACHE_TTL + ) { + return railpackVersionsCache.versions; + } + const res = await fetch( "https://api.github.com/repos/railwayapp/railpack/releases", - { headers: { Accept: "application/vnd.github+json" } }, + { + headers: { + Accept: "application/vnd.github+json", + ...(process.env.GITHUB_TOKEN + ? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } + : {}), + }, + }, ); if (!res.ok) { throw new TRPCError({ @@ -1115,7 +1134,9 @@ export const applicationRouter = createTRPCRouter({ }); } const releases = (await res.json()) as Array<{ tag_name: string }>; - return releases.map((r) => r.tag_name.replace(/^v/, "")); + const versions = releases.map((r) => r.tag_name.replace(/^v/i, "")); + railpackVersionsCache = { versions, fetchedAt: Date.now() }; + return versions; }), readLogs: protectedProcedure