mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' of https://github.com/mohabgabber/dokploy into canary
This commit is contained in:
23
.github/workflows/deploy.yml
vendored
23
.github/workflows/deploy.yml
vendored
@@ -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
|
||||
|
||||
118
.github/workflows/monitoring.yml
vendored
Normal file
118
.github/workflows/monitoring.yml
vendored
Normal file
@@ -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
|
||||
@@ -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<ContainerMetric>(
|
||||
{} as ContainerMetric,
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dataPoints, setDataPoints] =
|
||||
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
|
||||
const [refreshInterval, setRefreshInterval] = useState<string>("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 (
|
||||
<div className="mt-5 flex min-h-[55vh] w-full items-center justify-center p-4">
|
||||
<div className="max-w-xl text-center">
|
||||
@@ -166,7 +119,9 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
<strong className="text-primary">{appName}</strong>
|
||||
</p>
|
||||
<p className="whitespace-pre-line text-sm text-destructive">
|
||||
{error}
|
||||
{queryError instanceof Error
|
||||
? queryError.message
|
||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
||||
</p>
|
||||
<p className=" text-sm text-muted-foreground">URL: {baseUrl}</p>
|
||||
</div>
|
||||
@@ -176,54 +131,59 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Container Monitoring
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
||||
<Select
|
||||
value={dataPoints}
|
||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
||||
setDataPoints(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select points" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Refresh interval:
|
||||
</span>
|
||||
<Select
|
||||
value={refreshInterval}
|
||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
||||
setRefreshInterval(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
||||
<Select
|
||||
value={dataPoints}
|
||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
||||
setDataPoints(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select points" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Refresh interval:
|
||||
</span>
|
||||
<Select
|
||||
value={refreshInterval}
|
||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
||||
setRefreshInterval(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<Card className="p-6 bg-transparent">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -238,11 +198,11 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
<h3 className="text-sm font-medium">Memory Usage</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-bold">
|
||||
{metrics.Memory.percentage}%
|
||||
{metrics?.Memory?.percentage}%
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{metrics.Memory.used} {metrics.Memory.unit} / {metrics.Memory.total}{" "}
|
||||
{metrics.Memory.unit}
|
||||
{metrics?.Memory?.used} {metrics?.Memory?.unit} /{" "}
|
||||
{metrics?.Memory?.total} {metrics?.Memory?.unit}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -252,8 +212,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
<h3 className="text-sm font-medium">Network I/O</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-bold">
|
||||
{metrics.Network.input} {metrics.Network.inputUnit} /{" "}
|
||||
{metrics.Network.output} {metrics.Network.outputUnit}
|
||||
{metrics?.Network?.input} {metrics?.Network?.inputUnit} /{" "}
|
||||
{metrics?.Network?.output} {metrics?.Network?.outputUnit}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -263,8 +223,8 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
<h3 className="text-sm font-medium">Block I/O</h3>
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-bold">
|
||||
{metrics.BlockIO.read} {metrics.BlockIO.readUnit} /{" "}
|
||||
{metrics.BlockIO.write} {metrics.BlockIO.writeUnit}
|
||||
{metrics?.BlockIO?.read} {metrics?.BlockIO?.readUnit} /{" "}
|
||||
{metrics?.BlockIO?.write} {metrics?.BlockIO?.writeUnit}
|
||||
</p>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -281,13 +241,13 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-muted-foreground">Name</h4>
|
||||
<p className="mt-1">{metrics.Name}</p>
|
||||
<p className="mt-1 truncate">{metrics.Name}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
|
||||
<ContainerCPUChart data={historicalData} />
|
||||
<ContainerMemoryChart data={historicalData} />
|
||||
<ContainerBlockChart data={historicalData} />
|
||||
|
||||
@@ -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<SystemMetrics[]>([]);
|
||||
const [metrics, setMetrics] = useState<SystemMetrics>({} as SystemMetrics);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [dataPoints, setDataPoints] =
|
||||
useState<keyof typeof DATA_POINTS_OPTIONS>("50");
|
||||
const [refreshInterval, setRefreshInterval] = useState<string>("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 (
|
||||
<div className="flex h-[400px] w-full items-center justify-center">
|
||||
@@ -165,7 +132,7 @@ export const ShowPaidMonitoring = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (queryError) {
|
||||
return (
|
||||
<div className="flex min-h-[55vh] w-full items-center justify-center p-4">
|
||||
<div className="max-w-xl text-center">
|
||||
@@ -173,7 +140,9 @@ export const ShowPaidMonitoring = ({
|
||||
Error fetching metrics{" "}
|
||||
</p>
|
||||
<p className="whitespace-pre-line text-sm text-destructive">
|
||||
{error}
|
||||
{queryError instanceof Error
|
||||
? queryError.message
|
||||
: "Failed to fetch metrics, Please check your monitoring Instance is Configured correctly."}
|
||||
</p>
|
||||
<p className=" text-sm text-muted-foreground">URL: {BASE_URL}</p>
|
||||
</div>
|
||||
@@ -182,53 +151,58 @@ export const ShowPaidMonitoring = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pt-5 pb-10 w-full px-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="space-y-4 pt-5 pb-10 w-full md:px-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-2">
|
||||
<h2 className="text-2xl font-bold tracking-tight">System Monitoring</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
||||
<Select
|
||||
value={dataPoints}
|
||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
||||
setDataPoints(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select points" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Refresh interval:
|
||||
</span>
|
||||
<Select
|
||||
value={refreshInterval}
|
||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
||||
setRefreshInterval(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">Data points:</span>
|
||||
<Select
|
||||
value={dataPoints}
|
||||
onValueChange={(value: keyof typeof DATA_POINTS_OPTIONS) =>
|
||||
setDataPoints(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select points" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(DATA_POINTS_OPTIONS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Refresh interval:
|
||||
</span>
|
||||
<Select
|
||||
value={refreshInterval}
|
||||
onValueChange={(value: keyof typeof REFRESH_INTERVALS) =>
|
||||
setRefreshInterval(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select interval" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(REFRESH_INTERVALS).map(([value, label]) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 md:grid-cols-4">
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
@@ -291,7 +265,7 @@ export const ShowPaidMonitoring = ({
|
||||
</div>
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-2">
|
||||
<div className="grid gap-4 grid-cols-1 md:grid-cols-1 xl:grid-cols-2">
|
||||
<CPUChart data={historicalData} />
|
||||
<MemoryChart data={historicalData} />
|
||||
<DiskChart data={metrics} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.17.9",
|
||||
"version": "v0.18.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -399,6 +399,8 @@ export async function getServerSideProps(
|
||||
applicationId: params?.applicationId,
|
||||
});
|
||||
|
||||
await helpers.settings.isCloud.prefetch();
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
5
apps/dokploy/public/templates/frappe-hr.svg
Normal file
5
apps/dokploy/public/templates/frappe-hr.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="101" height="101" viewBox="0 0 101 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M72.2667 0.422852H29.4096C13.63 0.422852 0.838135 13.2147 0.838135 28.9943V71.8514C0.838135 87.631 13.63 100.423 29.4096 100.423H72.2667C88.0463 100.423 100.838 87.631 100.838 71.8514V28.9943C100.838 13.2147 88.0463 0.422852 72.2667 0.422852Z" fill="#06B58B"/>
|
||||
<path d="M31.1592 78.9948L26.3379 73.7091C33.0879 67.602 41.7664 64.209 50.8021 64.209C59.8378 64.209 68.5522 67.5662 75.2665 73.7091L70.4449 78.9948C65.0164 74.0662 58.0521 71.3518 50.8021 71.3518C43.5521 71.3518 36.5523 74.0662 31.1237 78.9948H31.1592Z" fill="white"/>
|
||||
<path d="M54.1236 21.8516H33.1948V28.9944H54.1236C58.0521 28.9944 61.2664 32.2087 61.2664 36.1373V42.7801C61.2664 46.7087 58.0521 49.9229 54.1236 49.9229H47.4805C43.552 49.9229 40.3377 46.7087 40.3377 42.7801V38.28H33.1948V42.7801C33.1948 50.6729 39.5877 57.0658 47.4805 57.0658H54.1236C62.0164 57.0658 68.4093 50.6729 68.4093 42.7801V36.1373C68.4093 28.2444 62.0164 21.8516 54.1236 21.8516Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -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;
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
@@ -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",
|
||||
|
||||
354
apps/dokploy/templates/frappe-hr/docker-compose.yml
Normal file
354
apps/dokploy/templates/frappe-hr/docker-compose.yml
Normal file
@@ -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:
|
||||
39
apps/dokploy/templates/frappe-hr/index.ts
Normal file
39
apps/dokploy/templates/frappe-hr/index.ts
Normal file
@@ -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 };
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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),
|
||||
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user