mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: implement custom Docker image update functionality in server settings
- Added support for pulling and updating the server with a custom Docker image URL. - Introduced new state management for custom updates in the UpdateServer component. - Updated API to handle optional image URL input for server updates.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -834,6 +837,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) => {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user