Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
7984497
Merge pull request #1785 from Dokploy/canary
Siumauricio Apr 27, 2025
19a525f
Merge pull request #1824 from Dokploy/canary
Siumauricio May 5, 2025
7be1084
Merge pull request #1828 from Dokploy/canary
Siumauricio May 5, 2025
5d5d95b
Merge pull request #1836 from Dokploy/canary
Siumauricio May 6, 2025
f337dd7
Merge pull request #1847 from Dokploy/canary
Siumauricio May 7, 2025
d779428
Merge pull request #1871 from Dokploy/canary
Siumauricio May 11, 2025
fa91a74
Merge pull request #1911 from Dokploy/canary
Siumauricio May 17, 2025
d9ffe51
Merge pull request #1920 from Dokploy/canary
Siumauricio May 18, 2025
d603654
Merge pull request #1965 from Dokploy/canary
Siumauricio May 28, 2025
6676a86
Merge pull request #2061 from Dokploy/canary
Siumauricio Jun 22, 2025
9b7abfb
Merge pull request #2063 from Dokploy/canary
Siumauricio Jun 22, 2025
65f0919
Merge pull request #2068 from Dokploy/canary
Siumauricio Jun 22, 2025
10d17de
Merge pull request #2070 from Dokploy/canary
Siumauricio Jun 22, 2025
4cbc91d
Merge pull request #2091 from Dokploy/canary
Siumauricio Jun 27, 2025
274f380
Merge pull request #2103 from Dokploy/canary
Siumauricio Jun 29, 2025
335a16b
Merge pull request #2114 from Dokploy/canary
Siumauricio Jul 2, 2025
b91067d
Merge pull request #2126 from Dokploy/canary
Siumauricio Jul 5, 2025
3b138f8
Merge pull request #2143 from Dokploy/canary
Siumauricio Jul 7, 2025
85d48ab
Merge pull request #2183 from Dokploy/canary
Siumauricio Jul 13, 2025
6c4efa4
Merge pull request #2191 from Dokploy/canary
Siumauricio Jul 14, 2025
b615d04
Merge pull request #2193 from Dokploy/canary
Siumauricio Jul 14, 2025
f9b0589
Merge pull request #2219 from Dokploy/canary
Siumauricio Jul 21, 2025
13e20e9
Merge pull request #2253 from Dokploy/canary
Siumauricio Jul 28, 2025
8b7d9c0
Merge pull request #2303 from Dokploy/canary
Siumauricio Aug 3, 2025
74caf14
Merge pull request #2323 from Dokploy/canary
Siumauricio Aug 4, 2025
fa3cdf1
Merge pull request #2324 from Dokploy/canary
Siumauricio Aug 4, 2025
fd267a6
Merge pull request #2354 from Dokploy/canary
Siumauricio Aug 10, 2025
222e487
Merge pull request #2360 from Dokploy/canary
Siumauricio Aug 11, 2025
5a46b87
Merge pull request #2390 from Dokploy/canary
Siumauricio Aug 17, 2025
d6050ce
Merge pull request #2408 from Dokploy/canary
Siumauricio Aug 19, 2025
ac8960e
Merge pull request #2483 from Dokploy/canary
Siumauricio Sep 7, 2025
976932f
Merge pull request #2557 from Dokploy/canary
Siumauricio Sep 7, 2025
ea805c1
Merge pull request #2612 from Dokploy/canary
Siumauricio Sep 16, 2025
b15ede8
Merge pull request #2658 from Dokploy/canary
Siumauricio Sep 21, 2025
76af74d
Merge pull request #2721 from Dokploy/canary
Siumauricio Sep 30, 2025
67d3e92
Merge pull request #2765 from Dokploy/canary
Siumauricio Oct 6, 2025
b45e7e4
Merge pull request #2901 from Dokploy/canary
Siumauricio Oct 26, 2025
f0ea1c8
Merge pull request #3043 from Dokploy/canary
Siumauricio Nov 19, 2025
40de13e
Merge pull request #3055 from Dokploy/canary
Siumauricio Nov 19, 2025
d1b639a
Merge pull request #3063 from Dokploy/canary
Siumauricio Nov 20, 2025
4832fd9
Merge pull request #3072 from Dokploy/canary
Siumauricio Nov 20, 2025
1c2307b
Merge pull request #3114 from Dokploy/canary
Siumauricio Nov 26, 2025
1352b85
Merge pull request #3166 from Dokploy/canary
Siumauricio Dec 8, 2025
5cd7de8
Merge pull request #3211 from Dokploy/canary
Siumauricio Dec 10, 2025
42c2076
Merge pull request #3254 from Dokploy/canary
Siumauricio Dec 13, 2025
304454b
Merge pull request #3312 from Dokploy/canary
Siumauricio Jan 2, 2026
1034c79
Merge pull request #3442 from Dokploy/canary
Siumauricio Jan 15, 2026
a177d34
Merge pull request #3456 from Dokploy/canary
Siumauricio Jan 15, 2026
1e57d48
Merge pull request #3499 from Dokploy/canary
Siumauricio Jan 27, 2026
4f57851
Merge pull request #3570 from Dokploy/canary
Siumauricio Jan 31, 2026
413ed9b
Merge pull request #3604 from Dokploy/canary
Siumauricio Feb 10, 2026
2c9ca65
Merge pull request #3668 from Dokploy/canary
Siumauricio Feb 10, 2026
5b6d80e
Merge pull request #3682 from Dokploy/canary
Siumauricio Feb 18, 2026
f24f1ad
Merge pull request #3805 from Dokploy/canary
Siumauricio Feb 27, 2026
e679a32
Merge pull request #3825 from Dokploy/canary
Siumauricio Feb 27, 2026
d4719ec
Merge pull request #3845 from Dokploy/canary
Siumauricio Mar 1, 2026
ea8e99d
Merge pull request #3875 from Dokploy/canary
Siumauricio Mar 3, 2026
628f16e
fix: update import statements to include file extensions for consistency
Siumauricio Mar 3, 2026
2362778
Merge pull request #3907 from Dokploy/canary
Siumauricio Mar 6, 2026
f3356cf
Merge pull request #3938 from Dokploy/canary
Siumauricio Mar 9, 2026
a2d6550
Merge pull request #3965 from Dokploy/canary
Siumauricio Mar 10, 2026
de3db08
Merge pull request #4020 from Dokploy/canary
Siumauricio Mar 18, 2026
4d8a2a3
Merge pull request #4029 from Dokploy/canary
Siumauricio Mar 19, 2026
9e2246d
feat(application): add scaling and rollout controls
cromulus Apr 13, 2026
496aeb8
Merge branch 'main' of https://github.com/Dokploy/dokploy into feat/z…
cromulus Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";

interface Props {
applicationId: string;
}

type DeploymentStrategy = "standard" | "zero-downtime";

type UpdateConfigSwarm = {
Parallelism?: number;
Delay?: number;
FailureAction?: string;
Monitor?: number;
MaxFailureRatio?: number;
Order?: string;
} | null;

const getDeploymentStrategy = (
updateConfigSwarm: UpdateConfigSwarm | undefined,
): DeploymentStrategy =>
updateConfigSwarm?.Order === "stop-first" ? "standard" : "zero-downtime";

const getEffectiveInstances = (
replicas?: number,
modeSwarm?: {
Replicated?: { Replicas?: number };
} | null,
) => modeSwarm?.Replicated?.Replicas ?? replicas ?? 1;

const buildUpdateConfigSwarm = (
currentUpdateConfigSwarm: UpdateConfigSwarm | undefined,
strategy: DeploymentStrategy,
) => {
const baseConfig = currentUpdateConfigSwarm ?? {
FailureAction: "rollback",
Parallelism: 1,
};

return {
...baseConfig,
Parallelism: currentUpdateConfigSwarm?.Parallelism ?? 1,
Order: strategy === "standard" ? "stop-first" : "start-first",
};
};

export const ShowScalingAndRollouts = ({ applicationId }: Props) => {
const { data: permissions } = api.user.getPermissions.useQuery();
const canUpdateService = permissions?.service.create ?? false;
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
const { mutateAsync: update } = api.application.update.useMutation();
const [instances, setInstances] = useState(1);
const [strategy, setStrategy] = useState<DeploymentStrategy>("zero-downtime");
const [isSaving, setIsSaving] = useState(false);

const effectiveInstances = getEffectiveInstances(
data?.replicas,
data?.modeSwarm,
);
const currentStrategy = getDeploymentStrategy(data?.updateConfigSwarm);
const hasHealthCheck = Boolean(data?.healthCheckSwarm);
const hasHostPublishedPorts =
(data?.ports?.some((port) => port.publishMode === "host") ||
data?.endpointSpecSwarm?.Ports?.some(
(port) => port.PublishMode === "host",
)) ??
false;
const hasCustomServiceMode = Boolean(
data?.modeSwarm?.Global ||
data?.modeSwarm?.ReplicatedJob ||
data?.modeSwarm?.GlobalJob,
);
const hasReplicatedModeOverride = Boolean(data?.modeSwarm?.Replicated);
const hasScalingOverride = hasCustomServiceMode || hasReplicatedModeOverride;
const isDirty =
instances !== effectiveInstances || strategy !== currentStrategy;

useEffect(() => {
setInstances(effectiveInstances);
setStrategy(currentStrategy);
}, [effectiveInstances, currentStrategy]);

const onSave = async () => {
if (instances < 1) {
toast.error("Instances must be at least 1");
return;
}

setIsSaving(true);
try {
await update({
applicationId,
replicas: instances,
modeSwarm: null,
updateConfigSwarm: buildUpdateConfigSwarm(
data?.updateConfigSwarm,
strategy,
),
});
toast.success("Scaling and rollout settings updated. Redeploy to apply.");
await refetch();
} catch {
toast.error("Error updating scaling and rollout settings");
} finally {
setIsSaving(false);
}
};

return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Scaling & Rollouts</CardTitle>
<CardDescription>
Control application instances and whether deploys replace containers
before or after the new task starts.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="application-instances">Instances</Label>
<Input
id="application-instances"
type="number"
min={1}
value={instances}
disabled={!canUpdateService}
onChange={(event) => {
const nextValue = Number(event.target.value);
setInstances(
Number.isNaN(nextValue) ? 1 : Math.max(1, nextValue),
);
}}
/>
<p className="text-sm text-muted-foreground">
Uses simple replicated scaling for this application.
</p>
</div>

<div className="space-y-2">
<Label htmlFor="application-deployment-strategy">
Deployment Strategy
</Label>
<Select
value={strategy}
disabled={!canUpdateService}
onValueChange={(value) =>
setStrategy(value as DeploymentStrategy)
}
>
<SelectTrigger id="application-deployment-strategy">
<SelectValue placeholder="Select a deployment strategy" />
</SelectTrigger>
<SelectContent>
<SelectItem value="standard">Standard</SelectItem>
<SelectItem value="zero-downtime">Zero Downtime</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
{strategy === "zero-downtime"
? "Starts the replacement task first. Best results require a health check."
: "Stops the current task before the replacement starts."}
</p>
</div>
</div>

{strategy === "zero-downtime" && !hasHealthCheck && (
<AlertBlock type="warning">
Zero downtime is best-effort without a health check. Configure one
in Advanced - Cluster Settings - Swarm Settings so Swarm knows when
the new task is actually ready.
</AlertBlock>
)}

{strategy === "zero-downtime" && hasHostPublishedPorts && (
<AlertBlock type="warning">
This application exposes one or more ports in <code>host</code>{" "}
mode. Start-first rollouts can still hit port-binding conflicts on a
node, so domain-routed traffic through Traefik is the safer path.
</AlertBlock>
)}

{hasScalingOverride && (
<AlertBlock type="info">
This app has custom swarm service mode settings. Saving here will
switch it back to simple replicated scaling and use the Instances
value above.
</AlertBlock>
)}

<AlertBlock type="info">
Custom health checks, delays, rollback behavior, and other raw swarm
settings still live under Advanced - Cluster Settings.
</AlertBlock>

<div className="flex flex-col gap-3 rounded-lg border p-4 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">Current effective settings</p>
<p className="text-sm text-muted-foreground">
{effectiveInstances} instance
{effectiveInstances === 1 ? "" : "s"} with{" "}
{currentStrategy === "zero-downtime"
? "start-first"
: "stop-first"}{" "}
rollouts.
</p>
<p className="text-sm text-muted-foreground">
Save changes here, then redeploy the application to apply them.
</p>
</div>
{canUpdateService && (
<Button
type="button"
onClick={onSave}
isLoading={isSaving}
disabled={!isDirty || isSaving}
>
Save Rollout Settings
</Button>
)}
</div>
</CardContent>
</Card>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
import { ShowScalingAndRollouts } from "@/components/dashboard/application/general/show-scaling-and-rollouts";
import { DialogAction } from "@/components/shared/dialog-action";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -329,6 +330,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
)}
</CardContent>
</Card>
<ShowScalingAndRollouts applicationId={applicationId} />
<ShowProviderForm applicationId={applicationId} />
<ShowBuildChooseForm applicationId={applicationId} />
</>
Expand Down
Loading