mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
Compare commits
13 Commits
v0.24.11
...
1126-allow
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0987295c6 | ||
|
|
d74295a3b6 | ||
|
|
7bfceb4ee0 | ||
|
|
c1e1492622 | ||
|
|
ff20bb2731 | ||
|
|
8a802b0739 | ||
|
|
e511173283 | ||
|
|
763b1a344a | ||
|
|
a4041185f1 | ||
|
|
522dcd6c08 | ||
|
|
b4d5935875 | ||
|
|
6d9a1db8af | ||
|
|
e3979d2c48 |
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.24.11",
|
||||
"version": "v0.24.12",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user