This commit is contained in:
Ayham
2025-06-22 18:25:41 +02:00
committed by GitHub
4 changed files with 262 additions and 129 deletions

View File

@@ -5,6 +5,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -45,8 +46,14 @@ export const UpdateServer = ({
const [isUpdateAvailable, setIsUpdateAvailable] = useState( const [isUpdateAvailable, setIsUpdateAvailable] = useState(
!!updateData?.updateAvailable, !!updateData?.updateAvailable,
); );
const [customImageUrl, setCustomImageUrl] = useState("");
const [isCustomUpdate, setIsCustomUpdate] = useState(false);
const { mutateAsync: getUpdateData, isLoading } = const { mutateAsync: getUpdateData, isLoading } =
api.settings.getUpdateData.useMutation(); 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: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { data: releaseTag } = api.settings.getReleaseTag.useQuery(); const { data: releaseTag } = api.settings.getReleaseTag.useQuery();
const [latestVersion, setLatestVersion] = useState( 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 isOpen = isOpenInternal || isOpenProp;
const onOpenChange = (open: boolean) => { const onOpenChange = (open: boolean) => {
setIsOpenInternal(open); setIsOpenInternal(open);
@@ -141,6 +183,46 @@ export const UpdateServer = ({
)} )}
</div> </div>
<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>
{isCustomUpdate ? (
<div className="space-y-4">
<div className="space-y-2">
<label
htmlFor="custom-image-url"
className="text-sm font-medium"
>
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>
) : (
<>
{/* Initial state */} {/* Initial state */}
{!hasCheckedUpdate && ( {!hasCheckedUpdate && (
<div className="mb-8"> <div className="mb-8">
@@ -148,8 +230,8 @@ export const UpdateServer = ({
Check for new releases and update Dokploy. Check for new releases and update Dokploy.
<br /> <br />
<br /> <br />
We recommend checking for updates regularly to ensure you have the We recommend checking for updates regularly to ensure you
latest features and security improvements. have the latest features and security improvements.
</p> </p>
</div> </div>
)} )}
@@ -171,8 +253,8 @@ export const UpdateServer = ({
<div className="space-y-4 text-muted-foreground"> <div className="space-y-4 text-muted-foreground">
<p className="text"> <p className="text">
A new version of the server software is available. Consider A new version of the server software is available.
updating if you: Consider updating if you:
</p> </p>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
@@ -184,8 +266,8 @@ export const UpdateServer = ({
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<Bug className="h-5 w-5 mt-0.5 text-[#5B9DFF]" /> <Bug className="h-5 w-5 mt-0.5 text-[#5B9DFF]" />
<span className="text"> <span className="text">
Are experiencing issues that may be resolved in the new Are experiencing issues that may be resolved in the
version new version
</span> </span>
</li> </li>
</ul> </ul>
@@ -205,8 +287,8 @@ export const UpdateServer = ({
You are using the latest version You are using the latest version
</h3> </h3>
<p className="text text-muted-foreground"> <p className="text text-muted-foreground">
Your server is up to date with all the latest features and Your server is up to date with all the latest features
security improvements. and security improvements.
</p> </p>
</div> </div>
</div> </div>
@@ -220,10 +302,12 @@ export const UpdateServer = ({
<RefreshCcw className="h-8 w-8 animate-spin" /> <RefreshCcw className="h-8 w-8 animate-spin" />
</div> </div>
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<h3 className="text-lg font-medium">Checking for updates...</h3> <h3 className="text-lg font-medium">
Checking for updates...
</h3>
<p className="text text-muted-foreground"> <p className="text text-muted-foreground">
Please wait while we pull the latest version information from Please wait while we pull the latest version information
Docker Hub. from Docker Hub.
</p> </p>
</div> </div>
</div> </div>
@@ -248,9 +332,12 @@ export const UpdateServer = ({
</div> </div>
</div> </div>
)} )}
</>
)}
</div>
<div className="flex items-center justify-between pt-2"> <div className="flex items-center justify-between pt-2">
<ToggleAutoCheckUpdates disabled={isLoading} /> <ToggleAutoCheckUpdates disabled={isLoading || isPullingCustom} />
</div> </div>
<div className="space-y-4 flex items-center justify-end"> <div className="space-y-4 flex items-center justify-end">
@@ -258,7 +345,29 @@ export const UpdateServer = ({
<Button variant="outline" onClick={() => onOpenChange?.(false)}> <Button variant="outline" onClick={() => onOpenChange?.(false)}>
Cancel Cancel
</Button> </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 /> <UpdateWebServer />
) : ( ) : (
<Button <Button

View File

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

View File

@@ -33,7 +33,8 @@ import {
paths, paths,
prepareEnvironmentVariables, prepareEnvironmentVariables,
processLogs, processLogs,
pullLatestRelease, pullCustomImage,
pullRelease,
readConfig, readConfig,
readConfigInPath, readConfigInPath,
readDirectory, readDirectory,
@@ -376,12 +377,14 @@ export const settingsRouter = createTRPCRouter({
return await getUpdateData(); return await getUpdateData();
}), }),
updateServer: adminProcedure.mutation(async () => { updateServer: adminProcedure
.input(z.object({ imageUrl: z.string().optional() }))
.mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await pullLatestRelease(); await pullRelease(input.imageUrl);
// This causes restart of dokploy, thus it will not finish executing properly, so don't await it // 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 // Status after restart is checked via frontend /api/health endpoint
@@ -390,7 +393,7 @@ export const settingsRouter = createTRPCRouter({
"update", "update",
"--force", "--force",
"--image", "--image",
getDokployImage(), getDokployImage(input.imageUrl),
"dokploy", "dokploy",
]); ]);
@@ -837,6 +840,15 @@ export const settingsRouter = createTRPCRouter({
getLogCleanupStatus: adminProcedure.query(async () => { getLogCleanupStatus: adminProcedure.query(async () => {
return getLogCleanupStatus(); 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) => { export const getTraefikPorts = async (serverId?: string) => {

View File

@@ -21,12 +21,13 @@ export const getDokployImageTag = () => {
return process.env.RELEASE_TAG || "latest"; return process.env.RELEASE_TAG || "latest";
}; };
export const getDokployImage = () => { export const getDokployImage = (customImage?: string) => {
return `dokploy/dokploy:${getDokployImageTag()}`; return customImage || `dokploy/dokploy:${getDokployImageTag()}`;
}; };
export const pullLatestRelease = async () => { export const pullRelease = async (customImage?: string) => {
const stream = await docker.pull(getDokployImage()); const imageToPull = getDokployImage(customImage);
const stream = await docker.pull(imageToPull);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
docker.modem.followProgress(stream, (err, res) => docker.modem.followProgress(stream, (err, res) =>
err ? reject(err) : resolve(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 */ /** Returns Dokploy docker service image digest */
export const getServiceImageDigest = async () => { export const getServiceImageDigest = async () => {
const { stdout } = await execAsync( const { stdout } = await execAsync(