Compare commits
271 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2644b638d1 | ||
|
|
8785282133 | ||
|
|
35c084af1d | ||
|
|
b63488baba | ||
|
|
7e5f21b28e | ||
|
|
8d41bafb93 | ||
|
|
6f5049efd5 | ||
|
|
6dd6b636e5 | ||
|
|
8488d530f3 | ||
|
|
6c61d5cdf5 | ||
|
|
8036455c2d | ||
|
|
bf44eeab3d | ||
|
|
0cd185696d | ||
|
|
339697437a | ||
|
|
e0b15fe971 | ||
|
|
afc5ea43da | ||
|
|
bb337e819e | ||
|
|
160dd10f77 | ||
|
|
49b096fef6 | ||
|
|
4828b840cb | ||
|
|
67af70448b | ||
|
|
946acf5245 | ||
|
|
390b3a835a | ||
|
|
2842bf9a91 | ||
|
|
8a0e10f6f4 | ||
|
|
67efa82b91 | ||
|
|
546d6b87ea | ||
|
|
d012d19253 | ||
|
|
e925ed9ea4 | ||
|
|
0c05809d7d | ||
|
|
29f1631950 | ||
|
|
9c0c58035a | ||
|
|
f4262569dd | ||
|
|
99cf6eae49 | ||
|
|
0488546706 | ||
|
|
dc32cd71e5 | ||
|
|
efdfd5d13c | ||
|
|
d31cab76f0 | ||
|
|
00ed202127 | ||
|
|
aaa4ca297d | ||
|
|
629871e683 | ||
|
|
a237c651c3 | ||
|
|
4f9b0d9d59 | ||
|
|
b701a0b504 | ||
|
|
39036202bb | ||
|
|
4225ad83e7 | ||
|
|
38c4e0ede1 | ||
|
|
25a64c703f | ||
|
|
ab5871add7 | ||
|
|
2b0e009f6a | ||
|
|
9b6ea99eea | ||
|
|
c4cf545d85 | ||
|
|
1edd717432 | ||
|
|
5b88af6158 | ||
|
|
e995d894d8 | ||
|
|
5f56512e56 | ||
|
|
24e4930fc1 | ||
|
|
541728805f | ||
|
|
9e4bac1386 | ||
|
|
ed8d32d050 | ||
|
|
7fb66bc58b | ||
|
|
58c06fba86 | ||
|
|
3cf27a068a | ||
|
|
7cfbea3f60 | ||
|
|
7907e33431 | ||
|
|
89f3078ce5 | ||
|
|
f3ce69b656 | ||
|
|
43555cdabe | ||
|
|
651bf3a303 | ||
|
|
4cde1a8a7d | ||
|
|
405efcac0b | ||
|
|
8397de0dca | ||
|
|
06b58e6495 | ||
|
|
24db4006cf | ||
|
|
84009c5e9b | ||
|
|
e32afde973 | ||
|
|
fecffac573 | ||
|
|
b4448e013c | ||
|
|
c3f06a6272 | ||
|
|
2be724f780 | ||
|
|
bf78326c96 | ||
|
|
4ca8722c6e | ||
|
|
e56e1eb687 | ||
|
|
5a5c302bdc | ||
|
|
997dc85985 | ||
|
|
09ef851372 | ||
|
|
7c4987d84d | ||
|
|
5cebf5540a | ||
|
|
3df2f8e58c | ||
|
|
a642d36a23 | ||
|
|
72bceec62d | ||
|
|
3d32314e80 | ||
|
|
7259830ac1 | ||
|
|
daa87c0dc7 | ||
|
|
a2ee55e0e9 | ||
|
|
de6aeac243 | ||
|
|
f640b4a87f | ||
|
|
3747db08d4 | ||
|
|
ab4677ac0e | ||
|
|
172d55311e | ||
|
|
3ce25e2ac8 | ||
|
|
388ded9aa5 | ||
|
|
767d3e1944 | ||
|
|
ec1d6c7430 | ||
|
|
8abeae5e63 | ||
|
|
cc90d9ec9b | ||
|
|
6b5de00fb0 | ||
|
|
5ed96fb0ce | ||
|
|
a73af1d578 | ||
|
|
5867a27901 | ||
|
|
8f7bffc349 | ||
|
|
21ee22d4f5 | ||
|
|
fb72132a4b | ||
|
|
ca904c15d9 | ||
|
|
682863f83e | ||
|
|
acd722678e | ||
|
|
3750977f41 | ||
|
|
9b401059b0 | ||
|
|
6a3ef5c860 | ||
|
|
a36518a8f0 | ||
|
|
a5eb4b0a72 | ||
|
|
b5c0876dd4 | ||
|
|
9745d12ac8 | ||
|
|
5c72e5a452 | ||
|
|
600f4b2106 | ||
|
|
c12d37fe0a | ||
|
|
bba8d00ba2 | ||
|
|
78665ffbfa | ||
|
|
d41c8c70c3 | ||
|
|
f13e5d449c | ||
|
|
d256998677 | ||
|
|
4aaf04ce74 | ||
|
|
ecfca9419a | ||
|
|
dfd6764320 | ||
|
|
73bf5274f5 | ||
|
|
fc38a42587 | ||
|
|
9b255964fe | ||
|
|
29f55ca1a0 | ||
|
|
6a5fb8faff | ||
|
|
5c225c8d42 | ||
|
|
c1c5fc978b | ||
|
|
ffd19f591d | ||
|
|
81a41a7f31 | ||
|
|
1c9b704ecc | ||
|
|
edf1fdedf0 | ||
|
|
8484649071 | ||
|
|
1e68248611 | ||
|
|
b3e35c5838 | ||
|
|
ddd4ba8135 | ||
|
|
539544d0de | ||
|
|
123b5d098b | ||
|
|
796a9ca11f | ||
|
|
2872ef3ccb | ||
|
|
06a772e344 | ||
|
|
e99666f4c0 | ||
|
|
bd243d79e2 | ||
|
|
18b4b23f79 | ||
|
|
071a9d5104 | ||
|
|
61ebd1b16e | ||
|
|
9836c988a0 | ||
|
|
03d7738032 | ||
|
|
98aa474975 | ||
|
|
7bd6b66551 | ||
|
|
727e50648e | ||
|
|
0b2b20caeb | ||
|
|
6cc64b4454 | ||
|
|
349bc89851 | ||
|
|
7046d05f63 | ||
|
|
cef21ac8b5 | ||
|
|
2ae7e562bb | ||
|
|
e4b998c608 | ||
|
|
9b7aacc934 | ||
|
|
7027f39c48 | ||
|
|
9f6f872536 | ||
|
|
cb03b153ac | ||
|
|
e5d7a0cb10 | ||
|
|
bf65bc9462 | ||
|
|
b48b9765cd | ||
|
|
7cce02f74d | ||
|
|
3dc3672406 | ||
|
|
bbfe095045 | ||
|
|
65c1001751 | ||
|
|
5212bde021 | ||
|
|
9059f42b03 | ||
|
|
3b9f5d6f5c | ||
|
|
0aff344bc0 | ||
|
|
4715f34e15 | ||
|
|
59386ed4b7 | ||
|
|
21dee4abac | ||
|
|
e378d89477 | ||
|
|
b04c1206e4 | ||
|
|
639bc0e8db | ||
|
|
9a850d388d | ||
|
|
6c5c374139 | ||
|
|
0b05f8b83c | ||
|
|
63d5b775e6 | ||
|
|
cb16de63df | ||
|
|
31a4a0814e | ||
|
|
14302ed240 | ||
|
|
31c55f772d | ||
|
|
f0f34df13c | ||
|
|
1a877340d3 | ||
|
|
f7e43fa1c1 | ||
|
|
906906102b | ||
|
|
245a5175a8 | ||
|
|
f427014f52 | ||
|
|
0465a71d86 | ||
|
|
3de8a18ef9 | ||
|
|
e317d0c808 | ||
|
|
ff482ffe28 | ||
|
|
82588f3e16 | ||
|
|
069f1a7b7a | ||
|
|
807137d3b1 | ||
|
|
c03c154fc4 | ||
|
|
698ff9e918 | ||
|
|
8bf6a22db8 | ||
|
|
497d45129c | ||
|
|
0b22b694e6 | ||
|
|
ee5516bb91 | ||
|
|
e90b98e629 | ||
|
|
ff382d2029 | ||
|
|
4a37f85a51 | ||
|
|
6bdc833413 | ||
|
|
17a64a9402 | ||
|
|
a22b0797b1 | ||
|
|
f3b351245a | ||
|
|
0cb74c5fde | ||
|
|
9a828d4966 | ||
|
|
4845c1ad5d | ||
|
|
6159786dfe | ||
|
|
b473062f40 | ||
|
|
6c08f33ebb | ||
|
|
5b0bf99cbf | ||
|
|
8e227a3286 | ||
|
|
54f855e738 | ||
|
|
79f39db502 | ||
|
|
a46e7759b2 | ||
|
|
869e58739f | ||
|
|
f001a50278 | ||
|
|
4c3bc8efdc | ||
|
|
a591e02ffa | ||
|
|
abe787593c | ||
|
|
9b312cd9d7 | ||
|
|
c84d39a20f | ||
|
|
847d6ecab1 | ||
|
|
8f83ecb9ef | ||
|
|
2f9448dde9 | ||
|
|
e1ec0aee69 | ||
|
|
7f378b12ae | ||
|
|
fac984d299 | ||
|
|
4f3eb7b362 | ||
|
|
d8d0b60cb3 | ||
|
|
19295ba746 | ||
|
|
0d3c978aad | ||
|
|
d2c8632c4f | ||
|
|
033bf66405 | ||
|
|
c549ea17d8 | ||
|
|
c412dabc54 | ||
|
|
0bd0da2ee4 | ||
|
|
bf58ae0f0f | ||
|
|
a8d714c20d | ||
|
|
86f1bf31b8 | ||
|
|
95f75fdccb | ||
|
|
5afe1645a0 | ||
|
|
cf06162be7 | ||
|
|
ea5349c844 | ||
|
|
6007427a6c | ||
|
|
0a889c5db1 | ||
|
|
3d60236b36 | ||
|
|
1a34ba175e | ||
|
|
bd0bbdea26 |
@@ -11,6 +11,7 @@ jobs:
|
||||
command: |
|
||||
cp apps/dokploy/.env.production.example .env.production
|
||||
cp apps/dokploy/.env.production.example apps/dokploy/.env.production
|
||||
|
||||
- run:
|
||||
name: Build and push AMD64 image
|
||||
command: |
|
||||
@@ -61,7 +62,7 @@ jobs:
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
echo $VERSION
|
||||
TAG="latest"
|
||||
|
||||
|
||||
docker manifest create dokploy/dokploy:${TAG} \
|
||||
dokploy/dokploy:${TAG}-amd64 \
|
||||
dokploy/dokploy:${TAG}-arm64
|
||||
|
||||
BIN
.github/sponsors/lxaer.png
vendored
|
Before Width: | Height: | Size: 267 KiB After Width: | Height: | Size: 248 KiB |
73
.github/workflows/deploy.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build Docs & Website Docker images
|
||||
name: Build Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -48,3 +48,74 @@ jobs:
|
||||
push: true
|
||||
tags: dokploy/website:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
build-and-push-cloud-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.cloud
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
|
||||
build-and-push-schedule-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.schedule
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
build-and-push-server-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.server
|
||||
push: true
|
||||
tags: |
|
||||
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
||||
platforms: linux/amd64
|
||||
3
.github/workflows/pull-request.yml
vendored
@@ -18,6 +18,7 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm biome ci
|
||||
- run: pnpm typecheck
|
||||
|
||||
@@ -32,6 +33,7 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm build
|
||||
|
||||
parallel-tests:
|
||||
@@ -44,4 +46,5 @@ jobs:
|
||||
node-version: 18.18.0
|
||||
cache: "pnpm"
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm run server:build
|
||||
- run: pnpm test
|
||||
|
||||
@@ -15,7 +15,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||
|
||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||
@@ -27,7 +29,7 @@ WORKDIR /app
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
@@ -42,7 +44,7 @@ COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||
|
||||
|
||||
# Install docker
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh
|
||||
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh && curl https://rclone.org/install.sh | bash
|
||||
|
||||
# Install Nixpacks and tsx
|
||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||
@@ -55,4 +57,4 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
CMD [ "pnpm", "start" ]
|
||||
52
Dockerfile.cloud
Normal file
@@ -0,0 +1,52 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/dokploy run build
|
||||
|
||||
RUN pnpm --filter=./apps/dokploy --prod deploy /prod/dokploy
|
||||
|
||||
RUN cp -R /usr/src/app/apps/dokploy/.next /prod/dokploy/.next
|
||||
RUN cp -R /usr/src/app/apps/dokploy/dist /prod/dokploy/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/dokploy/.next ./.next
|
||||
COPY --from=build /prod/dokploy/dist ./dist
|
||||
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
|
||||
COPY --from=build /prod/dokploy/public ./public
|
||||
COPY --from=build /prod/dokploy/package.json ./package.json
|
||||
COPY --from=build /prod/dokploy/drizzle ./drizzle
|
||||
COPY --from=build /prod/dokploy/components.json ./components.json
|
||||
COPY --from=build /prod/dokploy/node_modules ./node_modules
|
||||
|
||||
|
||||
# Install RCLONE
|
||||
RUN curl https://rclone.org/install.sh | bash
|
||||
|
||||
# tsx
|
||||
RUN pnpm install -g tsx
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "pnpm", "start" ]
|
||||
36
Dockerfile.schedule
Normal file
@@ -0,0 +1,36 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/schedules install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/schedules run build
|
||||
|
||||
RUN pnpm --filter=./apps/schedules --prod deploy /prod/schedules
|
||||
|
||||
RUN cp -R /usr/src/app/apps/schedules/dist /prod/schedules/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/schedules/dist ./dist
|
||||
COPY --from=build /prod/schedules/package.json ./package.json
|
||||
COPY --from=build /prod/schedules/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
36
Dockerfile.server
Normal file
@@ -0,0 +1,36 @@
|
||||
FROM node:18-slim AS base
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
FROM base AS build
|
||||
COPY . /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/api install --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=@dokploy/server build
|
||||
RUN pnpm --filter=./apps/api run build
|
||||
|
||||
RUN pnpm --filter=./apps/api --prod deploy /prod/api
|
||||
|
||||
RUN cp -R /usr/src/app/apps/api/dist /prod/api/dist
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/api/dist ./dist
|
||||
COPY --from=build /prod/api/package.json ./package.json
|
||||
COPY --from=build /prod/api/node_modules ./node_modules
|
||||
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
||||
11
README.md
@@ -32,6 +32,7 @@ Dokploy include multiples features to make your life easier.
|
||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||
|
||||
## 🚀 Getting Started
|
||||
@@ -58,7 +59,14 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
### Hero Sponsors 🎖
|
||||
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" ><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="200"/></a>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<img src=".github/sponsors/hostinger.jpg" alt="Hostinger" height="50"/>
|
||||
</a>
|
||||
<a href="https://www.lxaer.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||
<img src=".github/sponsors/lxaer.png" alt="LX Aer" height="50"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### Premium Supporters 🥇
|
||||
|
||||
@@ -81,6 +89,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
||||
@@ -1,15 +1,33 @@
|
||||
{
|
||||
"name": "my-app",
|
||||
"name": "@dokploy/api",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts"
|
||||
"dev": "PORT=4000 tsx watch src/index.ts",
|
||||
"build": "tsc --project tsconfig.json",
|
||||
"start": "node --experimental-specifier-resolution=node dist/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"pino": "9.4.0",
|
||||
"pino-pretty": "11.2.2",
|
||||
"@hono/zod-validator": "0.3.0",
|
||||
"zod": "^3.23.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@hono/node-server": "^1.12.1",
|
||||
"hono": "^4.5.8",
|
||||
"dotenv": "^16.3.1"
|
||||
"dotenv": "^16.3.1",
|
||||
"redis": "4.7.0",
|
||||
"@nerimity/mimiqueue": "1.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.2",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.2.15",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@9.5.0"
|
||||
}
|
||||
|
||||
@@ -1,66 +1,61 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { config } from "dotenv";
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { validateLemonSqueezyLicense } from "./utils";
|
||||
|
||||
config();
|
||||
import "dotenv/config";
|
||||
import { zValidator } from "@hono/zod-validator";
|
||||
import { Queue } from "@nerimity/mimiqueue";
|
||||
import { createClient } from "redis";
|
||||
import { logger } from "./logger";
|
||||
import { type DeployJob, deployJobSchema } from "./schema";
|
||||
import { deploy } from "./utils";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.use(
|
||||
"/*",
|
||||
cors({
|
||||
origin: ["http://localhost:3000", "http://localhost:3001"], // Ajusta esto a los orígenes de tu aplicación Next.js
|
||||
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allowHeaders: ["Content-Type", "Authorization"],
|
||||
exposeHeaders: ["Content-Length", "X-Kuma-Revision"],
|
||||
maxAge: 600,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
export const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||
export const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||
|
||||
app.get("/v1/health", (c) => {
|
||||
return c.text("Hello Hono!");
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
|
||||
app.post("/v1/validate-license", async (c) => {
|
||||
const { licenseKey } = await c.req.json();
|
||||
app.use(async (c, next) => {
|
||||
if (c.req.path === "/health") {
|
||||
return next();
|
||||
}
|
||||
const authHeader = c.req.header("X-API-Key");
|
||||
|
||||
if (!licenseKey) {
|
||||
return c.json({ error: "License key is required" }, 400);
|
||||
if (process.env.API_KEY !== authHeader) {
|
||||
return c.json({ message: "Invalid API Key" }, 403);
|
||||
}
|
||||
|
||||
try {
|
||||
const licenseValidation = await validateLemonSqueezyLicense(licenseKey);
|
||||
|
||||
if (licenseValidation.valid) {
|
||||
return c.json({
|
||||
valid: true,
|
||||
message: "License is valid",
|
||||
metadata: licenseValidation.meta,
|
||||
});
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
valid: false,
|
||||
message: licenseValidation.error || "Invalid license",
|
||||
},
|
||||
400,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error during license validation:", error);
|
||||
return c.json({ error: "Internal server error" }, 500);
|
||||
}
|
||||
return next();
|
||||
});
|
||||
|
||||
const port = 4000;
|
||||
console.log(`Server is running on port ${port}`);
|
||||
|
||||
serve({
|
||||
fetch: app.fetch,
|
||||
port,
|
||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||
const data = c.req.valid("json");
|
||||
const res = queue.add(data, { groupName: data.serverId });
|
||||
return c.json(
|
||||
{
|
||||
message: "Deployment Added",
|
||||
},
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
app.get("/health", async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
});
|
||||
|
||||
const queue = new Queue({
|
||||
name: "deployments",
|
||||
process: async (job: DeployJob) => {
|
||||
logger.info("Deploying job", job);
|
||||
return await deploy(job);
|
||||
},
|
||||
redisClient,
|
||||
});
|
||||
|
||||
(async () => {
|
||||
await redisClient.connect();
|
||||
await redisClient.flushAll();
|
||||
logger.info("Redis Cleaned");
|
||||
})();
|
||||
|
||||
const port = Number.parseInt(process.env.PORT || "3000");
|
||||
logger.info("Starting Deployments Server ✅", port);
|
||||
serve({ fetch: app.fetch, port });
|
||||
|
||||
10
apps/api/src/logger.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import pino from "pino";
|
||||
|
||||
export const logger = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
colorize: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
24
apps/api/src/schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
||||
z.object({
|
||||
applicationId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("application"),
|
||||
serverId: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
composeId: z.string(),
|
||||
titleLog: z.string(),
|
||||
descriptionLog: z.string(),
|
||||
server: z.boolean().optional(),
|
||||
type: z.enum(["deploy", "redeploy"]),
|
||||
applicationType: z.literal("compose"),
|
||||
serverId: z.string(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type DeployJob = z.infer<typeof deployJobSchema>;
|
||||
@@ -1,28 +1,96 @@
|
||||
import { LEMON_SQUEEZY_API_KEY, LEMON_SQUEEZY_STORE_ID } from ".";
|
||||
import {
|
||||
deployApplication,
|
||||
deployCompose,
|
||||
deployRemoteApplication,
|
||||
deployRemoteCompose,
|
||||
rebuildApplication,
|
||||
rebuildCompose,
|
||||
rebuildRemoteApplication,
|
||||
rebuildRemoteCompose,
|
||||
updateApplicationStatus,
|
||||
updateCompose,
|
||||
} from "@dokploy/server";
|
||||
import type { DeployJob } from "./schema";
|
||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||
|
||||
export const validateLemonSqueezyLicense = async (
|
||||
licenseKey: string,
|
||||
): Promise<LemonSqueezyLicenseResponse> => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
license_key: licenseKey,
|
||||
store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||
}),
|
||||
},
|
||||
);
|
||||
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
|
||||
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
|
||||
// export const validateLemonSqueezyLicense = async (
|
||||
// licenseKey: string,
|
||||
// ): Promise<LemonSqueezyLicenseResponse> => {
|
||||
// try {
|
||||
// const response = await fetch(
|
||||
// "https://api.lemonsqueezy.com/v1/licenses/validate",
|
||||
// {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// "x-api-key": LEMON_SQUEEZY_API_KEY as string,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// license_key: licenseKey,
|
||||
// store_id: LEMON_SQUEEZY_STORE_ID as string,
|
||||
// }),
|
||||
// },
|
||||
// );
|
||||
|
||||
return response.json();
|
||||
// return response.json();
|
||||
// } catch (error) {
|
||||
// console.error("Error validating license:", error);
|
||||
// return { valid: false, error: "Error validating license" };
|
||||
// }
|
||||
// };
|
||||
|
||||
export const deploy = async (job: DeployJob) => {
|
||||
try {
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "running");
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteApplication({
|
||||
applicationId: job.applicationId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "running",
|
||||
});
|
||||
|
||||
if (job.server) {
|
||||
if (job.type === "redeploy") {
|
||||
await rebuildRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
} else if (job.type === "deploy") {
|
||||
await deployRemoteCompose({
|
||||
composeId: job.composeId,
|
||||
titleLog: job.titleLog,
|
||||
descriptionLog: job.descriptionLog,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error validating license:", error);
|
||||
return { valid: false, error: "Error validating license" };
|
||||
console.log(error);
|
||||
if (job.applicationType === "application") {
|
||||
await updateApplicationStatus(job.applicationId, "error");
|
||||
} else if (job.applicationType === "compose") {
|
||||
await updateCompose(job.composeId, {
|
||||
composeStatus: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"],
|
||||
"outDir": "dist",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function generateMetadata({
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
creator: "@siumauricio",
|
||||
creator: "@getdokploy",
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
images: [
|
||||
|
||||
@@ -15,6 +15,8 @@ Configure the source of your code, the way your application is built, and also m
|
||||
|
||||
If you need to assign environment variables to your application, you can do so here.
|
||||
|
||||
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||
|
||||
## Monitoring
|
||||
|
||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||
|
||||
@@ -26,6 +26,8 @@ Actions like deploying, updating, and deleting your database, and stopping it.
|
||||
|
||||
If you need to assign environment variables to your application, you can do so here.
|
||||
|
||||
In case you need to use a multiline variable, you can wrap it in double quotes just like this `'"here_is_my_private_key"'`.
|
||||
|
||||
## Monitoring
|
||||
|
||||
Four graphs will be displayed for the use of memory, CPU, disk, and network. Note that the information is only updated if you are viewing the current page, otherwise it will not be updated.
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
---
|
||||
title: 'Comparison'
|
||||
description: 'A comparison of Dokploy, CapRover, Dokku, and Coolify'
|
||||
title: "Comparison"
|
||||
description: "A comparison of Dokploy, CapRover, Dokku, and Coolify"
|
||||
---
|
||||
|
||||
Comparison of the following deployment tools:
|
||||
|
||||
| Feature | Dokploy | CapRover | Dokku | Coolify |
|
||||
|-----------------------------------|---------------------------------------|--------------------------------------|--------------------------------------|--------------------------------------|
|
||||
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Docker compose support** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
|
||||
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
|
||||
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
|
||||
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Cloud/Paid Version** | ❌ | ✅ | ❌ | ✅ |
|
||||
| Feature | Dokploy | CapRover | Dokku | Coolify |
|
||||
| --------------------------------------- | ------- | --------------------- | --------------------- | ------- |
|
||||
| **User Interface** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Docker compose support** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **API/CLI** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Multi node support** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Traefik Integration** | ✅ | ✅ | Available via Plugins | ✅ |
|
||||
| **User Permission Management** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Advanced User Permission Management** | ✅ | ❌ | ❌ | ❌ |
|
||||
| **Terminal Access Built In** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Database Support** | ✅ | ✅ | ❌ | ✅ |
|
||||
| **Monitoring** | ✅ | ✅ | ❌ | ❌ |
|
||||
| **Backups** | ✅ | Available via Plugins | Available via Plugins | ✅ |
|
||||
| **Open Source** | ✅ | ✅ | ✅ | ✅ |
|
||||
| **Multi Server Support** | ✅ | ❌ | ❌ | ✅ |
|
||||
| **Cloud/Paid Version** | ❌ | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -29,7 +29,7 @@ We have tested on the following Linux Distros:
|
||||
|
||||
### Providers
|
||||
|
||||
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://hostinger.com?REFERRALCODE=1SIUMAURICI97)
|
||||
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% Discount using this referral link: [Referral Link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97)
|
||||
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get 200$ credits for free with this referral link: [Referral Link](https://m.do.co/c/db24efd43f35)
|
||||
- [Hetzner](https://www.hetzner.com/cloud/) Get 20€ credits for free with this referral link: [Referral Link](https://hetzner.cloud/?ref=vou4fhxJ1W2D)
|
||||
- [Linode](https://www.linode.com/es/pricing/#compute-shared)
|
||||
|
||||
@@ -64,6 +64,9 @@
|
||||
"docker/overview",
|
||||
"---Monitoring---",
|
||||
"monitoring/overview",
|
||||
"---Multi Server---",
|
||||
"multi-server/overview",
|
||||
"multi-server/example",
|
||||
"---Cluster---",
|
||||
"cluster/overview",
|
||||
"---Deployments---",
|
||||
|
||||
117
apps/docs/content/docs/core/multi-server/example.mdx
Normal file
@@ -0,0 +1,117 @@
|
||||
---
|
||||
title: Example
|
||||
description: "Example to setup a remote server and deploy application in a VPS."
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
|
||||
|
||||
## Requirements
|
||||
|
||||
1. To install Dokploy UI, follow the [installation guide](en/docs/core/get-started/installation).
|
||||
|
||||
2. Create an SSH key by going to `/dashboard/settings/ssh-keys` and add a new key. Be sure to copy the public key.
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/ssh-keys.png"
|
||||
alt="Architecture Diagram"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
3. Decide which remote server to deploy your apps on. We recommend these reliable providers:
|
||||
|
||||
- [Hostinger](https://www.hostinger.com/vps-hosting?ref=dokploy) Get 20% off with this [referral link](https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97).
|
||||
- [DigitalOcean](https://www.digitalocean.com/pricing/droplets#basic-droplets) Get $200 credits for free with this [referral link](https://m.do.co/c/db24efd43f35).
|
||||
- [Hetzner](https://www.hetzner.com/cloud/) Get €20 credits with this [referral link](https://hetzner.cloud/?ref=vou4fhxJ1W2D).
|
||||
- [Linode](https://www.linode.com/es/pricing/#compute-shared).
|
||||
- [Vultr](https://www.vultr.com/pricing/#cloud-compute).
|
||||
- [Scaleway](https://www.scaleway.com/en/pricing/?tags=baremetal,available).
|
||||
- [Google Cloud](https://cloud.google.com/).
|
||||
- [AWS](https://aws.amazon.com/ec2/pricing/).
|
||||
|
||||
4. When creating the server, it should ask for SSH keys. Ideally, use your computer's public key and the key you generated in the previous step. Here's how to add the public key in Hostinger:
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/hostinger-add-sshkey.png"
|
||||
alt="Adding SSH key"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
<Callout>The steps are similar across other providers.</Callout>
|
||||
|
||||
5. Copy the server’s IP address and ensure you know the username (often `root`). Fill in all fields and click `Create`.
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-add-server.png"
|
||||
alt="Add server"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
6. To test connectivity, open the server dropdown and click `Enter Terminal`. If everything is correct, you should be able to interact with the remote server.
|
||||
|
||||
7. Click `Setup Server` to proceed. There are two tabs: SSH Keys and Deployments. This guide explains the easy way, but you can follow the manual process via the Dokploy UI if you prefer.
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-setup-2.png"
|
||||
alt="Setup process"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
8. Click `Deployments`, then `Setup Server`. If everything is correct, you should see output similar to this:
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-setup-3.png"
|
||||
alt="Server setup output"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
<Callout>
|
||||
You only need to run this setup once. If Dokploy updates later, check the
|
||||
release notes to see if rerunning this command is required.
|
||||
</Callout>
|
||||
|
||||
9. You're ready to deploy your apps! Let's test it out:
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-add-app.png"
|
||||
alt="Add app"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
10. To check which server an app belongs to, you’ll see the server name at the top. If no server is selected, it defaults to `Dokploy Server`. Click `Deploy` to start building your app on the remote server. You can check the `Logs` tab to see the build process. For this example, we’ll use a test repo:
|
||||
Repo: `https://github.com/Dokploy/examples.git`
|
||||
Branch: `main`
|
||||
Build Path: `/astro`
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-setup-app.png"
|
||||
alt="App setup"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
11. Once the build is done, go to `Domains` and create a free domain. Just click `Create` and you’re good to go! 🎊
|
||||
|
||||
{" "}
|
||||
|
||||
<ImageZoom
|
||||
src="/assets/multi-server-finish.png"
|
||||
alt="Finished setup"
|
||||
width={1000}
|
||||
height={600}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
29
apps/docs/content/docs/core/multi-server/overview.mdx
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
title: Overview
|
||||
description: "Deploy your apps to multiple servers remotely."
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Multi server allows you to deploy your apps remotely to different servers without needing to build and run them where the Dokploy UI is installed.
|
||||
|
||||
To use the multi-server feature, you need to have Dokploy UI installed either locally or on a remote server. We recommend using a remote server for better connectivity, security, and isolation, for remote instances we install only a traefik instance.
|
||||
|
||||
If you plan to only deploy apps to remote servers and use Dokploy UI for managing deployments, Dokploy will use around 250 MB of RAM and minimal CPU, so a low-resource server should be sufficient.
|
||||
|
||||
All the features we have documented previously are supported by Dokploy Multi Server. The only feature not supported is remote server monitoring, due to performance reasons. However, all functionalities should work the same as when deploying on the same server where Dokploy UI is installed.
|
||||
|
||||
## Features
|
||||
|
||||
1. **Enter the terminal**: Allows you to access the terminal of the remote server.
|
||||
2. **Setup Server**: Allows you to configure the remote server.
|
||||
- **SSH Keys**: Steps to add SSH keys to the remote server.
|
||||
- **Deployments**: Steps to configure the remote server for deploying applications.
|
||||
3. **Edit Server**: Allows you to modify the remote server's details, such as SSH key, name, description, IP, etc.
|
||||
4. **View Actions**: Lets you perform actions like managing the Traefik instance, storage, and activating Docker cleanup.
|
||||
5. **Show Traefik File System**: Displays the contents of the remote server's directory.
|
||||
6. **Show Docker Containers**: Shows the Docker containers running on the remote server.
|
||||
|
||||
<Callout>
|
||||
Remote server monitoring is not supported due to performance reasons.
|
||||
</Callout>
|
||||
@@ -31,8 +31,7 @@ The following templates are available:
|
||||
- **Wordpress**: Open Source Content Management System
|
||||
- **Open WebUI**: Free and Open Source ChatGPT Alternative
|
||||
- **Teable**: Open Source Airtable Alternative, Developer Friendly, No-code Database Built on Postgres
|
||||
|
||||
|
||||
- **Roundcube**: Free and open source webmail software for the masses, written in PHP, uses SMTP[^1].
|
||||
|
||||
## Create your own template
|
||||
|
||||
@@ -41,3 +40,5 @@ We accept contributions to upload new templates to the dokploy repository.
|
||||
Make sure to follow the guidelines for creating a template:
|
||||
|
||||
[Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)
|
||||
|
||||
[^1]: Please note that if you're self-hosting a mail server you need port 25 to be open for SMTP (Mail Transmission Protocol that allows you to send and receive) to work properly. Some VPS providers like [Hetzner](https://docs.hetzner.com/cloud/servers/faq/#why-can-i-not-send-any-mails-from-my-server) block this port by default for new clients.
|
||||
|
||||
@@ -3,4 +3,90 @@ title: Overview
|
||||
description: Solve the most common problems that occur when using Dokploy.
|
||||
---
|
||||
|
||||
WIP
|
||||
## Applications Domain Not Working?
|
||||
|
||||
You see the deployment succeeded, and logs are running, but the domain isn't working? Here's what to check:
|
||||
|
||||
1. **Correct Port Mapping**: Ensure the domain is using the correct port for your application. For example, if you're using Next.js, the port should be `3000`, or for Laravel, it should be `8000`. If you change the app port, update the domain to reflect that.
|
||||
2. **Avoid Using `Ports` in Advanced Settings**: Generally, there's no need to use the `Ports` feature unless you want to access your app via `IP:port`. Leaving this feature enabled may interfere with your domain.
|
||||
|
||||
3. **Let's Encrypt Certificates**: It's crucial to point the domain to your server’s IP **before** adding it in Dokploy. If the domain is added first, the certificate won’t be generated, and you may need to recreate the domain or restart Traefik.
|
||||
|
||||
4. **Listen on 0.0.0.0, Not 127.0.0.1**: If your app is bound to `127.0.0.1` (which is common in Vite apps), switch it to `0.0.0.0` to allow external access.
|
||||
|
||||
## Logs and Monitoring Not Working After Changing Application Placement?
|
||||
|
||||
This is expected behavior. If the application is running on a different node (worker), the UI won’t have access to logs or monitoring, as they're not on the same node.
|
||||
|
||||
## Mounts Are Causing My Application Not to Run?
|
||||
|
||||
Docker Swarm won't run your application if there are invalid mounts, even if the deployment shows as successful. Double-check your mounts to ensure they are valid.
|
||||
|
||||
## Volumes in Docker Compose Not Working?
|
||||
|
||||
For Docker Compose, all file mounts defined in the `volumes` section will be stored in the `files` folder. This is the default directory structure:
|
||||
|
||||
## I added a volume to my docker compose, but is not finding the volume?
|
||||
|
||||
For docker compose all the file mounts you've created in the volumes section will be stored to files folder, this is the default structure of the docker compose.
|
||||
|
||||
```
|
||||
/application-name
|
||||
/code
|
||||
/files
|
||||
```
|
||||
|
||||
So instead of using this invalid way to mount a volume:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "/folder:/path/in/container" ❌
|
||||
```
|
||||
|
||||
You should use this format:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "../files/my-database:/var/lib/mysql" ✅
|
||||
- "../files/my-configs:/etc/my-app/config" ✅
|
||||
```
|
||||
|
||||
## Logs Not Loading When Deploying to a Remote Server?
|
||||
|
||||
There are a few potential reasons for this:
|
||||
|
||||
1. **Slow Server:**: If the server is too slow, it may struggle to handle concurrent requests, leading to SSL handshake errors.
|
||||
2. **Insufficient Disk Space:** If the server doesn't have enough disk space, the logs may not load.
|
||||
|
||||
## Docker Compose Domain Not Working?
|
||||
|
||||
When adding a domain in your Docker Compose file, it’s not necessary to expose the ports directly. Simply specify the port where your app is running. Exposing the ports can lead to conflicts with other applications or ports.
|
||||
|
||||
Example of what not to do:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: dokploy/dokploy:latest
|
||||
ports:
|
||||
- 3000:3000
|
||||
```
|
||||
|
||||
Recommended approach:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: dokploy/dokploy:latest
|
||||
ports:
|
||||
- 3000
|
||||
- 80
|
||||
```
|
||||
|
||||
Then, when creating the domain in Dokploy, specify the service name and port, like this:
|
||||
|
||||
```yaml
|
||||
domain: my-app.com
|
||||
serviceName: app
|
||||
port: 3000
|
||||
```
|
||||
|
||||
@@ -10,8 +10,10 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
|
||||
p: ({ children }) => (
|
||||
<p className="text-[#3E4342] dark:text-muted-foreground">{children}</p>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-[#3E4342] dark:text-muted-foreground">{children}</li>
|
||||
li: ({ children, id }) => (
|
||||
<li {...{ id }} className="text-[#3E4342] dark:text-muted-foreground">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,15 +21,11 @@
|
||||
"react-ga4": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "4.15.7",
|
||||
"@biomejs/biome": "1.8.1",
|
||||
"autoprefixer": "10.4.12",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/docs/public/assets/hostinger-add-sshkey.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
apps/docs/public/assets/multi-server-add-app.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
apps/docs/public/assets/multi-server-add-server.png
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
apps/docs/public/assets/multi-server-finish.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
apps/docs/public/assets/multi-server-overview.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
apps/docs/public/assets/multi-server-setup-2.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
apps/docs/public/assets/multi-server-setup-3.png
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
apps/docs/public/assets/multi-server-setup-app.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
apps/docs/public/assets/multi-server-setup.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
apps/docs/public/assets/ssh-keys.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
|
||||
|
||||
## Additional Terms for Specific Features
|
||||
|
||||
The following additional terms apply to the multi-node support and Docker Compose file support features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support and Docker Compose file support, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support and Docker Compose file support features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support and Docker Compose file support features must be distributed freely and cannot be sold or offered as a service.
|
||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
|
||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||
|
||||
For further inquiries or permissions, please contact us directly.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addSuffixToAllProperties } from "@/server/utils/docker/compose";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToConfigsRoot } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToConfigsInServices } from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToConfigsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllConfigs,
|
||||
addSuffixToConfigsRoot,
|
||||
} from "@/server/utils/docker/compose/configs";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import { createDomainLabels } from "@/server/utils/docker/domain";
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import { createDomainLabels } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("createDomainLabels", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addDokployNetworkToRoot } from "@/server/utils/docker/domain";
|
||||
import { addDokployNetworkToRoot } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToRoot", () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { addDokployNetworkToService } from "@/server/utils/docker/domain";
|
||||
import { addDokployNetworkToService } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("addDokployNetworkToService", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNetworks } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNetworks } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllNetworks,
|
||||
addSuffixToServiceNetworks,
|
||||
} from "@/server/utils/docker/compose/network";
|
||||
import { addSuffixToNetworksRoot } from "@/server/utils/docker/compose/network";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import { addSuffixToNetworksRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToSecretsRoot } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { dump, load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToSecretsInServices } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToSecretsInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { addSuffixToAllSecrets } from "@/server/utils/docker/compose/secrets";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
addSuffixToAllServiceNames,
|
||||
addSuffixToServiceNames,
|
||||
} from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToServiceNames } from "@/server/utils/docker/compose/service";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToServiceNames } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesRoot,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToVolumesRoot } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesRoot } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { addSuffixToVolumesInServices } from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import { addSuffixToVolumesInServices } from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { generateRandomHash } from "@/server/utils/docker/compose";
|
||||
import { generateRandomHash } from "@dokploy/server";
|
||||
import {
|
||||
addSuffixToAllVolumes,
|
||||
addSuffixToVolumesInServices,
|
||||
} from "@/server/utils/docker/compose/volume";
|
||||
import type { ComposeSpecification } from "@/server/utils/docker/types";
|
||||
} from "@dokploy/server";
|
||||
import type { ComposeSpecification } from "@dokploy/server";
|
||||
import { load } from "js-yaml";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { APPLICATIONS_PATH } from "@/server/constants";
|
||||
import { unzipDrop } from "@/server/utils/builders/drop";
|
||||
import { paths } from "@dokploy/server/dist/constants";
|
||||
const { APPLICATIONS_PATH } = paths();
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { unzipDrop } from "@dokploy/server";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -11,11 +13,87 @@ if (typeof window === "undefined") {
|
||||
globalThis.FileList = undici.FileList as any;
|
||||
}
|
||||
|
||||
vi.mock("@/server/constants", () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}));
|
||||
const baseApp: ApplicationNested = {
|
||||
applicationId: "",
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
buildArgs: null,
|
||||
buildPath: "/",
|
||||
gitlabPathNamespace: "",
|
||||
buildType: "nixpacks",
|
||||
bitbucketBranch: "",
|
||||
bitbucketBuildPath: "",
|
||||
bitbucketId: "",
|
||||
bitbucketRepository: "",
|
||||
bitbucketOwner: "",
|
||||
githubId: "",
|
||||
gitlabProjectId: 0,
|
||||
gitlabBranch: "",
|
||||
gitlabBuildPath: "",
|
||||
gitlabId: "",
|
||||
gitlabRepository: "",
|
||||
gitlabOwner: "",
|
||||
command: null,
|
||||
cpuLimit: null,
|
||||
cpuReservation: null,
|
||||
createdAt: "",
|
||||
customGitBranch: "",
|
||||
customGitBuildPath: "",
|
||||
customGitSSHKeyId: null,
|
||||
customGitUrl: "",
|
||||
description: "",
|
||||
dockerfile: null,
|
||||
dockerImage: null,
|
||||
dropBuildPath: null,
|
||||
enabled: null,
|
||||
env: null,
|
||||
healthCheckSwarm: null,
|
||||
labelsSwarm: null,
|
||||
memoryLimit: null,
|
||||
memoryReservation: null,
|
||||
modeSwarm: null,
|
||||
mounts: [],
|
||||
name: "",
|
||||
networkSwarm: null,
|
||||
owner: null,
|
||||
password: null,
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
registryId: null,
|
||||
replicas: 1,
|
||||
repository: null,
|
||||
restartPolicySwarm: null,
|
||||
rollbackConfigSwarm: null,
|
||||
security: [],
|
||||
sourceType: "git",
|
||||
subtitle: null,
|
||||
title: null,
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
// @ts-ignore
|
||||
...actual,
|
||||
paths: () => ({
|
||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||
}),
|
||||
};
|
||||
});
|
||||
describe("unzipDrop using real zip files", () => {
|
||||
// const { APPLICATIONS_PATH } = paths();
|
||||
beforeAll(async () => {
|
||||
await fs.rm(APPLICATIONS_PATH, { recursive: true, force: true });
|
||||
});
|
||||
@@ -25,39 +103,46 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder", async () => {
|
||||
const appName = "single-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
baseApp.appName = "single-file";
|
||||
// const appName = "single-file";
|
||||
try {
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
|
||||
console.log(`Output Path: ${outputPath}`);
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, baseApp);
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||
const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "folderwithfile";
|
||||
// const appName = "folderwithfile";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with multiple root folders", async () => {
|
||||
const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "two-folders";
|
||||
// const appName = "two-folders";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
@@ -66,13 +151,14 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a file", async () => {
|
||||
const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "nested";
|
||||
// const appName = "nested";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
@@ -82,13 +168,14 @@ describe("unzipDrop using real zip files", () => {
|
||||
});
|
||||
|
||||
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||
const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, appName, "code");
|
||||
baseApp.appName = "folder-with-sibling-file";
|
||||
// const appName = "folder-with-sibling-file";
|
||||
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||
|
||||
const zipBuffer = zip.toBuffer();
|
||||
const file = new File([zipBuffer], "single.zip");
|
||||
await unzipDrop(file, appName);
|
||||
await unzipDrop(file, baseApp);
|
||||
|
||||
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseRawConfig, processLogs } from "@/server/utils/access-log/utils";
|
||||
import { parseRawConfig, processLogs } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ vi.mock("node:fs", () => ({
|
||||
default: fs,
|
||||
}));
|
||||
|
||||
import type { Admin } from "@/server/api/services/admin";
|
||||
import { createDefaultServerTraefikConfig } from "@/server/setup/traefik-setup";
|
||||
import { loadOrCreateConfig } from "@/server/utils/traefik/application";
|
||||
import type { FileConfig } from "@/server/utils/traefik/file-types";
|
||||
import { updateServerTraefik } from "@/server/utils/traefik/web-server";
|
||||
import type { Admin, FileConfig } from "@dokploy/server";
|
||||
import {
|
||||
createDefaultServerTraefikConfig,
|
||||
loadOrCreateConfig,
|
||||
updateServerTraefik,
|
||||
} from "@dokploy/server";
|
||||
import { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Domain } from "@/server/api/services/domain";
|
||||
import type { Redirect } from "@/server/api/services/redirect";
|
||||
import type { ApplicationNested } from "@/server/utils/builders";
|
||||
import { createRouterConfig } from "@/server/utils/traefik/domain";
|
||||
import type { Domain } from "@dokploy/server";
|
||||
import type { Redirect } from "@dokploy/server";
|
||||
import type { ApplicationNested } from "@dokploy/server";
|
||||
import { createRouterConfig } from "@dokploy/server";
|
||||
import { expect, test } from "vitest";
|
||||
|
||||
const baseApp: ApplicationNested = {
|
||||
@@ -9,6 +9,7 @@ const baseApp: ApplicationNested = {
|
||||
applicationStatus: "done",
|
||||
appName: "",
|
||||
autoDeploy: true,
|
||||
serverId: "",
|
||||
branch: null,
|
||||
dockerBuildStage: "",
|
||||
buildArgs: null,
|
||||
|
||||
@@ -13,4 +13,9 @@ export default defineConfig({
|
||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||
pool: "forks",
|
||||
},
|
||||
define: {
|
||||
"process.env": {
|
||||
NODE: "test",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -278,6 +278,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
<div className="px-4">
|
||||
<AlertBlock type="info">
|
||||
Changing settings such as placements may cause the logs/monitoring
|
||||
to be unavailable.
|
||||
</AlertBlock>
|
||||
</div>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -125,28 +125,14 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Published Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1-65535"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(0);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="targetPort"
|
||||
@@ -154,22 +140,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Target Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="1-65535"
|
||||
{...field}
|
||||
value={field.value?.toString() || ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === "") {
|
||||
field.onChange(0);
|
||||
} else {
|
||||
const number = Number.parseInt(value, 10);
|
||||
if (!Number.isNaN(number)) {
|
||||
field.onChange(number);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Input placeholder="1-65535" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
@@ -19,6 +19,15 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
@@ -36,6 +45,36 @@ const AddRedirectchema = z.object({
|
||||
|
||||
type AddRedirect = z.infer<typeof AddRedirectchema>;
|
||||
|
||||
// Default presets
|
||||
const redirectPresets = [
|
||||
// {
|
||||
// label: "Allow www & non-www.",
|
||||
// redirect: {
|
||||
// regex: "",
|
||||
// permanent: false,
|
||||
// replacement: "",
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "to-www",
|
||||
label: "Redirect to www",
|
||||
redirect: {
|
||||
regex: "^https?://(?:www.)?(.+)",
|
||||
permanent: true,
|
||||
replacement: "https://www.$${1}",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "to-non-www",
|
||||
label: "Redirect to non-www",
|
||||
redirect: {
|
||||
regex: "^https?://www.(.+)",
|
||||
permanent: true,
|
||||
replacement: "https://$${1}",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
applicationId: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -43,9 +82,10 @@ interface Props {
|
||||
|
||||
export const AddRedirect = ({
|
||||
applicationId,
|
||||
children = <PlusIcon className="h-4 w-4" />,
|
||||
children = <PlusIcon className="w-4 h-4" />,
|
||||
}: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [presetSelected, setPresetSelected] = useState("");
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { mutateAsync, isLoading, error, isError } =
|
||||
@@ -81,19 +121,36 @@ export const AddRedirect = ({
|
||||
await utils.application.readTraefikConfig.invalidate({
|
||||
applicationId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
onDialogToggle(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to create the redirect");
|
||||
});
|
||||
};
|
||||
|
||||
const onDialogToggle = (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
// commented for the moment because not reseting the form if accidentally closed the dialog can be considered as a feature instead of a bug
|
||||
// setPresetSelected("");
|
||||
// form.reset();
|
||||
};
|
||||
|
||||
const onPresetSelect = (presetId: string) => {
|
||||
const redirectPreset = redirectPresets.find(
|
||||
(preset) => preset.id === presetId,
|
||||
)?.redirect;
|
||||
if (!redirectPreset) return;
|
||||
const { regex, permanent, replacement } = redirectPreset;
|
||||
form.reset({ regex, permanent, replacement }, { keepDefaultValues: true });
|
||||
setPresetSelected(presetId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog open={isOpen} onOpenChange={onDialogToggle}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>{children}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Redirects</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -102,6 +159,24 @@ export const AddRedirect = ({
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>Presets</Label>
|
||||
<Select onValueChange={onPresetSelect} value={presetSelected}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="No preset selected" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{redirectPresets.map((preset) => (
|
||||
<SelectItem key={preset.label} value={preset.id}>
|
||||
{preset.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form-add-redirect"
|
||||
@@ -142,7 +217,7 @@ export const AddRedirect = ({
|
||||
control={form.control}
|
||||
name="permanent"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Permanent</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
If you want to decrease or increase the resources to a specific.
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { File } from "lucide-react";
|
||||
import { File, Loader2 } from "lucide-react";
|
||||
import React from "react";
|
||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||
interface Props {
|
||||
@@ -15,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
const { data } = api.application.readTraefikConfig.useQuery(
|
||||
const { data, isLoading } = api.application.readTraefikConfig.useQuery(
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
@@ -35,7 +35,12 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
{data === null ? (
|
||||
{isLoading ? (
|
||||
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
|
||||
Loading...
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
) : !data ? (
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||
<File className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertTriangle, Package } from "lucide-react";
|
||||
import { Package } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AddVolumes } from "./add-volumes";
|
||||
import { DeleteVolume } from "./delete-volume";
|
||||
|
||||
@@ -11,24 +11,42 @@ interface Props {
|
||||
logPath: string | null;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
setData("");
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
wsRef.current = ws; // Store WebSocket instance in ref
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
setData((currentData) => currentData + e.data);
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error: ", error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket connection closed");
|
||||
wsRef.current = null; // Clear reference on close
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [logPath, open]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -44,7 +62,15 @@ export const ShowDeployment = ({ logPath, open, onClose }: Props) => {
|
||||
open={open}
|
||||
onOpenChange={(e) => {
|
||||
onClose();
|
||||
if (!e) setData("");
|
||||
if (!e) {
|
||||
setData("");
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{ applicationId },
|
||||
{
|
||||
enabled: !!applicationId,
|
||||
refetchInterval: 5000,
|
||||
refetchInterval: 1000,
|
||||
},
|
||||
);
|
||||
const [url, setUrl] = React.useState("");
|
||||
@@ -110,6 +110,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<ShowDeployment
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -140,7 +140,7 @@ export const AddDomain = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
@@ -175,6 +175,7 @@ export const AddDomain = ({
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
appName: application?.appName || "",
|
||||
serverId: application?.serverId || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
field.onChange(domain);
|
||||
@@ -227,19 +228,36 @@ export const AddDomain = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{form.getValues().https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -269,38 +287,12 @@ export const AddDomain = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
<Button isLoading={isLoading} form="hook-form" type="submit">
|
||||
{dictionary.submit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ShowDomains = ({ applicationId }: Props) => {
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application is required to set at least 1
|
||||
To access the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
|
||||
@@ -45,14 +45,17 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
toast.success("Deploying Application....");
|
||||
|
||||
await refetch();
|
||||
await deploy({
|
||||
applicationId,
|
||||
}).catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Application deployed succesfully");
|
||||
await refetch();
|
||||
})
|
||||
|
||||
.catch(() => {
|
||||
toast.error("Error to deploy Application");
|
||||
});
|
||||
|
||||
await refetch();
|
||||
}}
|
||||
|
||||
@@ -130,7 +130,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
||||
type="submit"
|
||||
className="w-fit"
|
||||
isLoading={isLoading}
|
||||
disabled={!zip}
|
||||
disabled={!zip || isLoading}
|
||||
>
|
||||
Deploy{" "}
|
||||
</Button>
|
||||
|
||||
@@ -66,7 +66,10 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
) : (
|
||||
<StopApplication applicationId={applicationId} />
|
||||
)}
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -30,12 +31,14 @@ export const DockerLogs = dynamic(
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -62,7 +65,14 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -79,6 +89,7 @@ export const ShowDockerLogs = ({ appName }: Props) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
|
||||
@@ -9,26 +9,50 @@ import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
logPath: string | null;
|
||||
serverId?: string;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
export const ShowDeploymentCompose = ({
|
||||
logPath,
|
||||
open,
|
||||
onClose,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const [data, setData] = useState("");
|
||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !logPath) return;
|
||||
|
||||
setData("");
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current = ws; // Store WebSocket instance in ref
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
setData((currentData) => currentData + e.data);
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error: ", error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("WebSocket connection closed");
|
||||
wsRef.current = null;
|
||||
};
|
||||
|
||||
return () => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [logPath, open]);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
@@ -44,7 +68,15 @@ export const ShowDeploymentCompose = ({ logPath, open, onClose }: Props) => {
|
||||
open={open}
|
||||
onOpenChange={(e) => {
|
||||
onClose();
|
||||
if (!e) setData("");
|
||||
if (!e) {
|
||||
setData("");
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||
|
||||
@@ -111,6 +111,7 @@ export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
<ShowDeploymentCompose
|
||||
serverId={data?.serverId || ""}
|
||||
open={activeLog !== null}
|
||||
onClose={() => setActiveLog(null)}
|
||||
logPath={activeLog}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input, NumberInput } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -161,7 +161,7 @@ export const AddDomainCompose = ({
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Domain</DialogTitle>
|
||||
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||
@@ -190,7 +190,7 @@ export const AddDomainCompose = ({
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
<div className="flex flex-row gap-4 w-full items-end">
|
||||
<div className="flex flex-row items-end w-full gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="serviceName"
|
||||
@@ -310,6 +310,7 @@ export const AddDomainCompose = ({
|
||||
isLoading={isLoadingGenerate}
|
||||
onClick={() => {
|
||||
generateDomain({
|
||||
serverId: compose?.serverId || "",
|
||||
appName: compose?.appName || "",
|
||||
})
|
||||
.then((domain) => {
|
||||
@@ -363,19 +364,36 @@ export const AddDomainCompose = ({
|
||||
<FormItem>
|
||||
<FormLabel>Container Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"3000"}
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(Number.parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
<NumberInput placeholder={"3000"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{https && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -405,28 +423,6 @@ export const AddDomainCompose = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
Automatically provision SSL Certificate.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
To access to the application is required to set at least 1
|
||||
To access to the application it is required to set at least 1
|
||||
domain
|
||||
</span>
|
||||
<div className="flex flex-row gap-4 flex-wrap">
|
||||
|
||||
@@ -75,7 +75,10 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
<StopCompose composeId={composeId} />
|
||||
)}
|
||||
|
||||
<DockerTerminalModal appName={data?.appName || ""}>
|
||||
<DockerTerminalModal
|
||||
appName={data?.appName || ""}
|
||||
serverId={data?.serverId || ""}
|
||||
>
|
||||
<Button variant="outline">
|
||||
<Terminal />
|
||||
Open Terminal
|
||||
|
||||
@@ -77,7 +77,6 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
toast.error("Error to update the compose config");
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { api } from "@/utils/api";
|
||||
import { Puzzle, RefreshCw } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
@@ -34,6 +34,16 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
|
||||
const { mutateAsync, isLoading } = api.compose.fetchSourceType.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
mutateAsync({ composeId })
|
||||
.then(() => {
|
||||
refetch();
|
||||
})
|
||||
.catch((err) => {});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader, Loader2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
export const DockerLogs = dynamic(
|
||||
@@ -30,14 +31,20 @@ export const DockerLogs = dynamic(
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
export const ShowDockerLogsCompose = ({
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName,
|
||||
appType,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -64,7 +71,14 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
<Label>Select a container to view logs</Label>
|
||||
<Select onValueChange={setContainerId} value={containerId}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -81,6 +95,7 @@ export const ShowDockerLogsCompose = ({ appName, appType }: Props) => {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DockerLogs
|
||||
serverId={serverId || ""}
|
||||
id="terminal"
|
||||
containerId={containerId || "select-a-container"}
|
||||
/>
|
||||
|
||||
@@ -17,23 +17,27 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { DockerMonitoring } from "../../monitoring/docker/show";
|
||||
|
||||
interface Props {
|
||||
appName: string;
|
||||
serverId?: string;
|
||||
appType: "stack" | "docker-compose";
|
||||
}
|
||||
|
||||
export const ShowMonitoringCompose = ({
|
||||
appName,
|
||||
appType = "stack",
|
||||
serverId,
|
||||
}: Props) => {
|
||||
const { data } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||
{
|
||||
appName: appName,
|
||||
appType,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!appName,
|
||||
@@ -46,7 +50,7 @@ export const ShowMonitoringCompose = ({
|
||||
|
||||
const [containerId, setContainerId] = useState<string | undefined>();
|
||||
|
||||
const { mutateAsync: restart, isLoading } =
|
||||
const { mutateAsync: restart, isLoading: isRestarting } =
|
||||
api.docker.restartContainer.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +81,14 @@ export const ShowMonitoringCompose = ({
|
||||
value={containerAppName}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a container" />
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder="Select a container" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -95,7 +106,7 @@ export const ShowMonitoringCompose = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
isLoading={isRestarting}
|
||||
onClick={async () => {
|
||||
if (!containerId) return;
|
||||
toast.success(`Restarting container ${containerAppName}`);
|
||||
|
||||
@@ -11,12 +11,14 @@ import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowContainerConfig = ({ containerId }: Props) => {
|
||||
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
const { data } = api.docker.getConfig.useQuery(
|
||||
{
|
||||
containerId,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!containerId,
|
||||
|
||||
@@ -1,24 +1,41 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { FitAddon } from "xterm-addon-fit";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
containerId: string;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
export const DockerLogsId: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
}) => {
|
||||
const [term, setTerm] = React.useState<Terminal>();
|
||||
const [lines, setLines] = React.useState<number>(40);
|
||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||
|
||||
const createTerminal = (): Terminal => {
|
||||
useEffect(() => {
|
||||
// if (containerId === "select-a-container") {
|
||||
// return;
|
||||
// }
|
||||
const container = document.getElementById(id);
|
||||
if (container) {
|
||||
container.innerHTML = "";
|
||||
}
|
||||
|
||||
if (wsRef.current) {
|
||||
console.log(wsRef.current);
|
||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
wsRef.current = null;
|
||||
}
|
||||
const termi = new Terminal({
|
||||
cursorBlink: true,
|
||||
cols: 80,
|
||||
@@ -38,9 +55,9 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
wsRef.current = ws;
|
||||
const fitAddon = new FitAddon();
|
||||
termi.loadAddon(fitAddon);
|
||||
// @ts-ignore
|
||||
@@ -49,6 +66,10 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
termi.focus();
|
||||
setTerm(termi);
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error: ", error);
|
||||
};
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
termi.write(e.data);
|
||||
};
|
||||
@@ -57,12 +78,14 @@ export const DockerLogsId: React.FC<Props> = ({ id, containerId }) => {
|
||||
console.log(e.reason);
|
||||
|
||||
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
||||
wsRef.current = null;
|
||||
};
|
||||
return () => {
|
||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
};
|
||||
return termi;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
createTerminal();
|
||||
}, [lines, containerId]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -22,9 +22,14 @@ export const DockerLogsId = dynamic(
|
||||
interface Props {
|
||||
containerId: string;
|
||||
children?: React.ReactNode;
|
||||
serverId?: string | null;
|
||||
}
|
||||
|
||||
export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
||||
export const ShowDockerModalLogs = ({
|
||||
containerId,
|
||||
children,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -41,7 +46,11 @@ export const ShowDockerModalLogs = ({ containerId, children }: Props) => {
|
||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex flex-col gap-4 pt-2.5">
|
||||
<DockerLogsId id="terminal" containerId={containerId || ""} />
|
||||
<DockerLogsId
|
||||
id="terminal"
|
||||
containerId={containerId || ""}
|
||||
serverId={serverId}
|
||||
/>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -114,11 +114,20 @@ export const columns: ColumnDef<Container>[] = [
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<ShowDockerModalLogs containerId={container.containerId}>
|
||||
<ShowDockerModalLogs
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId}
|
||||
>
|
||||
View Logs
|
||||
</ShowDockerModalLogs>
|
||||
<ShowContainerConfig containerId={container.containerId} />
|
||||
<DockerTerminalModal containerId={container.containerId}>
|
||||
<ShowContainerConfig
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
/>
|
||||
<DockerTerminalModal
|
||||
containerId={container.containerId}
|
||||
serverId={container.serverId || ""}
|
||||
>
|
||||
Terminal
|
||||
</DockerTerminalModal>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -34,8 +34,15 @@ export type Container = NonNullable<
|
||||
RouterOutputs["docker"]["getContainers"]
|
||||
>[0];
|
||||
|
||||
export const ShowContainers = () => {
|
||||
const { data, isLoading } = api.docker.getContainers.useQuery();
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowContainers = ({ serverId }: Props) => {
|
||||
const { data, isLoading } = api.docker.getContainers.useQuery({
|
||||
serverId,
|
||||
});
|
||||
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||
[],
|
||||
@@ -103,83 +110,99 @@ export const ShowContainers = () => {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : data?.length === 0 ? (
|
||||
<div className="flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No results.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>No results.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table?.getRowModel()?.rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>No results.</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
{data && data?.length > 0 && (
|
||||
<div className="flex items-center justify-end space-x-2 py-4">
|
||||
<div className="space-x-2 flex flex-wrap">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,10 +18,15 @@ const Terminal = dynamic(
|
||||
|
||||
interface Props {
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
||||
export const DockerTerminalModal = ({
|
||||
children,
|
||||
containerId,
|
||||
serverId,
|
||||
}: Props) => {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
@@ -40,7 +45,11 @@ export const DockerTerminalModal = ({ children, containerId }: Props) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Terminal id="terminal" containerId={containerId} />
|
||||
<Terminal
|
||||
id="terminal"
|
||||
containerId={containerId}
|
||||
serverId={serverId || ""}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -8,9 +8,14 @@ import { AttachAddon } from "@xterm/addon-attach";
|
||||
interface Props {
|
||||
id: string;
|
||||
containerId: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
export const DockerTerminal: React.FC<Props> = ({
|
||||
id,
|
||||
containerId,
|
||||
serverId,
|
||||
}) => {
|
||||
const termRef = useRef(null);
|
||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||
useEffect(() => {
|
||||
@@ -33,7 +38,7 @@ export const DockerTerminal: React.FC<Props> = ({ id, containerId }) => {
|
||||
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}`;
|
||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const ShowTraefikFile = ({ path }: Props) => {
|
||||
const { data, refetch } = api.settings.readTraefikFile.useQuery(
|
||||
export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
isLoading: isLoadingFile,
|
||||
} = api.settings.readTraefikFile.useQuery(
|
||||
{
|
||||
path,
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
enabled: !!path,
|
||||
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}
|
||||
form.reset({
|
||||
traefikConfig: data || "",
|
||||
});
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
|
||||
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
await mutateAsync({
|
||||
traefikConfig: data.traefikConfig,
|
||||
path,
|
||||
serverId,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Traefik config Updated");
|
||||
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
className="w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col overflow-auto">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormDescription className="break-all">
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
{isLoadingFile ? (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="traefikConfig"
|
||||
render={({ field }) => (
|
||||
<FormItem className="relative">
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormDescription className="break-all">
|
||||
{path}
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
rule: Host('domain.com')
|
||||
@@ -116,31 +130,36 @@ routers:
|
||||
tls: false
|
||||
middlewares: []
|
||||
`}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<pre>
|
||||
<FormMessage />
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
setCanEdit(!canEdit);
|
||||
}}
|
||||
>
|
||||
{canEdit ? "Unlock" : "Lock"}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button isLoading={isLoading} disabled={canEdit} type="submit">
|
||||
<Button
|
||||
isLoading={isLoading}
|
||||
disabled={canEdit || isLoading}
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,47 @@
|
||||
import React from "react";
|
||||
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Tree } from "@/components/ui/file-tree";
|
||||
import { api } from "@/utils/api";
|
||||
import { FileIcon, Folder, Workflow } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||
import React from "react";
|
||||
import { ShowTraefikFile } from "./show-traefik-file";
|
||||
|
||||
export const ShowTraefikSystem = () => {
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
}
|
||||
export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
const [file, setFile] = React.useState<null | string>(null);
|
||||
|
||||
const { data: directories } = api.settings.readDirectories.useQuery();
|
||||
const {
|
||||
data: directories,
|
||||
isLoading,
|
||||
error,
|
||||
isError,
|
||||
} = api.settings.readDirectories.useQuery(
|
||||
{
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
retry: 2,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn("mt-6 md:grid gap-4")}>
|
||||
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
|
||||
{isError && (
|
||||
<AlertBlock type="error" className="w-full">
|
||||
{error?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
Loading...
|
||||
</span>
|
||||
<Loader2 className="animate-spin size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
@@ -34,7 +61,7 @@ export const ShowTraefikSystem = () => {
|
||||
/>
|
||||
<div className="w-full">
|
||||
{file ? (
|
||||
<ShowTraefikFile path={file} />
|
||||
<ShowTraefikFile path={file} serverId={serverId} />
|
||||
) : (
|
||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
|
||||
@@ -79,7 +79,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">Resources</CardTitle>
|
||||
<CardDescription>
|
||||
If you want to decrease or increase the resources to a specific
|
||||
If you want to decrease or increase the resources to a specific.
|
||||
application or database
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||