diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5826da4f..bb7721468 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -77,26 +77,3 @@ jobs: tags: | siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }} platforms: linux/amd64 - - build-and-push-monitoring-image: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Log in to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - file: ./Dockerfile.monitoring - push: true - tags: | - siumauricio/monitoring:${{ github.ref_name == 'main' && 'latest' || 'canary' }} - platforms: linux/amd64 diff --git a/.github/workflows/monitoring.yml b/.github/workflows/monitoring.yml new file mode 100644 index 000000000..378b019d4 --- /dev/null +++ b/.github/workflows/monitoring.yml @@ -0,0 +1,118 @@ +name: Dokploy Monitoring Build + +on: + push: + branches: [main, canary] + +env: + IMAGE_NAME: dokploy/monitoring + +jobs: + docker-amd: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set tag + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-amd64" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.monitoring + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + docker-arm: + runs-on: ubuntu-24.04-arm + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set + id: meta + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + else + TAG="feature" + fi + echo "tags=${IMAGE_NAME}:${TAG}-arm64" >> $GITHUB_OUTPUT + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile.monitoring + platforms: linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + + combine-manifests: + needs: [docker-amd, docker-arm] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Create and push manifests + run: | + if [ "${{ github.ref }}" = "refs/heads/main" ]; then + TAG="latest" + + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + elif [ "${{ github.ref }}" = "refs/heads/canary" ]; then + TAG="canary" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + + else + TAG="feature" + docker buildx imagetools create -t ${IMAGE_NAME}:${TAG} \ + ${IMAGE_NAME}:${TAG}-amd64 \ + ${IMAGE_NAME}:${TAG}-arm64 + fi diff --git a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx index c49be0c47..3636a391a 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/container/show-paid-container-monitoring.tsx @@ -6,6 +6,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { api } from "@/utils/api"; import { Cpu, HardDrive, Loader2, MemoryStick, Network } from "lucide-react"; import { useEffect, useState } from "react"; import { ContainerBlockChart } from "./container-block-chart"; @@ -70,84 +71,36 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { const [metrics, setMetrics] = useState( {} as ContainerMetric, ); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); const [dataPoints, setDataPoints] = useState("50"); const [refreshInterval, setRefreshInterval] = useState("5000"); - const fetchMetrics = async () => { - try { - const url = new URL(`${baseUrl}/metrics/containers`); - - // if (dataPoints !== "all") { - url.searchParams.append("limit", dataPoints); - // } - - if (!appName) { - throw new Error( - [ - "No Application Selected:", - "", - "Make Sure to select an application to monitor.", - ].join("\n"), - ); - } - - url.searchParams.append("appName", appName); - - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Please verify that the application "${appName}" is running and this service is included in the monitoring configuration.`, - ); - } - - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - `No monitoring data available for "${appName}". This could be because:`, - "", - "1. The container was recently started - wait a few minutes for data to be collected", - "2. The container is not running - verify its status", - "3. The service is not included in your monitoring configuration", - ].join("\n"), - ); - } - - setHistoricalData(data); - setMetrics(data[data.length - 1]); - setError(null); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.", - ); - } finally { - setIsLoading(false); - } - }; + const { + data, + isLoading, + error: queryError, + } = api.admin.getContainerMetrics.useQuery( + { + url: baseUrl, + token, + dataPoints, + appName, + }, + { + refetchInterval: + dataPoints === "all" ? undefined : Number.parseInt(refreshInterval), + enabled: !!appName, + }, + ); useEffect(() => { - fetchMetrics(); + if (!data) return; - if (dataPoints === "all") { - return; - } - - const interval = setInterval(() => { - fetchMetrics(); - }, Number(refreshInterval)); - - return () => clearInterval(interval); - }, [dataPoints, appName, token, refreshInterval]); + // @ts-ignore + setHistoricalData(data); + // @ts-ignore + setMetrics(data[data.length - 1]); + }, [data]); if (isLoading) { return ( @@ -157,7 +110,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { ); } - if (error) { + if (queryError) { return (
@@ -166,7 +119,9 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { {appName}

- {error} + {queryError instanceof Error + ? queryError.message + : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

URL: {baseUrl}

@@ -176,54 +131,59 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => { return ( <> -
+

Container Monitoring

-
- Data points: - - - Refresh interval: - - +
+
+ Data points: + +
+ +
+ + Refresh interval: + + +
{/* Stats Cards */} -
+
@@ -238,11 +198,11 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Memory Usage

- {metrics.Memory.percentage}% + {metrics?.Memory?.percentage}%

- {metrics.Memory.used} {metrics.Memory.unit} / {metrics.Memory.total}{" "} - {metrics.Memory.unit} + {metrics?.Memory?.used} {metrics?.Memory?.unit} /{" "} + {metrics?.Memory?.total} {metrics?.Memory?.unit}

@@ -252,8 +212,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Network I/O

- {metrics.Network.input} {metrics.Network.inputUnit} /{" "} - {metrics.Network.output} {metrics.Network.outputUnit} + {metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "} + {metrics?.Network?.output} {metrics?.Network?.outputUnit}

@@ -263,8 +223,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Block I/O

- {metrics.BlockIO.read} {metrics.BlockIO.readUnit} /{" "} - {metrics.BlockIO.write} {metrics.BlockIO.writeUnit} + {metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "} + {metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}

@@ -281,13 +241,13 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {

Name

-

{metrics.Name}

+

{metrics.Name}

{/* Charts Grid */} -
+
diff --git a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx index 90148dfa3..043b5c625 100644 --- a/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx +++ b/apps/dokploy/components/dashboard/monitoring/paid/servers/show-paid-monitoring.tsx @@ -5,6 +5,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { api } from "@/utils/api"; import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react"; import type React from "react"; import { useEffect, useState } from "react"; @@ -64,76 +65,56 @@ export const ShowPaidMonitoring = ({ }: Props) => { const [historicalData, setHistoricalData] = useState([]); const [metrics, setMetrics] = useState({} as SystemMetrics); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); const [dataPoints, setDataPoints] = useState("50"); const [refreshInterval, setRefreshInterval] = useState("5000"); - const fetchMetrics = async () => { - try { - const url = new URL(BASE_URL); - url.searchParams.append("limit", dataPoints); - const response = await fetch(url.toString(), { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const { + data, + isLoading, + error: queryError, + } = api.admin.getServerMetrics.useQuery( + { + url: BASE_URL, + token, + dataPoints, + }, + { + refetchInterval: + dataPoints === "all" ? undefined : Number.parseInt(refreshInterval), + enabled: true, + }, + ); - if (!response.ok) { - throw new Error( - `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, - ); - } + useEffect(() => { + if (!data) return; - const data = await response.json(); - if (!Array.isArray(data) || data.length === 0) { - throw new Error( - [ - "No monitoring data available. This could be because:", - "", - "1. You don't have setup the monitoring service, you can do in web server section.", - "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", - ].join("\n"), - ); - } + const formattedData = data.map((metric: SystemMetrics) => ({ + timestamp: metric.timestamp, + cpu: Number.parseFloat(metric.cpu), + cpuModel: metric.cpuModel, + cpuCores: metric.cpuCores, + cpuPhysicalCores: metric.cpuPhysicalCores, + cpuSpeed: metric.cpuSpeed, + os: metric.os, + distro: metric.distro, + kernel: metric.kernel, + arch: metric.arch, + memUsed: Number.parseFloat(metric.memUsed), + memUsedGB: Number.parseFloat(metric.memUsedGB), + memTotal: Number.parseFloat(metric.memTotal), + networkIn: Number.parseFloat(metric.networkIn), + networkOut: Number.parseFloat(metric.networkOut), + diskUsed: Number.parseFloat(metric.diskUsed), + totalDisk: Number.parseFloat(metric.totalDisk), + uptime: metric.uptime, + })); - const formattedData = data.map((metric: SystemMetrics) => ({ - timestamp: metric.timestamp, - cpu: Number.parseFloat(metric.cpu), - cpuModel: metric.cpuModel, - cpuCores: metric.cpuCores, - cpuPhysicalCores: metric.cpuPhysicalCores, - cpuSpeed: metric.cpuSpeed, - os: metric.os, - distro: metric.distro, - kernel: metric.kernel, - arch: metric.arch, - memUsed: Number.parseFloat(metric.memUsed), - memUsedGB: Number.parseFloat(metric.memUsedGB), - memTotal: Number.parseFloat(metric.memTotal), - networkIn: Number.parseFloat(metric.networkIn), - networkOut: Number.parseFloat(metric.networkOut), - diskUsed: Number.parseFloat(metric.diskUsed), - totalDisk: Number.parseFloat(metric.totalDisk), - uptime: metric.uptime, - })); - - // @ts-ignore - setHistoricalData(formattedData); - // @ts-ignore - setMetrics(formattedData[formattedData.length - 1] || {}); - setError(null); - } catch (err) { - setError( - err instanceof Error - ? err.message - : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly.", - ); - } finally { - setIsLoading(false); - } - }; + // @ts-ignore + setHistoricalData(formattedData); + // @ts-ignore + setMetrics(formattedData[formattedData.length - 1] || {}); + }, [data]); const formatUptime = (seconds: number): string => { const days = Math.floor(seconds / (24 * 60 * 60)); @@ -143,20 +124,6 @@ export const ShowPaidMonitoring = ({ return `${days}d ${hours}h ${minutes}m`; }; - useEffect(() => { - fetchMetrics(); - - if (dataPoints === "all") { - return; - } - - const interval = setInterval(() => { - fetchMetrics(); - }, Number(refreshInterval)); - - return () => clearInterval(interval); - }, [dataPoints, token, refreshInterval]); - if (isLoading) { return (
@@ -165,7 +132,7 @@ export const ShowPaidMonitoring = ({ ); } - if (error) { + if (queryError) { return (
@@ -173,7 +140,9 @@ export const ShowPaidMonitoring = ({ Error fetching metrics{" "}

- {error} + {queryError instanceof Error + ? queryError.message + : "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}

URL: {BASE_URL}

@@ -182,53 +151,58 @@ export const ShowPaidMonitoring = ({ } return ( -
-
+
+

System Monitoring

-
- Data points: - - - Refresh interval: - - +
+
+ Data points: + +
+ +
+ + Refresh interval: + + +
{/* Stats Cards */} -
+
@@ -291,7 +265,7 @@ export const ShowPaidMonitoring = ({
{/* Charts Grid */} -
+
diff --git a/apps/dokploy/package.json b/apps/dokploy/package.json index ff93e575b..213c90739 100644 --- a/apps/dokploy/package.json +++ b/apps/dokploy/package.json @@ -1,6 +1,6 @@ { "name": "dokploy", - "version": "v0.17.9", + "version": "v0.18.2", "private": true, "license": "Apache-2.0", "type": "module", diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx index 781854f8d..9d3ffe4fb 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/application/[applicationId].tsx @@ -399,6 +399,8 @@ export async function getServerSideProps( applicationId: params?.applicationId, }); + await helpers.settings.isCloud.prefetch(); + return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index b9ed1d8c7..c68402142 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -394,7 +394,7 @@ export async function getServerSideProps( await helpers.compose.one.fetch({ composeId: params?.composeId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx index d441b08c3..a2ee90512 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mariadb/[mariadbId].tsx @@ -343,7 +343,7 @@ export async function getServerSideProps( await helpers.mariadb.one.fetch({ mariadbId: params?.mariadbId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx index 7fdb1e773..4f3947c2a 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mongo/[mongoId].tsx @@ -345,7 +345,7 @@ export async function getServerSideProps( await helpers.mongo.one.fetch({ mongoId: params?.mongoId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx index ba4efecc3..baf7e6f8b 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/mysql/[mysqlId].tsx @@ -350,7 +350,7 @@ export async function getServerSideProps( await helpers.mysql.one.fetch({ mysqlId: params?.mysqlId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx index 047d1c571..e3fd8b444 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/postgres/[postgresId].tsx @@ -346,7 +346,7 @@ export async function getServerSideProps( await helpers.postgres.one.fetch({ postgresId: params?.postgresId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx index 1b0416400..3421e759c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/redis/[redisId].tsx @@ -337,7 +337,7 @@ export async function getServerSideProps( await helpers.redis.one.fetch({ redisId: params?.redisId, }); - + await helpers.settings.isCloud.prefetch(); return { props: { trpcState: helpers.dehydrate(), diff --git a/apps/dokploy/public/templates/frappe-hr.svg b/apps/dokploy/public/templates/frappe-hr.svg new file mode 100644 index 000000000..4cbf51641 --- /dev/null +++ b/apps/dokploy/public/templates/frappe-hr.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/dokploy/server/api/routers/admin.ts b/apps/dokploy/server/api/routers/admin.ts index f10bc759a..31612fe06 100644 --- a/apps/dokploy/server/api/routers/admin.ts +++ b/apps/dokploy/server/api/routers/admin.ts @@ -22,6 +22,7 @@ import { } from "@dokploy/server"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; +import { z } from "zod"; import { adminProcedure, createTRPCRouter, @@ -169,4 +170,121 @@ export const adminRouter = createTRPCRouter({ metricsConfig: admin?.metricsConfig, }; }), + + getServerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ ctx, input }) => { + try { + const url = new URL(input.url); + url.searchParams.append("limit", input.dataPoints); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Ensure the container is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + "No monitoring data available. This could be because:", + "", + "1. You don't have setup the monitoring service, you can do in web server section.", + "2. If you already have setup the monitoring service, wait a few minutes and refresh the page.", + ].join("\n"), + ); + } + return data as { + cpu: string; + cpuModel: string; + cpuCores: number; + cpuPhysicalCores: number; + cpuSpeed: number; + os: string; + distro: string; + kernel: string; + arch: string; + memUsed: string; + memUsedGB: string; + memTotal: string; + uptime: number; + diskUsed: string; + totalDisk: string; + networkIn: string; + networkOut: string; + timestamp: string; + }[]; + } catch (error) { + throw error; + } + }), + getContainerMetrics: protectedProcedure + .input( + z.object({ + url: z.string(), + token: z.string(), + appName: z.string(), + dataPoints: z.string(), + }), + ) + .query(async ({ ctx, input }) => { + try { + if (!input.appName) { + throw new Error( + [ + "No Application Selected:", + "", + "Make Sure to select an application to monitor.", + ].join("\n"), + ); + } + const url = new URL(`${input.url}/metrics/containers`); + url.searchParams.append("limit", input.dataPoints); + url.searchParams.append("appName", input.appName); + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${input.token}`, + }, + }); + if (!response.ok) { + throw new Error( + `Error ${response.status}: ${response.statusText}. Please verify that the application "${input.appName}" is running and this service is included in the monitoring configuration.`, + ); + } + + const data = await response.json(); + if (!Array.isArray(data) || data.length === 0) { + throw new Error( + [ + `No monitoring data available for "${input.appName}". This could be because:`, + "", + "1. The container was recently started - wait a few minutes for data to be collected", + "2. The container is not running - verify its status", + "3. The service is not included in your monitoring configuration", + ].join("\n"), + ); + } + return data as { + containerId: string; + containerName: string; + containerImage: string; + containerLabels: string; + containerCommand: string; + containerCreated: string; + }[]; + } catch (error) { + throw error; + } + }), }); diff --git a/apps/dokploy/templates/erpnext/docker-compose.yml b/apps/dokploy/templates/erpnext/docker-compose.yml index def916be1..28cd8f6ab 100644 --- a/apps/dokploy/templates/erpnext/docker-compose.yml +++ b/apps/dokploy/templates/erpnext/docker-compose.yml @@ -189,7 +189,7 @@ services: bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; bench set-config -gp socketio_port $$SOCKETIO_PORT; environment: - DB_HOST: db + DB_HOST: "${DB_HOST:-db}" DB_PORT: "3306" REDIS_CACHE: redis-cache:6379 REDIS_QUEUE: redis-queue:6379 @@ -210,7 +210,7 @@ services: entrypoint: ["bash", "-c"] command: - > - wait-for-it -t 120 db:3306; + wait-for-it -t 120 $$DB_HOST:$$DB_PORT; wait-for-it -t 120 redis-cache:6379; wait-for-it -t 120 redis-queue:6379; export start=`date +%s`; @@ -231,10 +231,12 @@ services: volumes: - sites:/home/frappe/frappe-bench/sites environment: + SITE_NAME: ${SITE_NAME} ADMIN_PASSWORD: ${ADMIN_PASSWORD} + DB_HOST: ${DB_HOST:-db} + DB_PORT: "${DB_PORT:-3306}" DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} INSTALL_APP_ARGS: ${INSTALL_APP_ARGS} - SITE_NAME: ${SITE_NAME} networks: - bench-network @@ -262,6 +264,8 @@ services: db: image: mariadb:10.6 deploy: + mode: replicated + replicas: ${ENABLE_DB:-0} restart_policy: condition: always healthcheck: @@ -341,6 +345,10 @@ volumes: redis-queue-data: redis-socketio-data: sites: + driver_opts: + type: "${SITE_VOLUME_TYPE}" + o: "${SITE_VOLUME_OPTS}" + device: "${SITE_VOLUME_DEV}" networks: bench-network: \ No newline at end of file diff --git a/apps/dokploy/templates/erpnext/index.ts b/apps/dokploy/templates/erpnext/index.ts index 0be46c449..5b7543b91 100644 --- a/apps/dokploy/templates/erpnext/index.ts +++ b/apps/dokploy/templates/erpnext/index.ts @@ -24,6 +24,8 @@ export function generate(schema: Schema): Template { `ADMIN_PASSWORD=${adminPassword}`, `DB_ROOT_PASSWORD=${dbRootPassword}`, "MIGRATE=1", + "ENABLE_DB=1", + "DB_HOST=db", "CREATE_SITE=1", "CONFIGURE=1", "REGENERATE_APPS_TXT=1", diff --git a/apps/dokploy/templates/frappe-hr/docker-compose.yml b/apps/dokploy/templates/frappe-hr/docker-compose.yml new file mode 100644 index 000000000..a7ce9b262 --- /dev/null +++ b/apps/dokploy/templates/frappe-hr/docker-compose.yml @@ -0,0 +1,354 @@ +x-custom-image: &custom_image + image: ${IMAGE_NAME:-ghcr.io/frappe/hrms}:${VERSION:-version-15} + pull_policy: ${PULL_POLICY:-always} + deploy: + restart_policy: + condition: always + +services: + backend: + <<: *custom_image + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:8000' + interval: 2s + timeout: 10s + retries: 30 + + frontend: + <<: *custom_image + command: + - nginx-entrypoint.sh + depends_on: + backend: + condition: service_started + required: true + websocket: + condition: service_started + required: true + environment: + BACKEND: backend:8000 + FRAPPE_SITE_NAME_HEADER: ${FRAPPE_SITE_NAME_HEADER:-$$host} + SOCKETIO: websocket:9000 + UPSTREAM_REAL_IP_ADDRESS: 127.0.0.1 + UPSTREAM_REAL_IP_HEADER: X-Forwarded-For + UPSTREAM_REAL_IP_RECURSIVE: "off" + volumes: + - sites:/home/frappe/frappe-bench/sites + + networks: + - bench-network + + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:8080' + interval: 2s + timeout: 30s + retries: 30 + + queue-default: + <<: *custom_image + command: + - bench + - worker + - --queue + - default + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + queue-long: + <<: *custom_image + command: + - bench + - worker + - --queue + - long + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + queue-short: + <<: *custom_image + command: + - bench + - worker + - --queue + - short + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + depends_on: + configurator: + condition: service_completed_successfully + required: true + + scheduler: + <<: *custom_image + healthcheck: + test: + - CMD + - wait-for-it + - 'redis-queue:6379' + interval: 2s + timeout: 10s + retries: 30 + command: + - bench + - schedule + depends_on: + configurator: + condition: service_completed_successfully + required: true + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + websocket: + <<: *custom_image + healthcheck: + test: + - CMD + - wait-for-it + - '0.0.0.0:9000' + interval: 2s + timeout: 10s + retries: 30 + command: + - node + - /home/frappe/frappe-bench/apps/frappe/socketio.js + depends_on: + configurator: + condition: service_completed_successfully + required: true + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + configurator: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CONFIGURE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + [[ $${REGENERATE_APPS_TXT} == "1" ]] && ls -1 apps > sites/apps.txt; + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && exit 0; + bench set-config -g db_host $$DB_HOST; + bench set-config -gp db_port $$DB_PORT; + bench set-config -g redis_cache "redis://$$REDIS_CACHE"; + bench set-config -g redis_queue "redis://$$REDIS_QUEUE"; + bench set-config -g redis_socketio "redis://$$REDIS_QUEUE"; + bench set-config -gp socketio_port $$SOCKETIO_PORT; + environment: + DB_HOST: "${DB_HOST:-db}" + DB_PORT: "3306" + REDIS_CACHE: redis-cache:6379 + REDIS_QUEUE: redis-queue:6379 + SOCKETIO_PORT: "9000" + REGENERATE_APPS_TXT: "${REGENERATE_APPS_TXT:-0}" + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + create-site: + <<: *custom_image + deploy: + mode: replicated + replicas: ${CREATE_SITE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + wait-for-it -t 120 $$DB_HOST:$$DB_PORT; + wait-for-it -t 120 redis-cache:6379; + wait-for-it -t 120 redis-queue:6379; + export start=`date +%s`; + until [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".db_host // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_cache // empty"` ]] && \ + [[ -n `grep -hs ^ sites/common_site_config.json | jq -r ".redis_queue // empty"` ]]; + do + echo "Waiting for sites/common_site_config.json to be created"; + sleep 5; + if (( `date +%s`-start > 120 )); then + echo "could not find sites/common_site_config.json with required keys"; + exit 1 + fi + done; + echo "sites/common_site_config.json found"; + [[ -d "sites/${SITE_NAME}" ]] && echo "${SITE_NAME} already exists" && exit 0; + bench new-site --mariadb-user-host-login-scope='%' --admin-password=$${ADMIN_PASSWORD} --db-root-username=root --db-root-password=$${DB_ROOT_PASSWORD} $${INSTALL_APP_ARGS} $${SITE_NAME}; + volumes: + - sites:/home/frappe/frappe-bench/sites + environment: + SITE_NAME: ${SITE_NAME} + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + DB_HOST: ${DB_HOST:-db} + DB_PORT: "${DB_PORT:-3306}" + DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + INSTALL_APP_ARGS: ${INSTALL_APP_ARGS} + networks: + - bench-network + + migration: + <<: *custom_image + deploy: + mode: replicated + replicas: ${MIGRATE:-0} + restart_policy: + condition: none + entrypoint: ["bash", "-c"] + command: + - > + curl -f http://${SITE_NAME}:8080/api/method/ping || echo "Site busy" && exit 0; + bench --site all set-config -p maintenance_mode 1; + bench --site all set-config -p pause_scheduler 1; + bench --site all migrate; + bench --site all set-config -p maintenance_mode 0; + bench --site all set-config -p pause_scheduler 0; + volumes: + - sites:/home/frappe/frappe-bench/sites + networks: + - bench-network + + db: + image: mariadb:10.6 + deploy: + mode: replicated + replicas: ${ENABLE_DB:-0} + restart_policy: + condition: always + healthcheck: + test: mysqladmin ping -h localhost --password=${DB_ROOT_PASSWORD} + interval: 1s + retries: 20 + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --skip-character-set-client-handshake + - --skip-innodb-read-only-compressed + environment: + - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + volumes: + - db-data:/var/lib/mysql + networks: + - bench-network + + redis-cache: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-cache-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + + redis-queue: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-queue-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + + redis-socketio: + deploy: + restart_policy: + condition: always + image: redis:6.2-alpine + volumes: + - redis-socketio-data:/data + networks: + - bench-network + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 3 + +volumes: + db-data: + redis-cache-data: + redis-queue-data: + redis-socketio-data: + sites: + driver_opts: + type: "${SITE_VOLUME_TYPE}" + o: "${SITE_VOLUME_OPTS}" + device: "${SITE_VOLUME_DEV}" + +networks: + bench-network: \ No newline at end of file diff --git a/apps/dokploy/templates/frappe-hr/index.ts b/apps/dokploy/templates/frappe-hr/index.ts new file mode 100644 index 000000000..1e6b94745 --- /dev/null +++ b/apps/dokploy/templates/frappe-hr/index.ts @@ -0,0 +1,39 @@ +import { + type DomainSchema, + type Schema, + type Template, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const dbRootPassword = generatePassword(32); + const adminPassword = generatePassword(32); + const mainDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "frontend", + }, + ]; + + const envs = [ + `SITE_NAME=${mainDomain}`, + `ADMIN_PASSWORD=${adminPassword}`, + `DB_ROOT_PASSWORD=${dbRootPassword}`, + "MIGRATE=1", + "ENABLE_DB=1", + "DB_HOST=db", + "CREATE_SITE=1", + "CONFIGURE=1", + "REGENERATE_APPS_TXT=1", + "INSTALL_APP_ARGS=--install-app hrms", + "IMAGE_NAME=ghcr.io/frappe/hrms", + "VERSION=version-15", + "FRAPPE_SITE_NAME_HEADER=", + ]; + + return { envs, domains }; +} diff --git a/apps/dokploy/templates/pocketbase/docker-compose.yml b/apps/dokploy/templates/pocketbase/docker-compose.yml index 485c0f85b..cf5e94f1b 100644 --- a/apps/dokploy/templates/pocketbase/docker-compose.yml +++ b/apps/dokploy/templates/pocketbase/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.8" services: pocketbase: - image: spectado/pocketbase:0.25.0 + image: spectado/pocketbase:0.23.3 restart: unless-stopped volumes: - /etc/dokploy/templates/${HASH}/data:/pb_data diff --git a/apps/dokploy/templates/superset/index.ts b/apps/dokploy/templates/superset/index.ts index 4994b639d..954fc9710 100644 --- a/apps/dokploy/templates/superset/index.ts +++ b/apps/dokploy/templates/superset/index.ts @@ -35,6 +35,11 @@ export function generate(schema: Schema): Template { { filePath: "./superset/superset_config.py", content: ` +""" +For more configuration options, see: +- https://superset.apache.org/docs/configuration/configuring-superset +""" + import os SECRET_KEY = os.getenv("SECRET_KEY") @@ -53,8 +58,13 @@ CACHE_CONFIG = { FILTER_STATE_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_filter_"} EXPLORE_FORM_DATA_CACHE_CONFIG = {**CACHE_CONFIG, "CACHE_KEY_PREFIX": "superset_explore_form_"} -SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@{os.getenv('POSTGRES_HOST')}:5432/{os.getenv('POSTGRES_DB')}" SQLALCHEMY_TRACK_MODIFICATIONS = True +SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{os.getenv('POSTGRES_USER')}:{os.getenv('POSTGRES_PASSWORD')}@{os.getenv('POSTGRES_HOST')}:5432/{os.getenv('POSTGRES_DB')}" + +# Uncomment if you want to load example data (using "superset load_examples") at the +# same location as your metadata postgresql instance. Otherwise, the default sqlite +# will be used, which will not persist in volume when restarting superset by default. +#SQLALCHEMY_EXAMPLES_URI = SQLALCHEMY_DATABASE_URI `.trim(), }, ]; diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 83ae97f51..1ae19dec1 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1454,6 +1454,21 @@ export const templates: TemplateData[] = [ load: () => import("./shlink/index").then((m) => m.generate), }, { + id: "frappe-hr", + name: "Frappe HR", + version: "version-15", + description: + "Feature rich HR & Payroll software. 100% FOSS and customizable.", + logo: "frappe-hr.svg", + links: { + github: "https://github.com/frappe/hrms", + docs: "https://docs.frappe.io/hr", + website: "https://frappe.io/hr", + }, + tags: ["hrms", "payroll", "leaves", "expenses", "attendance", "performace"], + load: () => import("./frappe-hr/index").then((m) => m.generate), + }, + { id: "formbricks", name: "Formbricks", version: "v3.1.3", @@ -1467,5 +1482,6 @@ export const templates: TemplateData[] = [ }, tags: ["forms", "analytics"], load: () => import("./formbricks/index").then((m) => m.generate), + }, ]; diff --git a/packages/server/src/services/docker.ts b/packages/server/src/services/docker.ts index b7a5c4403..597c03fa1 100644 --- a/packages/server/src/services/docker.ts +++ b/packages/server/src/services/docker.ts @@ -58,7 +58,11 @@ export const getContainers = async (serverId?: string | null) => { serverId, }; }) - .filter((container) => !container.name.includes("dokploy")); + .filter( + (container) => + !container.name.includes("dokploy") || + container.name.includes("dokploy-monitoring"), + ); return containers; } catch (error) { diff --git a/packages/server/src/setup/monitoring-setup.ts b/packages/server/src/setup/monitoring-setup.ts index e7cd941d2..f72b22447 100644 --- a/packages/server/src/setup/monitoring-setup.ts +++ b/packages/server/src/setup/monitoring-setup.ts @@ -1,6 +1,8 @@ import { findServerById } from "@dokploy/server/services/server"; import type { ContainerCreateOptions } from "dockerode"; +import { IS_CLOUD } from "../constants"; import { findAdminById } from "../services/admin"; +import { getDokployImageTag } from "../services/settings"; import { pullImage, pullRemoteImage } from "../utils/docker/utils"; import { execAsync, execAsyncRemote } from "../utils/process/execAsync"; import { getRemoteDocker } from "../utils/servers/remote-docker"; @@ -8,8 +10,16 @@ import { getRemoteDocker } from "../utils/servers/remote-docker"; export const setupMonitoring = async (serverId: string) => { const server = await findServerById(serverId); - const containerName = "mauricio-monitoring"; - const imageName = "siumauricio/monitoring:canary"; + const containerName = "dokploy-monitoring"; + let imageName = "dokploy/monitoring:latest"; + + if ( + (getDokployImageTag() !== "latest" || + process.env.NODE_ENV === "development") && + !IS_CLOUD + ) { + imageName = "dokploy/monitoring:canary"; + } const settings: ContainerCreateOptions = { name: containerName, @@ -73,8 +83,16 @@ export const setupMonitoring = async (serverId: string) => { export const setupWebMonitoring = async (adminId: string) => { const admin = await findAdminById(adminId); - const containerName = "mauricio-monitoring"; - const imageName = "siumauricio/monitoring:canary"; + const containerName = "dokploy-monitoring"; + let imageName = "dokploy/monitoring:latest"; + + if ( + (getDokployImageTag() !== "latest" || + process.env.NODE_ENV === "development") && + !IS_CLOUD + ) { + imageName = "dokploy/monitoring:canary"; + } const settings: ContainerCreateOptions = { name: containerName, diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index af5607282..5e155271c 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -154,7 +154,6 @@ const sanitizeCommand = (command: string) => { export const createCommand = (compose: ComposeNested) => { const { composeType, appName, sourceType } = compose; - if (compose.command) { return `${sanitizeCommand(compose.command)}`; } @@ -163,7 +162,9 @@ export const createCommand = (compose: ComposeNested) => { sourceType === "raw" ? "docker-compose.yml" : compose.composePath; let command = ""; - if (composeType === "stack") { + if (composeType === "docker-compose") { + command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; + } else if (composeType === "stack") { command = `stack deploy -c ${path} ${appName} --prune`; }