Compare commits

..

90 Commits

Author SHA1 Message Date
Mauricio Siu
b965dedd7d Merge pull request #3407 from mhbdev/fix-ui-deployments-page
UI responsiveness in Deployments tab
2026-01-12 10:14:32 -06:00
Mauricio Siu
2b779f9fc6 Merge pull request #3444 from Dokploy/feat/add-railpack-selector-version
feat(build): add Railpack version selection with manual input option
2026-01-12 09:49:25 -06:00
Mauricio Siu
15b0ca7ab2 fix(input): add type safety for input reference handling 2026-01-12 09:49:13 -06:00
autofix-ci[bot]
fd6f61fd2a [autofix.ci] apply automated fixes 2026-01-12 15:47:51 +00:00
Mauricio Siu
8f95546535 Merge pull request #3410 from vikyw89/canary
fix: admin permission frontend side, should be able to see what owner can see
2026-01-12 09:32:28 -06:00
Mauricio Siu
8b370d4f7b Merge pull request #3370 from krishna2206/fix/gemini-ai-error
fix(selectAIProvider): add authorization header for Gemini provider
2026-01-12 09:28:21 -06:00
Mauricio Siu
1ed941b17c Merge pull request #3409 from mhbdev/auto-password-generator
Added a built-in password generator to the shared input
2026-01-12 09:21:28 -06:00
Mauricio Siu
18d980c3ff feat: enable password generator for database inputs and disable it for profile settings 2026-01-12 09:19:22 -06:00
Mauricio Siu
5ddcdd843c Merge branch 'canary' into auto-password-generator 2026-01-12 09:15:18 -06:00
Mauricio Siu
fdf88b1ff3 feat(build): add Railpack version selection with manual input option
- Introduced a dropdown for selecting Railpack versions, including a manual entry option for custom versions.
- Implemented state management to toggle between predefined versions and manual input.
- Updated form handling to accommodate the new selection method and provide user guidance.
2026-01-12 09:13:18 -06:00
autofix-ci[bot]
13b64e45ec [autofix.ci] apply automated fixes 2026-01-12 15:06:20 +00:00
Mauricio Siu
4383e46686 Merge pull request #3290 from amirhmoradi/claude/update-dockerfile-deps-WD7Lw
feat: Update build dependencies to their latest versions
2026-01-12 09:05:34 -06:00
Mauricio Siu
60d69d2915 Delete .claude/settings.local.json 2026-01-12 09:03:09 -06:00
autofix-ci[bot]
a2b16d4be8 [autofix.ci] apply automated fixes 2026-01-12 15:02:33 +00:00
Mauricio Siu
831a1815cf Merge pull request #3389 from tanmay-pathak/preview-deploy-rebuild
feat(preview):  add manual rebuild option for previews
2026-01-12 09:01:01 -06:00
Mauricio Siu
6b9bcbc539 feat(schema): extend deployJobSchema to include 'redeploy' type and enhance auth settings for development environment 2026-01-12 08:57:45 -06:00
Mauricio Siu
6ca6ff3530 Merge branch 'canary' into preview-deploy-rebuild 2026-01-12 08:46:19 -06:00
autofix-ci[bot]
7583d5f860 [autofix.ci] apply automated fixes 2026-01-12 14:45:09 +00:00
Mauricio Siu
7921f754fd Merge pull request #3427 from bdkopen/remove-@nerimity/mimiqueue
chore: uninstall `@nerimity/mimiqueue`
2026-01-12 08:44:24 -06:00
Mauricio Siu
0c0944d221 Update package.json 2026-01-11 22:16:50 -06:00
Mauricio Siu
d490111a58 Merge pull request #3441 from Dokploy/3260-dokploy-automatically-updates-itself-but-automated-updates-are-disabled-in-the-settings
chore(dependencies): update semver to version 7.7.3 and add @types/se…
2026-01-11 22:16:09 -06:00
Mauricio Siu
167daccee0 feat(settings): enhance getUpdateData and reloadDockerResource for image digest comparison
- Added logic to getUpdateData to compare current and latest image digests for canary and feature tags, indicating if an update is available.
- Updated reloadDockerResource to ensure the correct image tag is used during dokploy service updates based on the current image tag.
2026-01-11 22:12:39 -06:00
Mauricio Siu
11af6a5eb9 feat(docker): enhance reloadDockerResource to accept version parameter for dokploy updates
- Updated the reloadDockerResource function to include an optional version parameter.
- Modified the command for updating the dokploy service to specify the image version during updates.
2026-01-11 21:58:04 -06:00
Mauricio Siu
85424badcf chore(dependencies): update semver to version 7.7.3 and add @types/semver to package.json files; refactor getUpdateData function to accept current version as a parameter 2026-01-11 21:51:56 -06:00
Mauricio Siu
ccfd7f5189 Merge pull request #3439 from Dokploy/3102-web-server-backup-keep-the-latest-not-working
feat(backup): add functionality to keep the latest N backups after ru…
2026-01-11 20:44:39 -06:00
Mauricio Siu
6d94da1dee feat(backup): add functionality to keep the latest N backups after running a backup 2026-01-11 20:44:16 -06:00
Mauricio Siu
10c0de9d5f Merge pull request #3431 from Dokploy/copilot/fix-invalid-link-view-repository
Fix GitLab "View Repository" link to use full path namespace and custom URL
2026-01-11 20:29:10 -06:00
Mauricio Siu
2b0ae65f71 Merge pull request #3438 from Dokploy/feat/add-invoices-billing
Feat/add invoices billing
2026-01-11 20:25:21 -06:00
autofix-ci[bot]
2acaaede37 [autofix.ci] apply automated fixes 2026-01-12 02:22:33 +00:00
Mauricio Siu
f303962319 fix(database): update container name query to use exact match
- Modified the SQL queries in GetLastNContainerMetrics and GetAllMetricsContainer functions to use an exact match for container names instead of a LIKE clause, improving query accuracy and performance.
2026-01-11 20:21:41 -06:00
Mauricio Siu
edc8efe816 refactor(servers): replace DropdownMenuItem with Button for Setup Server action
- Updated the SetupServer component to use a Button instead of DropdownMenuItem for better accessibility and user experience.
- Enhanced the ShowServers component by adding tooltips for the Setup Server action, providing users with additional context on server configuration.
2026-01-11 19:21:29 -06:00
Mauricio Siu
4e0cb2a9c7 feat(billing): add billing invoices page and update billing components
- Introduced `ShowBillingInvoices` component to manage and display billing invoices.
- Updated `ShowBilling` component to include navigation for invoices and enhanced subscription management.
- Refactored `ShowInvoices` component for improved loading and display logic.
- Created a new invoices page with server-side validation and layout integration.
2026-01-11 18:34:14 -06:00
Mauricio Siu
4001f1d067 feat(billing): implement invoice display and retrieval functionality
- Added `ShowInvoices` component to display user invoices with status and actions.
- Integrated Stripe API to fetch invoices for the authenticated user.
- Updated `ShowBilling` component to conditionally render invoices if the user has a Stripe customer ID.
2026-01-11 18:27:19 -06:00
Mauricio Siu
d894b2a3bf feat(stripe): add customer_email to payment metadata 2026-01-11 18:17:19 -06:00
copilot-swe-agent[bot]
14d359dd14 Fix GitLab View Repository links to use correct URL and namespace
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-10 17:45:17 +00:00
copilot-swe-agent[bot]
1e11f603de Initial plan 2026-01-10 17:41:46 +00:00
bdkopen
d12f029e2b chore: uninstall @nerimity/mimiqueue 2026-01-10 00:11:26 -05:00
Amir Moradi
0c62bc0f29 fix: create migrations and update to latest railpack 2026-01-08 12:29:42 +01:00
Amir Moradi
b19d3e94eb Merge branch 'canary' of github.com:amirhmoradi/dokploy into claude/update-dockerfile-deps-WD7Lw 2026-01-08 11:53:55 +01:00
viky
5005f9198b fix: admin permission frontend side, should be able to see what owner can see 2026-01-06 23:52:40 +08:00
mhbdev
fe5efd7651 Added a built-in password generator to the shared input 2026-01-06 16:26:42 +03:30
mhbdev
8db7a421dc Made the deployments list items responsive by stacking the metadata/actions under the status on small screens, then restoring the side-by-side layout at sm and up. This keeps the date/duration and buttons from being squeezed or pushed off-screen in narrow widths. 2026-01-06 16:04:19 +03:30
Mauricio Siu
068deecb61 Merge pull request #3401 from bdkopen/remove-hi-base32-package
chore: uninstall `hi-base32` package
2026-01-05 22:44:50 -06:00
Mauricio Siu
9aa03efd13 Merge pull request #3402 from bdkopen/remove-otpauth-package
chore: uninstall `otpauth` package
2026-01-05 22:44:37 -06:00
bdkopen
016aa0248a chore: uninstall unused otpauth package 2026-01-05 22:27:57 -05:00
bdkopen
eb9d140c5d chore: uninstall ununused hi-base32 package 2026-01-05 21:13:25 -05:00
Tanmay Pathak
2eb73b988b feat(preview): add manual rebuild option for preview deployments 2026-01-04 15:24:25 -06:00
Mauricio Siu
d2ce587494 feat(compose): include composeId in deployment and redeployment responses close https://github.com/Dokploy/dokploy/issues/3359 2026-01-04 11:08:11 -06:00
Mauricio Siu
13ad8cb846 Merge pull request #3371 from mcfdez/feat/solid-color-avatars
feat: add solid colors for avatar
2026-01-04 11:07:13 -06:00
autofix-ci[bot]
0897417d7c [autofix.ci] apply automated fixes 2026-01-04 17:01:40 +00:00
Marc Fernandez
eb14a68bdd feat: add solid colors for avatar 2025-12-31 08:58:25 +01:00
Fitiavana Anhy Krishna
01c0b461b5 fix(selectAIProvider): add authorization header for Gemini provider 2025-12-31 10:13:20 +03:00
Mauricio Siu
9498fbeff3 Update package.json 2025-12-31 00:28:03 -06:00
Mauricio Siu
d2aa60ddf7 Update package.json 2025-12-30 23:53:30 -06:00
Mauricio Siu
58b75205af Merge pull request #3327 from Dokploy/refactor/separate-settings-from-users-table
refactor(settings): migrate user settings to webServerSettings schema…
2025-12-28 13:21:55 -06:00
Mauricio Siu
9e03625586 refactor(auth): simplify trustedOrigins logic by removing redundant admin check and using optional chaining for settings access 2025-12-28 13:18:20 -06:00
Mauricio Siu
260efdc2bb Merge pull request #3353 from bdkopen/remove-rotating-file-stream
chore: uninstall `rotating-file-stream`
2025-12-28 13:09:34 -06:00
bdkopen
1b5bfe051d chore: uninstall rotating-file-stream 2025-12-27 12:33:39 -05:00
Mauricio Siu
e4384075f2 Merge pull request #3341 from dpulpeiro/fix/stack-registry-auth
fix: pass registry auth to stack deploy
2025-12-25 03:29:33 -06:00
Mauricio Siu
b355d44605 fix(web-server-settings): use optional chaining for safer ID access in update function 2025-12-24 12:24:27 -06:00
Daniel García Pulpeiro
f39aa23803 fix: pass registry auth to stack deploy 2025-12-23 22:37:00 +01:00
Mauricio Siu
3abc4cdc3b refactor(access-log): consolidate web server settings imports and enhance log cleanup status retrieval 2025-12-21 01:46:27 -06:00
Mauricio Siu
ec56062f17 fix(settings): update getIp function to return an empty string for cloud environments 2025-12-21 01:45:49 -06:00
Mauricio Siu
10c4f882a5 Update packages/server/src/services/web-server-settings.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-21 01:44:46 -06:00
Mauricio Siu
f1dfa9c6a2 refactor(preview-deployment): remove dynamic import of getWebServerSettings and streamline IP retrieval logic 2025-12-21 01:43:09 -06:00
Mauricio Siu
6010643d9e refactor(server): update server configuration handling to utilize webServerSettings schema and improve code clarity 2025-12-21 01:41:33 -06:00
Mauricio Siu
1ccb205495 fix(admin): add optional chaining to safely access settings properties 2025-12-21 01:35:21 -06:00
autofix-ci[bot]
b2be5bc09f [autofix.ci] apply automated fixes 2025-12-21 07:33:59 +00:00
Mauricio Siu
babd30a110 refactor(settings): migrate user settings to webServerSettings schema and update related components 2025-12-21 01:33:18 -06:00
Amir Moradi
67d5e1a350 Update Docker version in server setup script 2025-12-20 07:46:31 +01:00
Amir Moradi
93fa19213e Downgrade package manager version to pnpm@9.12.0 2025-12-20 07:45:48 +01:00
Amir Moradi
1988a14b24 Downgrade package manager version to pnpm@9.12.0 2025-12-20 07:45:24 +01:00
Amir Moradi
3bdf029155 Downgrade pnpm version in package.json 2025-12-20 07:44:51 +01:00
Amir Moradi
e1896c2498 Downgrade pnpm version in package.json 2025-12-20 07:44:22 +01:00
Amir Moradi
a8064afd60 Downgrade pnpm version in package.json 2025-12-20 07:43:50 +01:00
Amir Moradi
3849a206e8 Downgrade pnpm version in Dockerfile.server 2025-12-20 07:43:23 +01:00
Amir Moradi
bb0a53d976 Downgrade pnpm version in Dockerfile.schedule 2025-12-20 07:43:00 +01:00
Amir Moradi
0a8753d0a9 Update pnpm version in Dockerfile.cloud 2025-12-20 07:42:30 +01:00
Amir Moradi
23b14cf0cf Update pnpm and Docker versions in Dockerfile
Updated pnpm version from 9.15.9 to 9.12.0 and Docker version from 29.1.3 to 28.5.2.
2025-12-20 07:41:10 +01:00
Amir Moradi
ed701df6ac Downgrade package manager to pnpm@9.15.9 2025-12-17 01:38:03 +01:00
Amir Moradi
dfc15cd621 Downgrade pnpm version in package.json 2025-12-17 01:37:11 +01:00
Amir Moradi
1ac3d1c1b0 Downgrade pnpm version in package.json 2025-12-17 01:36:40 +01:00
Amir Moradi
f6b756e711 Downgrade pnpm version in package.json 2025-12-17 01:36:05 +01:00
Amir Moradi
9f84dd4e0d Downgrade pnpm version in package.json 2025-12-17 01:35:12 +01:00
Amir Moradi
2e32b0a4af Update pnpm version in Dockerfile.server 2025-12-17 01:34:01 +01:00
Amir Moradi
0f69bbbd20 Downgrade pnpm version to 9.15.9 2025-12-17 01:33:36 +01:00
Amir Moradi
9e79314ef4 Downgrade pnpm version to 9.15.9 2025-12-17 01:33:14 +01:00
Amir Moradi
540b4039ac use pnpm 9.15.9 2025-12-17 01:32:59 +01:00
Claude
9e89edf167 chore(deps): update all tool versions across the codebase
Update to latest stable versions:
- pnpm: 9.12.0 → 10.26.0
- Docker: 28.5.0/28.5.2 → 29.1.3
- Nixpacks: 1.39.0 → 1.41.0
- Railpack: 0.2.2/0.15.0 → 0.15.1
- buildpacks/pack: 0.35.0 → 0.39.1

Files updated:
- All Dockerfiles (main, schedule, cloud, server)
- All package.json files (root, server, api, schedules, dokploy)
- GitHub workflow (pull-request.yml)
- Server setup script
- Database schema and DBML files
- Test fixtures
- UI components
2025-12-16 21:06:40 +00:00
Claude
e31d5a723b chore(deps): update Dockerfile dependencies to latest versions
- pnpm: 9.12.0 → 10.26.0
- Docker: 28.5.2 → 29.1.3
- Nixpacks: 1.39.0 → 1.41.0
- Railpack: 0.2.2 → 0.15.1
- buildpacks/pack: 0.35.0 → 0.39.1
2025-12-16 20:44:11 +00:00
87 changed files with 15721 additions and 604 deletions

View File

@@ -24,14 +24,14 @@ jobs:
- name: Install Nixpacks - name: Install Nixpacks
if: matrix.job == 'test' if: matrix.job == 'test'
run: | run: |
export NIXPACKS_VERSION=1.39.0 export NIXPACKS_VERSION=1.41.0
curl -sSL https://nixpacks.com/install.sh | bash curl -sSL https://nixpacks.com/install.sh | bash
echo "Nixpacks installed $NIXPACKS_VERSION" echo "Nixpacks installed $NIXPACKS_VERSION"
- name: Install Railpack - name: Install Railpack
if: matrix.job == 'test' if: matrix.job == 'test'
run: | run: |
export RAILPACK_VERSION=0.15.0 export RAILPACK_VERSION=0.15.4
curl -sSL https://railpack.com/install.sh | bash curl -sSL https://railpack.com/install.sh | bash
echo "Railpack installed $RAILPACK_VERSION" echo "Railpack installed $RAILPACK_VERSION"

5
.gitignore vendored
View File

@@ -43,4 +43,7 @@ yarn-error.log*
*.pem *.pem
.db .db
# Development environment
.devcontainer

View File

@@ -148,7 +148,7 @@ curl -sSL https://railpack.com/install.sh | sh
```bash ```bash
# Install Buildpacks # Install Buildpacks
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.35.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v0.39.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
``` ```
## Pull Request ## Pull Request

View File

@@ -51,18 +51,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --ver
# Install Nixpacks and tsx # Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash # | VERBOSE=1 VERSION=1.21.0 bash
ARG NIXPACKS_VERSION=1.39.0 ARG NIXPACKS_VERSION=1.41.0
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& chmod +x install.sh \ && chmod +x install.sh \
&& ./install.sh \ && ./install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.2.2 ARG RAILPACK_VERSION=0.15.4
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -60,4 +60,4 @@ RUN curl https://rclone.org/install.sh | bash
RUN pnpm install -g tsx RUN pnpm install -g tsx
EXPOSE 3000 EXPOSE 3000
CMD [ "pnpm", "start" ] CMD [ "pnpm", "start" ]

View File

@@ -35,4 +35,4 @@ COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start CMD HOSTNAME=0.0.0.0 && pnpm start

View File

@@ -35,4 +35,4 @@ COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start CMD HOSTNAME=0.0.0.0 && pnpm start

View File

@@ -13,7 +13,6 @@
"@dokploy/server": "workspace:*", "@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",
"@hono/zod-validator": "0.3.0", "@hono/zod-validator": "0.3.0",
"@nerimity/mimiqueue": "1.2.3",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"hono": "^4.7.10", "hono": "^4.7.10",
"pino": "9.4.0", "pino": "9.4.0",

View File

@@ -25,7 +25,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
titleLog: z.string().optional(), titleLog: z.string().optional(),
descriptionLog: z.string().optional(), descriptionLog: z.string().optional(),
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy"]), type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application-preview"), applicationType: z.literal("application-preview"),
serverId: z.string().min(1), serverId: z.string().min(1),
}), }),

View File

@@ -4,6 +4,7 @@ import {
deployPreviewApplication, deployPreviewApplication,
rebuildApplication, rebuildApplication,
rebuildCompose, rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -54,7 +55,14 @@ export const deploy = async (job: DeployJob) => {
previewStatus: "running", previewStatus: "running",
}); });
if (job.server) { if (job.server) {
if (job.type === "deploy") { if (job.type === "redeploy") {
await rebuildPreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog || "Rebuild Preview Deployment",
descriptionLog: job.descriptionLog || "",
previewDeploymentId: job.previewDeploymentId,
});
} else if (job.type === "deploy") {
await deployPreviewApplication({ await deployPreviewApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog || "Preview Deployment", titleLog: job.titleLog || "Preview Deployment",

View File

@@ -25,7 +25,7 @@ if (typeof window === "undefined") {
} }
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2", railpackVersion: "0.15.4",
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],
createEnvFile: true, createEnvFile: true,

View File

@@ -5,21 +5,27 @@ vi.mock("node:fs", () => ({
default: fs, default: fs,
})); }));
import type { FileConfig, User } from "@dokploy/server"; import type { FileConfig } from "@dokploy/server";
import { import {
createDefaultServerTraefikConfig, createDefaultServerTraefikConfig,
loadOrCreateConfig, loadOrCreateConfig,
updateServerTraefik, updateServerTraefik,
} from "@dokploy/server"; } from "@dokploy/server";
import type { webServerSettings } from "@dokploy/server/db/schema";
import { beforeEach, expect, test, vi } from "vitest"; import { beforeEach, expect, test, vi } from "vitest";
const baseAdmin: User = { type WebServerSettings = typeof webServerSettings.$inferSelect;
const baseSettings: WebServerSettings = {
id: "",
https: false, https: false,
enablePaidFeatures: false, certificateType: "none",
allowImpersonation: false, host: null,
role: "user", serverIp: null,
firstName: "", letsEncryptEmail: null,
lastName: "", sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
metricsConfig: { metricsConfig: {
containers: { containers: {
refreshRate: 20, refreshRate: 20,
@@ -45,29 +51,8 @@ const baseAdmin: User = {
cleanupCacheApplications: false, cleanupCacheApplications: false,
cleanupCacheOnCompose: false, cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false, cleanupCacheOnPreviews: false,
createdAt: new Date(), createdAt: null,
serverIp: null,
certificateType: "none",
host: null,
letsEncryptEmail: null,
sshPrivateKey: null,
enableDockerCleanup: false,
logCleanupCron: null,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
banExpires: new Date(),
banned: true,
banReason: "",
email: "",
expirationDate: "",
id: "",
isRegistered: false,
createdAt2: new Date().toISOString(),
emailVerified: false,
image: "",
updatedAt: new Date(), updatedAt: new Date(),
twoFactorEnabled: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -85,7 +70,7 @@ test("Should read the configuration file", () => {
test("Should apply redirect-to-https", () => { test("Should apply redirect-to-https", () => {
updateServerTraefik( updateServerTraefik(
{ {
...baseAdmin, ...baseSettings,
https: true, https: true,
certificateType: "letsencrypt", certificateType: "letsencrypt",
}, },
@@ -100,7 +85,7 @@ test("Should apply redirect-to-https", () => {
}); });
test("Should change only host when no certificate", () => { test("Should change only host when no certificate", () => {
updateServerTraefik(baseAdmin, "example.com"); updateServerTraefik(baseSettings, "example.com");
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -110,7 +95,7 @@ test("Should change only host when no certificate", () => {
test("Should not touch config without host", () => { test("Should not touch config without host", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik(baseAdmin, null); updateServerTraefik(baseSettings, null);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");
@@ -119,11 +104,14 @@ test("Should not touch config without host", () => {
test("Should remove websecure if https rollback to http", () => { test("Should remove websecure if https rollback to http", () => {
updateServerTraefik( updateServerTraefik(
{ ...baseAdmin, certificateType: "letsencrypt" }, { ...baseSettings, certificateType: "letsencrypt" },
"example.com", "example.com",
); );
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com"); updateServerTraefik(
{ ...baseSettings, certificateType: "none" },
"example.com",
);
const config: FileConfig = loadOrCreateConfig("dokploy"); const config: FileConfig = loadOrCreateConfig("dokploy");

View File

@@ -3,7 +3,7 @@ import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2", railpackVersion: "0.15.4",
rollbackActive: false, rollbackActive: false,
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],

View File

@@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react"; import { Cog } from "lucide-react";
import { useEffect } from "react"; 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";
@@ -20,8 +20,39 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
// Railpack versions from https://github.com/railwayapp/railpack/releases
export const RAILPACK_VERSIONS = [
"0.15.4",
"0.15.3",
"0.15.2",
"0.15.1",
"0.15.0",
"0.14.0",
"0.13.0",
"0.12.0",
"0.11.0",
"0.10.0",
"0.9.2",
"0.9.1",
"0.9.0",
"0.8.0",
"0.7.0",
"0.6.0",
"0.5.0",
"0.4.0",
"0.3.0",
"0.2.2",
] as const;
export enum BuildType { export enum BuildType {
dockerfile = "dockerfile", dockerfile = "dockerfile",
heroku_buildpacks = "heroku_buildpacks", heroku_buildpacks = "heroku_buildpacks",
@@ -65,7 +96,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"), railpackVersion: z.string().nullable().default("0.15.4"),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.static),
@@ -152,6 +183,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}); });
const buildType = form.watch("buildType"); const buildType = form.watch("buildType");
const railpackVersion = form.watch("railpackVersion");
const [isManualRailpackVersion, setIsManualRailpackVersion] = useState(false);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -163,6 +196,14 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}; };
form.reset(resetData(typedData)); form.reset(resetData(typedData));
// Check if railpack version is manual (not in the predefined list)
if (
data.railpackVersion &&
!RAILPACK_VERSIONS.includes(data.railpackVersion as any)
) {
setIsManualRailpackVersion(true);
}
} }
}, [data, form]); }, [data, form]);
@@ -186,7 +227,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === BuildType.static ? data.isStaticSpa : null, data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion: railpackVersion:
data.buildType === BuildType.railpack data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2" ? data.railpackVersion || "0.15.4"
: null, : null,
}) })
.then(async () => { .then(async () => {
@@ -403,23 +444,88 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
/> />
)} )}
{buildType === BuildType.railpack && ( {buildType === BuildType.railpack && (
<FormField <>
control={form.control} <FormField
name="railpackVersion" control={form.control}
render={({ field }) => ( name="railpackVersion"
<FormItem> render={({ field }) => (
<FormLabel>Railpack Version</FormLabel> <FormItem>
<FormControl> <FormLabel>Railpack Version</FormLabel>
<Input <FormControl>
placeholder="Railpack Version" {isManualRailpackVersion ? (
{...field} <div className="space-y-2">
value={field.value ?? ""} <Input
/> placeholder="Enter custom version (e.g., 0.15.4)"
</FormControl> {...field}
<FormMessage /> value={field.value ?? ""}
</FormItem> />
)} <Button
/> type="button"
variant="outline"
size="sm"
onClick={() => {
setIsManualRailpackVersion(false);
field.onChange("0.15.4");
}}
>
Use predefined versions
</Button>
</div>
) : (
<Select
onValueChange={(value) => {
if (value === "manual") {
setIsManualRailpackVersion(true);
field.onChange("");
} else {
field.onChange(value);
}
}}
value={field.value ?? "0.15.4"}
>
<SelectTrigger>
<SelectValue placeholder="Select Railpack version" />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">
<span className="font-medium">
Manual (Custom Version)
</span>
</SelectItem>
{RAILPACK_VERSIONS.map((version) => (
<SelectItem key={version} value={version}>
v{version}
{version === "0.15.4" && (
<Badge
variant="secondary"
className="ml-2 px-1 text-xs"
>
Latest
</Badge>
)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</FormControl>
<FormDescription>
Select a Railpack version or choose manual to enter a
custom version.{" "}
<a
href="https://github.com/railwayapp/railpack/releases"
target="_blank"
rel="noreferrer"
className="text-primary underline underline-offset-4"
>
View releases
</a>
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)} )}
<div className="flex w-full justify-end"> <div className="flex w-full justify-end">
<Button isLoading={isLoading} type="submit"> <Button isLoading={isLoading} type="submit">

View File

@@ -256,9 +256,9 @@ export const ShowDeployments = ({
return ( return (
<div <div
key={deployment.deploymentId} key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2" className="flex flex-col gap-4 rounded-lg border p-4 sm:flex-row sm:items-center sm:justify-between"
> >
<div className="flex flex-col"> <div className="flex flex-1 flex-col min-w-0">
<span className="flex items-center gap-4 font-medium capitalize text-foreground"> <span className="flex items-center gap-4 font-medium capitalize text-foreground">
{index + 1}. {deployment.status} {index + 1}. {deployment.status}
<StatusTooltip <StatusTooltip
@@ -313,8 +313,8 @@ export const ShowDeployments = ({
)} )}
</div> </div>
</div> </div>
<div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start"> <div className="flex w-full flex-col items-start gap-2 sm:w-auto sm:max-w-[300px] sm:items-end sm:justify-start">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2"> <div className="text-sm capitalize text-muted-foreground flex flex-wrap items-center gap-2">
<DateTooltip date={deployment.createdAt} /> <DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && ( {deployment.startedAt && deployment.finishedAt && (
<Badge <Badge
@@ -333,7 +333,7 @@ export const ShowDeployments = ({
)} )}
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row sm:items-center sm:justify-end">
{deployment.pid && deployment.status === "running" && ( {deployment.pid && deployment.status === "running" && (
<DialogAction <DialogAction
title="Kill Process" title="Kill Process"
@@ -355,6 +355,7 @@ export const ShowDeployments = ({
variant="destructive" variant="destructive"
size="sm" size="sm"
isLoading={isKillingProcess} isLoading={isKillingProcess}
className="w-full sm:w-auto"
> >
Kill Process Kill Process
</Button> </Button>
@@ -364,6 +365,7 @@ export const ShowDeployments = ({
onClick={() => { onClick={() => {
setActiveLog(deployment); setActiveLog(deployment);
}} }}
className="w-full sm:w-auto"
> >
View View
</Button> </Button>
@@ -405,6 +407,7 @@ export const ShowDeployments = ({
variant="secondary" variant="secondary"
size="sm" size="sm"
isLoading={isRollingBack} isLoading={isRollingBack}
className="w-full sm:w-auto"
> >
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" /> <RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
Rollback Rollback

View File

@@ -232,9 +232,9 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormItem className="md:col-span-2 flex flex-col"> <FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && ( {field.value.gitlabPathNamespace && (
<Link <Link
href={`${gitlabUrl}/${field.value.owner}/${field.value.repo}`} href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"

View File

@@ -2,6 +2,7 @@ import {
ExternalLink, ExternalLink,
FileText, FileText,
GitPullRequest, GitPullRequest,
Hammer,
Loader2, Loader2,
PenSquare, PenSquare,
RocketIcon, RocketIcon,
@@ -22,6 +23,13 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
@@ -38,6 +46,9 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const { mutateAsync: deletePreviewDeployment, isLoading } = const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation(); api.previewDeployment.delete.useMutation();
const { mutateAsync: redeployPreviewDeployment } =
api.previewDeployment.redeploy.useMutation();
const { const {
data: previewDeployments, data: previewDeployments,
refetch: refetchPreviewDeployments, refetch: refetchPreviewDeployments,
@@ -46,6 +57,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
{ applicationId }, { applicationId },
{ {
enabled: !!applicationId, enabled: !!applicationId,
refetchInterval: (data) =>
data?.some((d) => d.previewStatus === "running") ? 2000 : false,
}, },
); );
@@ -193,6 +206,58 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
</Button> </Button>
</ShowDeploymentsModal> </ShowDeploymentsModal>
<DialogAction
title="Rebuild Preview Deployment"
description="Are you sure you want to rebuild this preview deployment?"
type="default"
onClick={async () => {
await redeployPreviewDeployment({
previewDeploymentId:
deployment.previewDeploymentId,
})
.then(() => {
toast.success(
"Preview deployment rebuild started",
);
refetchPreviewDeployments();
})
.catch(() => {
toast.error(
"Error rebuilding preview deployment",
);
});
}}
>
<Button
variant="outline"
size="sm"
isLoading={status === "running"}
className="gap-2"
>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2">
<Hammer className="size-4" />
Rebuild
</div>
</TooltipTrigger>
<TooltipPrimitive.Portal>
<TooltipContent
sideOffset={5}
className="z-[60]"
>
<p>
Rebuild the preview deployment without
downloading new code
</p>
</TooltipContent>
</TooltipPrimitive.Portal>
</Tooltip>
</TooltipProvider>
</Button>
</DialogAction>
<AddPreviewDomain <AddPreviewDomain
previewDeploymentId={`${deployment.previewDeploymentId}`} previewDeploymentId={`${deployment.previewDeploymentId}`}
domainId={deployment.domain?.domainId} domainId={deployment.domain?.domainId}

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react"; import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useEffect } from "react"; import { useEffect, useMemo } 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";
@@ -97,6 +97,16 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
const repository = form.watch("repository"); const repository = form.watch("repository");
const gitlabId = form.watch("gitlabId"); const gitlabId = form.watch("gitlabId");
const gitlabUrl = useMemo(() => {
const url = gitlabProviders?.find(
(provider) => provider.gitlabId === gitlabId,
)?.gitlabUrl;
const gitlabUrl = url?.replace(/\/$/, "");
return gitlabUrl || "https://gitlab.com";
}, [gitlabId, gitlabProviders]);
const { const {
data: repositories, data: repositories,
isLoading: isLoadingRepositories, isLoading: isLoadingRepositories,
@@ -224,9 +234,9 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormItem className="md:col-span-2 flex flex-col"> <FormItem className="md:col-span-2 flex flex-col">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && ( {field.value.gitlabPathNamespace && (
<Link <Link
href={`https://gitlab.com/${field.value.owner}/${field.value.repo}`} href={`${gitlabUrl}/${field.value.gitlabPathNamespace}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"

View File

@@ -559,6 +559,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
type="password" type="password"
placeholder="******************" placeholder="******************"
autoComplete="one-time-code" autoComplete="one-time-code"
enablePasswordGenerator={true}
{...field} {...field}
/> />
</FormControl> </FormControl>
@@ -578,6 +579,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
<Input <Input
type="password" type="password"
placeholder="******************" placeholder="******************"
enablePasswordGenerator={true}
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -0,0 +1,74 @@
import { CreditCard, FileText } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { ShowInvoices } from "./show-invoices";
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
export const ShowBillingInvoices = () => {
const router = useRouter();
return (
<div className="w-full">
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl flex flex-row gap-2">
<CreditCard className="size-6 text-muted-foreground self-center" />
Billing
</CardTitle>
<CardDescription>
Manage your subscription and invoices
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 py-4 border-t">
<nav className="flex space-x-2 border-b">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="mt-6">
<ShowInvoices />
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -4,11 +4,13 @@ import {
AlertTriangle, AlertTriangle,
CheckIcon, CheckIcon,
CreditCard, CreditCard,
FileText,
Loader2, Loader2,
MinusIcon, MinusIcon,
PlusIcon, PlusIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react"; import { useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -37,7 +39,22 @@ export const calculatePrice = (count: number, isAnnual = false) => {
if (count <= 1) return 4.5; if (count <= 1) return 4.5;
return count * 3.5; return count * 3.5;
}; };
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
export const ShowBilling = () => { export const ShowBilling = () => {
const router = useRouter();
const { data: servers } = api.server.count.useQuery(); const { data: servers } = api.server.count.useQuery();
const { data: admin } = api.user.get.useQuery(); const { data: admin } = api.user.get.useQuery();
const { data, isLoading } = api.stripe.getProducts.useQuery(); const { data, isLoading } = api.stripe.getProducts.useQuery();
@@ -76,17 +93,41 @@ export const ShowBilling = () => {
return ( return (
<div className="w-full"> <div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto"> <Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md "> <div className="rounded-xl bg-background shadow-md">
<CardHeader className=""> <CardHeader>
<CardTitle className="text-xl flex flex-row gap-2"> <CardTitle className="text-xl flex flex-row gap-2">
<CreditCard className="size-6 text-muted-foreground self-center" /> <CreditCard className="size-6 text-muted-foreground self-center" />
Billing Billing
</CardTitle> </CardTitle>
<CardDescription>Manage your subscription</CardDescription> <CardDescription>
Manage your subscription and invoices
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2 py-8 border-t"> <CardContent className="space-y-4 py-4 border-t">
<div className="flex flex-col gap-4 w-full"> <nav className="flex space-x-2 border-b">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="flex flex-col gap-4 w-full mt-6">
<Tabs <Tabs
defaultValue="monthly" defaultValue="monthly"
value={isAnnual ? "annual" : "monthly"} value={isAnnual ? "annual" : "monthly"}

View File

@@ -0,0 +1,137 @@
import { Download, ExternalLink, FileText, Loader2 } from "lucide-react";
import type Stripe from "stripe";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
const formatDate = (timestamp: number | null) => {
if (!timestamp) return "-";
return new Date(timestamp * 1000).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
};
const formatAmount = (amount: number, currency: string) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: currency.toUpperCase(),
}).format(amount / 100);
};
const getStatusBadge = (status: Stripe.Invoice.Status | null) => {
const statusConfig: Record<
Stripe.Invoice.Status,
{ label: string; variant: "default" | "secondary" | "destructive" }
> = {
paid: { label: "Paid", variant: "default" },
open: { label: "Open", variant: "secondary" },
draft: { label: "Draft", variant: "secondary" },
void: { label: "Void", variant: "destructive" },
uncollectible: { label: "Uncollectible", variant: "destructive" },
};
if (!status) {
return <Badge variant="secondary">Unknown</Badge>;
}
const config = statusConfig[status] || {
label: status,
variant: "secondary" as const,
};
return <Badge variant={config.variant}>{config.label}</Badge>;
};
export const ShowInvoices = () => {
const { data: invoices, isLoading } = api.stripe.getInvoices.useQuery();
return (
<div className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center min-h-[20vh]">
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center">
Loading invoices...
<Loader2 className="animate-spin" />
</span>
</div>
) : invoices && invoices.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Date</TableHead>
<TableHead>Due Date</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoices.map((invoice) => (
<TableRow key={invoice.id}>
<TableCell className="font-medium">
{invoice.number || invoice.id.slice(0, 12)}
</TableCell>
<TableCell>{formatDate(invoice.created)}</TableCell>
<TableCell>{formatDate(invoice.dueDate)}</TableCell>
<TableCell>
{formatAmount(invoice.amountDue, invoice.currency)}
</TableCell>
<TableCell>{getStatusBadge(invoice.status)}</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
{invoice.hostedInvoiceUrl && (
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(
invoice.hostedInvoiceUrl || "",
"_blank",
)
}
>
<ExternalLink className="h-4 w-4" />
</Button>
)}
{invoice.invoicePdf && (
<Button
variant="outline"
size="sm"
onClick={() =>
window.open(invoice.invoicePdf || "", "_blank")
}
>
<Download className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex flex-col items-center justify-center min-h-[20vh] gap-2">
<FileText className="size-12 text-muted-foreground" />
<p className="text-base text-muted-foreground">No invoices found</p>
<p className="text-sm text-muted-foreground">
Your invoices will appear here once you have a subscription
</p>
</div>
)}
</div>
);
};

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Loader2, User } from "lucide-react"; import { Loader2, Palette, User } from "lucide-react";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useRef, 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";
@@ -27,6 +27,7 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { getAvatarType, isSolidColorAvatar } from "@/lib/avatar-utils";
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils"; import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Configure2FA } from "./configure-2fa"; import { Configure2FA } from "./configure-2fa";
@@ -74,6 +75,7 @@ export const ProfileForm = () => {
} = api.user.update.useMutation(); } = api.user.update.useMutation();
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const [gravatarHash, setGravatarHash] = useState<string | null>(null); const [gravatarHash, setGravatarHash] = useState<string | null>(null);
const colorInputRef = useRef<HTMLInputElement>(null);
const availableAvatars = useMemo(() => { const availableAvatars = useMemo(() => {
if (gravatarHash === null) return randomImages; if (gravatarHash === null) return randomImages;
@@ -274,16 +276,8 @@ export const ProfileForm = () => {
onValueChange={(e) => { onValueChange={(e) => {
field.onChange(e); field.onChange(e);
}} }}
defaultValue={ defaultValue={getAvatarType(field.value)}
field.value?.startsWith("data:") value={getAvatarType(field.value)}
? "upload"
: field.value
}
value={
field.value?.startsWith("data:")
? "upload"
: field.value
}
className="flex flex-row flex-wrap gap-2 max-xl:justify-center" className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
> >
<FormItem key="no-avatar"> <FormItem key="no-avatar">
@@ -370,6 +364,40 @@ export const ProfileForm = () => {
/> />
</FormLabel> </FormLabel>
</FormItem> </FormItem>
<FormItem key="color-avatar">
<FormLabel className="[&:has([data-state=checked])>.color-avatar]:border-primary [&:has([data-state=checked])>.color-avatar]:border-1 [&:has([data-state=checked])>.color-avatar]:p-px cursor-pointer relative">
<FormControl>
<RadioGroupItem
value="color"
className="sr-only"
/>
</FormControl>
<div
className="color-avatar h-12 w-12 rounded-full border hover:p-px hover:border-primary transition-colors flex items-center justify-center overflow-hidden cursor-pointer"
style={{
backgroundColor: isSolidColorAvatar(
field.value,
)
? field.value
: undefined,
}}
onClick={() =>
colorInputRef.current?.click()
}
>
{!isSolidColorAvatar(field.value) && (
<Palette className="h-5 w-5 text-muted-foreground" />
)}
</div>
<input
ref={colorInputRef}
type="color"
className="absolute opacity-0 pointer-events-none w-12 h-12 top-0 left-0"
value={field.value}
onChange={field.onChange}
/>
</FormLabel>
</FormItem>
{availableAvatars.map((image) => ( {availableAvatars.map((image) => (
<FormItem key={image}> <FormItem key={image}>
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer"> <FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">

View File

@@ -7,9 +7,12 @@ interface Props {
serverId?: string; serverId?: string;
} }
export const ToggleDockerCleanup = ({ serverId }: Props) => { export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.user.get.useQuery(undefined, { const { data, refetch } = api.settings.getWebServerSettings.useQuery(
enabled: !serverId, undefined,
}); {
enabled: !serverId,
},
);
const { data: server, refetch: refetchServer } = api.server.one.useQuery( const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{ {
@@ -22,7 +25,7 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
const enabled = serverId const enabled = serverId
? server?.enableDockerCleanup ? server?.enableDockerCleanup
: data?.user.enableDockerCleanup; : data?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation(); const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
@@ -30,7 +33,10 @@ export const ToggleDockerCleanup = ({ serverId }: Props) => {
try { try {
await mutateAsync({ await mutateAsync({
enableDockerCleanup: checked, enableDockerCleanup: checked,
serverId: serverId, ...(serverId && { serverId }),
} as {
enableDockerCleanup: boolean;
serverId?: string;
}); });
if (serverId) { if (serverId) {
await refetchServer(); await refetchServer();

View File

@@ -80,7 +80,7 @@ const Schema = z.object({
type Schema = z.infer<typeof Schema>; type Schema = z.infer<typeof Schema>;
export const SetupMonitoring = ({ serverId }: Props) => { export const SetupMonitoring = ({ serverId }: Props) => {
const { data } = serverId const { data: serverData } = serverId
? api.server.one.useQuery( ? api.server.one.useQuery(
{ {
serverId: serverId || "", serverId: serverId || "",
@@ -89,7 +89,14 @@ export const SetupMonitoring = ({ serverId }: Props) => {
enabled: !!serverId, enabled: !!serverId,
}, },
) )
: api.user.getServerMetrics.useQuery(); : { data: null };
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery(undefined, {
enabled: !serverId,
});
const data = serverId ? serverData : webServerSettings;
const url = useUrl(); const url = useUrl();

View File

@@ -22,7 +22,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -89,15 +88,15 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
) : ( ) : (
<DropdownMenuItem <Button
className="w-full cursor-pointer " className="w-full cursor-pointer "
onSelect={(e) => { size="sm"
e.preventDefault(); onClick={() => {
setIsOpen(true); setIsOpen(true);
}} }}
> >
Setup Server Setup Server <Settings className="size-4" />
</DropdownMenuItem> </Button>
)} )}
<DialogContent className="sm:max-w-4xl "> <DialogContent className="sm:max-w-4xl ">
<DialogHeader> <DialogHeader>

View File

@@ -6,9 +6,7 @@ import {
Loader2, Loader2,
MoreHorizontal, MoreHorizontal,
Network, Network,
Pencil,
ServerIcon, ServerIcon,
Settings,
Terminal, Terminal,
Trash2, Trash2,
User, User,
@@ -31,9 +29,7 @@ import {
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { import {
@@ -285,7 +281,32 @@ export const ShowServers = () => {
{/* Compact Actions */} {/* Compact Actions */}
{isActive && ( {isActive && (
<div className="flex items-center gap-2 pt-3 border-t mt-auto"> <div className="flex items-center gap-2 pt-3 border-t mt-auto flex-wrap">
<div className="flex items-center gap-2 w-full">
<Tooltip>
<TooltipTrigger asChild>
<SetupServer
serverId={server.serverId}
/>
</TooltipTrigger>
<TooltipContent
className="max-w-xs"
side="bottom"
>
<div className="space-y-1">
<p className="font-semibold">
Setup Server
</p>
<p className="text-xs text-muted-foreground">
Configure and initialize your
server with Docker, Traefik, and
other essential services
</p>
</div>
</TooltipContent>
</Tooltip>
</div>
<TooltipProvider> <TooltipProvider>
{server.sshKeyId && ( {server.sshKeyId && (
<Tooltip> <Tooltip>
@@ -311,20 +332,6 @@ export const ShowServers = () => {
</Tooltip> </Tooltip>
)} )}
<Tooltip>
<TooltipTrigger asChild>
<div>
<SetupServer
serverId={server.serverId}
asButton={true}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<p>Setup Server</p>
</TooltipContent>
</Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<div> <div>

View File

@@ -67,7 +67,7 @@ type AddServerDomain = z.infer<typeof addServerDomain>;
export const WebDomain = () => { export const WebDomain = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data, refetch } = api.user.get.useQuery(); const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync, isLoading } = const { mutateAsync, isLoading } =
api.settings.assignDomainServer.useMutation(); api.settings.assignDomainServer.useMutation();
@@ -82,15 +82,15 @@ export const WebDomain = () => {
}); });
const https = form.watch("https"); const https = form.watch("https");
const domain = form.watch("domain") || ""; const domain = form.watch("domain") || "";
const host = data?.user?.host || ""; const host = data?.host || "";
const hasChanged = domain !== host; const hasChanged = domain !== host;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
domain: data?.user?.host || "", domain: data?.host || "",
certificateType: data?.user?.certificateType, certificateType: data?.certificateType || "none",
letsEncryptEmail: data?.user?.letsEncryptEmail || "", letsEncryptEmail: data?.letsEncryptEmail || "",
https: data?.user?.https || false, https: data?.https || false,
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);

View File

@@ -16,7 +16,8 @@ import { UpdateServer } from "./web-server/update-server";
export const WebServer = () => { export const WebServer = () => {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { data } = api.user.get.useQuery(); const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
@@ -53,7 +54,7 @@ export const WebServer = () => {
<div className="flex items-center flex-wrap justify-between gap-4"> <div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Server IP: {data?.user.serverIp} Server IP: {webServerSettings?.serverIp}
</span> </span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Version: {dokployVersion} Version: {dokployVersion}

View File

@@ -46,15 +46,15 @@ interface Props {
export const UpdateServerIp = ({ children }: Props) => { export const UpdateServerIp = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { data } = api.user.get.useQuery(); const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { data: ip } = api.server.publicIp.useQuery(); const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.user.update.useMutation(); api.settings.updateServerIp.useMutation();
const form = useForm<Schema>({ const form = useForm<Schema>({
defaultValues: { defaultValues: {
serverIp: data?.user.serverIp || "", serverIp: data?.serverIp || "",
}, },
resolver: zodResolver(schema), resolver: zodResolver(schema),
}); });
@@ -62,13 +62,11 @@ export const UpdateServerIp = ({ children }: Props) => {
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
serverIp: data.user.serverIp || "", serverIp: data.serverIp || "",
}); });
} }
}, [form, form.reset, data]); }, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => { const setCurrentIp = () => {
if (!ip) return; if (!ip) return;
form.setValue("serverIp", ip); form.setValue("serverIp", ip);
@@ -80,7 +78,7 @@ export const UpdateServerIp = ({ children }: Props) => {
}) })
.then(async () => { .then(async () => {
toast.success("Server IP Updated"); toast.success("Server IP Updated");
await utils.user.get.invalidate(); await refetch();
setIsOpen(false); setIsOpen(false);
}) })
.catch(() => { .catch(() => {

View File

@@ -10,7 +10,7 @@ export const ToggleVisibilityInput = ({ ...props }: InputProps) => {
return ( return (
<div className="flex w-full items-center space-x-2"> <div className="flex w-full items-center space-x-2">
<Input ref={inputRef} type={"password"} {...props} /> <Input ref={inputRef} {...props} type="password" />
<Button <Button
variant={"secondary"} variant={"secondary"}
onClick={() => { onClick={() => {

View File

@@ -1,6 +1,6 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar"; import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react"; import * as React from "react";
import { isSolidColorAvatar } from "@/lib/avatar-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
const Avatar = React.forwardRef< const Avatar = React.forwardRef<
@@ -20,14 +20,33 @@ Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef< const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>, React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> & {
>(({ className, ...props }, ref) => ( src?: string | null;
<AvatarPrimitive.Image }
ref={ref} >(({ className, src, ...props }, ref) => {
className={cn("aspect-square h-full w-full", className)} if (isSolidColorAvatar(src)) {
{...props} return (
/> <div
)); key={`solid-${src}`}
ref={ref}
className={cn("aspect-square h-full w-full rounded-full", className)}
style={{
backgroundColor: src,
}}
{...props}
/>
);
}
return (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
src={src ?? ""}
{...props}
/>
);
});
AvatarImage.displayName = AvatarPrimitive.Image.displayName; AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef< const AvatarFallback = React.forwardRef<

View File

@@ -1,18 +1,75 @@
import { EyeIcon, EyeOffIcon } from "lucide-react"; import { EyeIcon, EyeOffIcon, RefreshCcw } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { generateRandomPassword } from "@/lib/password-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { extends React.InputHTMLAttributes<HTMLInputElement> {
errorMessage?: string; errorMessage?: string;
enablePasswordGenerator?: boolean;
passwordGeneratorLength?: number;
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, errorMessage, type, ...props }, ref) => { (
{
className,
errorMessage,
type,
enablePasswordGenerator = false,
passwordGeneratorLength,
...props
},
ref,
) => {
const [showPassword, setShowPassword] = React.useState(false); const [showPassword, setShowPassword] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null);
const isPassword = type === "password"; const isPassword = type === "password";
const shouldShowGenerator =
isPassword &&
enablePasswordGenerator !== false &&
!props.disabled &&
!props.readOnly;
const inputType = isPassword ? (showPassword ? "text" : "password") : type; const inputType = isPassword ? (showPassword ? "text" : "password") : type;
const setRefs = React.useCallback(
(node: HTMLInputElement | null) => {
// @ts-ignore
inputRef.current = node;
if (typeof ref === "function") {
ref(node);
} else if (ref) {
ref.current = node;
}
},
[ref],
);
const handleGeneratePassword = () => {
const nextValue =
typeof passwordGeneratorLength === "number" &&
passwordGeneratorLength > 0
? generateRandomPassword(Math.floor(passwordGeneratorLength))
: generateRandomPassword();
const input = inputRef.current;
if (!input) {
return;
}
const valueSetter = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
if (valueSetter) {
valueSetter.call(input, nextValue);
} else {
input.value = nextValue;
}
input.dispatchEvent(new Event("input", { bubbles: true }));
};
return ( return (
<> <>
<div className="relative w-full"> <div className="relative w-full">
@@ -21,25 +78,39 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
className={cn( className={cn(
// bg-gray // bg-gray
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50",
isPassword && "pr-10", // Add padding for the eye icon isPassword && (shouldShowGenerator ? "pr-16" : "pr-10"),
className, className,
)} )}
ref={ref} ref={setRefs}
{...props} {...props}
/> />
{isPassword && ( {isPassword && (
<button <div className="absolute inset-y-0 right-0 flex items-center gap-1 pr-3 text-muted-foreground">
type="button" {shouldShowGenerator && (
className="absolute inset-y-0 right-0 flex items-center pr-3 text-muted-foreground hover:text-foreground focus:outline-none" <button
onClick={() => setShowPassword(!showPassword)} type="button"
tabIndex={-1} className="hover:text-foreground focus:outline-none"
> onClick={handleGeneratePassword}
{showPassword ? ( aria-label="Generate password"
<EyeOffIcon className="h-4 w-4" /> title="Generate password"
) : ( tabIndex={-1}
<EyeIcon className="h-4 w-4" /> >
<RefreshCcw className="h-4 w-4" />
</button>
)} )}
</button> <button
type="button"
className="hover:text-foreground focus:outline-none"
onClick={() => setShowPassword(!showPassword)}
tabIndex={-1}
>
{showPassword ? (
<EyeOffIcon className="h-4 w-4" />
) : (
<EyeIcon className="h-4 w-4" />
)}
</button>
</div>
)} )}
</div> </div>
{errorMessage && ( {errorMessage && (

View File

@@ -0,0 +1,114 @@
CREATE TABLE "webServerSettings" (
"id" text PRIMARY KEY NOT NULL,
"serverIp" text,
"certificateType" "certificateType" DEFAULT 'none' NOT NULL,
"https" boolean DEFAULT false NOT NULL,
"host" text,
"letsEncryptEmail" text,
"sshPrivateKey" text,
"enableDockerCleanup" boolean DEFAULT true NOT NULL,
"logCleanupCron" text DEFAULT '0 0 * * *',
"metricsConfig" jsonb DEFAULT '{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb NOT NULL,
"cleanupCacheApplications" boolean DEFAULT false NOT NULL,
"cleanupCacheOnPreviews" boolean DEFAULT false NOT NULL,
"cleanupCacheOnCompose" boolean DEFAULT false NOT NULL,
"created_at" timestamp DEFAULT now(),
"updated_at" timestamp DEFAULT now() NOT NULL
);
-- Migrate data from user table to webServerSettings
-- Get the owner user's data and insert into webServerSettings
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
u."serverIp",
COALESCE(u."certificateType", 'none') as "certificateType",
COALESCE(u."https", false) as "https",
u."host",
u."letsEncryptEmail",
u."sshPrivateKey",
COALESCE(u."enableDockerCleanup", true) as "enableDockerCleanup",
COALESCE(u."logCleanupCron", '0 0 * * *') as "logCleanupCron",
COALESCE(
u."metricsConfig",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb
) as "metricsConfig",
COALESCE(u."cleanupCacheApplications", false) as "cleanupCacheApplications",
COALESCE(u."cleanupCacheOnPreviews", false) as "cleanupCacheOnPreviews",
COALESCE(u."cleanupCacheOnCompose", false) as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
FROM "user" u
INNER JOIN "member" m ON u."id" = m."user_id"
WHERE m."role" = 'owner'
ORDER BY m."created_at" ASC
LIMIT 1;
-- If no owner found, create a default entry
INSERT INTO "webServerSettings" (
"id",
"serverIp",
"certificateType",
"https",
"host",
"letsEncryptEmail",
"sshPrivateKey",
"enableDockerCleanup",
"logCleanupCron",
"metricsConfig",
"cleanupCacheApplications",
"cleanupCacheOnPreviews",
"cleanupCacheOnCompose",
"created_at",
"updated_at"
)
SELECT
gen_random_uuid()::text as "id",
NULL as "serverIp",
'none' as "certificateType",
false as "https",
NULL as "host",
NULL as "letsEncryptEmail",
NULL as "sshPrivateKey",
true as "enableDockerCleanup",
'0 0 * * *' as "logCleanupCron",
'{"server":{"type":"Dokploy","refreshRate":60,"port":4500,"token":"","retentionDays":2,"cronJob":"","urlCallback":"","thresholds":{"cpu":0,"memory":0}},"containers":{"refreshRate":60,"services":{"include":[],"exclude":[]}}}'::jsonb as "metricsConfig",
false as "cleanupCacheApplications",
false as "cleanupCacheOnPreviews",
false as "cleanupCacheOnCompose",
NOW() as "created_at",
NOW() as "updated_at"
WHERE NOT EXISTS (
SELECT 1 FROM "webServerSettings"
);
--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "serverIp";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "certificateType";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "https";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "host";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "letsEncryptEmail";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "sshPrivateKey";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "enableDockerCleanup";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "logCleanupCron";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "metricsConfig";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheApplications";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnPreviews";--> statement-breakpoint
ALTER TABLE "user" DROP COLUMN "cleanupCacheOnCompose";

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ALTER COLUMN "railpackVersion" SET DEFAULT '0.15.4';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -932,6 +932,20 @@
"when": 1765346573500, "when": 1765346573500,
"tag": "0132_clean_layla_miller", "tag": "0132_clean_layla_miller",
"breakpoints": true "breakpoints": true
},
{
"idx": 133,
"version": "7",
"when": 1766301478005,
"tag": "0133_striped_the_order",
"breakpoints": true
},
{
"idx": 134,
"version": "7",
"when": 1767871040249,
"tag": "0134_strong_hercules",
"breakpoints": true
} }
] ]
} }

View File

@@ -0,0 +1,30 @@
/**
* Checks if the given avatar value represents a solid color in hexadecimal format.
*
* @param value Avatar value to check.
*
* @return True if the avatar is a solid color, false otherwise.
*/
export function isSolidColorAvatar(value?: string | null) {
return (
(value?.startsWith("#") && /^#[0-9A-Fa-f]{6}$/.test(value)) ||
value?.startsWith("color:") ||
false
);
}
/**
* Gets the avatar type for form selection (RadioGroup value).
*
* @param value Avatar value.
*
* @return "upload" for base64 images, "color" for solid colors, or the original value for other types.
*/
export function getAvatarType(value?: string | null) {
if (!value) return "";
if (value.startsWith("data:")) return "upload";
if (isSolidColorAvatar(value)) return "color";
return value;
}

View File

@@ -0,0 +1,38 @@
const DEFAULT_PASSWORD_LENGTH = 20;
const DEFAULT_PASSWORD_CHARSET =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
export const generateRandomPassword = (
length: number = DEFAULT_PASSWORD_LENGTH,
charset: string = DEFAULT_PASSWORD_CHARSET,
) => {
const safeLength =
Number.isFinite(length) && length > 0
? Math.floor(length)
: DEFAULT_PASSWORD_LENGTH;
if (safeLength <= 0 || charset.length === 0) {
return "";
}
const cryptoApi =
typeof globalThis !== "undefined" ? globalThis.crypto : undefined;
if (!cryptoApi?.getRandomValues) {
let fallback = "";
for (let i = 0; i < safeLength; i += 1) {
fallback += charset[Math.floor(Math.random() * charset.length)];
}
return fallback;
}
const values = new Uint32Array(safeLength);
cryptoApi.getRandomValues(values);
let result = "";
for (const value of values) {
result += charset[value % charset.length];
}
return result;
};

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.26.3", "version": "v0.26.4",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",
@@ -109,7 +109,6 @@
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"fancy-ansi": "^0.1.3", "fancy-ansi": "^0.1.3",
"hi-base32": "^0.5.1",
"i18next": "^23.16.8", "i18next": "^23.16.8",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -126,7 +125,6 @@
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.4.0",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"postgres": "3.4.4", "postgres": "3.4.4",
@@ -140,7 +138,6 @@
"react-i18next": "^15.5.2", "react-i18next": "^15.5.2",
"react-markdown": "^9.1.0", "react-markdown": "^9.1.0",
"recharts": "^2.15.3", "recharts": "^2.15.3",
"rotating-file-stream": "3.2.3",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.7.4", "sonner": "^1.7.4",
"ssh2": "1.15.0", "ssh2": "1.15.0",
@@ -156,9 +153,11 @@
"xterm-addon-fit": "^0.8.0", "xterm-addon-fit": "^0.8.0",
"yaml": "2.8.1", "yaml": "2.8.1",
"zod": "^3.25.32", "zod": "^3.25.32",
"zod-form-data": "^2.0.7" "zod-form-data": "^2.0.7",
"semver": "7.7.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "7.7.1",
"@types/shell-quote": "^1.7.5", "@types/shell-quote": "^1.7.5",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",

View File

@@ -909,7 +909,9 @@ const EnvironmentPage = (
<ProjectEnvironment projectId={projectId}> <ProjectEnvironment projectId={projectId}>
<Button variant="outline">Project Environment</Button> <Button variant="outline">Project Environment</Button>
</ProjectEnvironment> </ProjectEnvironment>
{(auth?.role === "owner" || auth?.canCreateServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canCreateServices) && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button> <Button>
@@ -1032,6 +1034,7 @@ const EnvironmentPage = (
</Button> </Button>
</DialogAction> </DialogAction>
{(auth?.role === "owner" || {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && ( auth?.canDeleteServices) && (
<> <>
<DialogAction <DialogAction

View File

@@ -192,7 +192,9 @@ const Service = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateApplication applicationId={applicationId} /> <UpdateApplication applicationId={applicationId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={applicationId} type="application" /> <DeleteService id={applicationId} type="application" />
)} )}
</div> </div>

View File

@@ -182,7 +182,9 @@ const Service = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateCompose composeId={composeId} /> <UpdateCompose composeId={composeId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={composeId} type="compose" /> <DeleteService id={composeId} type="compose" />
)} )}
</div> </div>

View File

@@ -156,7 +156,9 @@ const Mariadb = (
</div> </div>
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMariadb mariadbId={mariadbId} /> <UpdateMariadb mariadbId={mariadbId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mariadbId} type="mariadb" /> <DeleteService id={mariadbId} type="mariadb" />
)} )}
</div> </div>

View File

@@ -155,7 +155,9 @@ const Mongo = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMongo mongoId={mongoId} /> <UpdateMongo mongoId={mongoId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mongoId} type="mongo" /> <DeleteService id={mongoId} type="mongo" />
)} )}
</div> </div>

View File

@@ -156,7 +156,9 @@ const MySql = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateMysql mysqlId={mysqlId} /> <UpdateMysql mysqlId={mysqlId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={mysqlId} type="mysql" /> <DeleteService id={mysqlId} type="mysql" />
)} )}
</div> </div>

View File

@@ -154,7 +154,9 @@ const Postgresql = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdatePostgres postgresId={postgresId} /> <UpdatePostgres postgresId={postgresId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={postgresId} type="postgres" /> <DeleteService id={postgresId} type="postgres" />
)} )}
</div> </div>

View File

@@ -154,7 +154,9 @@ const Redis = (
<div className="flex flex-row gap-2 justify-end"> <div className="flex flex-row gap-2 justify-end">
<UpdateRedis redisId={redisId} /> <UpdateRedis redisId={redisId} />
{(auth?.role === "owner" || auth?.canDeleteServices) && ( {(auth?.role === "owner" ||
auth?.role === "admin" ||
auth?.canDeleteServices) && (
<DeleteService id={redisId} type="redis" /> <DeleteService id={redisId} type="redis" />
)} )}
</div> </div>

View File

@@ -0,0 +1,63 @@
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ShowBillingInvoices } from "@/components/dashboard/settings/billing/show-billing-invoices";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
const Page = () => {
return <ShowBillingInvoices />;
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Invoices">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
destination: "/dashboard/projects",
},
};
}
const { req, res } = ctx;
const { user, session } = await validateRequest(req);
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const helpers = createServerSideHelpers({
router: appRouter,
ctx: {
req: req as any,
res: res as any,
db: null as any,
session: session as any,
user: user as any,
},
transformer: superjson,
});
await helpers.user.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,8 +1,8 @@
import { import {
findUserById, getWebServerSettings,
IS_CLOUD, IS_CLOUD,
setupWebMonitoring, setupWebMonitoring,
updateUser, updateWebServerSettings,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { apiUpdateWebServerMonitoring } from "@/server/db/schema"; import { apiUpdateWebServerMonitoring } from "@/server/db/schema";
@@ -11,7 +11,7 @@ import { adminProcedure, createTRPCRouter } from "../trpc";
export const adminRouter = createTRPCRouter({ export const adminRouter = createTRPCRouter({
setupMonitoring: adminProcedure setupMonitoring: adminProcedure
.input(apiUpdateWebServerMonitoring) .input(apiUpdateWebServerMonitoring)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
try { try {
if (IS_CLOUD) { if (IS_CLOUD) {
throw new TRPCError({ throw new TRPCError({
@@ -19,15 +19,8 @@ export const adminRouter = createTRPCRouter({
message: "Feature disabled on cloud", message: "Feature disabled on cloud",
}); });
} }
const user = await findUserById(ctx.user.ownerId);
if (user.id !== ctx.user.ownerId) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to setup the monitoring",
});
}
await updateUser(user.id, { await updateWebServerSettings({
metricsConfig: { metricsConfig: {
server: { server: {
type: "Dokploy", type: "Dokploy",
@@ -52,8 +45,9 @@ export const adminRouter = createTRPCRouter({
}, },
}); });
const currentServer = await setupWebMonitoring(user.id); await setupWebMonitoring();
return currentServer; const settings = await getWebServerSettings();
return settings;
} catch (error) { } catch (error) {
throw error; throw error;
} }

View File

@@ -285,6 +285,7 @@ export const backupRouter = createTRPCRouter({
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
const backup = await findBackupById(input.backupId); const backup = await findBackupById(input.backupId);
await runWebServerBackup(backup); await runWebServerBackup(backup);
await keepLatestNBackups(backup);
return true; return true;
}), }),
listBackupFiles: protectedProcedure listBackupFiles: protectedProcedure

View File

@@ -17,8 +17,8 @@ import {
findGitProviderById, findGitProviderById,
findProjectById, findProjectById,
findServerById, findServerById,
findUserById,
getComposeContainer, getComposeContainer,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
loadServices, loadServices,
randomizeComposeFile, randomizeComposeFile,
@@ -430,7 +430,11 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { success: true, message: "Deployment queued" }; return {
success: true,
message: "Deployment queued",
composeId: compose.composeId,
};
}), }),
redeploy: protectedProcedure redeploy: protectedProcedure
.input(apiRedeployCompose) .input(apiRedeployCompose)
@@ -468,7 +472,11 @@ export const composeRouter = createTRPCRouter({
removeOnFail: true, removeOnFail: true,
}, },
); );
return { success: true, message: "Redeployment queued" }; return {
success: true,
message: "Redeployment queued",
composeId: compose.composeId,
};
}), }),
stop: protectedProcedure stop: protectedProcedure
.input(apiFindCompose) .input(apiFindCompose)
@@ -569,8 +577,7 @@ export const composeRouter = createTRPCRouter({
const template = await fetchTemplateFiles(input.id, input.baseUrl); const template = await fetchTemplateFiles(input.id, input.baseUrl);
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
const project = await findProjectById(environment.projectId); const project = await findProjectById(environment.projectId);
@@ -579,6 +586,9 @@ export const composeRouter = createTRPCRouter({
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const projectName = slugify(`${project.name} ${input.id}`); const projectName = slugify(`${project.name} ${input.id}`);
@@ -803,14 +813,16 @@ export const composeRouter = createTRPCRouter({
const decodedData = Buffer.from(input.base64, "base64").toString( const decodedData = Buffer.from(input.base64, "base64").toString(
"utf-8", "utf-8",
); );
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);
const config = parse(templateData.config) as CompleteTemplate; const config = parse(templateData.config) as CompleteTemplate;
@@ -880,14 +892,16 @@ export const composeRouter = createTRPCRouter({
await removeDomainById(domain.domainId); await removeDomainById(domain.domainId);
} }
const admin = await findUserById(ctx.user.ownerId); let serverIp = "127.0.0.1";
let serverIp = admin.serverIp || "127.0.0.1";
if (compose.serverId) { if (compose.serverId) {
const server = await findServerById(compose.serverId); const server = await findServerById(compose.serverId);
serverIp = server.ipAddress; serverIp = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
serverIp = "127.0.0.1"; serverIp = "127.0.0.1";
} else {
const settings = await getWebServerSettings();
serverIp = settings?.serverIp || "127.0.0.1";
} }
const templateData = JSON.parse(decodedData); const templateData = JSON.parse(decodedData);

View File

@@ -9,6 +9,7 @@ import {
findPreviewDeploymentById, findPreviewDeploymentById,
findServerById, findServerById,
generateTraefikMeDomain, generateTraefikMeDomain,
getWebServerSettings,
manageDomain, manageDomain,
removeDomain, removeDomain,
removeDomainById, removeDomainById,
@@ -107,16 +108,13 @@ export const domainRouter = createTRPCRouter({
}), }),
canGenerateTraefikMeDomains: protectedProcedure canGenerateTraefikMeDomains: protectedProcedure
.input(z.object({ serverId: z.string() })) .input(z.object({ serverId: z.string() }))
.query(async ({ input, ctx }) => { .query(async ({ input }) => {
const organization = await findOrganizationById(
ctx.session.activeOrganizationId,
);
if (input.serverId) { if (input.serverId) {
const server = await findServerById(input.serverId); const server = await findServerById(input.serverId);
return server.ipAddress; return server.ipAddress;
} }
return organization?.owner.serverIp; const settings = await getWebServerSettings();
return settings?.serverIp || "";
}), }),
update: protectedProcedure update: protectedProcedure

View File

@@ -8,6 +8,7 @@ import {
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeNotificationById, removeNotificationById,
sendCustomNotification, sendCustomNotification,
@@ -66,7 +67,6 @@ import {
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
server, server,
user,
} from "@/server/db/schema"; } from "@/server/db/schema";
export const notificationRouter = createTRPCRouter({ export const notificationRouter = createTRPCRouter({
@@ -364,21 +364,20 @@ export const notificationRouter = createTRPCRouter({
let organizationId = ""; let organizationId = "";
let ServerName = ""; let ServerName = "";
if (input.ServerType === "Dokploy") { if (input.ServerType === "Dokploy") {
const result = await db const settings = await getWebServerSettings();
.select() if (
.from(user) !settings?.metricsConfig?.server?.token ||
.where( settings.metricsConfig.server.token !== input.Token
sql`${user.metricsConfig}::jsonb -> 'server' ->> 'token' = ${input.Token}`, ) {
);
if (!result?.[0]?.id) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Token not found", message: "Token not found",
}); });
} }
organizationId = result?.[0]?.id; // For Dokploy server type, we don't have a specific organizationId
// This might need to be adjusted based on your business logic
organizationId = "";
ServerName = "Dokploy"; ServerName = "Dokploy";
} else { } else {
const result = await db const result = await db

View File

@@ -2,11 +2,15 @@ import {
findApplicationById, findApplicationById,
findPreviewDeploymentById, findPreviewDeploymentById,
findPreviewDeploymentsByApplicationId, findPreviewDeploymentsByApplicationId,
IS_CLOUD,
removePreviewDeployment, removePreviewDeployment,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { apiFindAllByApplication } from "@/server/db/schema"; import { apiFindAllByApplication } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
export const previewDeploymentRouter = createTRPCRouter({ export const previewDeploymentRouter = createTRPCRouter({
@@ -60,4 +64,55 @@ export const previewDeploymentRouter = createTRPCRouter({
} }
return previewDeployment; return previewDeployment;
}), }),
redeploy: protectedProcedure
.input(
z.object({
previewDeploymentId: z.string(),
title: z.string().optional(),
description: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const previewDeployment = await findPreviewDeploymentById(
input.previewDeploymentId,
);
if (
previewDeployment.application.environment.project.organizationId !==
ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to redeploy this preview deployment",
});
}
const application = await findApplicationById(
previewDeployment.applicationId,
);
const jobData: DeploymentJob = {
applicationId: previewDeployment.applicationId,
titleLog: input.title || "Rebuild Preview Deployment",
descriptionLog: input.description || "",
type: "redeploy",
applicationType: "application-preview",
previewDeploymentId: input.previewDeploymentId,
server: !!application.serverId,
};
if (IS_CLOUD && application.serverId) {
jobData.serverId = application.serverId;
deploy(jobData).catch((error) => {
console.error("Background deployment failed:", error);
});
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
return true;
}),
}); });

View File

@@ -12,11 +12,11 @@ import {
DEFAULT_UPDATE_DATA, DEFAULT_UPDATE_DATA,
execAsync, execAsync,
findServerById, findServerById,
findUserById,
getDokployImage, getDokployImage,
getDokployImageTag, getDokployImageTag,
getLogCleanupStatus, getLogCleanupStatus,
getUpdateData, getUpdateData,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
parseRawConfig, parseRawConfig,
paths, paths,
@@ -40,7 +40,7 @@ import {
updateLetsEncryptEmail, updateLetsEncryptEmail,
updateServerById, updateServerById,
updateServerTraefik, updateServerTraefik,
updateUser, updateWebServerSettings,
writeConfig, writeConfig,
writeMainConfig, writeMainConfig,
writeTraefikConfigInPath, writeTraefikConfigInPath,
@@ -77,11 +77,18 @@ import {
} from "../trpc"; } from "../trpc";
export const settingsRouter = createTRPCRouter({ export const settingsRouter = createTRPCRouter({
getWebServerSettings: protectedProcedure.query(async () => {
if (IS_CLOUD) {
return null;
}
const settings = await getWebServerSettings();
return settings;
}),
reloadServer: adminProcedure.mutation(async () => { reloadServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await reloadDockerResource("dokploy"); await reloadDockerResource("dokploy", undefined, packageInfo.version);
return true; return true;
}), }),
cleanRedis: adminProcedure.mutation(async () => { cleanRedis: adminProcedure.mutation(async () => {
@@ -209,11 +216,11 @@ export const settingsRouter = createTRPCRouter({
}), }),
saveSSHPrivateKey: adminProcedure saveSSHPrivateKey: adminProcedure
.input(apiSaveSSHKey) .input(apiSaveSSHKey)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateUser(ctx.user.ownerId, { await updateWebServerSettings({
sshPrivateKey: input.sshPrivateKey, sshPrivateKey: input.sshPrivateKey,
}); });
@@ -221,36 +228,36 @@ export const settingsRouter = createTRPCRouter({
}), }),
assignDomainServer: adminProcedure assignDomainServer: adminProcedure
.input(apiAssignDomain) .input(apiAssignDomain)
.mutation(async ({ ctx, input }) => { .mutation(async ({ input }) => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
const user = await updateUser(ctx.user.ownerId, { const settings = await updateWebServerSettings({
host: input.host, host: input.host,
letsEncryptEmail: input.letsEncryptEmail, letsEncryptEmail: input.letsEncryptEmail,
certificateType: input.certificateType, certificateType: input.certificateType,
https: input.https, https: input.https,
}); });
if (!user) { if (!settings) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "User not found", message: "Web server settings not found",
}); });
} }
updateServerTraefik(user, input.host); updateServerTraefik(settings, input.host);
if (input.letsEncryptEmail) { if (input.letsEncryptEmail) {
updateLetsEncryptEmail(input.letsEncryptEmail); updateLetsEncryptEmail(input.letsEncryptEmail);
} }
return user; return settings;
}), }),
cleanSSHPrivateKey: adminProcedure.mutation(async ({ ctx }) => { cleanSSHPrivateKey: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return true;
} }
await updateUser(ctx.user.ownerId, { await updateWebServerSettings({
sshPrivateKey: null, sshPrivateKey: null,
}); });
return true; return true;
@@ -310,11 +317,11 @@ export const settingsRouter = createTRPCRouter({
} }
} }
} else if (!IS_CLOUD) { } else if (!IS_CLOUD) {
const userUpdated = await updateUser(ctx.user.ownerId, { const settingsUpdated = await updateWebServerSettings({
enableDockerCleanup: input.enableDockerCleanup, enableDockerCleanup: input.enableDockerCleanup,
}); });
if (userUpdated?.enableDockerCleanup) { if (settingsUpdated?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`, `Docker Cleanup ${new Date().toLocaleString()}] Running...`,
@@ -392,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
return DEFAULT_UPDATE_DATA; return DEFAULT_UPDATE_DATA;
} }
return await getUpdateData(); return await getUpdateData(packageInfo.version);
}), }),
updateServer: adminProcedure.mutation(async () => { updateServer: adminProcedure.mutation(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
@@ -488,13 +495,28 @@ export const settingsRouter = createTRPCRouter({
return readConfigInPath(input.path, input.serverId); return readConfigInPath(input.path, input.serverId);
}), }),
getIp: protectedProcedure.query(async ({ ctx }) => { getIp: protectedProcedure.query(async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return true; return "";
} }
const user = await findUserById(ctx.user.ownerId); const settings = await getWebServerSettings();
return user.serverIp; return settings?.serverIp || "";
}), }),
updateServerIp: adminProcedure
.input(
z.object({
serverIp: z.string(),
}),
)
.mutation(async ({ input }) => {
if (IS_CLOUD) {
return true;
}
const settings = await updateWebServerSettings({
serverIp: input.serverIp,
});
return settings;
}),
getOpenApiDocument: protectedProcedure.query( getOpenApiDocument: protectedProcedure.query(
async ({ ctx }): Promise<unknown> => { async ({ ctx }): Promise<unknown> => {

View File

@@ -81,6 +81,7 @@ export const stripeRouter = createTRPCRouter({
metadata: { metadata: {
adminId: owner.id, adminId: owner.id,
}, },
customer_email: owner.email,
allow_promotion_codes: true, allow_promotion_codes: true,
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`, success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`, cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
@@ -128,4 +129,39 @@ export const stripeRouter = createTRPCRouter({
return servers.length < user.serversQuantity; return servers.length < user.serversQuantity;
}), }),
getInvoices: adminProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const stripeCustomerId = user.stripeCustomerId;
if (!stripeCustomerId) {
return [];
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-09-30.acacia",
});
try {
const invoices = await stripe.invoices.list({
customer: stripeCustomerId,
limit: 100,
});
return invoices.data.map((invoice) => ({
id: invoice.id,
number: invoice.number,
status: invoice.status,
amountDue: invoice.amount_due,
amountPaid: invoice.amount_paid,
currency: invoice.currency,
created: invoice.created,
dueDate: invoice.due_date,
hostedInvoiceUrl: invoice.hosted_invoice_url,
invoicePdf: invoice.invoice_pdf,
}));
} catch (_) {
return [];
}
}),
}); });

View File

@@ -5,6 +5,7 @@ import {
findUserById, findUserById,
getDokployUrl, getDokployUrl,
getUserByToken, getUserByToken,
getWebServerSettings,
IS_CLOUD, IS_CLOUD,
removeUserById, removeUserById,
sendEmailNotification, sendEmailNotification,
@@ -214,10 +215,11 @@ export const userRouter = createTRPCRouter({
}), }),
getMetricsToken: protectedProcedure.query(async ({ ctx }) => { getMetricsToken: protectedProcedure.query(async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId); const user = await findUserById(ctx.user.ownerId);
const settings = await getWebServerSettings();
return { return {
serverIp: user.serverIp, serverIp: settings?.serverIp,
enabledFeatures: user.enablePaidFeatures, enabledFeatures: user.enablePaidFeatures,
metricsConfig: user?.metricsConfig, metricsConfig: settings?.metricsConfig,
}; };
}), }),
remove: protectedProcedure remove: protectedProcedure

View File

@@ -4,6 +4,7 @@ import {
deployPreviewApplication, deployPreviewApplication,
rebuildApplication, rebuildApplication,
rebuildCompose, rebuildCompose,
rebuildPreviewApplication,
updateApplicationStatus, updateApplicationStatus,
updateCompose, updateCompose,
updatePreviewDeployment, updatePreviewDeployment,
@@ -54,7 +55,14 @@ export const deploymentWorker = new Worker(
previewStatus: "running", previewStatus: "running",
}); });
if (job.data.type === "deploy") { if (job.data.type === "redeploy") {
await rebuildPreviewApplication({
applicationId: job.data.applicationId,
titleLog: job.data.titleLog,
descriptionLog: job.data.descriptionLog,
previewDeploymentId: job.data.previewDeploymentId,
});
} else if (job.data.type === "deploy") {
await deployPreviewApplication({ await deployPreviewApplication({
applicationId: job.data.applicationId, applicationId: job.data.applicationId,
titleLog: job.data.titleLog, titleLog: job.data.titleLog,

View File

@@ -22,7 +22,7 @@ type DeployJob =
titleLog: string; titleLog: string;
descriptionLog: string; descriptionLog: string;
server?: boolean; server?: boolean;
type: "deploy"; type: "deploy" | "redeploy";
applicationType: "application-preview"; applicationType: "application-preview";
previewDeploymentId: string; previewDeploymentId: string;
serverId?: string; serverId?: string;

View File

@@ -58,7 +58,7 @@ func (db *DB) GetLastNContainerMetrics(containerName string, limit int) ([]Conta
WITH recent_metrics AS ( WITH recent_metrics AS (
SELECT metrics_json SELECT metrics_json
FROM container_metrics FROM container_metrics
WHERE container_name LIKE ? || '%' WHERE container_name = ?
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT ? LIMIT ?
) )
@@ -98,7 +98,7 @@ func (db *DB) GetAllMetricsContainer(containerName string) ([]ContainerMetric, e
WITH recent_metrics AS ( WITH recent_metrics AS (
SELECT metrics_json SELECT metrics_json
FROM container_metrics FROM container_metrics
WHERE container_name LIKE ? || '%' WHERE container_name = ?
ORDER BY timestamp DESC ORDER BY timestamp DESC
) )
SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC SELECT metrics_json FROM recent_metrics ORDER BY json_extract(metrics_json, '$.timestamp') ASC

View File

@@ -1,32 +1,21 @@
{ {
"name": "@dokploy/server", "name": "@dokploy/server",
"version": "1.0.0", "version": "1.0.0",
"main": "./dist/index.js", "main": "./src/index.ts",
"type": "module", "type": "module",
"exports": { "exports": {
".": { ".": "./src/index.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs.js"
},
"./db": { "./db": {
"import": "./dist/db/index.js", "import": "./src/db/index.ts",
"require": "./dist/db/index.cjs.js" "require": "./dist/db/index.cjs.js"
}, },
"./*": { "./setup/*": {
"import": "./dist/*", "import": "./src/setup/*.ts",
"require": "./dist/*.cjs" "require": "./dist/setup/index.cjs.js"
}, },
"./dist": { "./constants": {
"import": "./dist/index.js", "import": "./src/constants/index.ts",
"require": "./dist/index.cjs.js" "require": "./dist/constants.cjs.js"
},
"./dist/db": {
"import": "./dist/db/index.js",
"require": "./dist/db/index.cjs.js"
},
"./dist/db/schema": {
"import": "./dist/db/schema/index.js",
"require": "./dist/db/schema/index.cjs.js"
} }
}, },
"scripts": { "scripts": {
@@ -68,7 +57,6 @@
"drizzle-dbml-generator": "0.10.0", "drizzle-dbml-generator": "0.10.0",
"drizzle-orm": "^0.39.3", "drizzle-orm": "^0.39.3",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"hi-base32": "^0.5.1",
"yaml": "2.8.1", "yaml": "2.8.1",
"lodash": "4.17.21", "lodash": "4.17.21",
"micromatch": "4.0.8", "micromatch": "4.0.8",
@@ -78,7 +66,6 @@
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"nodemailer": "6.9.14", "nodemailer": "6.9.14",
"octokit": "3.1.2", "octokit": "3.1.2",
"otpauth": "^9.4.0",
"pino": "9.4.0", "pino": "9.4.0",
"pino-pretty": "11.2.2", "pino-pretty": "11.2.2",
"postgres": "3.4.4", "postgres": "3.4.4",
@@ -86,15 +73,16 @@
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"rotating-file-stream": "3.2.3",
"shell-quote": "^1.8.1", "shell-quote": "^1.8.1",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"ssh2": "1.15.0", "ssh2": "1.15.0",
"toml": "3.0.0", "toml": "3.0.0",
"ws": "8.16.0", "ws": "8.16.0",
"zod": "^3.25.32" "zod": "^3.25.32",
"semver": "7.7.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "7.7.1",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/bcrypt": "5.0.2", "@types/bcrypt": "5.0.2",
"@types/dockerode": "3.3.23", "@types/dockerode": "3.3.23",
@@ -123,4 +111,4 @@
"node": "^20.16.0", "node": "^20.16.0",
"pnpm": ">=9.12.0" "pnpm": ">=9.12.0"
} }
} }

View File

@@ -277,7 +277,7 @@ table application {
replicas integer [not null, default: 1] replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks'] buildType buildType [not null, default: 'nixpacks']
railpackVersion text [default: '0.2.2'] railpackVersion text [default: '0.15.4']
herokuVersion text [default: '24'] herokuVersion text [default: '24']
publishDirectory text publishDirectory text
isStaticSpa boolean isStaticSpa boolean

View File

@@ -177,7 +177,7 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
railpackVersion: text("railpackVersion").default("0.2.2"), railpackVersion: text("railpackVersion").default("0.15.4"),
herokuVersion: text("herokuVersion").default("24"), herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"), publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"), isStaticSpa: boolean("isStaticSpa"),

View File

@@ -35,3 +35,4 @@ export * from "./ssh-key";
export * from "./user"; export * from "./user";
export * from "./utils"; export * from "./utils";
export * from "./volume-backups"; export * from "./volume-backups";
export * from "./web-server-settings";

View File

@@ -3,7 +3,6 @@ import { relations } from "drizzle-orm";
import { import {
boolean, boolean,
integer, integer,
jsonb,
pgTable, pgTable,
text, text,
timestamp, timestamp,
@@ -15,7 +14,6 @@ import { account, apikey, organization } from "./account";
import { backups } from "./backups"; import { backups } from "./backups";
import { projects } from "./project"; import { projects } from "./project";
import { schedules } from "./schedule"; import { schedules } from "./schedule";
import { certificateType } from "./shared";
/** /**
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
* database instance for multiple projects. * database instance for multiple projects.
@@ -51,73 +49,10 @@ export const user = pgTable("user", {
banExpires: timestamp("ban_expires"), banExpires: timestamp("ban_expires"),
updatedAt: timestamp("updated_at").notNull(), updatedAt: timestamp("updated_at").notNull(),
// Admin // Admin
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
role: text("role").notNull().default("user"), role: text("role").notNull().default("user"),
// Metrics // Metrics
enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false), enablePaidFeatures: boolean("enablePaidFeatures").notNull().default(false),
allowImpersonation: boolean("allowImpersonation").notNull().default(false), allowImpersonation: boolean("allowImpersonation").notNull().default(false),
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
stripeCustomerId: text("stripeCustomerId"), stripeCustomerId: text("stripeCustomerId"),
stripeSubscriptionId: text("stripeSubscriptionId"), stripeSubscriptionId: text("stripeSubscriptionId"),
serversQuantity: integer("serversQuantity").notNull().default(0), serversQuantity: integer("serversQuantity").notNull().default(0),
@@ -203,33 +138,6 @@ export const apiFindOneUserByAuth = createSchema
// authId: true, // authId: true,
}) })
.required(); .required();
export const apiSaveSSHKey = createSchema
.pick({
sshPrivateKey: true,
})
.required();
export const apiAssignDomain = createSchema
.pick({
host: true,
certificateType: true,
letsEncryptEmail: true,
https: true,
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({ export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1), traefikConfig: z.string().min(1),
@@ -298,32 +206,6 @@ export const apiReadStatsLogs = z.object({
.optional(), .optional(),
}); });
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});
export const apiUpdateUser = createSchema.partial().extend({ export const apiUpdateUser = createSchema.partial().extend({
email: z email: z
.string() .string()
@@ -334,29 +216,4 @@ export const apiUpdateUser = createSchema.partial().extend({
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
logCleanupCron: z.string().optional().nullable(),
}); });

View File

@@ -0,0 +1,178 @@
import { relations } from "drizzle-orm";
import { boolean, jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { certificateType } from "./shared";
export const webServerSettings = pgTable("webServerSettings", {
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
// Web Server Configuration
serverIp: text("serverIp"),
certificateType: certificateType("certificateType").notNull().default("none"),
https: boolean("https").notNull().default(false),
host: text("host"),
letsEncryptEmail: text("letsEncryptEmail"),
sshPrivateKey: text("sshPrivateKey"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(true),
logCleanupCron: text("logCleanupCron").default("0 0 * * *"),
// Metrics Configuration
metricsConfig: jsonb("metricsConfig")
.$type<{
server: {
type: "Dokploy" | "Remote";
refreshRate: number;
port: number;
token: string;
urlCallback: string;
retentionDays: number;
cronJob: string;
thresholds: {
cpu: number;
memory: number;
};
};
containers: {
refreshRate: number;
services: {
include: string[];
exclude: string[];
};
};
}>()
.notNull()
.default({
server: {
type: "Dokploy",
refreshRate: 60,
port: 4500,
token: "",
retentionDays: 2,
cronJob: "",
urlCallback: "",
thresholds: {
cpu: 0,
memory: 0,
},
},
containers: {
refreshRate: 60,
services: {
include: [],
exclude: [],
},
},
}),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
.default(false),
cleanupCacheOnPreviews: boolean("cleanupCacheOnPreviews")
.notNull()
.default(false),
cleanupCacheOnCompose: boolean("cleanupCacheOnCompose")
.notNull()
.default(false),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});
export const webServerSettingsRelations = relations(
webServerSettings,
() => ({}),
);
const createSchema = createInsertSchema(webServerSettings, {
id: z.string().min(1),
});
export const apiUpdateWebServerSettings = createSchema.partial().extend({
serverIp: z.string().optional(),
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
https: z.boolean().optional(),
host: z.string().optional(),
letsEncryptEmail: z.string().email().optional().nullable(),
sshPrivateKey: z.string().optional(),
enableDockerCleanup: z.boolean().optional(),
logCleanupCron: z.string().optional().nullable(),
metricsConfig: z
.object({
server: z.object({
type: z.enum(["Dokploy", "Remote"]),
refreshRate: z.number(),
port: z.number(),
token: z.string(),
urlCallback: z.string(),
retentionDays: z.number(),
cronJob: z.string(),
thresholds: z.object({
cpu: z.number(),
memory: z.number(),
}),
}),
containers: z.object({
refreshRate: z.number(),
services: z.object({
include: z.array(z.string()),
exclude: z.array(z.string()),
}),
}),
})
.optional(),
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
});
export const apiAssignDomain = z
.object({
host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(),
https: z.boolean().optional(),
})
.required()
.partial({
letsEncryptEmail: true,
https: true,
});
export const apiSaveSSHKey = z
.object({
sshPrivateKey: z.string(),
})
.required();
export const apiUpdateDockerCleanup = z.object({
enableDockerCleanup: z.boolean(),
serverId: z.string().optional(),
});
export const apiUpdateWebServerMonitoring = z.object({
metricsConfig: z
.object({
server: z.object({
refreshRate: z.number().min(2),
port: z.number().min(1),
token: z.string(),
urlCallback: z.string().url(),
retentionDays: z.number().min(1),
cronJob: z.string().min(1),
thresholds: z.object({
cpu: z.number().min(0),
memory: z.number().min(0),
}),
}),
containers: z.object({
refreshRate: z.number().min(2),
services: z.object({
include: z.array(z.string()).optional(),
exclude: z.array(z.string()).optional(),
}),
}),
})
.required(),
});

View File

@@ -41,6 +41,7 @@ export * from "./services/settings";
export * from "./services/ssh-key"; export * from "./services/ssh-key";
export * from "./services/user"; export * from "./services/user";
export * from "./services/volume-backups"; export * from "./services/volume-backups";
export * from "./services/web-server-settings";
export * from "./setup/config-paths"; export * from "./setup/config-paths";
export * from "./setup/monitoring-setup"; export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup"; export * from "./setup/postgres-setup";

View File

@@ -9,7 +9,10 @@ import { IS_CLOUD } from "../constants";
import { db } from "../db"; import { db } from "../db";
import * as schema from "../db/schema"; import * as schema from "../db/schema";
import { getUserByToken } from "../services/admin"; import { getUserByToken } from "../services/admin";
import { updateUser } from "../services/user"; import {
getWebServerSettings,
updateWebServerSettings,
} from "../services/web-server-settings";
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot"; import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
import { sendEmail } from "../verification/send-verification-email"; import { sendEmail } from "../verification/send-verification-email";
import { getPublicIpWithFallback } from "../wss/utils"; import { getPublicIpWithFallback } from "../wss/utils";
@@ -35,22 +38,20 @@ const { handler, api } = betterAuth({
}, },
...(!IS_CLOUD && { ...(!IS_CLOUD && {
async trustedOrigins() { async trustedOrigins() {
const admin = await db.query.member.findFirst({ const settings = await getWebServerSettings();
where: eq(schema.member.role, "owner"), if (!settings) {
with: { return [];
user: true,
},
});
if (admin?.user) {
return [
...(admin.user.serverIp
? [`http://${admin.user.serverIp}:3000`]
: []),
...(admin.user.host ? [`https://${admin.user.host}`] : []),
];
} }
return []; return [
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
...(settings?.host ? [`https://${settings?.host}`] : []),
...(process.env.NODE_ENV === "development"
? [
"http://localhost:3000",
"https://absolutely-handy-falcon.ngrok-free.app",
]
: []),
];
}, },
}), }),
emailVerification: { emailVerification: {
@@ -122,7 +123,7 @@ const { handler, api } = betterAuth({
}); });
if (!IS_CLOUD) { if (!IS_CLOUD) {
await updateUser(user.id, { await updateWebServerSettings({
serverIp: await getPublicIpWithFallback(), serverIp: await getPublicIpWithFallback(),
}); });
} }

View File

@@ -8,6 +8,7 @@ import {
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { getWebServerSettings } from "./web-server-settings";
export const findUserById = async (userId: string) => { export const findUserById = async (userId: string) => {
const userResult = await db.query.user.findFirst({ const userResult = await db.query.user.findFirst({
@@ -107,11 +108,11 @@ export const getDokployUrl = async () => {
if (IS_CLOUD) { if (IS_CLOUD) {
return "https://app.dokploy.com"; return "https://app.dokploy.com";
} }
const owner = await findOwner(); const settings = await getWebServerSettings();
if (owner.user.host) { if (settings?.host) {
const protocol = owner.user.https ? "https" : "http"; const protocol = settings?.https ? "https" : "http";
return `${protocol}://${owner.user.host}`; return `${protocol}://${settings?.host}`;
} }
return `http://${owner.user.serverIp}:${process.env.PORT}`; return `http://${settings?.serverIp}:${process.env.PORT}`;
}; };

View File

@@ -6,8 +6,8 @@ import { generateObject } from "ai";
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findOrganizationById } from "./admin";
import { findServerById } from "./server"; import { findServerById } from "./server";
import { getWebServerSettings } from "./web-server-settings";
export const getAiSettingsByOrganizationId = async (organizationId: string) => { export const getAiSettingsByOrganizationId = async (organizationId: string) => {
const aiSettings = await db.query.ai.findMany({ const aiSettings = await db.query.ai.findMany({
@@ -79,8 +79,8 @@ export const suggestVariants = async ({
let ip = ""; let ip = "";
if (!IS_CLOUD) { if (!IS_CLOUD) {
const organization = await findOrganizationById(organizationId); const settings = await getWebServerSettings();
ip = organization?.owner.serverIp || ""; ip = settings?.serverIp || "";
} }
if (serverId) { if (serverId) {

View File

@@ -452,6 +452,137 @@ export const deployPreviewApplication = async ({
return true; return true;
}; };
export const rebuildPreviewApplication = async ({
applicationId,
titleLog = "Rebuild Preview Deployment",
descriptionLog = "",
previewDeploymentId,
}: {
applicationId: string;
titleLog: string;
descriptionLog: string;
previewDeploymentId: string;
}) => {
const application = await findApplicationById(applicationId);
const previewDeployment =
await findPreviewDeploymentById(previewDeploymentId);
const deployment = await createDeploymentPreview({
title: titleLog,
description: descriptionLog,
previewDeploymentId: previewDeploymentId,
});
const previewDomain = getDomainHost(previewDeployment?.domain as Domain);
const issueParams = {
owner: application?.owner || "",
repository: application?.repository || "",
issue_number: previewDeployment.pullRequestNumber,
comment_id: Number.parseInt(previewDeployment.pullRequestCommentId),
githubId: application?.githubId || "",
};
try {
const commentExists = await issueCommentExists({
...issueParams,
});
if (!commentExists) {
const result = await createPreviewDeploymentComment({
...issueParams,
previewDomain,
appName: previewDeployment.appName,
githubId: application?.githubId || "",
previewDeploymentId,
});
if (!result) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Pull request comment not found",
});
}
issueParams.comment_id = Number.parseInt(result?.pullRequestCommentId);
}
const buildingComment = getIssueComment(
application.name,
"running",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${buildingComment}`,
});
// Set application properties for preview deployment
application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = `${application.previewBuildArgs}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildSecrets = `${application.previewBuildSecrets}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.rollbackActive = false;
application.buildRegistry = null;
application.rollbackRegistry = null;
application.registry = null;
const serverId = application.serverId;
let command = "set -e;";
// Only rebuild, don't clone repository
command += await getBuildCommand(application);
const commandWithLog = `(${command}) >> ${deployment.logPath} 2>&1`;
if (serverId) {
await execAsyncRemote(serverId, commandWithLog);
} else {
await execAsync(commandWithLog);
}
await mechanizeDockerContainer(application);
const successComment = getIssueComment(
application.name,
"success",
previewDomain,
);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${successComment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "done");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "done",
});
} catch (error) {
let command = "";
// Only log details for non-ExecError errors
if (!(error instanceof ExecError)) {
const message = error instanceof Error ? error.message : String(error);
const encodedMessage = encodeBase64(message);
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
}
command += `echo "\nError occurred ❌, check the logs for details." >> ${deployment.logPath};`;
const serverId = application.buildServerId || application.serverId;
if (serverId) {
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
const comment = getIssueComment(application.name, "error", previewDomain);
await updateIssueComment({
...issueParams,
body: `### Dokploy Preview Deployment\n\n${comment}`,
});
await updateDeploymentStatus(deployment.deploymentId, "error");
await updatePreviewDeployment(previewDeploymentId, {
previewStatus: "error",
});
throw error;
}
return true;
};
export const getApplicationStats = async (appName: string) => { export const getApplicationStats = async (appName: string) => {
if (appName === "dokploy") { if (appName === "dokploy") {
return await getAdvancedStats(appName); return await getAdvancedStats(appName);

View File

@@ -1,12 +1,12 @@
import dns from "node:dns"; import dns from "node:dns";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { db } from "@dokploy/server/db"; import { db } from "@dokploy/server/db";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { generateRandomDomain } from "@dokploy/server/templates"; import { generateRandomDomain } from "@dokploy/server/templates";
import { manageDomain } from "@dokploy/server/utils/traefik/domain"; import { manageDomain } from "@dokploy/server/utils/traefik/domain";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { type apiCreateDomain, domains } from "../db/schema"; import { type apiCreateDomain, domains } from "../db/schema";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { detectCDNProvider } from "./cdn"; import { detectCDNProvider } from "./cdn";
import { findServerById } from "./server"; import { findServerById } from "./server";
@@ -61,9 +61,9 @@ export const generateTraefikMeDomain = async (
projectName: appName, projectName: appName,
}); });
} }
const admin = await findUserById(userId); const settings = await getWebServerSettings();
return generateRandomDomain({ return generateRandomDomain({
serverIp: admin?.serverIp || "", serverIp: settings?.serverIp || "",
projectName: appName, projectName: appName,
}); });
}; };

View File

@@ -13,11 +13,11 @@ import { removeDirectoryCode } from "../utils/filesystem/directory";
import { authGithub } from "../utils/providers/github"; import { authGithub } from "../utils/providers/github";
import { removeTraefikConfig } from "../utils/traefik/application"; import { removeTraefikConfig } from "../utils/traefik/application";
import { manageDomain } from "../utils/traefik/domain"; import { manageDomain } from "../utils/traefik/domain";
import { findUserById } from "./admin";
import { findApplicationById } from "./application"; import { findApplicationById } from "./application";
import { removeDeploymentsByPreviewDeploymentId } from "./deployment"; import { removeDeploymentsByPreviewDeploymentId } from "./deployment";
import { createDomain } from "./domain"; import { createDomain } from "./domain";
import { type Github, getIssueComment } from "./github"; import { type Github, getIssueComment } from "./github";
import { getWebServerSettings } from "./web-server-settings";
export type PreviewDeployment = typeof previewDeployments.$inferSelect; export type PreviewDeployment = typeof previewDeployments.$inferSelect;
@@ -253,8 +253,8 @@ const generateWildcardDomain = async (
} }
if (!ip) { if (!ip) {
const admin = await findUserById(userId); const settings = await getWebServerSettings();
ip = admin?.serverIp || ""; ip = settings?.serverIp || "";
} }
const slugIp = ip.replaceAll(".", "-"); const slugIp = ip.replaceAll(".", "-");

View File

@@ -5,12 +5,12 @@ import {
execAsync, execAsync,
execAsyncRemote, execAsyncRemote,
} from "@dokploy/server/utils/process/execAsync"; } from "@dokploy/server/utils/process/execAsync";
import semver from "semver";
import { import {
initializeStandaloneTraefik, initializeStandaloneTraefik,
initializeTraefikService, initializeTraefikService,
type TraefikOptions, type TraefikOptions,
} from "../setup/traefik-setup"; } from "../setup/traefik-setup";
export interface IUpdateData { export interface IUpdateData {
latestVersion: string | null; latestVersion: string | null;
updateAvailable: boolean; updateAvailable: boolean;
@@ -55,56 +55,95 @@ export const getServiceImageDigest = async () => {
}; };
/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ /** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */
export const getUpdateData = async (): Promise<IUpdateData> => { export const getUpdateData = async (
let currentDigest: string; currentVersion: string,
): Promise<IUpdateData> => {
try { try {
currentDigest = await getServiceImageDigest(); const baseUrl =
} catch (error) { "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags";
// TODO: Docker versions 29.0.0 change the way to get the service image digest, so we need to update this in the future we upgrade to that version. let url: string | null = `${baseUrl}?page_size=100`;
return DEFAULT_UPDATE_DATA; let allResults: { digest: string; name: string }[] = [];
}
const baseUrl = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; // Fetch all tags from Docker Hub
let url: string | null = `${baseUrl}?page_size=100`; while (url) {
let allResults: { digest: string; name: string }[] = []; const response = await fetch(url, {
while (url) { method: "GET",
const response = await fetch(url, { headers: { "Content-Type": "application/json" },
method: "GET", });
headers: { "Content-Type": "application/json" },
});
const data = (await response.json()) as { const data = (await response.json()) as {
next: string | null; next: string | null;
results: { digest: string; name: string }[]; results: { digest: string; name: string }[];
}; };
allResults = allResults.concat(data.results); allResults = allResults.concat(data.results);
url = data?.next; url = data?.next;
} }
const imageTag = getDokployImageTag(); const currentImageTag = getDokployImageTag();
const searchedDigest = allResults.find((t) => t.name === imageTag)?.digest;
if (!searchedDigest) { // Special handling for canary and feature branches
return DEFAULT_UPDATE_DATA; // For development versions (canary/feature), don't perform update checks
} // These are unstable versions that change frequently, and users on these
// branches are expected to manually manage updates
if (currentImageTag === "canary" || currentImageTag === "feature") {
const currentDigest = await getServiceImageDigest();
const latestDigest = allResults.find(
(t) => t.name === currentImageTag,
)?.digest;
if (!latestDigest) {
return DEFAULT_UPDATE_DATA;
}
if (currentDigest !== latestDigest) {
return {
latestVersion: currentImageTag,
updateAvailable: true,
};
}
return {
latestVersion: currentImageTag,
updateAvailable: false,
};
}
if (imageTag === "latest") { // For stable versions, use semver comparison
const versionedTag = allResults.find( // Find the "latest" tag and get its digest
(t) => t.digest === searchedDigest && t.name.startsWith("v"), const latestTag = allResults.find((t) => t.name === "latest");
);
if (!versionedTag) { if (!latestTag) {
return DEFAULT_UPDATE_DATA; return DEFAULT_UPDATE_DATA;
} }
const { name: latestVersion, digest } = versionedTag; // Find the versioned tag (v0.x.x) that has the same digest as "latest"
const updateAvailable = digest !== currentDigest; const latestVersionTag = allResults.find(
(t) => t.digest === latestTag.digest && t.name.startsWith("v"),
);
return { latestVersion, updateAvailable }; if (!latestVersionTag) {
return DEFAULT_UPDATE_DATA;
}
const latestVersion = latestVersionTag.name;
// Use semver to compare versions for stable releases
const cleanedCurrent = semver.clean(currentVersion);
const cleanedLatest = semver.clean(latestVersion);
if (!cleanedCurrent || !cleanedLatest) {
return DEFAULT_UPDATE_DATA;
}
// Check if the latest version is greater than the current version
const updateAvailable = semver.gt(cleanedLatest, cleanedCurrent);
return {
latestVersion,
updateAvailable,
};
} catch (error) {
console.error("Error fetching update data:", error);
return DEFAULT_UPDATE_DATA;
} }
const updateAvailable = searchedDigest !== currentDigest;
return { latestVersion: imageTag, updateAvailable };
}; };
interface TreeDataItem { interface TreeDataItem {
@@ -254,11 +293,22 @@ fi`;
export const reloadDockerResource = async ( export const reloadDockerResource = async (
resourceName: string, resourceName: string,
serverId?: string, serverId?: string,
version?: string,
) => { ) => {
const resourceType = await getDockerResourceType(resourceName, serverId); const resourceType = await getDockerResourceType(resourceName, serverId);
let command = ""; let command = "";
if (resourceType === "service") { if (resourceType === "service") {
command = `docker service update --force ${resourceName}`; if (resourceName === "dokploy") {
const currentImageTag = getDokployImageTag();
let imageTag = version;
if (currentImageTag === "canary" || currentImageTag === "feature") {
imageTag = currentImageTag;
}
command = `docker service update --force --image dokploy/dokploy:${imageTag} ${resourceName}`;
} else {
command = `docker service update --force ${resourceName}`;
}
} else if (resourceType === "standalone") { } else if (resourceType === "standalone") {
command = `docker restart ${resourceName}`; command = `docker restart ${resourceName}`;
} else { } else {

View File

@@ -0,0 +1,44 @@
import { db } from "@dokploy/server/db";
import { webServerSettings } from "@dokploy/server/db/schema";
import { eq } from "drizzle-orm";
/**
* Get the web server settings (singleton - only one row should exist)
*/
export const getWebServerSettings = async () => {
const settings = await db.query.webServerSettings.findFirst({
orderBy: (settings, { asc }) => [asc(settings.createdAt)],
});
if (!settings) {
// Create default settings if none exist
const [newSettings] = await db
.insert(webServerSettings)
.values({})
.returning();
return newSettings;
}
return settings;
};
/**
* Update web server settings
*/
export const updateWebServerSettings = async (
updates: Partial<typeof webServerSettings.$inferInsert>,
) => {
const current = await getWebServerSettings();
const [updated] = await db
.update(webServerSettings)
.set({
...updates,
updatedAt: new Date(),
})
.where(eq(webServerSettings.id, current?.id ?? ""))
.returning();
return updated;
};

View File

@@ -1,7 +1,7 @@
import { findServerById } from "@dokploy/server/services/server"; import { findServerById } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import type { ContainerCreateOptions } from "dockerode"; import type { ContainerCreateOptions } from "dockerode";
import { IS_CLOUD } from "../constants"; import { IS_CLOUD } from "../constants";
import { findUserById } from "../services/admin";
import { getDokployImageTag } from "../services/settings"; import { getDokployImageTag } from "../services/settings";
import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { pullImage, pullRemoteImage } from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
@@ -83,8 +83,8 @@ export const setupMonitoring = async (serverId: string) => {
} }
}; };
export const setupWebMonitoring = async (userId: string) => { export const setupWebMonitoring = async () => {
const user = await findUserById(userId); const webServerSettings = await getWebServerSettings();
const containerName = "dokploy-monitoring"; const containerName = "dokploy-monitoring";
let imageName = "dokploy/monitoring:latest"; let imageName = "dokploy/monitoring:latest";
@@ -99,7 +99,7 @@ export const setupWebMonitoring = async (userId: string) => {
const settings: ContainerCreateOptions = { const settings: ContainerCreateOptions = {
name: containerName, name: containerName,
Env: [`METRICS_CONFIG=${JSON.stringify(user?.metricsConfig)}`], Env: [`METRICS_CONFIG=${JSON.stringify(webServerSettings?.metricsConfig)}`],
Image: imageName, Image: imageName,
HostConfig: { HostConfig: {
// Memory: 100 * 1024 * 1024, // 100MB en bytes // Memory: 100 * 1024 * 1024, // 100MB en bytes
@@ -110,9 +110,9 @@ export const setupWebMonitoring = async (userId: string) => {
Name: "always", Name: "always",
}, },
PortBindings: { PortBindings: {
[`${user?.metricsConfig?.server?.port}/tcp`]: [ [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: [
{ {
HostPort: user?.metricsConfig?.server?.port.toString(), HostPort: webServerSettings?.metricsConfig?.server?.port.toString(),
}, },
], ],
}, },
@@ -126,7 +126,7 @@ export const setupWebMonitoring = async (userId: string) => {
// NetworkMode: "host", // NetworkMode: "host",
}, },
ExposedPorts: { ExposedPorts: {
[`${user?.metricsConfig?.server?.port}/tcp`]: {}, [`${webServerSettings?.metricsConfig?.server?.port}/tcp`]: {},
}, },
}; };
const docker = await getRemoteDocker(); const docker = await getRemoteDocker();

View File

@@ -629,7 +629,7 @@ const installNixpacks = () => `
if command_exists nixpacks; then if command_exists nixpacks; then
echo "Nixpacks already installed ✅" echo "Nixpacks already installed ✅"
else else
export NIXPACKS_VERSION=1.39.0 export NIXPACKS_VERSION=1.41.0
bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅" echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi fi
@@ -639,7 +639,7 @@ const installRailpack = () => `
if command_exists railpack; then if command_exists railpack; then
echo "Railpack already installed ✅" echo "Railpack already installed ✅"
else else
export RAILPACK_VERSION=0.2.2 export RAILPACK_VERSION=0.15.4
bash -c "$(curl -fsSL https://railpack.com/install.sh)" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅" echo "Railpack version $RAILPACK_VERSION installed ✅"
fi fi
@@ -653,8 +653,8 @@ const installBuildpacks = () => `
if command_exists pack; then if command_exists pack; then
echo "Buildpacks already installed ✅" echo "Buildpacks already installed ✅"
else else
BUILDPACKS_VERSION=0.35.0 BUILDPACKS_VERSION=0.39.1
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅" echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
fi fi
`; `;

View File

@@ -1,6 +1,8 @@
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import { findOwner } from "@dokploy/server/services/admin"; import {
import { updateUser } from "@dokploy/server/services/user"; getWebServerSettings,
updateWebServerSettings,
} from "@dokploy/server/services/web-server-settings";
import { scheduledJobs, scheduleJob } from "node-schedule"; import { scheduledJobs, scheduleJob } from "node-schedule";
import { execAsync } from "../process/execAsync"; import { execAsync } from "../process/execAsync";
@@ -29,12 +31,9 @@ export const startLogCleanup = async (
} }
}); });
const owner = await findOwner(); await updateWebServerSettings({
if (owner) { logCleanupCron: cronExpression,
await updateUser(owner.user.id, { });
logCleanupCron: cronExpression,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -51,12 +50,9 @@ export const stopLogCleanup = async (): Promise<boolean> => {
} }
// Update database // Update database
const owner = await findOwner(); await updateWebServerSettings({
if (owner) { logCleanupCron: null,
await updateUser(owner.user.id, { });
logCleanupCron: null,
});
}
return true; return true;
} catch (error) { } catch (error) {
@@ -69,8 +65,8 @@ export const getLogCleanupStatus = async (): Promise<{
enabled: boolean; enabled: boolean;
cronExpression: string | null; cronExpression: string | null;
}> => { }> => {
const owner = await findOwner(); const settings = await getWebServerSettings();
const cronExpression = owner?.user.logCleanupCron ?? null; const cronExpression = settings?.logCleanupCron ?? null;
return { return {
enabled: cronExpression !== null, enabled: cronExpression !== null,
cronExpression, cronExpression,

View File

@@ -71,8 +71,9 @@ export function selectAIProvider(config: { apiUrl: string; apiKey: string }) {
return createOpenAICompatible({ return createOpenAICompatible({
name: "gemini", name: "gemini",
baseURL: config.apiUrl, baseURL: config.apiUrl,
queryParams: { key: config.apiKey }, headers: {
headers: {}, Authorization: `Bearer ${config.apiKey}`,
},
}); });
case "custom": case "custom":
return createOpenAICompatible({ return createOpenAICompatible({

View File

@@ -2,6 +2,7 @@ import path from "node:path";
import { member } from "@dokploy/server/db/schema"; import { member } from "@dokploy/server/db/schema";
import type { BackupSchedule } from "@dokploy/server/services/backup"; import type { BackupSchedule } from "@dokploy/server/services/backup";
import { getAllServers } from "@dokploy/server/services/server"; import { getAllServers } from "@dokploy/server/services/server";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { scheduleJob } from "node-schedule"; import { scheduleJob } from "node-schedule";
import { db } from "../../db/index"; import { db } from "../../db/index";
@@ -25,7 +26,9 @@ export const initCronJobs = async () => {
return; return;
} }
if (admin?.user?.enableDockerCleanup) { const webServerSettings = await getWebServerSettings();
if (webServerSettings?.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => { scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log( console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`, `Docker Cleanup ${new Date().toLocaleString()}] Running docker cleanup`,
@@ -82,9 +85,12 @@ export const initCronJobs = async () => {
} }
} }
if (admin?.user?.logCleanupCron) { if (webServerSettings?.logCleanupCron) {
console.log("Starting log requests cleanup", admin.user.logCleanupCron); console.log(
await startLogCleanup(admin.user.logCleanupCron); "Starting log requests cleanup",
webServerSettings.logCleanupCron,
);
await startLogCleanup(webServerSettings.logCleanupCron);
} }
}; };

View File

@@ -90,7 +90,7 @@ export const createCommand = (compose: ComposeNested) => {
if (composeType === "docker-compose") { if (composeType === "docker-compose") {
command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`;
} else if (composeType === "stack") { } else if (composeType === "stack") {
command = `stack deploy -c ${path} ${appName} --prune`; command = `stack deploy -c ${path} ${appName} --prune --with-registry-auth`;
} }
return command; return command;

View File

@@ -167,15 +167,9 @@ while true; do
fi fi
done done
# Execute command and capture exit code
${exec} ${exec}
EXIT_CODE=$?
# Wait for all background processes to complete to prevent zombie processes echo "Execution completed."
wait
echo "Execution completed with exit code: $EXIT_CODE"
exit $EXIT_CODE
`; `;
const cleanupCommands = { const cleanupCommands = {

View File

@@ -1,7 +1,7 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { paths } from "@dokploy/server/constants"; import { paths } from "@dokploy/server/constants";
import type { User } from "@dokploy/server/services/user"; import type { webServerSettings } from "@dokploy/server/db/schema/web-server-settings";
import { parse, stringify } from "yaml"; import { parse, stringify } from "yaml";
import { import {
loadOrCreateConfig, loadOrCreateConfig,
@@ -12,10 +12,10 @@ import type { FileConfig } from "./file-types";
import type { MainTraefikConfig } from "./types"; import type { MainTraefikConfig } from "./types";
export const updateServerTraefik = ( export const updateServerTraefik = (
user: User | null, settings: typeof webServerSettings.$inferSelect | null,
newHost: string | null, newHost: string | null,
) => { ) => {
const { https, certificateType } = user || {}; const { https, certificateType } = settings || {};
const appName = "dokploy"; const appName = "dokploy";
const config: FileConfig = loadOrCreateConfig(appName); const config: FileConfig = loadOrCreateConfig(appName);

99
pnpm-lock.yaml generated
View File

@@ -51,9 +51,6 @@ importers:
'@hono/zod-validator': '@hono/zod-validator':
specifier: 0.3.0 specifier: 0.3.0
version: 0.3.0(hono@4.7.10)(zod@3.25.32) version: 0.3.0(hono@4.7.10)(zod@3.25.32)
'@nerimity/mimiqueue':
specifier: 1.2.3
version: 1.2.3(redis@4.7.0)
dotenv: dotenv:
specifier: ^16.4.5 specifier: ^16.4.5
version: 16.4.5 version: 16.4.5
@@ -313,9 +310,6 @@ importers:
fancy-ansi: fancy-ansi:
specifier: ^0.1.3 specifier: ^0.1.3
version: 0.1.3 version: 0.1.3
hi-base32:
specifier: ^0.5.1
version: 0.5.1
i18next: i18next:
specifier: ^23.16.8 specifier: ^23.16.8
version: 23.16.8 version: 23.16.8
@@ -364,9 +358,6 @@ importers:
octokit: octokit:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
otpauth:
specifier: ^9.4.0
version: 9.4.0
pino: pino:
specifier: 9.4.0 specifier: 9.4.0
version: 9.4.0 version: 9.4.0
@@ -406,9 +397,9 @@ importers:
recharts: recharts:
specifier: ^2.15.3 specifier: ^2.15.3
version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 2.15.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
rotating-file-stream: semver:
specifier: 3.2.3 specifier: 7.7.3
version: 3.2.3 version: 7.7.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -494,6 +485,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: 18.3.0 specifier: 18.3.0
version: 18.3.0 version: 18.3.0
'@types/semver':
specifier: 7.7.1
version: 7.7.1
'@types/shell-quote': '@types/shell-quote':
specifier: ^1.7.5 specifier: ^1.7.5
version: 1.7.5 version: 1.7.5
@@ -681,9 +675,6 @@ importers:
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32) version: 0.5.1(drizzle-orm@0.39.3(@opentelemetry/api@1.9.0)(@types/pg@8.6.1)(kysely@0.28.7)(postgres@3.4.4))(zod@3.25.32)
hi-base32:
specifier: ^0.5.1
version: 0.5.1
lodash: lodash:
specifier: 4.17.21 specifier: 4.17.21
version: 4.17.21 version: 4.17.21
@@ -708,9 +699,6 @@ importers:
octokit: octokit:
specifier: 3.1.2 specifier: 3.1.2
version: 3.1.2 version: 3.1.2
otpauth:
specifier: ^9.4.0
version: 9.4.0
pino: pino:
specifier: 9.4.0 specifier: 9.4.0
version: 9.4.0 version: 9.4.0
@@ -732,9 +720,9 @@ importers:
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
rotating-file-stream: semver:
specifier: 3.2.3 specifier: 7.7.3
version: 3.2.3 version: 7.7.3
shell-quote: shell-quote:
specifier: ^1.8.1 specifier: ^1.8.1
version: 1.8.2 version: 1.8.2
@@ -790,6 +778,9 @@ importers:
'@types/react-dom': '@types/react-dom':
specifier: 18.3.0 specifier: 18.3.0
version: 18.3.0 version: 18.3.0
'@types/semver':
specifier: 7.7.1
version: 7.7.1
'@types/shell-quote': '@types/shell-quote':
specifier: ^1.7.5 specifier: ^1.7.5
version: 1.7.5 version: 1.7.5
@@ -1957,11 +1948,6 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@nerimity/mimiqueue@1.2.3':
resolution: {integrity: sha512-WPoGe417P+S0FLfl3psRBI5adcAWXb917vCF1qD2yGZ1ggBEnMH6UrUK464gzJEOpAlGt8BBbIp0tgCEazZ47A==}
peerDependencies:
redis: ^4.7.0
'@next/env@16.0.10': '@next/env@16.0.10':
resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==}
@@ -4066,6 +4052,9 @@ packages:
'@types/readable-stream@4.0.20': '@types/readable-stream@4.0.20':
resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==} resolution: {integrity: sha512-eLgbR5KwUh8+6pngBDxS32MymdCsCHnGtwHTrC0GDorbc7NbcnkZAWptDLgZiRk9VRas+B6TyRgPDucq4zRs8g==}
'@types/semver@7.7.1':
resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==}
'@types/shell-quote@1.7.5': '@types/shell-quote@1.7.5':
resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==}
@@ -4308,9 +4297,6 @@ packages:
assertion-error@1.1.0: assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
async-await-queue@2.1.4:
resolution: {integrity: sha512-3DpDtxkKO0O/FPlWbk/CrbexjuSxWm1CH1bXlVNVyMBIkKHhT5D85gzHmGJokG3ibNGWQ7pHBmStxUW/z/0LYQ==}
asynckit@0.4.0: asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -5395,9 +5381,6 @@ packages:
help-me@5.0.0: help-me@5.0.0:
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
hi-base32@0.5.1:
resolution: {integrity: sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==}
highlight.js@10.7.3: highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@@ -6432,9 +6415,6 @@ packages:
openapi-types@12.1.3: openapi-types@12.1.3:
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
otpauth@9.4.0:
resolution: {integrity: sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==}
p-cancelable@3.0.0: p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@@ -7064,10 +7044,6 @@ packages:
engines: {node: '>=18.0.0', npm: '>=8.0.0'} engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true hasBin: true
rotating-file-stream@3.2.3:
resolution: {integrity: sha512-cfmm3tqdnbuYw2FBmRTPBDaohYEbMJ3211T35o6eZdr4d7v69+ZeK1Av84Br7FLj2dlzyeZSbN6qTuXXE6dawQ==}
engines: {node: '>=14.0'}
rou3@0.5.1: rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
@@ -7097,11 +7073,6 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
semver@7.7.3: semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8129,7 +8100,7 @@ snapshots:
'@commitlint/is-ignored@19.8.1': '@commitlint/is-ignored@19.8.1':
dependencies: dependencies:
'@commitlint/types': 19.8.1 '@commitlint/types': 19.8.1
semver: 7.7.2 semver: 7.7.3
'@commitlint/lint@19.8.1': '@commitlint/lint@19.8.1':
dependencies: dependencies:
@@ -8746,7 +8717,7 @@ snapshots:
nopt: 5.0.0 nopt: 5.0.0
npmlog: 5.0.1 npmlog: 5.0.1
rimraf: 3.0.2 rimraf: 3.0.2
semver: 7.7.2 semver: 7.7.3
tar: 6.2.1 tar: 6.2.1
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@@ -8772,11 +8743,6 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true optional: true
'@nerimity/mimiqueue@1.2.3(redis@4.7.0)':
dependencies:
async-await-queue: 2.1.4
redis: 4.7.0
'@next/env@16.0.10': {} '@next/env@16.0.10': {}
'@next/swc-darwin-arm64@16.0.10': '@next/swc-darwin-arm64@16.0.10':
@@ -9337,7 +9303,7 @@ snapshots:
'@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.28.0 '@opentelemetry/semantic-conventions': 1.28.0
forwarded-parse: 2.1.2 forwarded-parse: 2.1.2
semver: 7.7.2 semver: 7.7.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9538,7 +9504,7 @@ snapshots:
'@types/shimmer': 1.2.0 '@types/shimmer': 1.2.0
import-in-the-middle: 1.14.2 import-in-the-middle: 1.14.2
require-in-the-middle: 7.5.2 require-in-the-middle: 7.5.2
semver: 7.7.2 semver: 7.7.3
shimmer: 1.2.1 shimmer: 1.2.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9683,7 +9649,7 @@ snapshots:
'@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0)
semver: 7.7.2 semver: 7.7.3
'@opentelemetry/semantic-conventions@1.28.0': {} '@opentelemetry/semantic-conventions@1.28.0': {}
@@ -11431,6 +11397,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 20.17.51 '@types/node': 20.17.51
'@types/semver@7.7.1': {}
'@types/shell-quote@1.7.5': {} '@types/shell-quote@1.7.5': {}
'@types/shimmer@1.2.0': {} '@types/shimmer@1.2.0': {}
@@ -11683,8 +11651,6 @@ snapshots:
assertion-error@1.1.0: {} assertion-error@1.1.0: {}
async-await-queue@2.1.4: {}
asynckit@0.4.0: {} asynckit@0.4.0: {}
atomic-sleep@1.0.0: {} atomic-sleep@1.0.0: {}
@@ -11830,7 +11796,7 @@ snapshots:
lodash: 4.17.21 lodash: 4.17.21
msgpackr: 1.11.4 msgpackr: 1.11.4
node-abort-controller: 3.1.1 node-abort-controller: 3.1.1
semver: 7.7.2 semver: 7.7.3
tslib: 2.8.1 tslib: 2.8.1
uuid: 9.0.1 uuid: 9.0.1
transitivePeerDependencies: transitivePeerDependencies:
@@ -12346,7 +12312,7 @@ snapshots:
'@one-ini/wasm': 0.1.1 '@one-ini/wasm': 0.1.1
commander: 10.0.1 commander: 10.0.1
minimatch: 9.0.1 minimatch: 9.0.1
semver: 7.7.2 semver: 7.7.3
electron-to-chromium@1.5.159: {} electron-to-chromium@1.5.159: {}
@@ -12660,7 +12626,7 @@ snapshots:
'@petamoriken/float16': 3.9.2 '@petamoriken/float16': 3.9.2
debug: 4.4.1 debug: 4.4.1
env-paths: 3.0.0 env-paths: 3.0.0
semver: 7.7.2 semver: 7.7.3
shell-quote: 1.8.2 shell-quote: 1.8.2
which: 4.0.0 which: 4.0.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -12834,8 +12800,6 @@ snapshots:
help-me@5.0.0: {} help-me@5.0.0: {}
hi-base32@0.5.1: {}
highlight.js@10.7.3: {} highlight.js@10.7.3: {}
highlightjs-vue@1.0.0: {} highlightjs-vue@1.0.0: {}
@@ -13148,7 +13112,7 @@ snapshots:
lodash.isstring: 4.0.1 lodash.isstring: 4.0.1
lodash.once: 4.1.1 lodash.once: 4.1.1
ms: 2.1.3 ms: 2.1.3
semver: 7.7.2 semver: 7.7.3
jss-plugin-camel-case@10.10.0: jss-plugin-camel-case@10.10.0:
dependencies: dependencies:
@@ -13972,10 +13936,6 @@ snapshots:
openapi-types@12.1.3: {} openapi-types@12.1.3: {}
otpauth@9.4.0:
dependencies:
'@noble/hashes': 1.7.1
p-cancelable@3.0.0: {} p-cancelable@3.0.0: {}
p-limit@2.3.0: p-limit@2.3.0:
@@ -14660,8 +14620,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc': 4.41.1 '@rollup/rollup-win32-x64-msvc': 4.41.1
fsevents: 2.3.3 fsevents: 2.3.3
rotating-file-stream@3.2.3: {}
rou3@0.5.1: {} rou3@0.5.1: {}
run-parallel@1.2.0: run-parallel@1.2.0:
@@ -14686,10 +14644,7 @@ snapshots:
semver@6.3.1: {} semver@6.3.1: {}
semver@7.7.2: {} semver@7.7.3: {}
semver@7.7.3:
optional: true
serialize-error-cjs@0.1.4: {} serialize-error-cjs@0.1.4: {}

View File

@@ -276,7 +276,7 @@ table application {
replicas integer [not null, default: 1] replicas integer [not null, default: 1]
applicationStatus applicationStatus [not null, default: 'idle'] applicationStatus applicationStatus [not null, default: 'idle']
buildType buildType [not null, default: 'nixpacks'] buildType buildType [not null, default: 'nixpacks']
railpackVersion text [default: '0.2.2'] railpackVersion text [default: '0.15.4']
herokuVersion text [default: '24'] herokuVersion text [default: '24']
publishDirectory text publishDirectory text
isStaticSpa boolean isStaticSpa boolean