This commit is contained in:
Ayham 2025-06-22 18:25:41 +02:00 committed by GitHub
commit ad3f6213c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 262 additions and 129 deletions

View File

@ -5,6 +5,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
@ -45,8 +46,14 @@ export const UpdateServer = ({
const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable,
);
const [customImageUrl, setCustomImageUrl] = useState("");
const [isCustomUpdate, setIsCustomUpdate] = useState(false);
const { mutateAsync: getUpdateData, isLoading } =
api.settings.getUpdateData.useMutation();
const { mutateAsync: pullCustomImage, isLoading: isPullingCustom } =
api.settings.pullCustomImage.useMutation();
const { mutateAsync: updateServer, isLoading: isUpdatingServer } =
api.settings.updateServer.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [latestVersion, setLatestVersion] = useState(
@ -79,6 +86,41 @@ export const UpdateServer = ({
}
};
const handleCustomUpdate = async () => {
if (!customImageUrl) {
toast.error("Please enter a valid Docker image URL");
return;
}
try {
const success = await pullCustomImage({
imageUrl: customImageUrl,
});
if (success) {
toast.success("Custom image pulled successfully");
// Update the Docker service to use the new image
await updateServer({ imageUrl: customImageUrl });
toast.success("Server updated successfully");
onOpenChange?.(false);
} else {
toast.error("Failed to pull custom image");
}
} catch (error) {
console.error("Error updating server:", error);
const errorMessage =
error instanceof Error
? error.message
.split("manifest unknown: manifest unknown")[0]
?.trim() || "Failed to pull image"
: "An error occurred while updating the server";
toast.error(errorMessage, {
description: "Please check if the image URL is correct and accessible",
});
}
};
const isOpen = isOpenInternal || isOpenProp;
const onOpenChange = (open: boolean) => {
setIsOpenInternal(open);
@ -141,116 +183,161 @@ export const UpdateServer = ({
)}
</div>
{/* Initial state */}
{!hasCheckedUpdate && (
<div className="mb-8">
<p className="text text-muted-foreground">
Check for new releases and update Dokploy.
<br />
<br />
We recommend checking for updates regularly to ensure you have the
latest features and security improvements.
</p>
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Button
variant={!isCustomUpdate ? "secondary" : "outline"}
size="sm"
onClick={() => setIsCustomUpdate(false)}
>
Standard Update
</Button>
<Button
variant={isCustomUpdate ? "secondary" : "outline"}
size="sm"
onClick={() => setIsCustomUpdate(true)}
>
Custom Image
</Button>
</div>
)}
{/* Update available state */}
{isUpdateAvailable && latestVersion && (
<div className="mb-8">
<div className="inline-flex items-center gap-2 rounded-lg px-3 py-2 border border-emerald-900 bg-emerald-900 dark:bg-emerald-900/40 mb-4 w-full">
<div className="flex items-center gap-1.5">
<Download className="h-4 w-4 text-emerald-400" />
<span className="text font-medium text-emerald-400 ">
New version available:
</span>
</div>
<span className="text font-semibold text-emerald-300">
{latestVersion}
</span>
</div>
<div className="space-y-4 text-muted-foreground">
<p className="text">
A new version of the server software is available. Consider
updating if you:
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<Stars className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Want to access the latest features and improvements
</span>
</li>
<li className="flex items-start gap-2">
<Bug className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Are experiencing issues that may be resolved in the new
version
</span>
</li>
</ul>
</div>
</div>
)}
{/* Up to date state */}
{hasCheckedUpdate && !isUpdateAvailable && !isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-emerald-400/40">
<Sparkles className="h-8 w-8 text-emerald-400" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
You are using the latest version
</h3>
<p className="text text-muted-foreground">
Your server is up to date with all the latest features and
security improvements.
</p>
</div>
</div>
</div>
)}
{hasCheckedUpdate && isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-[#5B9DFF]/40 text-foreground">
<RefreshCcw className="h-8 w-8 animate-spin" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">Checking for updates...</h3>
<p className="text text-muted-foreground">
Please wait while we pull the latest version information from
Docker Hub.
</p>
</div>
</div>
</div>
)}
{isUpdateAvailable && (
<div className="rounded-lg bg-[#16254D] p-4 mb-8">
<div className="flex gap-2">
<Info className="h-5 w-5 flex-shrink-0 text-[#5B9DFF]" />
<div className="text-[#5B9DFF]">
We recommend reviewing the{" "}
<Link
href="https://github.com/Dokploy/dokploy/releases"
target="_blank"
className="text-white underline hover:text-zinc-200"
{isCustomUpdate ? (
<div className="space-y-4">
<div className="space-y-2">
<label
htmlFor="custom-image-url"
className="text-sm font-medium"
>
release notes
</Link>{" "}
for any breaking changes before updating.
Docker Image URL
</label>
<Input
id="custom-image-url"
placeholder="e.g., dokploy/dokploy:v1.0.0 or custom-registry.com/image:tag"
value={customImageUrl}
onChange={(e) => setCustomImageUrl(e.target.value)}
/>
</div>
<p className="text-sm text-muted-foreground">
Enter the full Docker image URL you want to pull and update to.
</p>
</div>
</div>
)}
) : (
<>
{/* Initial state */}
{!hasCheckedUpdate && (
<div className="mb-8">
<p className="text text-muted-foreground">
Check for new releases and update Dokploy.
<br />
<br />
We recommend checking for updates regularly to ensure you
have the latest features and security improvements.
</p>
</div>
)}
{/* Update available state */}
{isUpdateAvailable && latestVersion && (
<div className="mb-8">
<div className="inline-flex items-center gap-2 rounded-lg px-3 py-2 border border-emerald-900 bg-emerald-900 dark:bg-emerald-900/40 mb-4 w-full">
<div className="flex items-center gap-1.5">
<Download className="h-4 w-4 text-emerald-400" />
<span className="text font-medium text-emerald-400 ">
New version available:
</span>
</div>
<span className="text font-semibold text-emerald-300">
{latestVersion}
</span>
</div>
<div className="space-y-4 text-muted-foreground">
<p className="text">
A new version of the server software is available.
Consider updating if you:
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<Stars className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Want to access the latest features and improvements
</span>
</li>
<li className="flex items-start gap-2">
<Bug className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text">
Are experiencing issues that may be resolved in the
new version
</span>
</li>
</ul>
</div>
</div>
)}
{/* Up to date state */}
{hasCheckedUpdate && !isUpdateAvailable && !isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-emerald-400/40">
<Sparkles className="h-8 w-8 text-emerald-400" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
You are using the latest version
</h3>
<p className="text text-muted-foreground">
Your server is up to date with all the latest features
and security improvements.
</p>
</div>
</div>
</div>
)}
{hasCheckedUpdate && isLoading && (
<div className="mb-8">
<div className="flex flex-col items-center gap-6 mb-6">
<div className="rounded-full p-4 bg-[#5B9DFF]/40 text-foreground">
<RefreshCcw className="h-8 w-8 animate-spin" />
</div>
<div className="text-center space-y-2">
<h3 className="text-lg font-medium">
Checking for updates...
</h3>
<p className="text text-muted-foreground">
Please wait while we pull the latest version information
from Docker Hub.
</p>
</div>
</div>
</div>
)}
{isUpdateAvailable && (
<div className="rounded-lg bg-[#16254D] p-4 mb-8">
<div className="flex gap-2">
<Info className="h-5 w-5 flex-shrink-0 text-[#5B9DFF]" />
<div className="text-[#5B9DFF]">
We recommend reviewing the{" "}
<Link
href="https://github.com/Dokploy/dokploy/releases"
target="_blank"
className="text-white underline hover:text-zinc-200"
>
release notes
</Link>{" "}
for any breaking changes before updating.
</div>
</div>
</div>
)}
</>
)}
</div>
<div className="flex items-center justify-between pt-2">
<ToggleAutoCheckUpdates disabled={isLoading} />
<ToggleAutoCheckUpdates disabled={isLoading || isPullingCustom} />
</div>
<div className="space-y-4 flex items-center justify-end">
@ -258,7 +345,29 @@ export const UpdateServer = ({
<Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel
</Button>
{isUpdateAvailable ? (
{isCustomUpdate ? (
<Button
variant="secondary"
onClick={handleCustomUpdate}
disabled={
isPullingCustom || isUpdatingServer || !customImageUrl
}
>
{isPullingCustom || isUpdatingServer ? (
<>
<RefreshCcw className="h-4 w-4 animate-spin" />
{isPullingCustom
? "Pulling Image..."
: "Updating Server..."}
</>
) : (
<>
<Download className="h-4 w-4" />
Pull & Update Server
</>
)}
</Button>
) : isUpdateAvailable ? (
<UpdateWebServer />
) : (
<Button

View File

@ -47,7 +47,7 @@ export const UpdateWebServer = () => {
const handleConfirm = async () => {
try {
setUpdating(true);
await updateServer();
await updateServer({ imageUrl: "" });
// Give some time for docker service restart before starting to check status
await new Promise((resolve) => setTimeout(resolve, 8000));

View File

@ -33,7 +33,8 @@ import {
paths,
prepareEnvironmentVariables,
processLogs,
pullLatestRelease,
pullCustomImage,
pullRelease,
readConfig,
readConfigInPath,
readDirectory,
@ -376,26 +377,28 @@ export const settingsRouter = createTRPCRouter({
return await getUpdateData();
}),
updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) {
updateServer: adminProcedure
.input(z.object({ imageUrl: z.string().optional() }))
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
await pullRelease(input.imageUrl);
// This causes restart of dokploy, thus it will not finish executing properly, so don't await it
// Status after restart is checked via frontend /api/health endpoint
void spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(input.imageUrl),
"dokploy",
]);
return true;
}
await pullLatestRelease();
// This causes restart of dokploy, thus it will not finish executing properly, so don't await it
// Status after restart is checked via frontend /api/health endpoint
void spawnAsync("docker", [
"service",
"update",
"--force",
"--image",
getDokployImage(),
"dokploy",
]);
return true;
}),
}),
getDokployVersion: protectedProcedure.query(() => {
return packageInfo.version;
@ -837,6 +840,15 @@ export const settingsRouter = createTRPCRouter({
getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus();
}),
pullCustomImage: adminProcedure
.input(z.object({ imageUrl: z.string() }))
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return false;
}
console.log("pulling custom image", input.imageUrl);
return await pullCustomImage(input.imageUrl);
}),
});
export const getTraefikPorts = async (serverId?: string) => {

View File

@ -21,12 +21,13 @@ export const getDokployImageTag = () => {
return process.env.RELEASE_TAG || "latest";
};
export const getDokployImage = () => {
return `dokploy/dokploy:${getDokployImageTag()}`;
export const getDokployImage = (customImage?: string) => {
return customImage || `dokploy/dokploy:${getDokployImageTag()}`;
};
export const pullLatestRelease = async () => {
const stream = await docker.pull(getDokployImage());
export const pullRelease = async (customImage?: string) => {
const imageToPull = getDokployImage(customImage);
const stream = await docker.pull(imageToPull);
await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (err, res) =>
err ? reject(err) : resolve(res),
@ -34,6 +35,17 @@ export const pullLatestRelease = async () => {
});
};
/** Pulls a custom Docker image and returns whether it was successful */
export const pullCustomImage = async (imageUrl: string): Promise<boolean> => {
try {
await pullRelease(imageUrl);
return true;
} catch (error) {
console.error("Error pulling custom image:", error);
return false;
}
};
/** Returns Dokploy docker service image digest */
export const getServiceImageDigest = async () => {
const { stdout } = await execAsync(