Compare commits

...

62 Commits

Author SHA1 Message Date
Mauricio Siu
75fc030984 Merge pull request #1508 from Dokploy/feat/add-invalidation-cache
feat(application): add cleanCache feature to application management
2025-03-16 03:21:42 -06:00
Mauricio Siu
060a170aee chore(package): bump version to v0.20.4 2025-03-16 03:21:08 -06:00
Mauricio Siu
40718293a1 feat(application): add cleanCache feature to application management
- Introduced a new boolean column `cleanCache` in the application schema to manage cache cleaning behavior.
- Updated the application form to include a toggle for `cleanCache`, allowing users to enable or disable cache cleaning.
- Enhanced application deployment logic to utilize the `cleanCache` setting, affecting build commands across various builders (Docker, Heroku, Nixpacks, Paketo, Railpack).
- Implemented success and error notifications for cache updates in the UI.
2025-03-16 03:20:47 -06:00
Mauricio Siu
9ac68985e0 Merge pull request #1506 from Dokploy/feat/add-swarm-to-remote-servers
feat(cluster-nodes): enhance node management by adding serverId prop …
2025-03-16 00:43:35 -06:00
Mauricio Siu
35ff8dcfe6 feat(cluster-nodes): enhance node management by adding serverId prop to components and implementing ShowNodesModal 2025-03-16 00:42:19 -06:00
Mauricio Siu
60c03e1ca7 refactor(manage-traefik-ports): remove error handling for port update failure 2025-03-16 00:18:08 -06:00
Mauricio Siu
d42fa738ea refactor(side-layout): adjust SidebarMenu gap for improved spacing 2025-03-15 23:59:18 -06:00
Mauricio Siu
160742c2cf refactor(manage-traefik-ports): remove publishMode from port management and update related logic 2025-03-15 23:55:29 -06:00
Mauricio Siu
4c5bc541d6 refactor(show-traefik-actions): remove error handling for Traefik reload failure 2025-03-15 23:00:54 -06:00
Mauricio Siu
d13871cd08 refactor(save-github-provider): remove unused GitHub link from save component 2025-03-15 22:51:09 -06:00
Mauricio Siu
a12beb6748 refactor(monitoring-card): simplify node mapping in dashboard component for better performance 2025-03-15 22:50:24 -06:00
Mauricio Siu
4c90f4754f refactor(monitoring-card): change node display from row to grid layout for improved responsiveness 2025-03-15 22:48:25 -06:00
Mauricio Siu
69fdda505d chore(package): bump version from v0.20.2 to v0.20.3 2025-03-15 22:37:30 -06:00
Mauricio Siu
16e84e431a feat(railpack): add Docker buildx container management to buildRailpack function 2025-03-15 22:36:43 -06:00
Mauricio Siu
5d4db4d0f3 Merge pull request #1504 from Dokploy/refactor/adjust-expiration-session
feat(auth): add session configuration with expiration and update age …
2025-03-15 22:11:56 -06:00
Mauricio Siu
10d2493bcc feat(auth): add session configuration with expiration and update age settings 2025-03-15 22:11:37 -06:00
Mauricio Siu
ce97bc6c27 Merge pull request #1503 from Dokploy/revert-1429-feat/update-zh-Hans-translation
Revert "feat(i18n): update zh-Hans translation"
2025-03-15 22:09:08 -06:00
Mauricio Siu
c2e05e86d9 Revert "feat(i18n): update zh-Hans translation" 2025-03-15 22:08:49 -06:00
Mauricio Siu
5cd743eb10 Merge pull request #1429 from PaiJi/feat/update-zh-Hans-translation
feat(i18n): update zh-Hans translation
2025-03-15 21:53:09 -06:00
Mauricio Siu
cd32c55031 chore: remove combine-translations script as it is no longer needed 2025-03-15 21:40:39 -06:00
Mauricio Siu
7f2ebab66c refactor: standardize translation usage across components and pages by removing specific namespace references 2025-03-15 21:38:49 -06:00
Mauricio Siu
0bc2734925 Merge branch 'canary' into feat/update-zh-Hans-translation 2025-03-15 20:55:16 -06:00
Mauricio Siu
f74d02381f Merge pull request #1477 from Mautriz/canary
Allow traefik labels customization in docker-composes
2025-03-15 20:48:46 -06:00
Mauricio Siu
d46afbef2d Merge pull request #1502 from Dokploy/1493-railpack-spawns-multiple-build-kit-containers
1493 railpack spawns multiple build kit containers
2025-03-15 20:45:47 -06:00
Mauricio Siu
be64a1554d chore: remove commented-out Docker build command from Railpack builder utility 2025-03-15 20:45:38 -06:00
Mauricio Siu
8d9d00d0c6 refactor: streamline container parsing logic in Docker service functions 2025-03-15 20:43:22 -06:00
Mauricio Siu
31164c9798 chore: remove console log statements from WebSocket connection handling and ensure builder container for Railpack is created 2025-03-15 20:42:53 -06:00
Mauricio Siu
4d4de1424e Merge pull request #1501 from Dokploy/1492-deploy-vs-rebuild-on-docker-compose-are-using-different-volumes
refactor: remove console log statements on WebSocket connection close…
2025-03-15 18:37:36 -06:00
Mauricio Siu
fa954c3bbd refactor: remove console log statements on WebSocket connection close and adjust compose file handling based on source type 2025-03-15 18:36:40 -06:00
Mauricio Siu
005f73d665 refactor: enhance Railpack build process by introducing preparation step and environment variable handling 2025-03-15 17:11:20 -06:00
Mauricio Siu
bbe7d5bdc5 Merge pull request #1499 from Dokploy/1455-invalid-origin-on-login
chore: update better-auth package to version 1.2.4 and kysely to vers…
2025-03-15 14:46:28 -06:00
Mauricio Siu
6f7a5609a3 chore: update better-auth package to version 1.2.4 and kysely to version 0.27.6; enhance error handling in 2FA feature 2025-03-15 14:45:21 -06:00
Mauricio Siu
c3a5e2a8d6 Merge pull request #1498 from Dokploy/1486-mongodb-external-url-visual-bug
feat: add alert block for IP address requirement in database credenti…
2025-03-15 14:30:09 -06:00
Mauricio Siu
1ca965268e feat: add alert block for IP address requirement in database credential components 2025-03-15 14:29:16 -06:00
Mauricio Siu
e323ade29e Merge pull request #1473 from gentslava/fix/service_layout
fix(ui): projects layout
2025-03-15 13:41:08 -06:00
Mauricio Siu
8c916bc431 Merge pull request #1491 from tswymer/fix/duplicate-percentage-unit
fix: removed duplicate percentage label
2025-03-15 13:39:57 -06:00
Mauricio Siu
0670f9b910 Merge pull request #1474 from drudge/canary
Various Improvements
2025-03-15 13:24:39 -06:00
Mauricio Siu
44f002d8d0 Merge pull request #1497 from Dokploy/fix/adjust-images-templates
fix: update template logo URL to use the new domain for consistency
2025-03-15 13:23:17 -06:00
Mauricio Siu
27f6c945e0 fix: update template logo URL to use the new domain for consistency 2025-03-15 13:22:47 -06:00
Tobias Wymer
e61c216ea0 fix: removed duplicate percentage label 2025-03-14 19:34:15 +01:00
Nicholas Penree
9f9492af79 fix: generate domains from templates using slugified project name 2025-03-12 22:44:49 -04:00
Nicholas Penree
68f608bdc9 chore(ui): replace placeholder company name 2025-03-12 22:44:49 -04:00
Nicholas Penree
8f671d1691 chore(ui): standardize view logs / terminal menu items 2025-03-12 22:44:49 -04:00
Nicholas Penree
7afbe8b208 chore(ui): standardize status badge for containers 2025-03-12 22:44:48 -04:00
Nicholas Penree
8c05214e78 fix(monitoring): remove extra percent from cpu usage 2025-03-12 22:44:48 -04:00
Mauro Insacco
07769e69d6 Allow traefik labels customization in docker-composes 2025-03-13 01:44:04 +01:00
Vyacheslav Shcherbinin
2ace36f035 fix(ui): projects layout for large screen 2025-03-12 19:16:16 +07:00
Vyacheslav Shcherbinin
b7196a3494 fix(config): large screens support 2025-03-12 19:16:16 +07:00
Mauricio Siu
3b737ca55b Merge pull request #1468 from ChrisvanChip/style-remove-gap-from-container
style: remove inconsistent gap between header and content
2025-03-12 00:55:32 -06:00
Chris
581e590f65 style: remove inconsistent gap between header and content 2025-03-11 12:18:17 +00:00
Mauricio Siu
d66a5d55a3 docs: update template contribution guidelines to reference external repository 2025-03-11 01:36:20 -06:00
JiPai
6df0878ed4 feat(i18n):add i18n for auth page 2025-03-09 23:12:35 +08:00
JiPai
a1bbfaebf4 feat(i18n): add translations to dashboard pages 2025-03-09 23:12:35 +08:00
JiPai
ed89f5aa8a chore(i18n): add home.json demo file 2025-03-09 23:12:35 +08:00
JiPai
888e904d75 feat(i18n): add i18n for organization management 2025-03-09 23:12:35 +08:00
JiPai
3e522b9cae feat(i18n): update sidebar tooltips for internationalization 2025-03-09 23:12:35 +08:00
JiPai
7903ddba89 feat(i18n): add i18n support for sidebar 2025-03-09 23:12:34 +08:00
JiPai
3a0dbc26d1 feat(i18n): add date-fns locale support 2025-03-09 23:12:34 +08:00
JiPai
6df680e9da feat(i18n): add internationalization support for 2FA setup and error messages 2025-03-09 23:11:15 +08:00
JiPai
2bced3e9b6 feat(i18n): update password labels in profile form for better clarity 2025-03-09 23:11:15 +08:00
JiPai
911a7730f9 feat(i18n): enable reload on prerender in development mode 2025-03-09 23:11:15 +08:00
JiPai
2902648188 chore(package.json): auto format package.json 2025-03-09 23:11:14 +08:00
58 changed files with 5842 additions and 733 deletions

View File

@@ -165,86 +165,8 @@ Thank you for your contribution!
## Templates ## Templates
To add a new template, go to `templates` folder and create a new folder with the name of the template. To add a new template, go to `https://github.com/Dokploy/templates` repository and read the README.md file.
Let's take the example of `plausible` template.
1. create a folder in `templates/plausible`
2. create a `docker-compose.yml` file inside the folder with the content of compose.
3. create a `index.ts` file inside the folder with the following code as base:
4. When creating a pull request, please provide a video of the template working in action.
```typescript
// EXAMPLE
import {
generateBase64,
generateHash,
generateRandomDomain,
type Template,
type Schema,
type DomainSchema,
} from "../utils";
export function generate(schema: Schema): Template {
// do your stuff here, like create a new domain, generate random passwords, mounts.
const mainServiceHash = generateHash(schema.projectName);
const mainDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32);
const domains: DomainSchema[] = [
{
host: mainDomain,
port: 8000,
serviceName: "plausible",
},
];
const envs = [
`BASE_URL=http://${mainDomain}`,
`SECRET_KEY_BASE=${secretBase}`,
`TOTP_VAULT_KEY=${toptKeyBase}`,
`HASH=${mainServiceHash}`,
];
const mounts: Template["mounts"] = [
{
filePath: "./clickhouse/clickhouse-config.xml",
content: "some content......",
},
];
return {
envs,
mounts,
domains,
};
}
```
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
```typescript
{
id: "plausible",
name: "Plausible",
version: "v2.1.0",
description:
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
logo: "plausible.svg", // we defined the name and the extension of the logo
links: {
github: "https://github.com/plausible/plausible",
website: "https://plausible.io/",
docs: "https://plausible.io/docs",
},
tags: ["analytics"],
load: () => import("./plausible/index").then((m) => m.generate),
},
```
5. Add the logo or image of the template to `public/templates/plausible.svg`
### Recommendations ### Recommendations

View File

@@ -1,242 +0,0 @@
# Contributing
Hey, thanks for your interest in contributing to Dokploy! We appreciate your help and taking your time to contribute.
Before you start, please first discuss the feature/bug you want to add with the owners and comunity via github issues.
We have a few guidelines to follow when contributing to this project:
- [Commit Convention](#commit-convention)
- [Setup](#setup)
- [Development](#development)
- [Build](#build)
- [Pull Request](#pull-request)
## Commit Convention
Before you craete a Pull Request, please make sure your commit message follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
### Commit Message Format
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
#### Type
Must be one of the following:
* **feat**: A new feature
* **fix**: A bug fix
* **docs**: Documentation only changes
* **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
* **refactor**: A code change that neither fixes a bug nor adds a feature
* **perf**: A code change that improves performance
* **test**: Adding missing tests or correcting existing tests
* **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
* **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
* **chore**: Other changes that don't modify `src` or `test` files
* **revert**: Reverts a previous commit
Example:
```
feat: add new feature
```
## Setup
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
```bash
git clone https://github.com/dokploy/dokploy.git
cd dokploy
pnpm install
cp .env.example .env
```
## Development
Is required to have **Docker** installed on your machine.
### Setup
Run the command that will spin up all the required services and files.
```bash
pnpm run setup
```
Now run the development server.
```bash
pnpm run dev
```
Go to http://localhost:3000 to see the development server
## Build
```bash
pnpm run build
```
## Docker
To build the docker image
```bash
pnpm run docker:build
```
To push the docker image
```bash
pnpm run docker:push
```
## Password Reset
In the case you lost your password, you can reset it using the following command
```bash
pnpm run reset-password
```
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel`
```bash
bunx lt --port 3000
```
If you run into permission issues of docker run the following command
```bash
sudo chown -R USERNAME dokploy or sudo chown -R $(whoami) ~/.docker
```
## Application deploy
In case you want to deploy the application on your machine and you selected nixpacks or buildpacks, you need to install first.
```bash
# Install Nixpacks
curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \
&& ./install.sh
```
```bash
# Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
```
## Pull Request
- The `main` 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.
- When creating a pull request, please provide a clear and concise description of the changes made.
- If you include a video or screenshot, would be awesome so we can see the changes in action.
- 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.
Thank you for your contribution!
## Templates
To add a new template, go to `templates` folder and create a new folder with the name of the template.
Let's take the example of `plausible` template.
1. create a folder in `templates/plausible`
2. create a `docker-compose.yml` file inside the folder with the content of compose.
3. create a `index.ts` file inside the folder with the following code as base:
4. When creating a pull request, please provide a video of the template working in action.
```typescript
// EXAMPLE
import {
generateHash,
generateRandomDomain,
type Template,
type Schema,
} from "../utils";
export function generate(schema: Schema): Template {
// do your stuff here, like create a new domain, generate random passwords, mounts.
const mainServiceHash = generateHash(schema.projectName);
const randomDomain = generateRandomDomain(schema);
const secretBase = generateBase64(64);
const toptKeyBase = generateBase64(32);
const envs = [
// If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
`PLAUSIBLE_HOST=${randomDomain}`,
"PLAUSIBLE_PORT=8000",
`BASE_URL=http://${randomDomain}`,
`SECRET_KEY_BASE=${secretBase}`,
`TOTP_VAULT_KEY=${toptKeyBase}`,
`HASH=${mainServiceHash}`,
];
const mounts: Template["mounts"] = [
{
mountPath: "./clickhouse/clickhouse-config.xml",
content: `some content......`,
},
];
return {
envs,
mounts,
};
}
```
4. Now you need to add the information about the template to the `templates/templates.ts` is a object with the following properties:
**Make sure the id of the template is the same as the folder name and don't have any spaces, only slugified names and lowercase.**
```typescript
{
id: "plausible",
name: "Plausible",
version: "v2.1.0",
description:
"Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.",
logo: "plausible.svg", // we defined the name and the extension of the logo
links: {
github: "https://github.com/plausible/plausible",
website: "https://plausible.io/",
docs: "https://plausible.io/docs",
},
tags: ["analytics"],
load: () => import("./plausible/index").then((m) => m.generate),
},
```
5. Add the logo or image of the template to `public/templates/plausible.svg`
### Recomendations
- Use the same name of the folder as the id of the template.
- The logo should be in the public folder.
- If you want to show a domain in the UI, please add the prefix _HOST at the end of the variable name.
- Test first on a vps or a server to make sure the template works.

View File

@@ -468,16 +468,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
Save Save
</Button> </Button>
</div> </div>
{/* create github link */}
<div className="flex w-full justify-end">
<Link
href={`https://github.com/${repository?.owner}/${repository?.repo}`}
target="_blank"
className="w-fit"
>
Repository
</Link>
</div>
</form> </form>
</Form> </Form>
</div> </div>

View File

@@ -267,6 +267,28 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
className="flex flex-row gap-2 items-center" className="flex flex-row gap-2 items-center"
/> />
</div> </div>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Clean Cache</span>
<Switch
aria-label="Toggle italic"
checked={data?.cleanCache || false}
onCheckedChange={async (enabled) => {
await update({
applicationId,
cleanCache: enabled,
})
.then(async () => {
toast.success("Clean Cache Updated");
await refetch();
})
.catch(() => {
toast.error("Error updating Clean Cache");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
</CardContent> </CardContent>
</Card> </Card>
<ShowProviderForm applicationId={applicationId} /> <ShowProviderForm applicationId={applicationId} />

View File

@@ -121,7 +121,7 @@ export const UpdateApplication = ({ applicationId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -121,7 +121,7 @@ export const UpdateCompose = ({ composeId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -1,5 +1,6 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,7 @@ import { useEffect, useState } 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"; import { z } from "zod";
import Link from "next/link";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
@@ -106,6 +108,17 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -119,7 +119,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -1,5 +1,6 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,7 @@ import { useEffect, useState } 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"; import { z } from "zod";
import Link from "next/link";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
@@ -106,6 +108,17 @@ export const ShowExternalMongoCredentials = ({ mongoId }: Props) => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -121,7 +121,7 @@ export const UpdateMongo = ({ mongoId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -218,7 +218,7 @@ export const ContainerFreeMonitoring = ({
<CardContent> <CardContent>
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Used: {currentData.cpu.value}% Used: {currentData.cpu.value}
</span> </span>
<Progress value={currentData.cpu.value} className="w-[100%]" /> <Progress value={currentData.cpu.value} className="w-[100%]" />
<DockerCpuChart acummulativeData={acummulativeData.cpu} /> <DockerCpuChart acummulativeData={acummulativeData.cpu} />

View File

@@ -1,5 +1,6 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,7 @@ import { useEffect, useState } 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"; import { z } from "zod";
import Link from "next/link";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
@@ -106,6 +108,17 @@ export const ShowExternalMysqlCredentials = ({ mysqlId }: Props) => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -119,7 +119,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -1,5 +1,6 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,7 @@ import { useEffect, useState } 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"; import { z } from "zod";
import Link from "next/link";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
@@ -108,6 +110,17 @@ export const ShowExternalPostgresCredentials = ({ postgresId }: Props) => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -121,7 +121,7 @@ export const UpdatePostgres = ({ postgresId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -324,7 +324,7 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
)} )}
> >
<img <img
src={`${customBaseUrl || "https://dokploy.github.io/templates"}/blueprints/${template.id}/${template.logo}`} src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template.id}/${template.logo}`}
className={cn( className={cn(
"object-contain", "object-contain",
viewMode === "detailed" ? "size-24" : "size-16", viewMode === "detailed" ? "size-24" : "size-16",

View File

@@ -148,7 +148,7 @@ export const HandleProject = ({ projectId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -115,7 +115,7 @@ export const ShowProjects = () => {
</span> </span>
</div> </div>
)} )}
<div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 flex-wrap gap-5"> <div className="w-full grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 3xl:grid-cols-5 flex-wrap gap-5">
{filteredProjects?.map((project) => { {filteredProjects?.map((project) => {
const emptyServices = const emptyServices =
project?.mariadb.length === 0 && project?.mariadb.length === 0 &&

View File

@@ -1,5 +1,6 @@
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input"; import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { AlertBlock } from "@/components/shared/alert-block";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,7 @@ import { useEffect, useState } 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"; import { z } from "zod";
import Link from "next/link";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
externalPort: z.preprocess((a) => { externalPort: z.preprocess((a) => {
@@ -100,6 +102,17 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="flex w-full flex-col gap-4"> <CardContent className="flex w-full flex-col gap-4">
{!getIp && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link href="/dashboard/settings" className="text-primary">
{data?.serverId
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to fix the database url connection.
</AlertBlock>
)}
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}

View File

@@ -119,7 +119,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Tesla" {...field} /> <Input placeholder="Vandelay Industries" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -13,7 +13,11 @@ import Link from "next/link";
import { AddManager } from "./manager/add-manager"; import { AddManager } from "./manager/add-manager";
import { AddWorker } from "./workers/add-worker"; import { AddWorker } from "./workers/add-worker";
export const AddNode = () => { interface Props {
serverId?: string;
}
export const AddNode = ({ serverId }: Props) => {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -53,10 +57,10 @@ export const AddNode = () => {
<TabsTrigger value="manager">Manager</TabsTrigger> <TabsTrigger value="manager">Manager</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="worker" className="pt-4"> <TabsContent value="worker" className="pt-4">
<AddWorker /> <AddWorker serverId={serverId} />
</TabsContent> </TabsContent>
<TabsContent value="manager" className="pt-4"> <TabsContent value="manager" className="pt-4">
<AddManager /> <AddManager serverId={serverId} />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>

View File

@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddManager = () => { interface Props {
const { data } = api.cluster.addManager.useQuery(); serverId?: string;
}
export const AddManager = ({ serverId }: Props) => {
const { data } = api.cluster.addManager.useQuery({ serverId });
return ( return (
<> <>

View File

@@ -0,0 +1,30 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowNodes } from "./show-nodes";
interface Props {
serverId: string;
}
export const ShowNodesModal = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
Show Nodes
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
<div className="grid w-full gap-1">
<ShowNodes serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -32,13 +32,25 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Boxes, HelpCircle, LockIcon, MoreHorizontal } from "lucide-react"; import {
Boxes,
HelpCircle,
LockIcon,
MoreHorizontal,
Loader2,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AddNode } from "./add-node"; import { AddNode } from "./add-node";
import { ShowNodeData } from "./show-node-data"; import { ShowNodeData } from "./show-node-data";
export const ShowNodes = () => { interface Props {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery(); serverId?: string;
}
export const ShowNodes = ({ serverId }: Props) => {
const { data, isLoading, refetch } = api.cluster.getNodes.useQuery({
serverId,
});
const { data: registry } = api.registry.all.useQuery(); const { data: registry } = api.registry.all.useQuery();
const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation(); const { mutateAsync: deleteNode } = api.cluster.removeWorker.useMutation();
@@ -58,14 +70,17 @@ export const ShowNodes = () => {
</div> </div>
{haveAtLeastOneRegistry && ( {haveAtLeastOneRegistry && (
<div className="flex flex-row gap-2"> <div className="flex flex-row gap-2">
<AddNode /> <AddNode serverId={serverId} />
</div> </div>
)} )}
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t min-h-[35vh]"> <CardContent className="space-y-2 py-8 border-t min-h-[35vh]">
{haveAtLeastOneRegistry ? ( {isLoading ? (
<div className="flex items-center justify-center w-full h-[40vh]">
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
) : haveAtLeastOneRegistry ? (
<div className="grid md:grid-cols-1 gap-4"> <div className="grid md:grid-cols-1 gap-4">
{isLoading && <div>Loading...</div>}
<Table> <Table>
<TableCaption> <TableCaption>
A list of your managers / workers. A list of your managers / workers.
@@ -137,6 +152,7 @@ export const ShowNodes = () => {
onClick={async () => { onClick={async () => {
await deleteNode({ await deleteNode({
nodeId: node.ID, nodeId: node.ID,
serverId,
}) })
.then(() => { .then(() => {
refetch(); refetch();

View File

@@ -9,8 +9,12 @@ import copy from "copy-to-clipboard";
import { CopyIcon } from "lucide-react"; import { CopyIcon } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
export const AddWorker = () => { interface Props {
const { data } = api.cluster.addWorker.useQuery(); serverId?: string;
}
export const AddWorker = ({ serverId }: Props) => {
const { data } = api.cluster.addWorker.useQuery({ serverId });
return ( return (
<div> <div>

View File

@@ -64,12 +64,12 @@ export const Enable2FA = () => {
const handlePasswordSubmit = async (formData: PasswordForm) => { const handlePasswordSubmit = async (formData: PasswordForm) => {
setIsPasswordLoading(true); setIsPasswordLoading(true);
try { try {
const { data: enableData } = await authClient.twoFactor.enable({ const { data: enableData, error } = await authClient.twoFactor.enable({
password: formData.password, password: formData.password,
}); });
if (!enableData) { if (!enableData) {
throw new Error("No data received from server"); throw new Error(error?.message || "Error enabling 2FA");
} }
if (enableData.backupCodes) { if (enableData.backupCodes) {
@@ -95,7 +95,8 @@ export const Enable2FA = () => {
error instanceof Error ? error.message : "Error setting up 2FA", error instanceof Error ? error.message : "Error setting up 2FA",
); );
passwordForm.setError("password", { passwordForm.setError("password", {
message: "Error verifying password", message:
error instanceof Error ? error.message : "Error setting up 2FA",
}); });
} finally { } finally {
setIsPasswordLoading(false); setIsPasswordLoading(false);

View File

@@ -59,9 +59,7 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
.then(async () => { .then(async () => {
toast.success("Traefik Reloaded"); toast.success("Traefik Reloaded");
}) })
.catch(() => { .catch(() => {});
toast.error("Error reloading Traefik");
});
}} }}
className="cursor-pointer" className="cursor-pointer"
> >

View File

@@ -42,6 +42,7 @@ import { ShowMonitoringModal } from "./show-monitoring-modal";
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal"; import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal"; import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription"; import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
export const ShowServers = () => { export const ShowServers = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
@@ -328,6 +329,9 @@ export const ShowServers = () => {
<ShowSwarmOverviewModal <ShowSwarmOverviewModal
serverId={server.serverId} serverId={server.serverId}
/> />
<ShowNodesModal
serverId={server.serverId}
/>
</> </>
)} )}
</DropdownMenuContent> </DropdownMenuContent>

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -23,6 +24,7 @@ import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { badgeStateColor } from "../../application/logs/show";
const Terminal = dynamic( const Terminal = dynamic(
() => () =>
@@ -109,7 +111,10 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
key={container.containerId} key={container.containerId}
value={container.containerId} value={container.containerId}
> >
{container.name} ({container.containerId}) {container.state} {container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem> </SelectItem>
))} ))}
<SelectLabel>Containers ({data?.length})</SelectLabel> <SelectLabel>Containers ({data?.length})</SelectLabel>

View File

@@ -19,13 +19,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
@@ -44,7 +37,6 @@ interface Props {
const PortSchema = z.object({ const PortSchema = z.object({
targetPort: z.number().min(1, "Target port is required"), targetPort: z.number().min(1, "Target port is required"),
publishedPort: z.number().min(1, "Published port is required"), publishedPort: z.number().min(1, "Published port is required"),
publishMode: z.enum(["ingress", "host"]),
}); });
const TraefikPortsSchema = z.object({ const TraefikPortsSchema = z.object({
@@ -88,7 +80,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
}, [currentPorts, form]); }, [currentPorts, form]);
const handleAddPort = () => { const handleAddPort = () => {
append({ targetPort: 0, publishedPort: 0, publishMode: "host" }); append({ targetPort: 0, publishedPort: 0 });
}; };
const onSubmit = async (data: TraefikPortsForm) => { const onSubmit = async (data: TraefikPortsForm) => {
@@ -99,9 +91,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
}); });
toast.success(t("settings.server.webServer.traefik.portsUpdated")); toast.success(t("settings.server.webServer.traefik.portsUpdated"));
setOpen(false); setOpen(false);
} catch (_error) { } catch (_error) {}
toast.error(t("settings.server.webServer.traefik.portsUpdateError"));
}
}; };
return ( return (
@@ -154,7 +144,7 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<div className="grid gap-4"> <div className="grid gap-4">
{fields.map((field, index) => ( {fields.map((field, index) => (
<Card key={field.id}> <Card key={field.id}>
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent"> <CardContent className="grid grid-cols-[1fr_1fr_auto] gap-4 p-4 transparent">
<FormField <FormField
control={form.control} control={form.control}
name={`ports.${index}.targetPort`} name={`ports.${index}.targetPort`}
@@ -207,39 +197,6 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
)} )}
/> />
<FormField
control={form.control}
name={`ports.${index}.publishMode`}
render={({ field }) => (
<FormItem>
<FormLabel className="text-sm font-medium text-muted-foreground">
{t(
"settings.server.webServer.traefik.publishMode",
)}
</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
>
<FormControl>
<SelectTrigger className="dark:bg-black">
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="host">
Host Mode
</SelectItem>
<SelectItem value="ingress">
Ingress Mode
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-end"> <div className="flex items-end">
<Button <Button
onClick={() => remove(index)} onClick={() => remove(index)}
@@ -263,30 +220,23 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
<span className="text-sm"> <span className="text-sm">
<strong> <strong>
Each port mapping defines how external traffic reaches Each port mapping defines how external traffic reaches
your containers. your containers through Traefik.
</strong> </strong>
<ul className="pt-2"> <ul className="pt-2">
<li> <li>
<strong>Host Mode:</strong> Directly binds the port <strong>Target Port:</strong> The port inside your
to the host machine. container that the service is listening on.
<ul className="p-2 list-inside list-disc">
<li>
Best for single-node deployments or when you
need guaranteed port availability.
</li>
</ul>
</li> </li>
<li> <li>
<strong>Ingress Mode:</strong> Routes through Docker <strong>Published Port:</strong> The port on your
Swarm's load balancer. host machine that will be mapped to the target port.
<ul className="p-2 list-inside list-disc">
<li>
Recommended for multi-node deployments and
better scalability.
</li>
</ul>
</li> </li>
</ul> </ul>
<p className="mt-2">
All ports are bound directly to the host machine,
allowing Traefik to handle incoming traffic and route
it appropriately to your services.
</p>
</span> </span>
</div> </div>
</AlertBlock> </AlertBlock>

View File

@@ -21,6 +21,8 @@ import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import type React from "react"; import type React from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { badgeStateColor } from "../../application/logs/show";
import { Badge } from "@/components/ui/badge";
export const DockerLogsId = dynamic( export const DockerLogsId = dynamic(
() => () =>
@@ -90,7 +92,10 @@ export const ShowModalLogs = ({
key={container.containerId} key={container.containerId}
value={container.containerId} value={container.containerId}
> >
{container.name} ({container.containerId}) {container.state} {container.name} ({container.containerId}){" "}
<Badge variant={badgeStateColor(container.state)}>
{container.state}
</Badge>
</SelectItem> </SelectItem>
))} ))}
<SelectLabel>Containers ({data?.length})</SelectLabel> <SelectLabel>Containers ({data?.length})</SelectLabel>

View File

@@ -176,7 +176,7 @@ export default function SwarmMonitorCard({ serverId }: Props) {
</Card> </Card>
</div> </div>
<div className="flex flex-row gap-4"> <div className="grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4">
{nodes.map((node) => ( {nodes.map((node) => (
<NodeCard key={node.ID} node={node} serverId={serverId} /> <NodeCard key={node.ID} node={node} serverId={serverId} />
))} ))}

View File

@@ -908,7 +908,7 @@ export default function Page({ children }: Props) {
</SidebarGroup> </SidebarGroup>
<SidebarGroup> <SidebarGroup>
<SidebarGroupLabel>Settings</SidebarGroupLabel> <SidebarGroupLabel>Settings</SidebarGroupLabel>
<SidebarMenu className="gap-2"> <SidebarMenu className="gap-1">
{filteredSettings.map((item) => { {filteredSettings.map((item) => {
const isSingle = item.isSingle !== false; const isSingle = item.isSingle !== false;
const isActive = isSingle const isActive = isSingle
@@ -1068,7 +1068,7 @@ export default function Page({ children }: Props) {
</header> </header>
)} )}
<div className="flex flex-col w-full gap-4 p-4 pt-0">{children}</div> <div className="flex flex-col w-full p-4 pt-0">{children}</div>
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
); );

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "cleanCache" boolean DEFAULT false;

File diff suppressed because it is too large Load Diff

View File

@@ -547,6 +547,13 @@
"when": 1741510086231, "when": 1741510086231,
"tag": "0077_chemical_dreadnoughts", "tag": "0077_chemical_dreadnoughts",
"breakpoints": true "breakpoints": true
},
{
"idx": 78,
"version": "7",
"when": 1742112194375,
"tag": "0078_uneven_omega_sentinel",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.20.2", "version": "v0.20.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -93,7 +93,7 @@
"adm-zip": "^0.5.14", "adm-zip": "^0.5.14",
"ai": "^4.0.23", "ai": "^4.0.23",
"bcrypt": "5.1.1", "bcrypt": "5.1.1",
"better-auth": "1.2.0", "better-auth": "1.2.4",
"bl": "6.0.11", "bl": "6.0.11",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"bullmq": "5.4.2", "bullmq": "5.4.2",

View File

@@ -1,6 +1,6 @@
{ {
"settings.common.save": "Save", "settings.common.save": "Save",
"settings.common.enterTerminal": "Enter the terminal", "settings.common.enterTerminal": "Terminal",
"settings.server.domain.title": "Server Domain", "settings.server.domain.title": "Server Domain",
"settings.server.domain.description": "Add a domain to your server application.", "settings.server.domain.description": "Add a domain to your server application.",
"settings.server.domain.form.domain": "Domain", "settings.server.domain.form.domain": "Domain",
@@ -14,7 +14,7 @@
"settings.server.webServer.description": "Reload or clean the web server.", "settings.server.webServer.description": "Reload or clean the web server.",
"settings.server.webServer.actions": "Actions", "settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Reload", "settings.server.webServer.reload": "Reload",
"settings.server.webServer.watchLogs": "Watch logs", "settings.server.webServer.watchLogs": "View Logs",
"settings.server.webServer.updateServerIp": "Update Server IP", "settings.server.webServer.updateServerIp": "Update Server IP",
"settings.server.webServer.server.label": "Server", "settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.label": "Traefik",

View File

@@ -1,22 +1,35 @@
import { getPublicIpWithFallback } from "@/server/wss/terminal"; import { getPublicIpWithFallback } from "@/server/wss/terminal";
import { type DockerNode, IS_CLOUD, docker, execAsync } from "@dokploy/server"; import {
type DockerNode,
IS_CLOUD,
execAsync,
getRemoteDocker,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const clusterRouter = createTRPCRouter({ export const clusterRouter = createTRPCRouter({
getNodes: protectedProcedure.query(async () => { getNodes: protectedProcedure
if (IS_CLOUD) { .input(
return []; z.object({
} serverId: z.string().optional(),
const workers: DockerNode[] = await docker.listNodes(); }),
)
.query(async ({ input }) => {
if (IS_CLOUD) {
return [];
}
return workers; const docker = await getRemoteDocker(input.serverId);
}), const workers: DockerNode[] = await docker.listNodes();
return workers;
}),
removeWorker: protectedProcedure removeWorker: protectedProcedure
.input( .input(
z.object({ z.object({
nodeId: z.string(), nodeId: z.string(),
serverId: z.string().optional(),
}), }),
) )
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -40,37 +53,51 @@ export const clusterRouter = createTRPCRouter({
}); });
} }
}), }),
addWorker: protectedProcedure.query(async () => { addWorker: protectedProcedure
if (IS_CLOUD) { .input(
return { z.object({
command: "", serverId: z.string().optional(),
version: "", }),
}; )
} .query(async ({ input }) => {
const result = await docker.swarmInspect(); if (IS_CLOUD) {
const docker_version = await docker.version(); return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
addManager: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return { return {
command: "", command: `docker swarm join --token ${
version: "", result.JoinTokens.Worker
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
}; };
} }),
const result = await docker.swarmInspect(); addManager: protectedProcedure
const docker_version = await docker.version(); .input(
return { z.object({
command: `docker swarm join --token ${ serverId: z.string().optional(),
result.JoinTokens.Manager }),
} ${await getPublicIpWithFallback()}:2377`, )
version: docker_version.Version, .query(async ({ input }) => {
}; if (IS_CLOUD) {
}), return {
command: "",
version: "",
};
}
const docker = await getRemoteDocker(input.serverId);
const result = await docker.swarmInspect();
const docker_version = await docker.version();
return {
command: `docker swarm join --token ${
result.JoinTokens.Manager
} ${await getPublicIpWithFallback()}:2377`,
version: docker_version.Version,
};
}),
}); });

View File

@@ -437,13 +437,12 @@ export const composeRouter = createTRPCRouter({
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} }
const projectName = slugify(`${project.name} ${input.id}`);
const generate = processTemplate(template.config, { const generate = processTemplate(template.config, {
serverIp: serverIp, serverIp: serverIp,
projectName: project.name, projectName: projectName,
}); });
const projectName = slugify(`${project.name} ${input.id}`);
const compose = await createComposeByTemplate({ const compose = await createComposeByTemplate({
...input, ...input,
composeFile: template.dockerCompose, composeFile: template.dockerCompose,

View File

@@ -97,14 +97,20 @@ export const settingsRouter = createTRPCRouter({
toggleDashboard: adminProcedure toggleDashboard: adminProcedure
.input(apiEnableDashboard) .input(apiEnableDashboard)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const ports = (await getTraefikPorts(input.serverId)).filter(
(port) =>
port.targetPort !== 80 &&
port.targetPort !== 443 &&
port.targetPort !== 8080,
);
await initializeTraefik({ await initializeTraefik({
additionalPorts: ports,
enableDashboard: input.enableDashboard, enableDashboard: input.enableDashboard,
serverId: input.serverId, serverId: input.serverId,
force: true, force: true,
}); });
return true; return true;
}), }),
cleanUnusedImages: adminProcedure cleanUnusedImages: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
@@ -749,7 +755,6 @@ export const settingsRouter = createTRPCRouter({
z.object({ z.object({
targetPort: z.number(), targetPort: z.number(),
publishedPort: z.number(), publishedPort: z.number(),
publishMode: z.enum(["ingress", "host"]).default("host"),
}), }),
), ),
}), }),
@@ -782,59 +787,7 @@ export const settingsRouter = createTRPCRouter({
getTraefikPorts: adminProcedure getTraefikPorts: adminProcedure
.input(apiServerSchema) .input(apiServerSchema)
.query(async ({ input }) => { .query(async ({ input }) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`; return await getTraefikPorts(input?.serverId);
try {
let stdout = "";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
publishMode: "host" | "ingress";
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443, 8080].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
publishMode: "host", // Docker standalone uses host mode by default
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
}), }),
updateLogCleanup: adminProcedure updateLogCleanup: adminProcedure
.input( .input(
@@ -853,3 +806,56 @@ export const settingsRouter = createTRPCRouter({
return getLogCleanupStatus(); return getLogCleanupStatus();
}), }),
}); });
export const getTraefikPorts = async (serverId?: string) => {
const command = `docker container inspect --format='{{json .NetworkSettings.Ports}}' dokploy-traefik`;
try {
let stdout = "";
if (serverId) {
const result = await execAsyncRemote(serverId, command);
stdout = result.stdout;
} else if (!IS_CLOUD) {
const result = await execAsync(command);
stdout = result.stdout;
}
const portsMap = JSON.parse(stdout.trim());
const additionalPorts: Array<{
targetPort: number;
publishedPort: number;
}> = [];
// Convert the Docker container port format to our expected format
for (const [containerPort, bindings] of Object.entries(portsMap)) {
if (!bindings) continue;
const [port = ""] = containerPort.split("/");
if (!port) continue;
const targetPortNum = Number.parseInt(port, 10);
if (Number.isNaN(targetPortNum)) continue;
// Skip default ports
if ([80, 443].includes(targetPortNum)) continue;
for (const binding of bindings as Array<{ HostPort: string }>) {
if (!binding.HostPort) continue;
const publishedPort = Number.parseInt(binding.HostPort, 10);
if (Number.isNaN(publishedPort)) continue;
additionalPorts.push({
targetPort: targetPortNum,
publishedPort,
});
}
}
return additionalPorts;
} catch (error) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Failed to get Traefik ports",
cause: error,
});
}
};

View File

@@ -61,7 +61,6 @@ export const setupDeploymentLogsWebSocketServer = (
} }
stream stream
.on("close", () => { .on("close", () => {
console.log("Connection closed ✅");
client.end(); client.end();
ws.close(); ws.close();
}) })
@@ -86,7 +85,6 @@ export const setupDeploymentLogsWebSocketServer = (
}); });
ws.on("close", () => { ws.on("close", () => {
console.log("Connection closed ✅, From WS");
client.end(); client.end();
}); });
} else { } else {

View File

@@ -22,6 +22,9 @@ const config = {
fontFamily: { fontFamily: {
sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans], sans: ["var(--font-inter)", ...defaultTheme.fontFamily.sans],
}, },
screens: {
"3xl": "120rem",
},
maxWidth: { maxWidth: {
"2xl": "40rem", "2xl": "40rem",
"8xl": "85rem", "8xl": "85rem",

View File

@@ -40,7 +40,7 @@
"@oslojs/encoding":"1.1.0", "@oslojs/encoding":"1.1.0",
"@oslojs/crypto":"1.0.1", "@oslojs/crypto":"1.0.1",
"drizzle-dbml-generator":"0.10.0", "drizzle-dbml-generator":"0.10.0",
"better-auth":"1.2.0", "better-auth":"1.2.4",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@lucia-auth/adapter-drizzle": "1.0.7", "@lucia-auth/adapter-drizzle": "1.0.7",
"@octokit/auth-app": "^6.0.4", "@octokit/auth-app": "^6.0.4",

View File

@@ -141,6 +141,7 @@ export const applications = pgTable("application", {
command: text("command"), command: text("command"),
refreshToken: text("refreshToken").$defaultFn(() => nanoid()), refreshToken: text("refreshToken").$defaultFn(() => nanoid()),
sourceType: sourceType("sourceType").notNull().default("github"), sourceType: sourceType("sourceType").notNull().default("github"),
cleanCache: boolean("cleanCache").default(false),
// Github // Github
repository: text("repository"), repository: text("repository"),
owner: text("owner"), owner: text("owner"),
@@ -408,6 +409,7 @@ const createSchema = createInsertSchema(applications, {
previewPath: z.string().optional(), previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
watchPaths: z.array(z.string()).optional(), watchPaths: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
}); });
export const apiCreateApplication = createSchema.pick({ export const apiCreateApplication = createSchema.pick({

View File

@@ -159,6 +159,7 @@ table application {
command text command text
refreshToken text refreshToken text
sourceType sourceType [not null, default: 'github'] sourceType sourceType [not null, default: 'github']
cleanCache boolean [default: false]
repository text repository text
owner text owner text
branch text branch text

View File

@@ -28,6 +28,26 @@ const { handler, api } = betterAuth({
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}, },
}, },
...(!IS_CLOUD && {
async trustedOrigins() {
const admin = await db.query.member.findFirst({
where: eq(schema.member.role, "owner"),
with: {
user: true,
},
});
if (admin) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
}
return [];
},
}),
emailVerification: { emailVerification: {
sendOnSignUp: true, sendOnSignUp: true,
autoSignInAfterVerification: true, autoSignInAfterVerification: true,
@@ -117,6 +137,10 @@ const { handler, api } = betterAuth({
}, },
}, },
}, },
session: {
expiresIn: 60 * 60 * 24 * 3,
updateAge: 60 * 60 * 24,
},
user: { user: {
modelName: "users_temp", modelName: "users_temp",
additionalFields: { additionalFields: {

View File

@@ -182,12 +182,6 @@ export const deployApplication = async ({
}); });
try { try {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...application, ...application,
@@ -257,11 +251,6 @@ export const rebuildApplication = async ({
}); });
try { try {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await buildApplication(application, deployment.logPath); await buildApplication(application, deployment.logPath);
} else if (application.sourceType === "gitlab") { } else if (application.sourceType === "gitlab") {
@@ -306,11 +295,6 @@ export const deployRemoteApplication = async ({
try { try {
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
command += await getGithubCloneCommand({ command += await getGithubCloneCommand({
@@ -451,12 +435,6 @@ export const deployPreviewApplication = async ({
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...application, ...application,
@@ -565,11 +543,6 @@ export const deployRemotePreviewApplication = async ({
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheOnPreviews) {
// await cleanupFullDocker(application?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (application.sourceType === "github") { if (application.sourceType === "github") {
command += await getGithubCloneCommand({ command += await getGithubCloneCommand({
@@ -634,11 +607,6 @@ export const rebuildRemoteApplication = async ({
try { try {
if (application.serverId) { if (application.serverId) {
// const admin = await findUserById(application.project.userId);
// if (admin.cleanupCacheApplications) {
// await cleanupFullDocker(application?.serverId);
// }
if (application.sourceType !== "docker") { if (application.sourceType !== "docker") {
let command = "set -e;"; let command = "set -e;";
command += getBuildCommand(application, deployment.logPath); command += getBuildCommand(application, deployment.logPath);

View File

@@ -216,10 +216,6 @@ export const deployCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
...compose, ...compose,
@@ -285,15 +281,10 @@ export const rebuildCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId); if (compose.sourceType === "raw") {
// if (admin.cleanupCacheOnCompose) { await createComposeFile(compose, deployment.logPath);
// await cleanupFullDocker(compose?.serverId);
// }
if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath);
} else {
await buildCompose(compose, deployment.logPath);
} }
await buildCompose(compose, deployment.logPath);
await updateDeploymentStatus(deployment.deploymentId, "done"); await updateDeploymentStatus(deployment.deploymentId, "done");
await updateCompose(composeId, { await updateCompose(composeId, {
@@ -331,10 +322,6 @@ export const deployRemoteCompose = async ({
}); });
try { try {
if (compose.serverId) { if (compose.serverId) {
// const admin = await findUserById(compose.project.userId);
// if (admin.cleanupCacheOnCompose) {
// await cleanupFullDocker(compose?.serverId);
// }
let command = "set -e;"; let command = "set -e;";
if (compose.sourceType === "github") { if (compose.sourceType === "github") {
@@ -429,10 +416,10 @@ export const rebuildRemoteCompose = async ({
}); });
try { try {
// const admin = await findUserById(compose.project.userId); if (compose.sourceType === "raw") {
// if (admin.cleanupCacheOnCompose) { const command = getCreateComposeFileCommand(compose, deployment.logPath);
// await cleanupFullDocker(compose?.serverId); await execAsyncRemote(compose.serverId, command);
// } }
if (compose.serverId) { if (compose.serverId) {
await getBuildComposeCommand(compose, deployment.logPath); await getBuildComposeCommand(compose, deployment.logPath);
} }

View File

@@ -136,26 +136,24 @@ export const getContainersByAppNameMatch = async (
result = stdout.trim().split("\n"); result = stdout.trim().split("\n");
} }
const containers = result const containers = result.map((line) => {
.map((line) => { const parts = line.split(" | ");
const parts = line.split(" | "); const containerId = parts[0]
const containerId = parts[0] ? parts[0].replace("CONTAINER ID : ", "").trim()
? parts[0].replace("CONTAINER ID : ", "").trim() : "No container id";
: "No container id"; const name = parts[1]
const name = parts[1] ? parts[1].replace("Name: ", "").trim()
? parts[1].replace("Name: ", "").trim() : "No container name";
: "No container name";
const state = parts[2] const state = parts[2]
? parts[2].replace("State: ", "").trim() ? parts[2].replace("State: ", "").trim()
: "No state"; : "No state";
return { return {
containerId, containerId,
name, name,
state, state,
}; };
}) });
.sort((a, b) => a.name.localeCompare(b.name));
return containers || []; return containers || [];
} catch (_error) {} } catch (_error) {}
@@ -192,30 +190,28 @@ export const getStackContainersByAppName = async (
result = stdout.trim().split("\n"); result = stdout.trim().split("\n");
} }
const containers = result const containers = result.map((line) => {
.map((line) => { const parts = line.split(" | ");
const parts = line.split(" | "); const containerId = parts[0]
const containerId = parts[0] ? parts[0].replace("CONTAINER ID : ", "").trim()
? parts[0].replace("CONTAINER ID : ", "").trim() : "No container id";
: "No container id"; const name = parts[1]
const name = parts[1] ? parts[1].replace("Name: ", "").trim()
? parts[1].replace("Name: ", "").trim() : "No container name";
: "No container name";
const state = parts[2] const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase() ? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state"; : "No state";
const node = parts[3] const node = parts[3]
? parts[3].replace("Node: ", "").trim() ? parts[3].replace("Node: ", "").trim()
: "No specific node"; : "No specific node";
return { return {
containerId, containerId,
name, name,
state, state,
node, node,
}; };
}) });
.sort((a, b) => a.name.localeCompare(b.name));
return containers || []; return containers || [];
} catch (_error) {} } catch (_error) {}
@@ -253,31 +249,29 @@ export const getServiceContainersByAppName = async (
result = stdout.trim().split("\n"); result = stdout.trim().split("\n");
} }
const containers = result const containers = result.map((line) => {
.map((line) => { const parts = line.split(" | ");
const parts = line.split(" | "); const containerId = parts[0]
const containerId = parts[0] ? parts[0].replace("CONTAINER ID : ", "").trim()
? parts[0].replace("CONTAINER ID : ", "").trim() : "No container id";
: "No container id"; const name = parts[1]
const name = parts[1] ? parts[1].replace("Name: ", "").trim()
? parts[1].replace("Name: ", "").trim() : "No container name";
: "No container name";
const state = parts[2] const state = parts[2]
? parts[2].replace("State: ", "").trim().toLowerCase() ? parts[2].replace("State: ", "").trim().toLowerCase()
: "No state"; : "No state";
const node = parts[3] const node = parts[3]
? parts[3].replace("Node: ", "").trim() ? parts[3].replace("Node: ", "").trim()
: "No specific node"; : "No specific node";
return { return {
containerId, containerId,
name, name,
state, state,
node, node,
}; };
}) });
.sort((a, b) => a.name.localeCompare(b.name));
return containers || []; return containers || [];
} catch (_error) {} } catch (_error) {}
@@ -318,25 +312,23 @@ export const getContainersByAppLabel = async (
const lines = stdout.trim().split("\n"); const lines = stdout.trim().split("\n");
const containers = lines const containers = lines.map((line) => {
.map((line) => { const parts = line.split(" | ");
const parts = line.split(" | "); const containerId = parts[0]
const containerId = parts[0] ? parts[0].replace("CONTAINER ID : ", "").trim()
? parts[0].replace("CONTAINER ID : ", "").trim() : "No container id";
: "No container id"; const name = parts[1]
const name = parts[1] ? parts[1].replace("Name: ", "").trim()
? parts[1].replace("Name: ", "").trim() : "No container name";
: "No container name"; const state = parts[2]
const state = parts[2] ? parts[2].replace("State: ", "").trim()
? parts[2].replace("State: ", "").trim() : "No state";
: "No state"; return {
return { containerId,
containerId, name,
name, state,
state, };
}; });
})
.sort((a, b) => a.name.localeCompare(b.name));
return containers || []; return containers || [];
} catch (_error) {} } catch (_error) {}

View File

@@ -22,7 +22,6 @@ interface TraefikOptions {
additionalPorts?: { additionalPorts?: {
targetPort: number; targetPort: number;
publishedPort: number; publishedPort: number;
publishMode?: "ingress" | "host";
}[]; }[];
force?: boolean; force?: boolean;
} }

View File

@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
dockerBuildStage,
cleanCache,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
@@ -29,6 +35,10 @@ export const buildCustomDocker = async (
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
if (cleanCache) {
commandArgs.push("--no-cache");
}
if (dockerBuildStage) { if (dockerBuildStage) {
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
@@ -65,8 +75,14 @@ export const getDockerCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } = const {
application; appName,
env,
publishDirectory,
buildArgs,
dockerBuildStage,
cleanCache,
} = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
@@ -88,6 +104,10 @@ export const getDockerCommand = (
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
if (cleanCache) {
commandArgs.push("--no-cache");
}
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }

View File

@@ -9,7 +9,7 @@ export const buildHeroku = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -29,6 +29,10 @@ export const buildHeroku = async (
args.push("--env", env); args.push("--env", env);
} }
if (cleanCache) {
args.push("--clear-cache");
}
await spawnAsync("pack", args, (data) => { await spawnAsync("pack", args, (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
@@ -44,7 +48,7 @@ export const getHerokuCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
@@ -61,6 +65,10 @@ export const getHerokuCommand = (
`heroku/builder:${application.herokuVersion || "24"}`, `heroku/builder:${application.herokuVersion || "24"}`,
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -14,7 +14,7 @@ export const buildNixpacks = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName, publishDirectory } = application; const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`; const buildContainerId = `${appName}-${nanoid(10)}`;
@@ -32,6 +32,10 @@ export const buildNixpacks = async (
try { try {
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
if (cleanCache) {
args.push("--no-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); args.push("--env", env);
} }
@@ -91,7 +95,7 @@ export const getNixpacksCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName, publishDirectory } = application; const { env, appName, publishDirectory, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`; const buildContainerId = `${appName}-${nanoid(10)}`;
@@ -102,6 +106,10 @@ export const getNixpacksCommand = (
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
if (cleanCache) {
args.push("--no-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -8,7 +8,7 @@ export const buildPaketo = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -24,6 +24,10 @@ export const buildPaketo = async (
"paketobuildpacks/builder-jammy-full", "paketobuildpacks/builder-jammy-full",
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); args.push("--env", env);
} }
@@ -43,7 +47,7 @@ export const getPaketoCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
@@ -60,6 +64,10 @@ export const getPaketoCommand = (
"paketobuildpacks/builder-jammy-full", "paketobuildpacks/builder-jammy-full",
]; ];
if (cleanCache) {
args.push("--clear-cache");
}
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", `'${env}'`); args.push("--env", `'${env}'`);
} }

View File

@@ -4,12 +4,22 @@ import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
import { nanoid } from "nanoid";
import { createHash } from "node:crypto";
const calculateSecretsHash = (envVariables: string[]): string => {
const hash = createHash("sha256");
for (const env of envVariables.sort()) {
hash.update(env);
}
return hash.digest("hex");
};
export const buildRailpack = async ( export const buildRailpack = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
@@ -17,32 +27,80 @@ export const buildRailpack = async (
); );
try { try {
// Ensure buildkit container is running, create if it doesn't exist
await execAsync( await execAsync(
"docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit", "docker buildx create --use --name builder-containerd --driver docker-container || true",
); );
// Build the application using railpack await execAsync("docker buildx use builder-containerd");
const args = ["build", buildAppDirectory, "--name", appName];
// Add environment variables // First prepare the build plan and info
const prepareArgs = [
"prepare",
buildAppDirectory,
"--plan-out",
`${buildAppDirectory}/railpack-plan.json`,
"--info-out",
`${buildAppDirectory}/railpack-info.json`,
];
// Add environment variables to prepare command
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); prepareArgs.push("--env", env);
} }
// Run prepare command
await spawnAsync("railpack", prepareArgs, (data) => {
if (writeStream.writable) {
writeStream.write(data);
}
});
// Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
// Build with BuildKit using the Railpack frontend
const cacheKey = cleanCache ? nanoid(10) : undefined;
const buildArgs = [
"buildx",
"build",
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
`type=docker,name=${appName}`,
];
// Add secrets properly formatted
const env: { [key: string]: string } = {};
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
env[key] = value;
}
}
buildArgs.push(buildAppDirectory);
await spawnAsync( await spawnAsync(
"railpack", "docker",
args, buildArgs,
(data) => { (data) => {
if (writeStream.writable) { if (writeStream.writable) {
writeStream.write(data); writeStream.write(data);
} }
}, },
{ {
env: { env: { ...process.env, ...env },
...process.env,
BUILDKIT_HOST: "docker-container://buildkit",
},
}, },
); );
@@ -56,32 +114,84 @@ export const getRailpackCommand = (
application: ApplicationNested, application: ApplicationNested,
logPath: string, logPath: string,
) => { ) => {
const { env, appName } = application; const { env, appName, cleanCache } = application;
const buildAppDirectory = getBuildAppDirectory(application); const buildAppDirectory = getBuildAppDirectory(application);
const envVariables = prepareEnvironmentVariables( const envVariables = prepareEnvironmentVariables(
env, env,
application.project.env, application.project.env,
); );
// Build the application using railpack // Prepare command
const args = ["build", buildAppDirectory, "--name", appName]; const prepareArgs = [
"prepare",
buildAppDirectory,
"--plan-out",
`${buildAppDirectory}/railpack-plan.json`,
"--info-out",
`${buildAppDirectory}/railpack-info.json`,
];
// Add environment variables
for (const env of envVariables) { for (const env of envVariables) {
args.push("--env", env); prepareArgs.push("--env", env);
} }
const command = `railpack ${args.join(" ")}`; // Calculate secrets hash for layer invalidation
const secretsHash = calculateSecretsHash(envVariables);
const cacheKey = cleanCache ? nanoid(10) : undefined;
// Build command
const buildArgs = [
"buildx",
"build",
...(cacheKey
? [
"--build-arg",
`secrets-hash=${secretsHash}`,
"--build-arg",
`cache-key=${cacheKey}`,
]
: []),
"--build-arg",
"BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v0.0.55",
"-f",
`${buildAppDirectory}/railpack-plan.json`,
"--output",
`type=docker,name=${appName}`,
];
// Add secrets properly formatted
const exportEnvs = [];
for (const envVar of envVariables) {
const [key, value] = envVar.split("=");
if (key && value) {
buildArgs.push("--secret", `id=${key},env=${key}`);
exportEnvs.push(`export ${key}=${value}`);
}
}
buildArgs.push(buildAppDirectory);
const bashCommand = ` const bashCommand = `
echo "Building with Railpack..." >> "${logPath}"; # Ensure we have a builder with containerd
docker container inspect buildkit >/dev/null 2>&1 || docker run --rm --privileged -d --name buildkit moby/buildkit; docker buildx create --use --name builder-containerd --driver docker-container || true
export BUILDKIT_HOST=docker-container://buildkit; docker buildx use builder-containerd
${command} >> ${logPath} 2>> ${logPath} || {
echo " Railpack build failed" >> ${logPath}; echo "Preparing Railpack build plan..." >> "${logPath}";
exit 1; railpack ${prepareArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
} echo "❌ Railpack prepare failed" >> ${logPath};
echo "✅ Railpack build completed." >> ${logPath}; exit 1;
`; }
echo "✅ Railpack prepare completed." >> ${logPath};
echo "Building with Railpack frontend..." >> "${logPath}";
# Export environment variables for secrets
${exportEnvs.join("\n")}
docker ${buildArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
echo "❌ Railpack build failed" >> ${logPath};
exit 1;
}
echo "✅ Railpack build completed." >> ${logPath};
`;
return bashCommand; return bashCommand;
}; };

View File

@@ -238,9 +238,9 @@ export const addDomainToCompose = async (
if (Array.isArray(labels)) { if (Array.isArray(labels)) {
if (!labels.includes("traefik.enable=true")) { if (!labels.includes("traefik.enable=true")) {
labels.push("traefik.enable=true"); labels.unshift("traefik.enable=true");
} }
labels.push(...httpLabels); labels.unshift(...httpLabels);
} }
if (!compose.isolatedDeployment) { if (!compose.isolatedDeployment) {

46
pnpm-lock.yaml generated
View File

@@ -269,8 +269,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.0 specifier: 1.2.4
version: 1.2.0(typescript@5.5.3) version: 1.2.4(typescript@5.5.3)
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@@ -303,10 +303,10 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-orm: drizzle-orm:
specifier: ^0.39.1 specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
fancy-ansi: fancy-ansi:
specifier: ^0.1.3 specifier: ^0.1.3
version: 0.1.3 version: 0.1.3
@@ -547,7 +547,7 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-orm: drizzle-orm:
specifier: ^0.39.1 specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
hono: hono:
specifier: ^4.5.8 specifier: ^4.5.8
version: 4.5.8 version: 4.5.8
@@ -646,8 +646,8 @@ importers:
specifier: 5.1.1 specifier: 5.1.1
version: 5.1.1(encoding@0.1.13) version: 5.1.1(encoding@0.1.13)
better-auth: better-auth:
specifier: 1.2.0 specifier: 1.2.4
version: 1.2.0(typescript@5.5.3) version: 1.2.4(typescript@5.5.3)
bl: bl:
specifier: 6.0.11 specifier: 6.0.11
version: 6.0.11 version: 6.0.11
@@ -665,13 +665,13 @@ importers:
version: 16.4.5 version: 16.4.5
drizzle-dbml-generator: drizzle-dbml-generator:
specifier: 0.10.0 specifier: 0.10.0
version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)) version: 0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))
drizzle-orm: drizzle-orm:
specifier: ^0.39.1 specifier: ^0.39.1
version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) version: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8) version: 0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8)
hi-base32: hi-base32:
specifier: ^0.5.1 specifier: ^0.5.1
version: 0.5.1 version: 0.5.1
@@ -4030,8 +4030,8 @@ packages:
before-after-hook@2.2.3: before-after-hook@2.2.3:
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
better-auth@1.2.0: better-auth@1.2.4:
resolution: {integrity: sha512-eIRGOXfix25bh4fgs8jslZAZssufpIkxfEeEokQu5G4wICoDee1wPctkFb8v80PvhtI4dPm28SuAoZaAdRc6Wg==} resolution: {integrity: sha512-/ZK2jbUjm8JwdeCLFrUWUBmexPyI9PkaLVXWLWtN60sMDHTY8B5G72wcHglo1QMFBaw4G0qFkP5ayl9k6XfDaA==}
better-call@1.0.3: better-call@1.0.3:
resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==} resolution: {integrity: sha512-DUKImKoDIy5UtCvQbHTg0wuBRse6gu1Yvznn7+1B3I5TeY8sclRPFce0HI+4WF2bcb+9PqmkET8nXZubrHQh9A==}
@@ -5486,8 +5486,8 @@ packages:
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
kysely@0.27.5: kysely@0.27.6:
resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==} resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
leac@0.6.0: leac@0.6.0:
@@ -10902,7 +10902,7 @@ snapshots:
before-after-hook@2.2.3: {} before-after-hook@2.2.3: {}
better-auth@1.2.0(typescript@5.5.3): better-auth@1.2.4(typescript@5.5.3):
dependencies: dependencies:
'@better-auth/utils': 0.2.3 '@better-auth/utils': 0.2.3
'@better-fetch/fetch': 1.1.15 '@better-fetch/fetch': 1.1.15
@@ -10913,7 +10913,7 @@ snapshots:
better-call: 1.0.3 better-call: 1.0.3
defu: 6.1.4 defu: 6.1.4
jose: 5.9.6 jose: 5.9.6
kysely: 0.27.5 kysely: 0.27.6
nanostores: 0.11.3 nanostores: 0.11.3
valibot: 1.0.0-beta.15(typescript@5.5.3) valibot: 1.0.0-beta.15(typescript@5.5.3)
zod: 3.24.1 zod: 3.24.1
@@ -11534,9 +11534,9 @@ snapshots:
drange@1.1.1: {} drange@1.1.1: {}
drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)): drizzle-dbml-generator@0.10.0(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)):
dependencies: dependencies:
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
drizzle-kit@0.30.4: drizzle-kit@0.30.4:
dependencies: dependencies:
@@ -11547,18 +11547,18 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7): drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7):
optionalDependencies: optionalDependencies:
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
'@types/react': 18.3.5 '@types/react': 18.3.5
kysely: 0.27.5 kysely: 0.27.6
postgres: 3.4.4 postgres: 3.4.4
react: 18.2.0 react: 18.2.0
sqlite3: 5.1.7 sqlite3: 5.1.7
drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8): drizzle-zod@0.5.1(drizzle-orm@0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7))(zod@3.23.8):
dependencies: dependencies:
drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.5)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7) drizzle-orm: 0.39.1(@opentelemetry/api@1.9.0)(@types/react@18.3.5)(kysely@0.27.6)(postgres@3.4.4)(react@18.2.0)(sqlite3@5.1.7)
zod: 3.23.8 zod: 3.23.8
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
@@ -12450,7 +12450,7 @@ snapshots:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
kysely@0.27.5: {} kysely@0.27.6: {}
leac@0.6.0: {} leac@0.6.0: {}