Compare commits

..

24 Commits

Author SHA1 Message Date
Mauricio Siu
029eed7755 refactor(dashboard): improve layout and spacing in ShowConvertedCompose component 2025-08-03 18:10:19 -06:00
Mauricio Siu
f017536396 chore(package): bump version to v0.24.7 2025-08-03 18:04:24 -06:00
Mauricio Siu
ba5505cf81 feat(auth): add logger configuration to disable logging in production environment 2025-08-03 18:04:09 -06:00
Mauricio Siu
5ff5da9ff9 Merge pull request #2321 from Dokploy/1738-database-placement-constraints
feat(cluster-settings): Add swarm settings for databases
2025-08-03 17:41:08 -06:00
Mauricio Siu
e96a8ea4ad feat(databases): increment ForceUpdate in TaskTemplate for service updates across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 17:39:35 -06:00
Mauricio Siu
42864d2472 feat(docker): update generateConfigContainer to accept Partial<ApplicationNested> and enhance mount checks 2025-08-03 17:21:09 -06:00
Mauricio Siu
ae25ea265c feat(databases): enhance database service configuration by integrating health checks, restart policies, and additional settings across MariaDB, MongoDB, MySQL, Postgres, and Redis 2025-08-03 16:52:43 -06:00
Mauricio Siu
0755f28307 refactor(cluster-settings): update dialog content and improve layout in swarm settings component 2025-08-03 16:45:07 -06:00
Mauricio Siu
3d10d48425 fix(cluster-settings): ensure data validation for registryId in form defaults 2025-08-03 16:40:24 -06:00
Mauricio Siu
6e79183f6a feat(cluster-settings): refactor cluster settings components to support multiple database types and add new swarm configuration fields 2025-08-03 16:36:14 -06:00
Mauricio Siu
2318fb062a Merge pull request #2320 from Dokploy/2004-how-to-pause-log-output-in-doploy
feat(docker-logs): add pause/resume functionality for log streaming
2025-08-03 15:45:54 -06:00
Mauricio Siu
14b4bc9d85 feat(docker-logs): add pause/resume functionality for log streaming with message buffering 2025-08-03 15:43:53 -06:00
Mauricio Siu
4c72f1894c Merge pull request #2319 from Dokploy/1614-sort-project-alphabetically-and-by-other-criteria
feat(show-projects): implement project sorting and filtering function…
2025-08-03 15:25:52 -06:00
Mauricio Siu
ebd632df04 feat(show-projects): implement project sorting and filtering functionality 2025-08-03 15:24:56 -06:00
Mauricio Siu
4878ed2b6f Merge pull request #2318 from Dokploy/2223-local-deployment-doesnt-replace-old-container
feat(handle-ports): add publish mode warning for host mode limitations
2025-08-03 15:15:05 -06:00
Mauricio Siu
1d3ab2bafa feat(handle-ports): add publish mode warning for host mode limitations 2025-08-03 15:13:11 -06:00
Mauricio Siu
7cb7cfa2a8 feat(pr-template): add a pull request template to standardize submissions 2025-08-03 13:38:17 -06:00
Mauricio Siu
6009697710 Merge pull request #2312 from JamBalaya56562/create-pr
ci(create-pr): remove unused id
2025-08-03 13:30:29 -06:00
Mauricio Siu
6be86b49bb Merge pull request #2292 from JamBalaya56562/lint-shared
refactor: lint apps/components/shared files
2025-08-03 13:27:11 -06:00
Mauricio Siu
1e81244e0b docs(contributing): update pull request guidelines and clarify branch roles 2025-08-03 13:21:02 -06:00
Mauricio Siu
f659ea463d Merge pull request #2311 from JamBalaya56562/contributing
docs(contributing): use Alerts syntax of GitHub markdown
2025-08-03 13:17:16 -06:00
JamBalaya56562
caf57276a4 ci(create-pr): remove unused id 2025-08-03 18:09:33 +09:00
JamBalaya56562
15c6c7e657 docs(contributing): use Alerts syntax of GitHub markdown 2025-08-03 17:50:29 +09:00
JamBalaya56562
8be0db385a refactor: lint apps/components/shared files 2025-08-03 17:25:06 +09:00
36 changed files with 7422 additions and 425 deletions

21
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,21 @@
## What is this PR about?
Please describe in a short paragraph what this PR is about.
## Checklist
Before submitting this PR, please make sure that:
- [ ] You created a dedicated branch based on the `canary` branch.
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [ ] You have tested this PR in your local instance.
## Issues related (if applicable)
Close automatically the related issues using the keywords: `closes #ISSUE_NUMBER`, `fixes #ISSUE_NUMBER`, `resolves #ISSUE_NUMBER`
Example: `closes #123`
## Screenshots (if applicable)
If you include a video or screenshot, would be awesome so we can see the changes in action.

View File

@@ -19,17 +19,14 @@ jobs:
fetch-depth: 0
- name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag
id: latest_tag
run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG
- name: Compare versions
id: compare_versions
run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true"
@@ -42,7 +39,6 @@ jobs:
echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists
id: check_pr
run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV

View File

@@ -87,7 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
> [!NOTE]
> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
pnpm run reset-password
```
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/)
```bash
bunx lt --port 3000
pnpm dlx localtunnel --port 3000
```
If you run into permission issues of docker run the following command
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release.
- The `canary` branch is the source of truth and should always reflect the latest stable release.
- Create a new branch for each feature or bug fix.
- Make sure to add tests for your changes.
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
- If your pull request fixes an open issue, please reference the issue in the pull request description.
- Once your pull request is merged, you will be automatically added as a contributor to the project.
**Important Considerations for Pull Requests:**
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
Thank you for your contribution!
## Templates

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
@@ -26,12 +32,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const HealthCheckSwarmSchema = z
.object({
@@ -181,21 +181,38 @@ const addSwarmSettings = z.object({
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
interface Props {
applicationId: string;
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
}
export const AddSwarmSettings = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery(
{
applicationId,
},
{
enabled: !!applicationId,
},
);
export const AddSwarmSettings = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync, isError, error, isLoading } =
api.application.update.useMutation();
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddSwarmSettings>({
defaultValues: {
@@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddSwarmSettings) => {
await mutateAsync({
applicationId,
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
healthCheckSwarm: data.healthCheckSwarm,
restartPolicySwarm: data.restartPolicySwarm,
placementSwarm: data.placementSwarm,
@@ -270,7 +292,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
Swarm Settings
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl p-0">
<DialogContent className="sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Swarm Settings</DialogTitle>
<DialogDescription>
@@ -278,10 +300,10 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4">
<div>
<AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring
to be unavailable.
Changing settings such as placements may cause the logs/monitoring,
backups and other features to be unavailable.
</AlertBlock>
</div>
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
<form
id="hook-form-add-permissions"
onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative"
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4"
>
<FormField
control={form.control}
name="healthCheckSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Health Check</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="restartPolicySwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Restart Policy</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="placementSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Placement</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="updateConfigSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Update Config</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="rollbackConfigSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Rollback Config</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="modeSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Mode</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="networkSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
<FormItem className="relative ">
<FormLabel>Network</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>
@@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control}
name="labelsSwarm"
render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 ">
<FormItem className="relative ">
<FormLabel>Labels</FormLabel>
<TooltipProvider delayDuration={0}>
<Tooltip>

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -26,43 +33,57 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AddSwarmSettings } from "./modify-swarm-settings";
interface Props {
applicationId: string;
id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
}
const AddRedirectchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(),
registryId: z.string().optional(),
});
type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ applicationId }: Props) => {
const { data } = api.application.one.useQuery(
{
applicationId,
},
{ enabled: !!applicationId },
);
export const ShowClusterSettings = ({ id, type }: Props) => {
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { data: registries } = api.registry.all.useQuery();
const utils = api.useUtils();
const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = api.application.update.useMutation();
const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddCommand>({
defaultValues: {
registryId: data?.registryId || "",
...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1,
},
resolver: zodResolver(AddRedirectchema),
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
useEffect(() => {
if (data?.command) {
form.reset({
registryId: data?.registryId || "",
...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1,
});
}
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddCommand) => {
await mutateAsync({
applicationId,
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
...(type === "application"
? {
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
}
: {}),
replicas: data?.replicas,
})
.then(async () => {
toast.success("Command Updated");
await utils.application.one.invalidate({
applicationId,
});
await refetch();
})
.catch(() => {
toast.error("Error updating the command");
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
<div>
<CardTitle className="text-xl">Cluster Settings</CardTitle>
<CardDescription>
Add the registry and the replicas of the application
Modify swarm settings for the service.
</CardDescription>
</div>
<AddSwarmSettings applicationId={applicationId} />
<AddSwarmSettings id={id} type={type} />
</CardHeader>
<CardContent className="flex flex-col gap-4">
<AlertBlock type="info">
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
/>
</div>
{registries && registries?.length === 0 ? (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least a
registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
{type === "application" && (
<>
<FormField
control={form.control}
name="registryId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a registry</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a registry" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
{registries && registries?.length === 0 ? (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least
a registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
<>
<FormField
control={form.control}
name="registryId"
render={({ field }) => (
<FormItem>
<FormLabel>Select a registry</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a registry" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)}
</>
)}

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
@@ -26,12 +32,6 @@ import {
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535),
@@ -80,6 +80,11 @@ export const HandlePorts = ({
resolver: zodResolver(AddPortSchema),
});
const publishMode = useWatch({
control: form.control,
name: "publishMode",
});
useEffect(() => {
form.reset({
publishedPort: data?.publishedPort ?? 0,
@@ -253,6 +258,16 @@ export const HandlePorts = ({
</div>
</form>
{publishMode === "host" && (
<AlertBlock type="warning" className="mt-4">
<strong>Host Mode Limitation:</strong> When using Host publish
mode, Docker Swarm has limitations that prevent proper container
updates during deployments. Old containers may not be replaced
automatically. Consider using Ingress mode instead, or be prepared
to manually stop/start the application after deployments.
</AlertBlock>
)}
<DialogFooter>
<Button
isLoading={isLoading}

View File

@@ -1,3 +1,6 @@
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
@@ -10,9 +13,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { api } from "@/utils/api";
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
interface Props {
composeId: string;
@@ -79,7 +79,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
</div>
) : (
<>
<div className="flex flex-row gap-2 justify-end">
<div className="flex flex-row gap-2 justify-end my-4">
<Button
variant="secondary"
isLoading={isLoading}

View File

@@ -1,13 +1,14 @@
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { Download as DownloadIcon, Loader2 } from "lucide-react";
import React, { useEffect, useRef } from "react";
import { LineCountFilter } from "./line-count-filter";
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
import { StatusLogsFilter } from "./status-logs-filter";
import { TerminalLine } from "./terminal-line";
import { type LogLine, getLogType, parseLogs } from "./utils";
import { getLogType, type LogLine, parseLogs } from "./utils";
interface Props {
containerId: string;
@@ -61,6 +62,9 @@ export const DockerLogsId: React.FC<Props> = ({
const [showTimestamp, setShowTimestamp] = React.useState(true);
const [since, setSince] = React.useState<TimeFilter>("all");
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
const [isPaused, setIsPaused] = React.useState(false);
const [messageBuffer, setMessageBuffer] = React.useState<string[]>([]);
const isPausedRef = useRef(false);
const scrollRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = React.useState(false);
@@ -85,15 +89,38 @@ export const DockerLogsId: React.FC<Props> = ({
const handleLines = (lines: number) => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
setLines(lines);
};
const handleSince = (value: TimeFilter) => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
setSince(value);
};
const handlePauseResume = () => {
if (isPaused) {
// Resume: Apply all buffered messages
if (messageBuffer.length > 0) {
const bufferedContent = messageBuffer.join("");
setRawLogs((prev) => {
const updated = prev + bufferedContent;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
setMessageBuffer([]);
}
}
const newPausedState = !isPaused;
setIsPaused(newPausedState);
isPausedRef.current = newPausedState;
};
useEffect(() => {
if (!containerId) return;
@@ -102,6 +129,10 @@ export const DockerLogsId: React.FC<Props> = ({
setIsLoading(true);
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
// Reset pause state when container changes
setIsPaused(false);
isPausedRef.current = false;
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const params = new globalThis.URLSearchParams({
@@ -140,14 +171,22 @@ export const DockerLogsId: React.FC<Props> = ({
ws.onmessage = (e) => {
if (!isCurrentConnection) return;
setRawLogs((prev) => {
const updated = prev + e.data;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
if (isPausedRef.current) {
// When paused, buffer the messages instead of displaying them
setMessageBuffer((prev) => [...prev, e.data]);
} else {
// When not paused, display messages normally
setRawLogs((prev) => {
const updated = prev + e.data;
const splitLines = updated.split("\n");
if (splitLines.length > lines) {
return splitLines.slice(-lines).join("\n");
}
return updated;
});
}
setIsLoading(false);
if (noDataTimeout) clearTimeout(noDataTimeout);
};
@@ -210,9 +249,15 @@ export const DockerLogsId: React.FC<Props> = ({
});
};
// Sync isPausedRef with isPaused state
useEffect(() => {
isPausedRef.current = isPaused;
}, [isPaused]);
useEffect(() => {
setRawLogs("");
setFilteredLogs([]);
setMessageBuffer([]);
}, [containerId]);
useEffect(() => {
@@ -260,17 +305,48 @@ export const DockerLogsId: React.FC<Props> = ({
/>
</div>
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="h-9"
onClick={handlePauseResume}
title={isPaused ? "Resume logs" : "Pause logs"}
>
{isPaused ? (
<Play className="mr-2 h-4 w-4" />
) : (
<Pause className="mr-2 h-4 w-4" />
)}
{isPaused ? "Resume" : "Pause"}
</Button>
<Button
variant="outline"
size="sm"
className="h-9 sm:w-auto w-full"
onClick={handleDownload}
disabled={filteredLogs.length === 0 || !data?.Name}
>
<DownloadIcon className="mr-2 h-4 w-4" />
Download logs
</Button>
</div>
</div>
{isPaused && (
<AlertBlock type="warning">
<div className="flex items-center gap-2">
<Pause className="h-4 w-4" />
<span>
Logs paused
{messageBuffer.length > 0 && (
<span className="ml-1 font-medium">
({messageBuffer.length} messages buffered)
</span>
)}
</span>
</div>
</AlertBlock>
)}
<div
ref={scrollRef}
onScroll={handleScroll}

View File

@@ -1,3 +1,17 @@
import {
AlertTriangle,
ArrowUpDown,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
Search,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -31,20 +45,14 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import {
AlertTriangle,
BookIcon,
ExternalLinkIcon,
FolderInput,
Loader2,
MoreHorizontalIcon,
Search,
TrashIcon,
} from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
import { toast } from "sonner";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { HandleProject } from "./handle-project";
import { ProjectEnvironment } from "./project-environment";
@@ -54,15 +62,65 @@ export const ShowProjects = () => {
const { data: auth } = api.user.get.useQuery();
const { mutateAsync } = api.project.remove.useMutation();
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<string>(() => {
if (typeof window !== "undefined") {
return localStorage.getItem("projectsSort") || "createdAt-desc";
}
return "createdAt-desc";
});
useEffect(() => {
localStorage.setItem("projectsSort", sortBy);
}, [sortBy]);
const filteredProjects = useMemo(() => {
if (!data) return [];
return data.filter(
// First filter by search query
const filtered = data.filter(
(project) =>
project.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
project.description?.toLowerCase().includes(searchQuery.toLowerCase()),
);
}, [data, searchQuery]);
// Then sort the filtered results
const [field, direction] = sortBy.split("-");
return [...filtered].sort((a, b) => {
let comparison = 0;
switch (field) {
case "name":
comparison = a.name.localeCompare(b.name);
break;
case "createdAt":
comparison =
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
break;
case "services": {
const aTotalServices =
a.mariadb.length +
a.mongo.length +
a.mysql.length +
a.postgres.length +
a.redis.length +
a.applications.length +
a.compose.length;
const bTotalServices =
b.mariadb.length +
b.mongo.length +
b.mysql.length +
b.postgres.length +
b.redis.length +
b.applications.length +
b.compose.length;
comparison = aTotalServices - bTotalServices;
break;
}
default:
comparison = 0;
}
return direction === "asc" ? comparison : -comparison;
});
}, [data, searchQuery, sortBy]);
return (
<>
@@ -98,14 +156,40 @@ export const ShowProjects = () => {
</div>
) : (
<>
<div className="w-full relative">
<Input
placeholder="Filter projects..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<div className="flex max-sm:flex-col gap-4 items-center w-full">
<div className="flex-1 relative max-sm:w-full">
<Input
placeholder="Filter projects..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
</div>
<div className="flex items-center gap-2 min-w-48 max-sm:w-full">
<ArrowUpDown className="size-4 text-muted-foreground" />
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Sort by..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="name-asc">Name (A-Z)</SelectItem>
<SelectItem value="name-desc">Name (Z-A)</SelectItem>
<SelectItem value="createdAt-desc">
Newest first
</SelectItem>
<SelectItem value="createdAt-asc">
Oldest first
</SelectItem>
<SelectItem value="services-desc">
Most services
</SelectItem>
<SelectItem value="services-asc">
Least services
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{filteredProjects?.length === 0 && (
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">

View File

@@ -1,6 +1,7 @@
import { ShowResources } from "@/components/dashboard/application/advanced/show-resources";
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
import { ShowCustomCommand } from "@/components/dashboard/postgres/advanced/show-custom-command";
import { ShowClusterSettings } from "../application/advanced/cluster/show-cluster-settings";
import { RebuildDatabase } from "./rebuild-database";
interface Props {
@@ -12,6 +13,7 @@ export const ShowDatabaseAdvancedSettings = ({ id, type }: Props) => {
return (
<div className="flex w-full flex-col gap-5">
<ShowCustomCommand id={id} type={type} />
<ShowClusterSettings id={id} type={type} />
<ShowVolumes id={id} type={type} />
<ShowResources id={id} type={type} />
<RebuildDatabase id={id} type={type} />

View File

@@ -10,7 +10,7 @@ interface ChatwootWidgetProps {
launcherTitle?: string;
darkMode?: boolean;
hideMessageBubble?: boolean;
placement?: "right" | "left";
placement?: "left" | "right";
showPopoutButton?: boolean;
widgetStyle?: "standard" | "bubble";
};
@@ -41,7 +41,7 @@ export const ChatwootWidget = ({
position: "right",
};
(window as any).chatwootSDKReady = () => {
window.chatwootSDKReady = () => {
window.chatwootSDK?.run({ websiteToken, baseUrl });
const trySetUser = () => {
@@ -63,7 +63,7 @@ export const ChatwootWidget = ({
<Script
src={`${baseUrl}/packs/js/sdk.js`}
strategy="lazyOnload"
onLoad={() => (window as any).chatwootSDKReady?.()}
onLoad={() => window.chatwootSDKReady?.()}
/>
);
};

View File

@@ -1,11 +1,11 @@
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
import { cn } from "@/lib/utils";
interface Props extends React.ComponentPropsWithoutRef<"div"> {
icon?: React.ReactNode;
type?: "info" | "success" | "warning" | "error";
}
import { cn } from "@/lib/utils";
import { AlertCircle, AlertTriangle, CheckCircle2, Info } from "lucide-react";
const iconMap = {
info: {
className: "bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400",

View File

@@ -1,3 +1,5 @@
import Link from "next/link";
import { Fragment } from "react";
import {
Breadcrumb,
BreadcrumbItem,
@@ -7,8 +9,6 @@ import {
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import { SidebarTrigger } from "@/components/ui/sidebar";
import Link from "next/link";
import { Fragment } from "react";
interface Props {
list: {
@@ -26,7 +26,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
{list.map((item, _index) => (
{list.map((item, index) => (
<Fragment key={item.name}>
<BreadcrumbItem className="block">
<BreadcrumbLink href={item.href} asChild={!!item.href}>
@@ -37,7 +37,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
)}
</BreadcrumbLink>
</BreadcrumbItem>
{_index + 1 < list.length && (
{index + 1 < list.length && (
<BreadcrumbSeparator className="block" />
)}
</Fragment>

View File

@@ -1,20 +1,19 @@
import { cn } from "@/lib/utils";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import {
autocompletion,
type Completion,
type CompletionContext,
type CompletionResult,
autocompletion,
} from "@codemirror/autocomplete";
import { json } from "@codemirror/lang-json";
import { yaml } from "@codemirror/lang-yaml";
import { StreamLanguage } from "@codemirror/language";
import { properties } from "@codemirror/legacy-modes/mode/properties";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { EditorView } from "@codemirror/view";
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
import { useTheme } from "next-themes";
import { cn } from "@/lib/utils";
// Docker Compose completion options
const dockerComposeServices = [
@@ -101,9 +100,7 @@ function dockerComposeComplete(
context: CompletionContext,
): CompletionResult | null {
const word = context.matchBefore(/\w*/);
if (!word) return null;
if (!word.text && !context.explicit) return null;
if (!word || (!word.text && !context.explicit)) return null;
// Check if we're at the root level
const line = context.state.doc.lineAt(context.pos);

View File

@@ -1,3 +1,4 @@
import { format, formatDistanceToNow } from "date-fns";
import {
Tooltip,
TooltipContent,
@@ -5,7 +6,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { format, formatDistanceToNow } from "date-fns";
interface Props {
date: string;

View File

@@ -1,3 +1,5 @@
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import {
Sheet,
SheetContent,
@@ -5,8 +7,6 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../dashboard/docker/logs/terminal-line";
import type { LogLine } from "../dashboard/docker/logs/utils";
@@ -43,11 +43,11 @@ export const DrawerLogs = ({ isOpen, onClose, filteredLogs }: Props) => {
return (
<Sheet
open={!!isOpen}
onOpenChange={(_open) => {
onOpenChange={() => {
onClose();
}}
>
<SheetContent className="sm:max-w-[740px] flex flex-col">
<SheetContent className="sm:max-w-[740px] flex flex-col">
<SheetHeader>
<SheetTitle>Deployment Logs</SheetTitle>
<SheetDescription>Details of the request log entry.</SheetDescription>

View File

@@ -13,10 +13,13 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
setIsPasswordVisible((prevVisibility) => !prevVisibility);
};
const inputType = isPasswordVisible ? "text" : "password";
return (
<div className="flex w-full items-center space-x-2">
<Input ref={inputRef} type={inputType} {...props} />
<Input
ref={inputRef}
type={isPasswordVisible ? "text" : "password"}
{...props}
/>
<Button
variant={"secondary"}
onClick={() => {
@@ -27,10 +30,10 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
<Clipboard className="size-4 text-muted-foreground" />
</Button>
<Button onClick={togglePasswordVisibility} variant={"secondary"}>
{inputType === "password" ? (
<EyeIcon className="size-4 text-muted-foreground" />
) : (
{isPasswordVisible ? (
<EyeOffIcon className="size-4 text-muted-foreground" />
) : (
<EyeIcon className="size-4 text-muted-foreground" />
)}
</Button>
</div>

View File

@@ -0,0 +1,45 @@
ALTER TABLE "postgres" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "postgres" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mariadb" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mongo" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "mysql" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "healthCheckSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "restartPolicySwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "placementSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "updateConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "rollbackConfigSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "modeSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "labelsSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "networkSwarm" json;--> statement-breakpoint
ALTER TABLE "redis" ADD COLUMN "replicas" integer DEFAULT 1 NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@@ -736,6 +736,13 @@
"when": 1754207407121,
"tag": "0104_omniscient_randall",
"breakpoints": true
},
{
"idx": 105,
"version": "7",
"when": 1754259281559,
"tag": "0105_clumsy_quicksilver",
"breakpoints": true
}
]
}

View File

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

View File

@@ -1,3 +1,17 @@
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowClusterSettings } from "@/components/dashboard/application/advanced/cluster/show-cluster-settings";
import { AddCommand } from "@/components/dashboard/application/advanced/general/add-command";
import { ShowPorts } from "@/components/dashboard/application/advanced/ports/show-port";
@@ -39,20 +53,6 @@ import {
} from "@/components/ui/tooltip";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
} from "next";
import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useEffect, useState } from "react";
import { toast } from "sonner";
import superjson from "superjson";
type TabState =
| "projects"
@@ -345,7 +345,10 @@ const Service = (
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowClusterSettings
id={applicationId}
type="application"
/>
<ShowResources id={applicationId} type="application" />
<ShowVolumes id={applicationId} type="application" />

View File

@@ -24,7 +24,25 @@ import { redirects } from "./redirects";
import { registry } from "./registry";
import { security } from "./security";
import { server } from "./server";
import { applicationStatus, certificateType, triggerType } from "./shared";
import {
applicationStatus,
certificateType,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
triggerType,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [
@@ -46,64 +64,6 @@ export const buildType = pgEnum("buildType", [
"railpack",
]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
Timeout?: number | undefined;
StartPeriod?: number | undefined;
Retries?: number | undefined;
}
export interface RestartPolicySwarm {
Condition?: string | undefined;
Delay?: number | undefined;
MaxAttempts?: number | undefined;
Window?: number | undefined;
}
export interface PlacementSwarm {
Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined;
Platforms?:
| Array<{
Architecture: string;
OS: string;
}>
| undefined;
}
export interface UpdateConfigSwarm {
Parallelism: number;
Delay?: number | undefined;
FailureAction?: string | undefined;
Monitor?: number | undefined;
MaxFailureRatio?: number | undefined;
Order: string;
}
export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined;
ReplicatedJob?:
| {
MaxConcurrent?: number | undefined;
TotalCompletions?: number | undefined;
}
| undefined;
GlobalJob?: {} | undefined;
}
export interface NetworkSwarm {
Target?: string | undefined;
Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined;
}
export interface LabelsSwarm {
[name: string]: string;
}
export const applications = pgTable("application", {
applicationId: text("applicationId")
.notNull()
@@ -283,94 +243,6 @@ export const applicationsRelations = relations(
}),
);
const HealthCheckSwarmSchema = z
.object({
Test: z.array(z.string()).optional(),
Interval: z.number().optional(),
Timeout: z.number().optional(),
StartPeriod: z.number().optional(),
Retries: z.number().optional(),
})
.strict();
const RestartPolicySwarmSchema = z
.object({
Condition: z.string().optional(),
Delay: z.number().optional(),
MaxAttempts: z.number().optional(),
Window: z.number().optional(),
})
.strict();
const PreferenceSchema = z
.object({
Spread: z.object({
SpreadDescriptor: z.string(),
}),
})
.strict();
const PlatformSchema = z
.object({
Architecture: z.string(),
OS: z.string(),
})
.strict();
const PlacementSwarmSchema = z
.object({
Constraints: z.array(z.string()).optional(),
Preferences: z.array(PreferenceSchema).optional(),
MaxReplicas: z.number().optional(),
Platforms: z.array(PlatformSchema).optional(),
})
.strict();
const UpdateConfigSwarmSchema = z
.object({
Parallelism: z.number(),
Delay: z.number().optional(),
FailureAction: z.string().optional(),
Monitor: z.number().optional(),
MaxFailureRatio: z.number().optional(),
Order: z.string(),
})
.strict();
const ReplicatedSchema = z
.object({
Replicas: z.number().optional(),
})
.strict();
const ReplicatedJobSchema = z
.object({
MaxConcurrent: z.number().optional(),
TotalCompletions: z.number().optional(),
})
.strict();
const ServiceModeSwarmSchema = z
.object({
Replicated: ReplicatedSchema.optional(),
Global: z.object({}).optional(),
ReplicatedJob: ReplicatedJobSchema.optional(),
GlobalJob: z.object({}).optional(),
})
.strict();
const NetworkSwarmSchema = z.array(
z
.object({
Target: z.string().optional(),
Aliases: z.array(z.string()).optional(),
DriverOpts: z.object({}).optional(),
})
.strict(),
);
const LabelsSwarmSchema = z.record(z.string());
const createSchema = createInsertSchema(applications, {
appName: z.string(),
createdAt: z.string(),

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const mariadb = pgTable("mariadb", {
@@ -38,6 +54,15 @@ export const mariadb = pgTable("mariadb", {
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -83,6 +108,14 @@ const createSchema = createInsertSchema(mariadb, {
externalPort: z.number(),
description: z.string().optional(),
serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreateMariaDB = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const mongo = pgTable("mongo", {
@@ -34,6 +50,15 @@ export const mongo = pgTable("mongo", {
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -79,6 +104,14 @@ const createSchema = createInsertSchema(mongo, {
description: z.string().optional(),
serverId: z.string().optional(),
replicaSets: z.boolean().default(false),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreateMongo = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const mysql = pgTable("mysql", {
@@ -36,6 +52,15 @@ export const mysql = pgTable("mysql", {
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -81,6 +106,14 @@ const createSchema = createInsertSchema(mysql, {
externalPort: z.number(),
description: z.string().optional(),
serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreateMySql = createSchema

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -7,7 +7,23 @@ import { backups } from "./backups";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const postgres = pgTable("postgres", {
@@ -35,6 +51,16 @@ export const postgres = pgTable("postgres", {
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
@@ -78,6 +104,14 @@ const createSchema = createInsertSchema(postgres, {
createdAt: z.string(),
description: z.string().optional(),
serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreatePostgres = createSchema

View File

@@ -1,12 +1,28 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { integer, json, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { mounts } from "./mount";
import { projects } from "./project";
import { server } from "./server";
import { applicationStatus } from "./shared";
import {
applicationStatus,
type HealthCheckSwarm,
HealthCheckSwarmSchema,
type LabelsSwarm,
LabelsSwarmSchema,
type NetworkSwarm,
NetworkSwarmSchema,
type PlacementSwarm,
PlacementSwarmSchema,
type RestartPolicySwarm,
RestartPolicySwarmSchema,
type ServiceModeSwarm,
ServiceModeSwarmSchema,
type UpdateConfigSwarm,
UpdateConfigSwarmSchema,
} from "./shared";
import { generateAppName } from "./utils";
export const redis = pgTable("redis", {
@@ -35,6 +51,15 @@ export const redis = pgTable("redis", {
applicationStatus: applicationStatus("applicationStatus")
.notNull()
.default("idle"),
healthCheckSwarm: json("healthCheckSwarm").$type<HealthCheckSwarm>(),
restartPolicySwarm: json("restartPolicySwarm").$type<RestartPolicySwarm>(),
placementSwarm: json("placementSwarm").$type<PlacementSwarm>(),
updateConfigSwarm: json("updateConfigSwarm").$type<UpdateConfigSwarm>(),
rollbackConfigSwarm: json("rollbackConfigSwarm").$type<UpdateConfigSwarm>(),
modeSwarm: json("modeSwarm").$type<ServiceModeSwarm>(),
labelsSwarm: json("labelsSwarm").$type<LabelsSwarm>(),
networkSwarm: json("networkSwarm").$type<NetworkSwarm[]>(),
replicas: integer("replicas").default(1).notNull(),
projectId: text("projectId")
.notNull()
.references(() => projects.projectId, { onDelete: "cascade" }),
@@ -73,6 +98,14 @@ const createSchema = createInsertSchema(redis, {
externalPort: z.number(),
description: z.string().optional(),
serverId: z.string().optional(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
placementSwarm: PlacementSwarmSchema.nullable(),
updateConfigSwarm: UpdateConfigSwarmSchema.nullable(),
rollbackConfigSwarm: UpdateConfigSwarmSchema.nullable(),
modeSwarm: ServiceModeSwarmSchema.nullable(),
labelsSwarm: LabelsSwarmSchema.nullable(),
networkSwarm: NetworkSwarmSchema.nullable(),
});
export const apiCreateRedis = createSchema

View File

@@ -1,4 +1,5 @@
import { pgEnum } from "drizzle-orm/pg-core";
import { z } from "zod";
export const applicationStatus = pgEnum("applicationStatus", [
"idle",
@@ -14,3 +15,149 @@ export const certificateType = pgEnum("certificateType", [
]);
export const triggerType = pgEnum("triggerType", ["push", "tag"]);
export interface HealthCheckSwarm {
Test?: string[] | undefined;
Interval?: number | undefined;
Timeout?: number | undefined;
StartPeriod?: number | undefined;
Retries?: number | undefined;
}
export interface RestartPolicySwarm {
Condition?: string | undefined;
Delay?: number | undefined;
MaxAttempts?: number | undefined;
Window?: number | undefined;
}
export interface PlacementSwarm {
Constraints?: string[] | undefined;
Preferences?: Array<{ Spread: { SpreadDescriptor: string } }> | undefined;
MaxReplicas?: number | undefined;
Platforms?:
| Array<{
Architecture: string;
OS: string;
}>
| undefined;
}
export interface UpdateConfigSwarm {
Parallelism: number;
Delay?: number | undefined;
FailureAction?: string | undefined;
Monitor?: number | undefined;
MaxFailureRatio?: number | undefined;
Order: string;
}
export interface ServiceModeSwarm {
Replicated?: { Replicas?: number | undefined } | undefined;
Global?: {} | undefined;
ReplicatedJob?:
| {
MaxConcurrent?: number | undefined;
TotalCompletions?: number | undefined;
}
| undefined;
GlobalJob?: {} | undefined;
}
export interface NetworkSwarm {
Target?: string | undefined;
Aliases?: string[] | undefined;
DriverOpts?: { [key: string]: string } | undefined;
}
export interface LabelsSwarm {
[name: string]: string;
}
export const HealthCheckSwarmSchema = z
.object({
Test: z.array(z.string()).optional(),
Interval: z.number().optional(),
Timeout: z.number().optional(),
StartPeriod: z.number().optional(),
Retries: z.number().optional(),
})
.strict();
export const RestartPolicySwarmSchema = z
.object({
Condition: z.string().optional(),
Delay: z.number().optional(),
MaxAttempts: z.number().optional(),
Window: z.number().optional(),
})
.strict();
export const PreferenceSchema = z
.object({
Spread: z.object({
SpreadDescriptor: z.string(),
}),
})
.strict();
export const PlatformSchema = z
.object({
Architecture: z.string(),
OS: z.string(),
})
.strict();
export const PlacementSwarmSchema = z
.object({
Constraints: z.array(z.string()).optional(),
Preferences: z.array(PreferenceSchema).optional(),
MaxReplicas: z.number().optional(),
Platforms: z.array(PlatformSchema).optional(),
})
.strict();
export const UpdateConfigSwarmSchema = z
.object({
Parallelism: z.number(),
Delay: z.number().optional(),
FailureAction: z.string().optional(),
Monitor: z.number().optional(),
MaxFailureRatio: z.number().optional(),
Order: z.string(),
})
.strict();
export const ReplicatedSchema = z
.object({
Replicas: z.number().optional(),
})
.strict();
export const ReplicatedJobSchema = z
.object({
MaxConcurrent: z.number().optional(),
TotalCompletions: z.number().optional(),
})
.strict();
export const ServiceModeSwarmSchema = z
.object({
Replicated: ReplicatedSchema.optional(),
Global: z.object({}).optional(),
ReplicatedJob: ReplicatedJobSchema.optional(),
GlobalJob: z.object({}).optional(),
})
.strict();
export const NetworkSwarmSchema = z.array(
z
.object({
Target: z.string().optional(),
Aliases: z.array(z.string()).optional(),
DriverOpts: z.object({}).optional(),
})
.strict(),
);
export const LabelsSwarmSchema = z.record(z.string());

View File

@@ -29,6 +29,9 @@ const { handler, api } = betterAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
},
logger: {
disabled: process.env.NODE_ENV === "production",
},
...(!IS_CLOUD && {
async trustedOrigins() {
const admin = await db.query.member.findFirst({

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
@@ -34,6 +35,17 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
const defaultMariadbEnv = `MARIADB_DATABASE="${databaseName}"\nMARIADB_USER="${databaseUser}"\nMARIADB_PASSWORD="${databasePassword}"\nMARIADB_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mariadb);
const resources = calculateResources({
memoryLimit,
memoryReservation,
@@ -54,6 +66,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -63,20 +76,17 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
Args: ["-c", command],
}
: {}),
Labels,
},
Networks: [{ Target: "dokploy-network" }],
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
@@ -90,6 +100,7 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
]
: [],
},
UpdateConfig,
};
try {
const service = docker.getService(appName);
@@ -97,6 +108,10 @@ export const buildMariadb = async (mariadb: MariadbNested) => {
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
@@ -81,6 +82,17 @@ ${command ?? "wait $MONGOD_PID"}`;
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mongo);
const resources = calculateResources({
memoryLimit,
memoryReservation,
@@ -102,6 +114,7 @@ ${command ?? "wait $MONGOD_PID"}`;
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -116,20 +129,17 @@ ${command ?? "wait $MONGOD_PID"}`;
Args: ["-c", command],
}),
}),
Labels,
},
Networks: [{ Target: "dokploy-network" }],
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
@@ -143,6 +153,7 @@ ${command ?? "wait $MONGOD_PID"}`;
]
: [],
},
UpdateConfig,
};
try {
@@ -151,6 +162,10 @@ ${command ?? "wait $MONGOD_PID"}`;
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
@@ -40,6 +41,17 @@ export const buildMysql = async (mysql: MysqlNested) => {
: `MYSQL_DATABASE="${databaseName}"\nMYSQL_ROOT_PASSWORD="${databaseRootPassword}"${
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(mysql);
const resources = calculateResources({
memoryLimit,
memoryReservation,
@@ -60,6 +72,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -69,20 +82,17 @@ export const buildMysql = async (mysql: MysqlNested) => {
Args: ["-c", command],
}
: {}),
Labels,
},
Networks: [{ Target: "dokploy-network" }],
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
@@ -96,6 +106,7 @@ export const buildMysql = async (mysql: MysqlNested) => {
]
: [],
},
UpdateConfig,
};
try {
const service = docker.getService(appName);
@@ -103,6 +114,10 @@ export const buildMysql = async (mysql: MysqlNested) => {
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
await docker.createService(settings);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
@@ -33,6 +34,17 @@ export const buildPostgres = async (postgres: PostgresNested) => {
const defaultPostgresEnv = `POSTGRES_DB="${databaseName}"\nPOSTGRES_USER="${databaseUser}"\nPOSTGRES_PASSWORD="${databasePassword}"${
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(postgres);
const resources = calculateResources({
memoryLimit,
memoryReservation,
@@ -53,6 +65,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -62,20 +75,17 @@ export const buildPostgres = async (postgres: PostgresNested) => {
Args: ["-c", command],
}
: {}),
Labels,
},
Networks: [{ Target: "dokploy-network" }],
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
@@ -89,6 +99,7 @@ export const buildPostgres = async (postgres: PostgresNested) => {
]
: [],
},
UpdateConfig,
};
try {
const service = docker.getService(appName);
@@ -96,6 +107,10 @@ export const buildPostgres = async (postgres: PostgresNested) => {
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch (error) {
console.log("error", error);

View File

@@ -3,6 +3,7 @@ import type { CreateServiceOptions } from "dockerode";
import {
calculateResources,
generateBindMounts,
generateConfigContainer,
generateFileMounts,
generateVolumeMounts,
prepareEnvironmentVariables,
@@ -31,6 +32,17 @@ export const buildRedis = async (redis: RedisNested) => {
const defaultRedisEnv = `REDIS_PASSWORD="${databasePassword}"${
env ? `\n${env}` : ""
}`;
const {
HealthCheck,
RestartPolicy,
Placement,
Labels,
Mode,
RollbackConfig,
UpdateConfig,
Networks,
} = generateConfigContainer(redis);
const resources = calculateResources({
memoryLimit,
memoryReservation,
@@ -51,6 +63,7 @@ export const buildRedis = async (redis: RedisNested) => {
Name: appName,
TaskTemplate: {
ContainerSpec: {
HealthCheck,
Image: dockerImage,
Env: envVariables,
Mounts: [...volumesMount, ...bindsMount, ...filesMount],
@@ -59,20 +72,17 @@ export const buildRedis = async (redis: RedisNested) => {
"-c",
command ? command : `redis-server --requirepass ${databasePassword}`,
],
Labels,
},
Networks: [{ Target: "dokploy-network" }],
Networks,
RestartPolicy,
Placement,
Resources: {
...resources,
},
Placement: {
Constraints: ["node.role==manager"],
},
},
Mode: {
Replicated: {
Replicas: 1,
},
},
Mode,
RollbackConfig,
EndpointSpec: {
Mode: "dnsrr",
Ports: externalPort
@@ -86,6 +96,7 @@ export const buildRedis = async (redis: RedisNested) => {
]
: [],
},
UpdateConfig,
};
try {
@@ -94,6 +105,10 @@ export const buildRedis = async (redis: RedisNested) => {
await service.update({
version: Number.parseInt(inspect.Version.Index),
...settings,
TaskTemplate: {
...settings.TaskTemplate,
ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1,
},
});
} catch {
await docker.createService(settings);

View File

@@ -348,7 +348,9 @@ export const calculateResources = ({
};
};
export const generateConfigContainer = (application: ApplicationNested) => {
export const generateConfigContainer = (
application: Partial<ApplicationNested>,
) => {
const {
healthCheckSwarm,
restartPolicySwarm,
@@ -362,7 +364,7 @@ export const generateConfigContainer = (application: ApplicationNested) => {
networkSwarm,
} = application;
const haveMounts = mounts.length > 0;
const haveMounts = mounts && mounts.length > 0;
return {
...(healthCheckSwarm && {