Merge branch 'canary' into feat/shared-ssh

This commit is contained in:
Mauricio Siu
2024-07-26 01:14:55 -06:00
committed by GitHub
21 changed files with 253 additions and 427 deletions

View File

@@ -28,84 +28,103 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react";
import { PlusIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod";
// const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/; import { domain } from "@/server/db/validations";
// .regex(hostnameRegex import { zodResolver } from "@hookform/resolvers/zod";
const addDomain = z.object({ import type z from "zod";
host: z.string().min(1, "Hostname is required"),
path: z.string().min(1),
port: z.number(),
https: z.boolean(),
certificateType: z.enum(["letsencrypt", "none"]),
});
type AddDomain = z.infer<typeof addDomain>; type Domain = z.infer<typeof domain>;
interface Props { interface Props {
applicationId: string; applicationId: string;
children?: React.ReactNode; domainId?: string;
children: React.ReactNode;
} }
export const AddDomain = ({ export const AddDomain = ({
applicationId, applicationId,
children = <PlusIcon className="h-4 w-4" />, domainId = "",
children,
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
const { mutateAsync, isError, error } = api.domain.create.useMutation(); {
domainId,
const form = useForm<AddDomain>({
defaultValues: {
host: "",
https: false,
path: "/",
port: 3000,
certificateType: "none",
}, },
resolver: zodResolver(addDomain), {
enabled: !!domainId,
},
);
const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const form = useForm<Domain>({
resolver: zodResolver(domain),
}); });
useEffect(() => { useEffect(() => {
form.reset(); if (data) {
}, [form, form.reset, form.formState.isSubmitSuccessful]); form.reset({
...data,
/* Convert null to undefined */
path: data?.path || undefined,
port: data?.port || undefined,
});
}
const onSubmit = async (data: AddDomain) => { if (!domainId) {
form.reset({});
}
}, [form, form.reset, data, isLoading]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
: "In this section you can add domains",
};
const onSubmit = async (data: Domain) => {
await mutateAsync({ await mutateAsync({
domainId,
applicationId, applicationId,
host: data.host, ...data,
https: data.https,
path: data.path,
port: data.port,
certificateType: data.certificateType,
}) })
.then(async () => { .then(async () => {
toast.success("Domain Created"); toast.success(dictionary.success);
await utils.domain.byApplicationId.invalidate({ await utils.domain.byApplicationId.invalidate({
applicationId, applicationId,
}); });
await utils.application.readTraefikConfig.invalidate({ applicationId }); await utils.application.readTraefikConfig.invalidate({ applicationId });
if (domainId) {
refetch();
}
setIsOpen(false);
}) })
.catch(() => { .catch(() => {
toast.error("Error to create the domain"); toast.error(dictionary.error);
}); });
}; };
return ( return (
<Dialog> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild> <DialogTrigger className="" asChild>
<Button>{children}</Button> {children}
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl"> <DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Domain</DialogTitle> <DialogTitle>Domain</DialogTitle>
<DialogDescription> <DialogDescription>{dictionary.dialogDescription}</DialogDescription>
In this section you can add custom domains
</DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
@@ -169,33 +188,36 @@ export const AddDomain = ({
); );
}} }}
/> />
<FormField {form.getValues().https && (
control={form.control} <FormField
name="certificateType" control={form.control}
render={({ field }) => ( name="certificateType"
<FormItem className="col-span-2"> render={({ field }) => (
<FormLabel>Certificate</FormLabel> <FormItem className="col-span-2">
<Select <FormLabel>Certificate</FormLabel>
onValueChange={field.onChange} <Select
defaultValue={field.value || ""} onValueChange={field.onChange}
> defaultValue={field.value || ""}
<FormControl> >
<SelectTrigger> <FormControl>
<SelectValue placeholder="Select a certificate" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Select a certificate" />
</FormControl> </SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="https" name="https"
@@ -206,6 +228,7 @@ export const AddDomain = ({
<FormDescription> <FormDescription>
Automatically provision SSL Certificate. Automatically provision SSL Certificate.
</FormDescription> </FormDescription>
<FormMessage />
</div> </div>
<FormControl> <FormControl>
<Switch <Switch
@@ -226,7 +249,7 @@ export const AddDomain = ({
form="hook-form" form="hook-form"
type="submit" type="submit"
> >
Create {dictionary.submit}
</Button> </Button>
</DialogFooter> </DialogFooter>
</Form> </Form>

View File

@@ -8,13 +8,11 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ExternalLink, GlobeIcon, RefreshCcw } from "lucide-react"; import { ExternalLink, GlobeIcon, PenBoxIcon } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import React from "react";
import { AddDomain } from "./add-domain"; import { AddDomain } from "./add-domain";
import { DeleteDomain } from "./delete-domain"; import { DeleteDomain } from "./delete-domain";
import { GenerateDomain } from "./generate-domain"; import { GenerateDomain } from "./generate-domain";
import { UpdateDomain } from "./update-domain";
interface Props { interface Props {
applicationId: string; applicationId: string;
@@ -43,7 +41,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">
{data && data?.length > 0 && ( {data && data?.length > 0 && (
<AddDomain applicationId={applicationId}> <AddDomain applicationId={applicationId}>
<GlobeIcon className="size-4" /> Add Domain <Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomain> </AddDomain>
)} )}
{data && data?.length > 0 && ( {data && data?.length > 0 && (
@@ -61,7 +61,9 @@ export const ShowDomains = ({ applicationId }: Props) => {
</span> </span>
<div className="flex flex-row gap-4 flex-wrap"> <div className="flex flex-row gap-4 flex-wrap">
<AddDomain applicationId={applicationId}> <AddDomain applicationId={applicationId}>
<GlobeIcon className="size-4" /> Add Domain <Button>
<GlobeIcon className="size-4" /> Add Domain
</Button>
</AddDomain> </AddDomain>
<GenerateDomain applicationId={applicationId} /> <GenerateDomain applicationId={applicationId} />
@@ -90,7 +92,14 @@ export const ShowDomains = ({ applicationId }: Props) => {
{item.https ? "HTTPS" : "HTTP"} {item.https ? "HTTPS" : "HTTP"}
</Button> </Button>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
<UpdateDomain domainId={item.domainId} /> <AddDomain
applicationId={applicationId}
domainId={item.domainId}
>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</AddDomain>
<DeleteDomain domainId={item.domainId} /> <DeleteDomain domainId={item.domainId} />
</div> </div>
</div> </div>

View File

@@ -1,254 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
const updateDomain = z.object({
host: z.string().regex(hostnameRegex, { message: "Invalid hostname" }),
path: z.string().min(1),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" }),
https: z.boolean(),
certificateType: z.enum(["letsencrypt", "none"]),
});
type UpdateDomain = z.infer<typeof updateDomain>;
interface Props {
domainId: string;
}
export const UpdateDomain = ({ domainId }: Props) => {
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
{
domainId,
},
{
enabled: !!domainId,
},
);
const { mutateAsync, isError, error } = api.domain.update.useMutation();
const form = useForm<UpdateDomain>({
defaultValues: {
host: "",
https: true,
path: "/",
port: 3000,
certificateType: "none",
},
resolver: zodResolver(updateDomain),
});
useEffect(() => {
if (data) {
form.reset({
host: data.host || "",
port: data.port || 3000,
path: data.path || "/",
https: data.https,
certificateType: data.certificateType,
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateDomain) => {
await mutateAsync({
domainId,
host: data.host,
https: data.https,
path: data.path,
port: data.port,
certificateType: data.certificateType,
})
.then(async (data) => {
toast.success("Domain Updated");
await refetch();
await utils.domain.byApplicationId.invalidate({
applicationId: data?.applicationId,
});
await utils.application.readTraefikConfig.invalidate({
applicationId: data?.applicationId,
});
})
.catch(() => {
toast.error("Error to update the domain");
});
};
return (
<Dialog>
<DialogTrigger className="" asChild>
<Button variant="ghost">
<PenBoxIcon className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>
In this section you can add custom domains
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder={"/"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<Input
placeholder={"3000"}
{...field}
onChange={(e) => {
field.onChange(Number.parseInt(e.target.value));
}}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value={"none"}>None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
</div>
</form>
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -23,8 +23,11 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
cursorBlink: true, cursorBlink: true,
cols: 80, cols: 80,
rows: 30, rows: 30,
lineHeight: 1.4, lineHeight: 1.25,
fontWeight: 400, fontWeight: 400,
fontSize: 14,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
convertEol: true, convertEol: true,
theme: { theme: {

View File

@@ -90,6 +90,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
form, form,
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
ip,
]); ]);
return ( return (
<> <>

View File

@@ -90,6 +90,7 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseUser, data?.databaseUser,
ip,
]); ]);
return ( return (

View File

@@ -91,6 +91,7 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
data?.databaseName, data?.databaseName,
data?.databaseUser, data?.databaseUser,
form, form,
ip,
]); ]);
return ( return (
<> <>

View File

@@ -92,6 +92,7 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
data?.databasePassword, data?.databasePassword,
form, form,
data?.databaseName, data?.databaseName,
ip,
]); ]);
return ( return (

View File

@@ -85,7 +85,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
}; };
setConnectionUrl(buildConnectionUrl()); setConnectionUrl(buildConnectionUrl());
}, [data?.appName, data?.externalPort, data?.databasePassword, form]); }, [data?.appName, data?.externalPort, data?.databasePassword, form, ip]);
return ( return (
<> <>
<div className="flex w-full flex-col gap-5 "> <div className="flex w-full flex-col gap-5 ">

View File

@@ -30,11 +30,6 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
exit 1 exit 1
fi fi
command_exists() { command_exists() {
command -v "$@" > /dev/null 2>&1 command -v "$@" > /dev/null 2>&1
} }
@@ -46,7 +41,25 @@ else
fi fi
docker swarm leave --force 2>/dev/null docker swarm leave --force 2>/dev/null
docker swarm init;
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv4" ]; then
echo "$ipv4"
else
# Try to get IPv6
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
if [ -n "$ipv6" ]; then
echo "$ipv6"
fi
fi
}
advertise_addr=$(get_ip)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized" echo "Swarm initialized"
@@ -71,19 +84,28 @@ docker service create \
--publish published=3000,target=3000,mode=host \ --publish published=3000,target=3000,mode=host \
--update-parallelism 1 \ --update-parallelism 1 \
--update-order stop-first \ --update-order stop-first \
--constraint 'node.role == manager' \
dokploy/dokploy:latest dokploy/dokploy:latest
public_ip=$(hostname -I | awk '{print $1}')
GREEN="\033[0;32m" GREEN="\033[0;32m"
YELLOW="\033[1;33m" YELLOW="\033[1;33m"
BLUE="\033[0;34m" BLUE="\033[0;34m"
NC="\033[0m" # No Color NC="\033[0m" # No Color
format_ip_for_url() {
local ip="$1"
if echo "$ip" | grep -q ':'; then
# IPv6
echo "[${ip}]"
else
# IPv4
echo "${ip}"
fi
}
formatted_addr=$(format_ip_for_url "$advertise_addr")
echo "" echo ""
printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n" printf "${GREEN}Congratulations, Dokploy is installed!${NC}\n"
printf "${BLUE}Wait 15 seconds for the server to start${NC}\n" printf "${BLUE}Wait 15 seconds for the server to start${NC}\n"
printf "${YELLOW}Please go to http://${public_ip}:3000${NC}\n\n" printf "${YELLOW}Please go to http://${formatted_addr}:3000${NC}\n\n"
echo "" echo ""

View File

@@ -1,8 +1,8 @@
import { domain } from "@/server/db/validations";
import { relations } from "drizzle-orm"; import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core"; import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { z } from "zod";
import { applications } from "./application"; import { applications } from "./application";
import { certificateType } from "./shared"; import { certificateType } from "./shared";
@@ -31,27 +31,17 @@ export const domainsRelations = relations(domains, ({ one }) => ({
references: [applications.applicationId], references: [applications.applicationId],
}), }),
})); }));
const hostnameRegex = /^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/;
const createSchema = createInsertSchema(domains, {
domainId: z.string().min(1),
host: z.string().min(1),
path: z.string().min(1),
port: z.number(),
https: z.boolean(),
applicationId: z.string(),
certificateType: z.enum(["letsencrypt", "none"]),
});
export const apiCreateDomain = createSchema const createSchema = createInsertSchema(domains, domain._def.schema.shape);
.pick({
host: true, export const apiCreateDomain = createSchema.pick({
path: true, host: true,
port: true, path: true,
https: true, port: true,
applicationId: true, https: true,
certificateType: true, applicationId: true,
}) certificateType: true,
.required(); });
export const apiFindDomain = createSchema export const apiFindDomain = createSchema
.pick({ .pick({
@@ -59,19 +49,16 @@ export const apiFindDomain = createSchema
}) })
.required(); .required();
export const apiFindDomainByApplication = createSchema export const apiFindDomainByApplication = createSchema.pick({
.pick({ applicationId: true,
applicationId: true, });
})
.required();
export const apiUpdateDomain = createSchema export const apiUpdateDomain = createSchema
.pick({ .pick({
domainId: true,
host: true, host: true,
path: true, path: true,
port: true, port: true,
https: true, https: true,
certificateType: true, certificateType: true,
}) })
.required(); .merge(createSchema.pick({ domainId: true }).required());

View File

@@ -33,3 +33,28 @@ export const sshKeyUpdate = sshKeyCreate.pick({
}); });
export const sshKeyType = z.enum(["rsa", "ed25519"]).optional(); export const sshKeyType = z.enum(["rsa", "ed25519"]).optional();
export const domain = z
.object({
host: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9\.-]*\.[a-zA-Z]{2,}$/, {
message: "Invalid hostname",
}),
path: z.string().min(1).optional(),
port: z
.number()
.min(1, { message: "Port must be at least 1" })
.max(65535, { message: "Port must be 65535 or below" })
.optional(),
https: z.boolean().optional(),
certificateType: z.enum(["letsencrypt", "none"]).optional(),
})
.superRefine((input, ctx) => {
if (input.https && !input.certificateType) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["certificateType"],
message: "Required",
});
}
});

View File

@@ -1,4 +1,4 @@
import { existsSync, mkdirSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { import {
APPLICATIONS_PATH, APPLICATIONS_PATH,
BASE_PATH, BASE_PATH,
@@ -32,6 +32,9 @@ export const setupDirectories = () => {
for (const dir of directories) { for (const dir of directories) {
try { try {
createDirectoryIfNotExist(dir); createDirectoryIfNotExist(dir);
if (dir === SSH_PATH) {
chmodSync(SSH_PATH, "600");
}
} catch (error) { } catch (error) {
console.log(error, " On path: ", dir); console.log(error, " On path: ", dir);
} }

View File

@@ -1,4 +1,4 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import type { CreateServiceOptions } from "dockerode"; import type { CreateServiceOptions } from "dockerode";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
@@ -86,6 +86,7 @@ export const initializeTraefik = async () => {
export const createDefaultServerTraefikConfig = () => { export const createDefaultServerTraefikConfig = () => {
const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml"); const configFilePath = path.join(DYNAMIC_TRAEFIK_PATH, "dokploy.yml");
if (existsSync(configFilePath)) { if (existsSync(configFilePath)) {
console.log("Default traefik config already exists"); console.log("Default traefik config already exists");
return; return;
@@ -125,6 +126,11 @@ export const createDefaultServerTraefikConfig = () => {
export const createDefaultTraefikConfig = () => { export const createDefaultTraefikConfig = () => {
const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml"); const mainConfig = path.join(MAIN_TRAEFIK_PATH, "traefik.yml");
const acmeJsonPath = path.join(DYNAMIC_TRAEFIK_PATH, "acme.json");
if (existsSync(acmeJsonPath)) {
chmodSync(acmeJsonPath, "600");
}
if (existsSync(mainConfig)) { if (existsSync(mainConfig)) {
console.log("Main config already exists"); console.log("Main config already exists");
return; return;

View File

@@ -1,9 +1,8 @@
import type { WriteStream } from "node:fs"; import type { WriteStream } from "node:fs";
import { docker } from "@/server/constants"; import { prepareEnvironmentVariables } from "@/server/utils/docker/utils";
import { prepareBuildArgs } from "@/server/utils/docker/utils";
import * as tar from "tar-fs";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync";
import { createEnvFile } from "./utils"; import { createEnvFile } from "./utils";
export const buildCustomDocker = async ( export const buildCustomDocker = async (
@@ -14,29 +13,30 @@ export const buildCustomDocker = async (
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
const contextPath = const contextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const tarStream = tar.pack(contextPath); const args = prepareEnvironmentVariables(buildArgs);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
createEnvFile(dockerFilePath, env); createEnvFile(dockerFilePath, env);
await spawnAsync(
const stream = await docker.buildImage(tarStream, { "docker",
t: image, commandArgs,
buildargs: prepareBuildArgs(buildArgs), (data) => {
dockerfile: dockerFilePath.substring(dockerFilePath.lastIndexOf("/") + 1), if (writeStream.writable) {
}); writeStream.write(data);
}
await new Promise((resolve, reject) => { },
docker.modem.followProgress( {
stream, cwd: contextPath,
(err, res) => (err ? reject(err) : resolve(res)), },
(event) => { );
if (event.stream) {
writeStream.write(event.stream);
}
},
);
});
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -102,12 +102,6 @@
} }
} }
#terminal span {
font-family: "Inter", sans-serif;
font-weight: 500;
letter-spacing: 0px !important;
}
/* Codemirror */ /* Codemirror */
.cm-editor { .cm-editor {
@apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none; @apply w-full h-full rounded-md overflow-hidden border border-solid border-border outline-none;

View File

@@ -1,18 +1,18 @@
version: "3.8" version: "3.8"
services: services:
appsmith: appsmith:
image: index.docker.io/appsmith/appsmith-ee:v1.29 image: index.docker.io/appsmith/appsmith-ee:v1.29
networks: networks:
- dokploy-network - dokploy-network
ports: ports:
- ${APP_SMITH_PORT} - ${APP_SMITH_PORT}
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)" - "traefik.http.routers.${HASH}.rule=Host(`${APP_SMITH_HOST}`)"
- "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}" - "traefik.http.services.${HASH}.loadbalancer.server.port=${APP_SMITH_PORT}"
volumes: volumes:
- ./stacks:/appsmith-stacks - ../files/stacks:/appsmith-stacks
networks: networks:
dokploy-network: dokploy-network:
external: true external: true

View File

@@ -23,8 +23,8 @@ services:
ports: ports:
- 8055 - 8055
volumes: volumes:
- ./uploads:/directus/uploads - ../files/uploads:/directus/uploads
- ./extensions:/directus/extensions - ../files/extensions:/directus/extensions
depends_on: depends_on:
- cache - cache
- database - database
@@ -53,4 +53,4 @@ networks:
dokploy-network: dokploy-network:
external: true external: true
volumes: volumes:
directus: directus:

View File

@@ -23,10 +23,15 @@ services:
networks: networks:
- dokploy-network - dokploy-network
volumes: volumes:
- ./config.toml:/listmonk/config.toml - ../files/config.toml:/listmonk/config.toml
depends_on: depends_on:
- db - db
command: [sh, -c, "sleep 3 && ./listmonk --install --idempotent --yes --config config.toml"] command:
[
sh,
-c,
"sleep 3 && ./listmonk --install --idempotent --yes --config config.toml",
]
app: app:
restart: unless-stopped restart: unless-stopped
@@ -41,7 +46,7 @@ services:
- db - db
- setup - setup
volumes: volumes:
- ./config.toml:/listmonk/config.toml - ../files/config.toml:/listmonk/config.toml
labels: labels:
- "traefik.enable=true" - "traefik.enable=true"
- "traefik.http.routers.${HASH}.rule=Host(`${LISTMONK_HOST}`)" - "traefik.http.routers.${HASH}.rule=Host(`${LISTMONK_HOST}`)"
@@ -50,7 +55,7 @@ services:
volumes: volumes:
listmonk-data: listmonk-data:
driver: local driver: local
networks: networks:
dokploy-network: dokploy-network:
external: true external: true

View File

@@ -1,4 +1,4 @@
version: '3.8' version: "3.8"
services: services:
web: web:
image: odoo:16.0 image: odoo:16.0
@@ -18,8 +18,8 @@ services:
- "traefik.http.services.${HASH}.loadbalancer.server.port=${ODOO_PORT}" - "traefik.http.services.${HASH}.loadbalancer.server.port=${ODOO_PORT}"
volumes: volumes:
- odoo-web-data:/var/lib/odoo - odoo-web-data:/var/lib/odoo
- ./config:/etc/odoo - ../files/config:/etc/odoo
- ./addons:/mnt/extra-addons - ../files/addons:/mnt/extra-addons
db: db:
image: postgres:13 image: postgres:13
@@ -36,7 +36,6 @@ volumes:
odoo-web-data: odoo-web-data:
odoo-db-data: odoo-db-data:
networks: networks:
dokploy-network: dokploy-network:
external: true external: true

View File

@@ -18,8 +18,8 @@ services:
volumes: volumes:
- event-data:/var/lib/clickhouse - event-data:/var/lib/clickhouse
- event-logs:/var/log/clickhouse-server - event-logs:/var/log/clickhouse-server
- ./clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro - ../files/clickhouse/clickhouse-config.xml:/etc/clickhouse-server/config.d/logging.xml:ro
- ./clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro - ../files/clickhouse/clickhouse-user-config.xml:/etc/clickhouse-server/users.d/logging.xml:ro
ulimits: ulimits:
nofile: nofile:
soft: 262144 soft: 262144
@@ -50,7 +50,7 @@ volumes:
driver: local driver: local
event-logs: event-logs:
driver: local driver: local
networks: networks:
dokploy-network: dokploy-network:
external: true external: true