Compare commits

...

13 Commits

Author SHA1 Message Date
Mauricio Siu
c0987295c6 feat(setup): implement Docker permissions setup and configuration for user access 2025-08-23 15:30:21 -06:00
Mauricio Siu
d74295a3b6 feat(setup): add Docker permissions setup and enhance command execution 2025-08-23 15:25:30 -06:00
Mauricio Siu
7bfceb4ee0 refactor(setup): simplify privilege checks and remove unnecessary sudo commands 2025-08-23 14:49:13 -06:00
Mauricio Siu
c1e1492622 feat(setup): enhance script to check for root privileges and add user to Docker group 2025-08-23 14:27:48 -06:00
Mauricio Siu
ff20bb2731 chore(package): bump version to v0.24.12 2025-08-19 00:31:53 -06:00
Mauricio Siu
8a802b0739 Merge pull request #2402 from gentslava/fix/scroll-gutters
fix(ui): scroll gutters stable
2025-08-19 00:21:19 -06:00
Mauricio Siu
e511173283 Merge pull request #2404 from gentslava/fix/modal-popover-handle
fix(ui): modal popover handle close
2025-08-19 00:19:44 -06:00
Mauricio Siu
763b1a344a Merge pull request #2407 from Dokploy/feat/prevent-delete-service-while-build-is-running
feat(ui): add alert blocks for running services in delete confirmatio…
2025-08-19 00:16:01 -06:00
Mauricio Siu
a4041185f1 feat(ui): add alert blocks for running services in delete confirmation dialogs 2025-08-19 00:14:50 -06:00
Vyacheslav Scherbinin
522dcd6c08 fix(ui): modal pointer events predefine 2025-08-18 23:52:30 +07:00
Vyacheslav Scherbinin
b4d5935875 fix(ui): modal popover handle close 2025-08-18 23:27:49 +07:00
Vyacheslav Scherbinin
6d9a1db8af fix(ui): scrollbar gutter stable 2025-08-17 12:36:40 +07:00
Vyacheslav Scherbinin
e3979d2c48 fix(ui): fixed viewport 2025-08-17 12:09:26 +07:00
6 changed files with 125 additions and 16 deletions

View File

@@ -7,6 +7,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -114,6 +115,12 @@ export const DeleteService = ({ id, type }: Props) => {
}
};
const isDisabled =
(data &&
"applicationStatus" in data &&
data?.applicationStatus === "running") ||
(data && "composeStatus" in data && data?.composeStatus === "running");
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
@@ -202,6 +209,12 @@ export const DeleteService = ({ id, type }: Props) => {
</form>
</Form>
</div>
{isDisabled && (
<AlertBlock type="warning" className="w-full mt-5">
Cannot delete the service while it is running. Please wait for the
build to finish and then try again.
</AlertBlock>
)}
<DialogFooter>
<Button
variant="secondary"
@@ -211,8 +224,10 @@ export const DeleteService = ({ id, type }: Props) => {
>
Cancel
</Button>
<Button
isLoading={isLoading}
disabled={isDisabled}
form="hook-form-delete-compose"
type="submit"
variant="destructive"

View File

@@ -129,10 +129,11 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 pointer-events-auto w-full max-w-lg translate-x-[-50%] translate-y-[-50%] border bg-background shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"flex flex-col max-h-[90vh]",
className,
)}
style={{ pointerEvents: "auto" }}
onInteractOutside={(event) => event.preventDefault()}
{...props}
>

View File

@@ -148,7 +148,7 @@ const SidebarProvider = React.forwardRef<
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
"group/sidebar-wrapper flex h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className,
)}
ref={ref}
@@ -329,10 +329,11 @@ const SidebarInset = React.forwardRef<
<main
ref={ref}
className={cn(
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
"relative flex min-h-svh overflow-auto w-full flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className,
)}
style={{ scrollbarGutter: "stable" }}
{...props}
/>
);

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.24.11",
"version": "v0.24.12",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -40,6 +40,7 @@ import {
RedisIcon,
} from "@/components/icons/data-tools-icons";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { AlertBlock } from "@/components/shared/alert-block";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action";
@@ -598,6 +599,13 @@ const Project = (
return sortServices(filtered);
}, [applications, searchQuery, selectedTypes, sortBy]);
const selectedServicesWithRunningStatus = useMemo(() => {
return filteredServices.filter(
(service) =>
selectedServices.includes(service.id) && service.status === "running",
);
}, [filteredServices, selectedServices]);
return (
<div>
<BreadcrumbSidebar
@@ -740,8 +748,34 @@ const Project = (
<>
<DialogAction
title="Delete Services"
description={`Are you sure you want to delete ${selectedServices.length} services? This action cannot be undone.`}
description={
<div className="space-y-3">
<p>
Are you sure you want to delete{" "}
{selectedServices.length} services? This
action cannot be undone.
</p>
{selectedServicesWithRunningStatus.length >
0 && (
<AlertBlock type="warning">
Warning:{" "}
{
selectedServicesWithRunningStatus.length
}{" "}
of the selected services are currently
running. Please stop these services
first before deleting:{" "}
{selectedServicesWithRunningStatus
.map((s) => s.name)
.join(", ")}
</AlertBlock>
)}
</div>
}
type="destructive"
disabled={
selectedServicesWithRunningStatus.length > 0
}
onClick={handleBulkDelete}
>
<Button

View File

@@ -15,6 +15,7 @@ import {
} from "@dokploy/server/setup/traefik-setup";
import slug from "slugify";
import { Client } from "ssh2";
import { encodeBase64 } from "../utils/docker/utils";
import { recreateDirectory } from "../utils/filesystem/directory";
export const slugify = (text: string | undefined) => {
@@ -74,10 +75,7 @@ SYS_ARCH=$(uname -m)
CURRENT_USER=$USER
echo "Installing requirements for: OS: $OS_TYPE"
if [ $EUID != 0 ]; then
echo "Please run this script as root or with sudo ❌"
exit
fi
# Check if the OS is manjaro, if so, change it to arch
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
@@ -146,6 +144,9 @@ ${installRClone()}
echo -e "4. Installing Docker. "
${installDocker()}
echo -e "4.1. Setting up Docker permissions"
${setupDockerPermissions()}
echo -e "5. Setting up Docker Swarm"
${setupSwarm()}
@@ -192,7 +193,15 @@ const installRequirements = async (
return new Promise<void>((resolve, reject) => {
client
.once("ready", () => {
const command = server.command || defaultCommand();
const base64Command = encodeBase64(server.command || defaultCommand());
const newCommand = `
echo "${base64Command}" | base64 -d > setup.sh
chmod +x setup.sh
sudo bash setup.sh
rm setup.sh
`;
const command = newCommand;
client.exec(command, (err, stream) => {
if (err) {
onData?.(err.message);
@@ -216,16 +225,16 @@ const installRequirements = async (
client.end();
if (err.level === "client-authentication") {
onData?.(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
"Authentication failed: Invalid SSH private key. ❌ Error: $err.message$err.level",
);
reject(
new Error(
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
"Authentication failed: Invalid SSH private key. ❌ Error: $err.message$err.level",
),
);
} else {
onData?.(`SSH connection error: ${err.message} ${err.level}`);
reject(new Error(`SSH connection error: ${err.message}`));
onData?.("SSH connection error: $err.message$err.level");
reject(new Error("SSH connection error: $err.message"));
}
})
.connect({
@@ -247,8 +256,8 @@ const setupDirectories = () => {
const chmodCommand = `chmod 700 "${SSH_PATH}"`;
const command = `
${createDirsCommand}
${chmodCommand}
$createDirsCommand
$chmodCommand
`;
return command;
@@ -526,6 +535,9 @@ const createTraefikConfig = () => {
const config = getDefaultServerTraefikConfig();
const command = `
# Create Traefik directories if they don't exist
mkdir -p /etc/dokploy/traefik/dynamic
if [ -f "/etc/dokploy/traefik/dynamic/acme.json" ]; then
chmod 600 "/etc/dokploy/traefik/dynamic/acme.json"
fi
@@ -542,6 +554,9 @@ const createTraefikConfig = () => {
const createDefaultMiddlewares = () => {
const config = getDefaultMiddlewares();
const command = `
# Ensure dynamic directory exists
mkdir -p /etc/dokploy/traefik/dynamic
if [ -f "/etc/dokploy/traefik/dynamic/middlewares.yml" ]; then
echo "Middlewares config already exists ✅"
else
@@ -561,6 +576,49 @@ export const installRClone = () => `
fi
`;
const setupDockerPermissions = () => `
# Get the original user who ran sudo (if any)
ORIGINAL_USER=\${SUDO_USER:-\$USER}
# Add user to docker group
if getent group docker > /dev/null 2>&1; then
if ! groups \$ORIGINAL_USER | grep -q docker; then
echo "Adding user \$ORIGINAL_USER to docker group..."
usermod -aG docker \$ORIGINAL_USER
echo "User \$ORIGINAL_USER added to docker group ✅"
else
echo "User \$ORIGINAL_USER already in docker group ✅"
fi
else
echo "Docker group not found, creating it..."
groupadd docker
usermod -aG docker \$ORIGINAL_USER
echo "Docker group created and user added ✅"
fi
# Configure sudo to allow docker commands without password for the user
if [ "\$ORIGINAL_USER" != "root" ]; then
echo "Configuring passwordless sudo for Docker commands..."
echo "\$ORIGINAL_USER ALL=(ALL) NOPASSWD: /usr/bin/docker, /usr/bin/docker-compose, /usr/local/bin/docker-compose" > /etc/sudoers.d/\$ORIGINAL_USER-docker
chmod 440 /etc/sudoers.d/\$ORIGINAL_USER-docker
echo "Docker sudo configuration completed ✅"
fi
# Restart docker service to ensure group changes take effect
systemctl restart docker
echo "Docker service restarted ✅"
# Create a configuration file to tell Dokploy to use sudo for Docker commands
if [ "\$ORIGINAL_USER" != "root" ]; then
echo "Creating Dokploy Docker configuration..."
mkdir -p /etc/dokploy/config
echo "USE_SUDO_FOR_DOCKER=true" > /etc/dokploy/config/docker.conf
echo "DOCKER_USER=\$ORIGINAL_USER" >> /etc/dokploy/config/docker.conf
chown -R \$ORIGINAL_USER:\$ORIGINAL_USER /etc/dokploy/config
echo "Dokploy Docker configuration created ✅"
fi
`;
export const createTraefikInstance = () => {
const command = `
# Check if dokpyloy-traefik exists