mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge branch 'canary' into 216-domains-for-services-created-via-template
This commit is contained in:
commit
29ca894a97
@ -58,7 +58,7 @@ jobs:
|
||||
docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN
|
||||
|
||||
if [ "${CIRCLE_BRANCH}" == "main" ]; then
|
||||
VERSION=$(node -p "require('apps/dokploy/package.json').version")
|
||||
VERSION=$(node -p "require('./apps/dokploy/package.json').version")
|
||||
echo $VERSION
|
||||
TAG="latest"
|
||||
|
||||
|
50
.github/workflows/deploy.yml
vendored
Normal file
50
.github/workflows/deploy.yml
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
name: Build Docs & Website Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["canary", "main"]
|
||||
|
||||
jobs:
|
||||
build-and-push-image-docs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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.docs
|
||||
push: true
|
||||
tags: dokploy/docs:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-and-push-image-website:
|
||||
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.website
|
||||
push: true
|
||||
tags: dokploy/website:latest
|
||||
platforms: linux/amd64
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -33,6 +33,10 @@ npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
35
Dockerfile.docs
Normal file
35
Dockerfile.docs
Normal file
@ -0,0 +1,35 @@
|
||||
FROM node:18-alpine 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
|
||||
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --filter=./apps/docs --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=./apps/docs run build
|
||||
RUN pnpm --filter=./apps/docs --prod deploy /prod/docs
|
||||
|
||||
RUN cp -R /usr/src/app/apps/docs/.next /prod/docs/.next
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/docs/.next ./.next
|
||||
COPY --from=build /prod/docs/public ./public
|
||||
COPY --from=build /prod/docs/package.json ./package.json
|
||||
COPY --from=build /prod/docs/node_modules ./node_modules
|
||||
|
||||
EXPOSE 3000
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
35
Dockerfile.website
Normal file
35
Dockerfile.website
Normal file
@ -0,0 +1,35 @@
|
||||
FROM node:18-alpine 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
|
||||
|
||||
|
||||
# Install dependencies
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --filter=./apps/website --frozen-lockfile
|
||||
|
||||
# Deploy only the dokploy app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN pnpm --filter=./apps/website run build
|
||||
RUN pnpm --filter=./apps/website --prod deploy /prod/website
|
||||
|
||||
RUN cp -R /usr/src/app/apps/website/.next /prod/website/.next
|
||||
|
||||
FROM base AS dokploy
|
||||
WORKDIR /app
|
||||
|
||||
# Set production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy only the necessary files
|
||||
COPY --from=build /prod/website/.next ./.next
|
||||
COPY --from=build /prod/website/public ./public
|
||||
COPY --from=build /prod/website/package.json ./package.json
|
||||
COPY --from=build /prod/website/node_modules ./node_modules
|
||||
|
||||
EXPOSE 3000
|
||||
CMD HOSTNAME=0.0.0.0 && pnpm start
|
17
README.md
17
README.md
@ -1,12 +1,19 @@
|
||||
<div align="center">
|
||||
<h1 align="center">Dokploy</h1>
|
||||
<div>
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://raw.githubusercontent.com/Dokploy/docs/main/public/logo.png" >
|
||||
<img style="object-fit: cover; border-radius:20px;" align="center" width="50%"src="https://dokploy.com/og.png" >
|
||||
|
||||
</div>
|
||||
|
||||
</br>
|
||||
<div align="center">
|
||||
<div>Join us on Discord for help, feedback, and discussions!</div>
|
||||
</br>
|
||||
<a href="https://discord.gg/ZXwG32bw">
|
||||
<img src="https://discordapp.com/api/guilds/1234073262418563112/widget.png?style=banner2" alt="Discord Shield"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||
|
||||
@ -52,7 +59,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
### Premium Supporters 🥇
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://supafort.com/" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="bc direct logo" width="190"/></a>
|
||||
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
||||
</div>
|
||||
|
||||
<!-- Elite Contributors 🥈 -->
|
||||
@ -62,13 +69,13 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
||||
### Supporting Members 🥉
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://lightspeed.run/"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
<a href="https://lightspeed.run/?ref=dokploy"><img src="https://github.com/lightspeedrun.png" width="60px" alt="Lightspeed.run"/></a>
|
||||
</div>
|
||||
|
||||
### Community Backers 🤝
|
||||
|
||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||
<a href="https://steamsets.com/"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Lightspeed.run"/></a>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
#### Organizations:
|
||||
|
@ -3,13 +3,14 @@ title: "Overview"
|
||||
description: "Learn how to use Docker Compose with Dokploy"
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
|
||||
Dokploy integrates with Docker Compose and Docker Stack to provide flexible deployment solutions. Whether you are developing locally or deploying at scale, Dokploy facilitates application management through these powerful Docker tools.
|
||||
|
||||
### Configuration Methods
|
||||
|
||||
Dokploy provides two methods for creating Docker Compose configurations:
|
||||
|
||||
|
||||
- **Docker Compose**: Ideal for standard Docker Compose configurations.
|
||||
- **Stack**: Geared towards orchestrating applications using Docker Swarm. Note that some Docker Compose features, such as `build`, are not available in this mode.
|
||||
|
||||
@ -21,7 +22,7 @@ Configure the source of your code, the way your application is built, and also m
|
||||
|
||||
A code editor within Dokploy allows you to specify environment variables for your Docker Compose file. By default, Dokploy creates a `.env` file in the specified Docker Compose file path.
|
||||
|
||||
### Monitoring
|
||||
### Monitoring
|
||||
|
||||
Monitor each service individually within Dokploy. If your application consists of multiple services, each can be monitored separately to ensure optimal performance.
|
||||
|
||||
@ -29,7 +30,6 @@ Monitor each service individually within Dokploy. If your application consists o
|
||||
|
||||
Access detailed logs for each service through the Dokploy log viewer, which can help in troubleshooting and ensuring the stability of your services.
|
||||
|
||||
|
||||
### Deployments
|
||||
|
||||
You can view the last 10 deployments of your application. When you deploy your application in real time, a new deployment record will be created and it will gradually show you how your application is being built.
|
||||
@ -38,7 +38,6 @@ We also offer a button to cancel deployments that are in queue. Note that those
|
||||
|
||||
We provide a webhook so that you can trigger your own deployments by pushing to your GitHub, Gitea, GitLab, Bitbucket repository.
|
||||
|
||||
|
||||
### Advanced
|
||||
|
||||
This section provides advanced configuration options for experienced users. It includes tools for custom commands within the container and volumes.
|
||||
@ -46,4 +45,32 @@ This section provides advanced configuration options for experienced users. It i
|
||||
- **Command**: Dokploy has a defined command to run the Docker Compose file, ensuring complete control through the UI. However, you can append flags or options to the command.
|
||||
- **Volumes**: To ensure data persistence across deployments, configure storage volumes for your application.
|
||||
|
||||
<ImageZoom src="/assets/images/compose/overview.png" width={800} height={630} quality={100} priority alt='home og image' className="rounded-lg" />
|
||||
<ImageZoom
|
||||
src="/assets/images/compose/overview.png"
|
||||
width={800}
|
||||
height={630}
|
||||
quality={100}
|
||||
priority
|
||||
alt="home og image"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
||||
<Callout title="Volumes">
|
||||
Docker volumes are a way to persist data generated and used by Docker containers. They are particularly useful for maintaining data between container restarts or for sharing data among different containers.
|
||||
|
||||
To bind a volume to the host machine, you can use the following syntax in your docker-compose.yml file, but this way will clean up the volumes when a new deployment is made:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "/folder:/path/in/container" ❌
|
||||
```
|
||||
|
||||
It's recommended to use the ../files folder to ensure your data persists between deployments. For example:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- "../files/my-database:/var/lib/mysql" ✅
|
||||
- "../files/my-configs:/etc/my-app/config" ✅
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
@ -32,7 +32,8 @@ The following templates are available:
|
||||
- **Metabase**: Open Source Business Intelligence
|
||||
- **Grafana**: Open Source Dashboard for your metrics
|
||||
- **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
|
||||
|
||||
|
||||
|
||||
|
@ -10,10 +10,10 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"fumadocs-core": "12.2.2",
|
||||
"fumadocs-mdx": "8.2.33",
|
||||
"fumadocs-openapi": "^3.1.3",
|
||||
"fumadocs-ui": "12.2.2",
|
||||
"fumadocs-core": "^12.5.6",
|
||||
"fumadocs-mdx": "^8.2.34",
|
||||
"fumadocs-openapi": "^3.3.0",
|
||||
"fumadocs-ui": "^12.5.6",
|
||||
"lucide-react": "^0.394.0",
|
||||
"next": "^14.2.4",
|
||||
"react": "^18.3.1",
|
||||
|
@ -0,0 +1,97 @@
|
||||
import { fs, vol } from "memfs";
|
||||
|
||||
vi.mock("node:fs", () => ({
|
||||
...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 { beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
const baseAdmin: Admin = {
|
||||
createdAt: "",
|
||||
authId: "",
|
||||
adminId: "string",
|
||||
githubAppId: null,
|
||||
githubAppName: null,
|
||||
serverIp: null,
|
||||
certificateType: "none",
|
||||
host: null,
|
||||
githubClientId: null,
|
||||
githubClientSecret: null,
|
||||
githubInstallationId: null,
|
||||
githubPrivateKey: null,
|
||||
githubWebhookSecret: null,
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
createDefaultServerTraefikConfig();
|
||||
});
|
||||
|
||||
test("Should read the configuration file", () => {
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app"]?.service).toBe(
|
||||
"dokploy-service-app",
|
||||
);
|
||||
});
|
||||
|
||||
test("Should apply redirect-to-https", () => {
|
||||
updateServerTraefik(
|
||||
{
|
||||
...baseAdmin,
|
||||
certificateType: "letsencrypt",
|
||||
},
|
||||
"example.com",
|
||||
);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app"]?.middlewares).toContain(
|
||||
"redirect-to-https",
|
||||
);
|
||||
});
|
||||
|
||||
test("Should change only host when no certificate", () => {
|
||||
updateServerTraefik(baseAdmin, "example.com");
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app-secure"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("Should not touch config without host", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(baseAdmin, null);
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(originalConfig).toEqual(config);
|
||||
});
|
||||
|
||||
test("Should remove websecure if https rollback to http", () => {
|
||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
updateServerTraefik(
|
||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||
"example.com",
|
||||
);
|
||||
|
||||
updateServerTraefik({ ...baseAdmin, certificateType: "none" }, "example.com");
|
||||
|
||||
const config: FileConfig = loadOrCreateConfig("dokploy");
|
||||
|
||||
expect(config.http?.routers?.["dokploy-router-app-secure"]).toBeUndefined();
|
||||
expect(
|
||||
config.http?.routers?.["dokploy-router-app"]?.middlewares,
|
||||
).not.toContain("redirect-to-https");
|
||||
});
|
@ -40,6 +40,7 @@ const baseApp: ApplicationNested = {
|
||||
placementSwarm: null,
|
||||
ports: [],
|
||||
projectId: "",
|
||||
publishDirectory: null,
|
||||
redirects: [],
|
||||
refreshToken: "",
|
||||
registry: null,
|
||||
@ -54,6 +55,7 @@ const baseApp: ApplicationNested = {
|
||||
title: null,
|
||||
updateConfigSwarm: null,
|
||||
username: null,
|
||||
dockerContextPath: null,
|
||||
};
|
||||
|
||||
const baseDomain: Domain = {
|
||||
@ -88,6 +90,17 @@ test("Web entrypoint on http domain", async () => {
|
||||
);
|
||||
|
||||
expect(router.middlewares).not.toContain("redirect-to-https");
|
||||
expect(router.rule).not.toContain("PathPrefix");
|
||||
});
|
||||
|
||||
test("Web entrypoint on http domain with custom path", async () => {
|
||||
const router = await createRouterConfig(
|
||||
baseApp,
|
||||
{ ...baseDomain, path: "/foo", https: false },
|
||||
"web",
|
||||
);
|
||||
|
||||
expect(router.rule).toContain("PathPrefix(`/foo`)");
|
||||
});
|
||||
|
||||
test("Web entrypoint on http domain with redirect", async () => {
|
||||
|
@ -46,6 +46,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
value={data || "Empty"}
|
||||
disabled
|
||||
className="font-mono"
|
||||
|
@ -144,6 +144,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
@ -23,6 +24,7 @@ enum BuildType {
|
||||
heroku_buildpacks = "heroku_buildpacks",
|
||||
paketo_buildpacks = "paketo_buildpacks",
|
||||
nixpacks = "nixpacks",
|
||||
static = "static",
|
||||
}
|
||||
|
||||
const mySchema = z.discriminatedUnion("buildType", [
|
||||
@ -34,6 +36,7 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
invalid_type_error: "Dockerfile path is required",
|
||||
})
|
||||
.min(1, "Dockerfile required"),
|
||||
dockerContextPath: z.string().nullable().default(""),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("heroku_buildpacks"),
|
||||
@ -43,6 +46,10 @@ const mySchema = z.discriminatedUnion("buildType", [
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("nixpacks"),
|
||||
publishDirectory: z.string().optional(),
|
||||
}),
|
||||
z.object({
|
||||
buildType: z.literal("static"),
|
||||
}),
|
||||
]);
|
||||
|
||||
@ -73,17 +80,18 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
const buildType = form.watch("buildType");
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
// TODO: refactor this
|
||||
if (data.buildType === "dockerfile") {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
...(data.buildType && {
|
||||
dockerfile: data.dockerfile || "",
|
||||
dockerContextPath: data.dockerContextPath || "",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
form.reset({
|
||||
buildType: data.buildType,
|
||||
publishDirectory: data.publishDirectory || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -93,7 +101,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
await mutateAsync({
|
||||
applicationId,
|
||||
buildType: data.buildType,
|
||||
publishDirectory:
|
||||
data.buildType === "nixpacks" ? data.publishDirectory : null,
|
||||
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
|
||||
dockerContextPath:
|
||||
data.buildType === "dockerfile" ? data.dockerContextPath : null,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Build type saved");
|
||||
@ -171,6 +183,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
Paketo Buildpacks
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="static" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Static</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -179,16 +197,71 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
||||
}}
|
||||
/>
|
||||
{buildType === "dockerfile" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerfile"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Path of your docker file"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerContextPath"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker Context Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={
|
||||
"Path of your docker context default: ."
|
||||
}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{buildType === "nixpacks" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dockerfile"
|
||||
name="publishDirectory"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Docker File</FormLabel>
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Publish Directory</FormLabel>
|
||||
<FormDescription>
|
||||
Allows you to serve a single directory via NGINX after
|
||||
the build phase. Useful if the final build assets
|
||||
should be served as a static site.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Path of your docker file"}
|
||||
placeholder={"Publish Directory"}
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
|
@ -53,7 +53,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<span className="text-muted-foreground">
|
||||
<span className="break-all text-muted-foreground">
|
||||
{`${url}/api/deploy/${data?.refreshToken}`}
|
||||
</span>
|
||||
<RefreshToken applicationId={applicationId} />
|
||||
@ -72,7 +72,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{deployments?.map((deployment) => (
|
||||
<div
|
||||
key={deployment.deploymentId}
|
||||
className="flex items-center justify-between rounded-lg border p-4"
|
||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||
@ -87,7 +87,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
||||
{deployment.title}
|
||||
</span>
|
||||
{deployment.description && (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="break-all text-sm text-muted-foreground">
|
||||
{deployment.description}
|
||||
</span>
|
||||
)}
|
||||
|
@ -114,7 +114,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Password" {...field} />
|
||||
<Input placeholder="Password" {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
@ -104,6 +104,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
@ -55,6 +55,7 @@ interface Props {
|
||||
|
||||
export const AddTemplate = ({ projectId }: Props) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data } = api.compose.templates.useQuery();
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const { data: tags, isLoading: isLoadingTags } =
|
||||
@ -75,14 +76,14 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<PuzzleIcon className="size-4 text-muted-foreground" />
|
||||
<span>Templates</span>
|
||||
<span>Template</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
|
||||
@ -283,6 +284,7 @@ export const AddTemplate = ({ projectId }: Props) => {
|
||||
utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
setOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
|
@ -9,36 +9,11 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
import { format } from "date-fns";
|
||||
import { BadgeCheck } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { RemoveGithubApp } from "./remove-github-app";
|
||||
export const generateName = () => {
|
||||
const n1 = ["Blue", "Green", "Red", "Orange", "Violet", "Indigo", "Yellow"];
|
||||
const n2 = [
|
||||
"One",
|
||||
"Two",
|
||||
"Three",
|
||||
"Four",
|
||||
"Five",
|
||||
"Six",
|
||||
"Seven",
|
||||
"Eight",
|
||||
"Nine",
|
||||
"Zero",
|
||||
];
|
||||
return `Dokploy-${n1[Math.round(Math.random() * (n1.length - 1))]}-${
|
||||
n2[Math.round(Math.random() * (n2.length - 1))]
|
||||
}`;
|
||||
};
|
||||
function slugify(text: string) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[\s\^&*()+=!]+/g, "-")
|
||||
.replace(/[\$.,*+~()'"!:@^&]+/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
export const GithubSetup = () => {
|
||||
const [isOrganization, setIsOrganization] = useState(false);
|
||||
@ -52,10 +27,9 @@ export const GithubSetup = () => {
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/redirect?authId=${data?.authId}`,
|
||||
name: generateName(),
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
// JUST FOR TESTING
|
||||
url: `${url}/api/deploy/github`,
|
||||
// url: `${origin}/api/webhook`, // Aquí especificas la URL del endpoint de tu webhook
|
||||
},
|
||||
@ -95,8 +69,8 @@ export const GithubSetup = () => {
|
||||
</div>
|
||||
<div className="flex items-end gap-4 flex-wrap">
|
||||
<RemoveGithubApp />
|
||||
{/* <Link
|
||||
href={`https://github.com/settings/apps/${data?.githubAppName}`}
|
||||
<Link
|
||||
href={`${data?.githubAppName}`}
|
||||
target="_blank"
|
||||
className={buttonVariants({
|
||||
className: "w-fit",
|
||||
@ -104,7 +78,7 @@ export const GithubSetup = () => {
|
||||
})}
|
||||
>
|
||||
<span className="text-sm">Manage Github App</span>
|
||||
</Link> */}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -119,9 +93,9 @@ export const GithubSetup = () => {
|
||||
|
||||
<div className="flex flex-row gap-4">
|
||||
<Link
|
||||
href={`https://github.com/apps/${slugify(
|
||||
data.githubAppName,
|
||||
)}/installations/new?state=gh_setup:${data?.authId}`}
|
||||
href={`${
|
||||
data.githubAppName
|
||||
}/installations/new?state=gh_setup:${data?.authId}`}
|
||||
className={buttonVariants({ className: "w-fit" })}
|
||||
>
|
||||
Install Github App
|
||||
|
@ -22,7 +22,7 @@ export const ShowDestinations = () => {
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Use SSH to beeing able cloning from private repositories.
|
||||
Use SSH to be able to clone from private repositories.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4">
|
||||
|
@ -39,6 +39,7 @@ const addPermissions = z.object({
|
||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||
canAccessToDocker: z.boolean().optional().default(false),
|
||||
canAccessToAPI: z.boolean().optional().default(false),
|
||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
type AddPermissions = z.infer<typeof addPermissions>;
|
||||
@ -82,6 +83,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
});
|
||||
}
|
||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
||||
@ -98,6 +100,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
accesedServices: data.accesedServices || [],
|
||||
canAccessToDocker: data.canAccessToDocker,
|
||||
canAccessToAPI: data.canAccessToAPI,
|
||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Permissions updated");
|
||||
@ -270,6 +273,26 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="canAccessToSSHKeys"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Access to SSH Keys</FormLabel>
|
||||
<FormDescription>
|
||||
Allow to users to access to the SSH Keys section
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accesedProjects"
|
||||
|
@ -106,6 +106,7 @@ export const ShowMainTraefikConfig = ({ children }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`providers:
|
||||
docker:
|
||||
|
@ -109,6 +109,7 @@ export const ShowServerTraefikConfig = ({ children }: Props) => {
|
||||
<FormLabel>Traefik config</FormLabel>
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
|
@ -79,6 +79,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(user?.canAccessToSSHKeys
|
||||
? [
|
||||
{
|
||||
title: "SSH Keys",
|
||||
label: "",
|
||||
icon: KeyRound,
|
||||
href: "/dashboard/settings/ssh-keys",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@ import { json } from "@codemirror/lang-json";
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { StreamLanguage } from "@codemirror/language";
|
||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||
import { useTheme } from "next-themes";
|
||||
@ -10,6 +11,7 @@ interface Props extends ReactCodeMirrorProps {
|
||||
wrapperClassName?: string;
|
||||
disabled?: boolean;
|
||||
language?: "yaml" | "json" | "properties";
|
||||
lineWrapping?: boolean;
|
||||
}
|
||||
|
||||
export const CodeEditor = ({
|
||||
@ -36,6 +38,7 @@ export const CodeEditor = ({
|
||||
: language === "json"
|
||||
? json()
|
||||
: StreamLanguage.define(properties),
|
||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||
]}
|
||||
{...props}
|
||||
editable={!props.disabled}
|
||||
|
1
apps/dokploy/drizzle/0027_red_lady_bullseye.sql
Normal file
1
apps/dokploy/drizzle/0027_red_lady_bullseye.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "publishDirectory" text;
|
1
apps/dokploy/drizzle/0028_jittery_eternity.sql
Normal file
1
apps/dokploy/drizzle/0028_jittery_eternity.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TYPE "buildType" ADD VALUE 'static';
|
1
apps/dokploy/drizzle/0029_colossal_zodiak.sql
Normal file
1
apps/dokploy/drizzle/0029_colossal_zodiak.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "application" ADD COLUMN "dockerContextPath" text;
|
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
1
apps/dokploy/drizzle/0030_little_kabuki.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "user" ADD COLUMN "canAccessToSSHKeys" boolean DEFAULT false NOT NULL;
|
3016
apps/dokploy/drizzle/meta/0027_snapshot.json
Normal file
3016
apps/dokploy/drizzle/meta/0027_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3017
apps/dokploy/drizzle/meta/0028_snapshot.json
Normal file
3017
apps/dokploy/drizzle/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3023
apps/dokploy/drizzle/meta/0029_snapshot.json
Normal file
3023
apps/dokploy/drizzle/meta/0029_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
3030
apps/dokploy/drizzle/meta/0030_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -190,6 +190,34 @@
|
||||
"when": 1721979220929,
|
||||
"tag": "0026_known_dormammu",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "6",
|
||||
"when": 1722445099203,
|
||||
"tag": "0027_red_lady_bullseye",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 28,
|
||||
"version": "6",
|
||||
"when": 1722503439951,
|
||||
"tag": "0028_jittery_eternity",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 29,
|
||||
"version": "6",
|
||||
"when": 1722578386823,
|
||||
"tag": "0029_colossal_zodiak",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 30,
|
||||
"version": "6",
|
||||
"when": 1723608499147,
|
||||
"tag": "0030_little_kabuki",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.5.0",
|
||||
"version": "v0.6.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@ -39,6 +39,7 @@
|
||||
"@codemirror/lang-yaml": "^6.1.1",
|
||||
"@codemirror/language": "^6.10.1",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/trpc-openapi": "0.0.4",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
@ -129,7 +130,6 @@
|
||||
"zod-form-data": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
@ -154,10 +154,12 @@
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.7",
|
||||
"localtunnel": "2.0.2",
|
||||
"memfs": "^4.11.0",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.2.4",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.4.2",
|
||||
"vite-tsconfig-paths": "4.3.2",
|
||||
|
@ -35,7 +35,7 @@ export default async function handler(
|
||||
.update(admins)
|
||||
.set({
|
||||
githubAppId: data.id,
|
||||
githubAppName: data.name,
|
||||
githubAppName: data.html_url,
|
||||
githubClientId: data.client_id,
|
||||
githubClientSecret: data.client_secret,
|
||||
githubWebhookSecret: data.webhook_secret,
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { ShowDestinations } from "@/components/dashboard/settings/ssh-keys/show-ssh-keys";
|
||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { validateRequest } from "@/server/auth/auth";
|
||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import React, { type ReactElement } from "react";
|
||||
import superjson from "superjson";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
@ -26,7 +29,7 @@ export async function getServerSideProps(
|
||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||
) {
|
||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||
if (!user || user.rol === "user") {
|
||||
if (!user) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
@ -34,8 +37,45 @@ export async function getServerSideProps(
|
||||
},
|
||||
};
|
||||
}
|
||||
const { req, res, resolvedUrl } = ctx;
|
||||
const helpers = createServerSideHelpers({
|
||||
router: appRouter,
|
||||
ctx: {
|
||||
req: req as any,
|
||||
res: res as any,
|
||||
db: null as any,
|
||||
session: session,
|
||||
user: user,
|
||||
},
|
||||
transformer: superjson,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
try {
|
||||
await helpers.project.all.prefetch();
|
||||
const auth = await helpers.auth.get.fetch();
|
||||
|
||||
if (auth.rol === "user") {
|
||||
const user = await helpers.user.byAuthId.fetch({
|
||||
authId: auth.id,
|
||||
});
|
||||
|
||||
if (!user.canAccessToSSHKeys) {
|
||||
return {
|
||||
redirect: {
|
||||
permanent: true,
|
||||
destination: "/",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
trpcState: helpers.dehydrate(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
BIN
apps/dokploy/public/templates/teable.png
Normal file
BIN
apps/dokploy/public/templates/teable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 757 B |
@ -191,6 +191,8 @@ export const applicationRouter = createTRPCRouter({
|
||||
await updateApplication(input.applicationId, {
|
||||
buildType: input.buildType,
|
||||
dockerfile: input.dockerfile,
|
||||
publishDirectory: input.publishDirectory,
|
||||
dockerContextPath: input.dockerContextPath,
|
||||
});
|
||||
|
||||
return true;
|
||||
|
@ -257,7 +257,8 @@ export const composeRouter = createTRPCRouter({
|
||||
if (mounts && mounts?.length > 0) {
|
||||
for (const mount of mounts) {
|
||||
await createMount({
|
||||
mountPath: mount.mountPath,
|
||||
filePath: mount.filePath,
|
||||
mountPath: "",
|
||||
content: mount.content,
|
||||
serviceId: compose.composeId,
|
||||
serviceType: "compose",
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from "@/server/utils/docker/utils";
|
||||
import { recreateDirectory } from "@/server/utils/filesystem/directory";
|
||||
import { sendDockerCleanupNotifications } from "@/server/utils/notifications/docker-cleanup";
|
||||
import { execAsync } from "@/server/utils/process/execAsync";
|
||||
import { spawnAsync } from "@/server/utils/process/spawnAsync";
|
||||
import {
|
||||
readConfig,
|
||||
@ -49,14 +50,10 @@ import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const settingsRouter = createTRPCRouter({
|
||||
reloadServer: adminProcedure.mutation(async () => {
|
||||
await spawnAsync("docker", [
|
||||
"service",
|
||||
"update",
|
||||
"--force",
|
||||
"--image",
|
||||
getDokployImage(),
|
||||
"dokploy",
|
||||
]);
|
||||
const { stdout } = await execAsync(
|
||||
"docker service inspect dokploy --format '{{.ID}}'",
|
||||
);
|
||||
await execAsync(`docker service update --force ${stdout.trim()}`);
|
||||
return true;
|
||||
}),
|
||||
reloadTraefik: adminProcedure.mutation(async () => {
|
||||
|
@ -34,21 +34,23 @@ export const sshRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: adminProcedure.input(apiRemoveSshKey).mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeSSHKeyById(input.sshKeyId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this ssh key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveSshKey)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await removeSSHKeyById(input.sshKeyId);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to delete this ssh key",
|
||||
});
|
||||
}
|
||||
}),
|
||||
one: protectedProcedure.input(apiFindOneSshKey).query(async ({ input }) => {
|
||||
const sshKey = await findSSHKeyById(input.sshKeyId);
|
||||
return sshKey;
|
||||
}),
|
||||
all: adminProcedure.query(async () => {
|
||||
all: protectedProcedure.query(async () => {
|
||||
return await db.query.sshKeys.findMany({});
|
||||
}),
|
||||
generate: protectedProcedure
|
||||
@ -56,15 +58,17 @@ export const sshRouter = createTRPCRouter({
|
||||
.mutation(async ({ input }) => {
|
||||
return await generateSSHKey(input.type);
|
||||
}),
|
||||
update: adminProcedure.input(apiUpdateSshKey).mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateSSHKeyById(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this ssh key",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(apiUpdateSshKey)
|
||||
.mutation(async ({ input }) => {
|
||||
try {
|
||||
return await updateSSHKeyById(input);
|
||||
} catch (error) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Error to update this ssh key",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
@ -35,6 +35,7 @@ export const buildType = pgEnum("buildType", [
|
||||
"heroku_buildpacks",
|
||||
"paketo_buildpacks",
|
||||
"nixpacks",
|
||||
"static",
|
||||
]);
|
||||
|
||||
// TODO: refactor this types
|
||||
@ -140,6 +141,7 @@ export const applications = pgTable("application", {
|
||||
},
|
||||
),
|
||||
dockerfile: text("dockerfile"),
|
||||
dockerContextPath: text("dockerContextPath"),
|
||||
// Drop
|
||||
dropBuildPath: text("dropBuildPath"),
|
||||
// Docker swarm json
|
||||
@ -157,6 +159,7 @@ export const applications = pgTable("application", {
|
||||
.notNull()
|
||||
.default("idle"),
|
||||
buildType: buildType("buildType").notNull().default("nixpacks"),
|
||||
publishDirectory: text("publishDirectory"),
|
||||
createdAt: text("createdAt")
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
@ -315,7 +318,9 @@ const createSchema = createInsertSchema(applications, {
|
||||
"heroku_buildpacks",
|
||||
"paketo_buildpacks",
|
||||
"nixpacks",
|
||||
"static",
|
||||
]),
|
||||
publishDirectory: z.string().optional(),
|
||||
owner: z.string(),
|
||||
healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
|
||||
restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
|
||||
@ -352,8 +357,10 @@ export const apiSaveBuildType = createSchema
|
||||
applicationId: true,
|
||||
buildType: true,
|
||||
dockerfile: true,
|
||||
dockerContextPath: true,
|
||||
})
|
||||
.required();
|
||||
.required()
|
||||
.merge(createSchema.pick({ publishDirectory: true }));
|
||||
|
||||
export const apiSaveGithubProvider = createSchema
|
||||
.pick({
|
||||
|
@ -28,6 +28,7 @@ export const users = pgTable("user", {
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date().toISOString()),
|
||||
canCreateProjects: boolean("canCreateProjects").notNull().default(false),
|
||||
canAccessToSSHKeys: boolean("canAccessToSSHKeys").notNull().default(false),
|
||||
canCreateServices: boolean("canCreateServices").notNull().default(false),
|
||||
canDeleteProjects: boolean("canDeleteProjects").notNull().default(false),
|
||||
canDeleteServices: boolean("canDeleteServices").notNull().default(false),
|
||||
@ -107,6 +108,7 @@ export const apiAssignPermissions = createSchema
|
||||
canAccessToTraefikFiles: true,
|
||||
canAccessToDocker: true,
|
||||
canAccessToAPI: true,
|
||||
canAccessToSSHKeys: true,
|
||||
})
|
||||
.required();
|
||||
|
||||
|
@ -36,10 +36,9 @@ export const initializePostgres = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 5432,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 5432 }
|
||||
: {}),
|
||||
PublishedPort: process.env.NODE_ENV === "development" ? 5432 : 0,
|
||||
Protocol: "tcp",
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -33,10 +33,9 @@ export const initializeRedis = async () => {
|
||||
Ports: [
|
||||
{
|
||||
TargetPort: 6379,
|
||||
...(process.env.NODE_ENV === "development"
|
||||
? { PublishedPort: 6379 }
|
||||
: {}),
|
||||
PublishedPort: process.env.NODE_ENV === "development" ? 6379 : 0,
|
||||
Protocol: "tcp",
|
||||
PublishMode: "host",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -104,8 +104,7 @@ export const createDefaultServerTraefikConfig = () => {
|
||||
[`${appName}-router-app`]: {
|
||||
rule: `Host(\`${appName}.docker.localhost\`) && PathPrefix(\`/\`)`,
|
||||
service: `${appName}-service-app`,
|
||||
entryPoints: ["web", "websecure"],
|
||||
tls: {},
|
||||
entryPoints: ["web"],
|
||||
},
|
||||
},
|
||||
services: {
|
||||
|
@ -1,7 +1,10 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { prepareEnvironmentVariables } from "@/server/utils/docker/utils";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import {
|
||||
getBuildAppDirectory,
|
||||
getDockerContextPath,
|
||||
} from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
import { createEnvFile } from "./utils";
|
||||
|
||||
@ -9,22 +12,30 @@ export const buildCustomDocker = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { appName, env, buildArgs } = application;
|
||||
const { appName, env, publishDirectory, buildArgs } = application;
|
||||
const dockerFilePath = getBuildAppDirectory(application);
|
||||
try {
|
||||
const image = `${appName}`;
|
||||
|
||||
const contextPath =
|
||||
const defaultContextPath =
|
||||
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
|
||||
const args = prepareEnvironmentVariables(buildArgs);
|
||||
|
||||
const dockerContextPath = getDockerContextPath(application);
|
||||
|
||||
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
|
||||
|
||||
for (const arg of args) {
|
||||
commandArgs.push("--build-arg", arg);
|
||||
}
|
||||
/*
|
||||
Do not generate an environment file when publishDirectory is specified,
|
||||
as it could be publicly exposed.
|
||||
*/
|
||||
if (!publishDirectory) {
|
||||
createEnvFile(dockerFilePath, env);
|
||||
}
|
||||
|
||||
createEnvFile(dockerFilePath, env);
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
commandArgs,
|
||||
@ -34,7 +45,7 @@ export const buildCustomDocker = async (
|
||||
}
|
||||
},
|
||||
{
|
||||
cwd: contextPath,
|
||||
cwd: dockerContextPath || defaultContextPath,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
|
@ -15,6 +15,7 @@ import { buildCustomDocker } from "./docker-file";
|
||||
import { buildHeroku } from "./heroku";
|
||||
import { buildNixpacks } from "./nixpacks";
|
||||
import { buildPaketo } from "./paketo";
|
||||
import { buildStatic } from "./static";
|
||||
|
||||
// NIXPACKS codeDirectory = where is the path of the code directory
|
||||
// HEROKU codeDirectory = where is the path of the code directory
|
||||
@ -43,6 +44,8 @@ export const buildApplication = async (
|
||||
await buildPaketo(application, writeStream);
|
||||
} else if (buildType === "dockerfile") {
|
||||
await buildCustomDocker(application, writeStream);
|
||||
} else if (buildType === "static") {
|
||||
await buildStatic(application, writeStream);
|
||||
}
|
||||
|
||||
if (application.registryId) {
|
||||
@ -51,7 +54,11 @@ export const buildApplication = async (
|
||||
await mechanizeDockerContainer(application);
|
||||
writeStream.write("Docker Deployed: ✅");
|
||||
} catch (error) {
|
||||
writeStream.write("Error ❌");
|
||||
if (error instanceof Error) {
|
||||
writeStream.write(`Error ❌\n${error?.message}`);
|
||||
} else {
|
||||
writeStream.write("Error ❌");
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
writeStream.end();
|
||||
|
@ -1,18 +1,28 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { buildStatic } from "@/server/utils/builders/static";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { prepareEnvironmentVariables } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
import { spawnAsync } from "../process/spawnAsync";
|
||||
|
||||
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
|
||||
export const buildNixpacks = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { env, appName } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const { env, appName, publishDirectory } = application;
|
||||
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
const buildContainerId = `${appName}-${nanoid(10)}`;
|
||||
const envVariables = prepareEnvironmentVariables(env);
|
||||
|
||||
const writeToStream = (data: string) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const args = ["build", buildAppDirectory, "--name", appName];
|
||||
|
||||
@ -20,13 +30,44 @@ export const buildNixpacks = async (
|
||||
args.push("--env", env);
|
||||
}
|
||||
|
||||
await spawnAsync("nixpacks", args, (data) => {
|
||||
if (writeStream.writable) {
|
||||
writeStream.write(data);
|
||||
}
|
||||
});
|
||||
if (publishDirectory) {
|
||||
/* No need for any start command, since we'll use nginx later on */
|
||||
args.push("--no-error-without-start");
|
||||
}
|
||||
|
||||
await spawnAsync("nixpacks", args, writeToStream);
|
||||
|
||||
/*
|
||||
Run the container with the image created by nixpacks,
|
||||
and copy the artifacts on the host filesystem.
|
||||
Then, remove the container and create a static build.
|
||||
*/
|
||||
|
||||
if (publishDirectory) {
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
["create", "--name", buildContainerId, appName],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
await spawnAsync(
|
||||
"docker",
|
||||
[
|
||||
"cp",
|
||||
`${buildContainerId}:/app/${publishDirectory}`,
|
||||
path.join(buildAppDirectory, publishDirectory),
|
||||
],
|
||||
writeToStream,
|
||||
);
|
||||
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
await buildStatic(application, writeStream);
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
38
apps/dokploy/server/utils/builders/static.ts
Normal file
38
apps/dokploy/server/utils/builders/static.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { WriteStream } from "node:fs";
|
||||
import { buildCustomDocker } from "@/server/utils/builders/docker-file";
|
||||
import type { ApplicationNested } from ".";
|
||||
import { createFile } from "../docker/utils";
|
||||
import { getBuildAppDirectory } from "../filesystem/directory";
|
||||
|
||||
export const buildStatic = async (
|
||||
application: ApplicationNested,
|
||||
writeStream: WriteStream,
|
||||
) => {
|
||||
const { publishDirectory } = application;
|
||||
const buildAppDirectory = getBuildAppDirectory(application);
|
||||
|
||||
try {
|
||||
createFile(
|
||||
buildAppDirectory,
|
||||
"Dockerfile",
|
||||
[
|
||||
"FROM nginx:alpine",
|
||||
"WORKDIR /usr/share/nginx/html/",
|
||||
`COPY ${publishDirectory || "."} .`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
await buildCustomDocker(
|
||||
{
|
||||
...application,
|
||||
buildType: "dockerfile",
|
||||
dockerfile: "Dockerfile",
|
||||
},
|
||||
writeStream,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
@ -242,12 +242,7 @@ export const generateConfigContainer = (application: ApplicationNested) => {
|
||||
? {
|
||||
RestartPolicy: restartPolicySwarm,
|
||||
}
|
||||
: {
|
||||
// if no restartPolicySwarm provided use default
|
||||
RestartPolicy: {
|
||||
Condition: "on-failure",
|
||||
},
|
||||
}),
|
||||
: {}),
|
||||
...(placementSwarm
|
||||
? {
|
||||
Placement: placementSwarm,
|
||||
|
@ -89,3 +89,12 @@ export const getBuildAppDirectory = (application: Application) => {
|
||||
|
||||
return path.join(APPLICATIONS_PATH, appName, "code", buildPath ?? "");
|
||||
};
|
||||
|
||||
export const getDockerContextPath = (application: Application) => {
|
||||
const { appName, dockerContextPath } = application;
|
||||
|
||||
if (!dockerContextPath) {
|
||||
return null;
|
||||
}
|
||||
return path.join(APPLICATIONS_PATH, appName, "code", dockerContextPath);
|
||||
};
|
||||
|
@ -33,7 +33,9 @@ export const cloneGitRepository = async (
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
|
||||
try {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
if (!isHttpOrHttps(customGitUrl)) {
|
||||
await addHostToKnownHosts(customGitUrl);
|
||||
}
|
||||
await recreateDirectory(outputPath);
|
||||
// const command = `GIT_SSH_COMMAND="ssh -i ${keyPath} -o UserKnownHostsFile=${knownHostsPath}" git clone --branch ${customGitBranch} --depth 1 ${customGitUrl} ${gitCopyPath} --progress`;
|
||||
// const { stdout, stderr } = await execAsync(command);
|
||||
@ -56,6 +58,7 @@ export const cloneGitRepository = async (
|
||||
customGitBranch,
|
||||
"--depth",
|
||||
"1",
|
||||
"--recurse-submodules",
|
||||
customGitUrl,
|
||||
outputPath,
|
||||
"--progress",
|
||||
@ -84,6 +87,11 @@ export const cloneGitRepository = async (
|
||||
}
|
||||
};
|
||||
|
||||
const isHttpOrHttps = (url: string): boolean => {
|
||||
const regex = /^https?:\/\//;
|
||||
return regex.test(url);
|
||||
};
|
||||
|
||||
const addHostToKnownHosts = async (repositoryURL: string) => {
|
||||
const { domain, port } = sanitizeRepoPathSSH(repositoryURL);
|
||||
const knownHostsPath = path.join(SSH_PATH, "known_hosts");
|
||||
@ -121,7 +129,7 @@ const sanitizeRepoPathSSH = (input: string) => {
|
||||
return {
|
||||
user: found.groups?.user ?? "git",
|
||||
domain: found.groups?.domain,
|
||||
port: 22,
|
||||
port: Number(found.groups?.port ?? 22),
|
||||
owner: found.groups?.owner ?? "",
|
||||
repo: found.groups?.repo,
|
||||
get repoPath() {
|
||||
|
@ -77,7 +77,7 @@ export const createRouterConfig = async (
|
||||
|
||||
const { host, path, https, uniqueConfigKey } = domain;
|
||||
const routerConfig: HttpRouter = {
|
||||
rule: `Host(\`${host}\`)${path ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
rule: `Host(\`${host}\`)${path !== null && path !== "/" ? ` && PathPrefix(\`${path}\`)` : ""}`,
|
||||
service: `${appName}-service-${uniqueConfigKey}`,
|
||||
middlewares: [],
|
||||
entryPoints: [entryPoint],
|
||||
|
@ -19,14 +19,20 @@ export const updateServerTraefik = (
|
||||
|
||||
const currentRouterConfig = config.http.routers[`${appName}-router-app`];
|
||||
|
||||
if (currentRouterConfig) {
|
||||
if (newHost) {
|
||||
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
|
||||
}
|
||||
if (currentRouterConfig && newHost) {
|
||||
currentRouterConfig.rule = `Host(\`${newHost}\`)`;
|
||||
|
||||
if (admin?.certificateType === "letsencrypt") {
|
||||
currentRouterConfig.tls = { certResolver: "letsencrypt" };
|
||||
} else if (admin?.certificateType === "none") {
|
||||
currentRouterConfig.tls = undefined;
|
||||
config.http.routers[`${appName}-router-app-secure`] = {
|
||||
...currentRouterConfig,
|
||||
entryPoints: ["websecure"],
|
||||
tls: { certResolver: "letsencrypt" },
|
||||
};
|
||||
|
||||
currentRouterConfig.middlewares = ["redirect-to-https"];
|
||||
} else {
|
||||
delete config.http.routers[`${appName}-router-app-secure`];
|
||||
currentRouterConfig.middlewares = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./config.toml",
|
||||
filePath: "config.toml",
|
||||
content: `[app]
|
||||
address = "0.0.0.0:9000"
|
||||
|
||||
|
@ -23,7 +23,7 @@ export function generate(schema: Schema): Template {
|
||||
|
||||
const mounts: Template["mounts"] = [
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-config.xml",
|
||||
filePath: "/clickhouse/clickhouse-config.xml",
|
||||
content: `
|
||||
<clickhouse>
|
||||
<logger>
|
||||
@ -45,7 +45,7 @@ export function generate(schema: Schema): Template {
|
||||
`,
|
||||
},
|
||||
{
|
||||
mountPath: "./clickhouse/clickhouse-user-config.xml",
|
||||
filePath: "/clickhouse/clickhouse-user-config.xml",
|
||||
content: `
|
||||
<clickhouse>
|
||||
<profiles>
|
||||
|
81
apps/dokploy/templates/teable/docker-compose.yml
Normal file
81
apps/dokploy/templates/teable/docker-compose.yml
Normal file
@ -0,0 +1,81 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
teable:
|
||||
image: ghcr.io/teableio/teable:1.3.1-alpha-build.460
|
||||
restart: always
|
||||
ports:
|
||||
- ${TEABLE_PORT}
|
||||
volumes:
|
||||
- teable-data:/app/.assets
|
||||
# you may use a bind-mounted host directory instead,
|
||||
# so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/teable/data:/app/.assets:rw
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- NEXT_ENV_IMAGES_ALL_REMOTE=true
|
||||
- PUBLIC_ORIGIN=${PUBLIC_ORIGIN}
|
||||
- PRISMA_DATABASE_URL=${PRISMA_DATABASE_URL}
|
||||
- PUBLIC_DATABASE_PROXY=${PUBLIC_DATABASE_PROXY}
|
||||
- BACKEND_MAIL_HOST=${BACKEND_MAIL_HOST}
|
||||
- BACKEND_MAIL_PORT=${BACKEND_MAIL_PORT}
|
||||
- BACKEND_MAIL_SECURE=${BACKEND_MAIL_SECURE}
|
||||
- BACKEND_MAIL_SENDER=${BACKEND_MAIL_SENDER}
|
||||
- BACKEND_MAIL_SENDER_NAME=${BACKEND_MAIL_SENDER_NAME}
|
||||
- BACKEND_MAIL_AUTH_USER=${BACKEND_MAIL_AUTH_USER}
|
||||
- BACKEND_MAIL_AUTH_PASS=${BACKEND_MAIL_AUTH_PASS}
|
||||
networks:
|
||||
- dokploy-network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.${HASH}.rule=Host(`${TEABLE_HOST}`)"
|
||||
- "traefik.http.services.${HASH}.loadbalancer.server.port=${TEABLE_PORT}"
|
||||
depends_on:
|
||||
teable-db-migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
teable-db:
|
||||
image: postgres:15.4
|
||||
restart: always
|
||||
ports:
|
||||
- "${TEABLE_DB_PORT}:${POSTGRES_PORT}"
|
||||
volumes:
|
||||
- teable-db:/var/lib/postgresql/data
|
||||
# you may use a bind-mounted host directory instead,
|
||||
# so that it is harder to accidentally remove the volume and lose all your data!
|
||||
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
networks:
|
||||
- dokploy-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"sh -c 'pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}'",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
teable-db-migrate:
|
||||
image: ghcr.io/teableio/teable-db-migrate:latest
|
||||
environment:
|
||||
- TZ=${TIMEZONE}
|
||||
- PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
|
||||
networks:
|
||||
- dokploy-network
|
||||
depends_on:
|
||||
teable-db:
|
||||
condition: service_healthy
|
||||
|
||||
networks:
|
||||
dokploy-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
teable-data: {}
|
||||
teable-db: {}
|
48
apps/dokploy/templates/teable/index.ts
Normal file
48
apps/dokploy/templates/teable/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {
|
||||
type Schema,
|
||||
type Template,
|
||||
generateHash,
|
||||
generatePassword,
|
||||
generateRandomDomain,
|
||||
} from "../utils";
|
||||
|
||||
export function generate(schema: Schema): Template {
|
||||
const mainServiceHash = generateHash(schema.projectName);
|
||||
const password = generatePassword();
|
||||
const randomDomain = generateRandomDomain(schema);
|
||||
|
||||
const publicDbPort = ((min: number, max: number) => {
|
||||
return Math.round(Math.random() * (max - min) + min);
|
||||
})(32769, 65534);
|
||||
|
||||
const envs = [
|
||||
`TEABLE_HOST=${randomDomain}`,
|
||||
"TEABLE_PORT=3000",
|
||||
`TEABLE_DB_PORT=${publicDbPort}`,
|
||||
`HASH=${mainServiceHash}`,
|
||||
"TIMEZONE=UTC",
|
||||
"# Postgres",
|
||||
"POSTGRES_HOST=teable-db",
|
||||
"POSTGRES_PORT=5432",
|
||||
"POSTGRES_DB=teable",
|
||||
"POSTGRES_USER=teable",
|
||||
`POSTGRES_PASSWORD=${password}`,
|
||||
"# App",
|
||||
"PUBLIC_ORIGIN=https://${TEABLE_HOST}",
|
||||
"PRISMA_DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}",
|
||||
"PUBLIC_DATABASE_PROXY=${TEABLE_HOST}:${TEABLE_DB_PORT}",
|
||||
"# Need to support sending emails to enable the following configurations",
|
||||
"# You need to modify the configuration according to the actual situation, otherwise it will not be able to send emails correctly.",
|
||||
"#BACKEND_MAIL_HOST=smtp.teable.io",
|
||||
"#BACKEND_MAIL_PORT=465",
|
||||
"#BACKEND_MAIL_SECURE=true",
|
||||
"#BACKEND_MAIL_SENDER=noreply.teable.io",
|
||||
"#BACKEND_MAIL_SENDER_NAME=Teable",
|
||||
"#BACKEND_MAIL_AUTH_USER=username",
|
||||
"#BACKEND_MAIL_AUTH_PASS=password",
|
||||
];
|
||||
|
||||
return {
|
||||
envs,
|
||||
};
|
||||
}
|
@ -393,4 +393,19 @@ export const templates: TemplateData[] = [
|
||||
tags: ["media system"],
|
||||
load: () => import("./jellyfin/index").then((m) => m.generate),
|
||||
},
|
||||
{
|
||||
id: "teable",
|
||||
name: "teable",
|
||||
version: "v1.3.1-alpha-build.460",
|
||||
description:
|
||||
"Teable is a Super fast, Real-time, Professional, Developer friendly, No-code database built on Postgres. It uses a simple, spreadsheet-like interface to create complex enterprise-level database applications. Unlock efficient app development with no-code, free from the hurdles of data security and scalability.",
|
||||
logo: "teable.png",
|
||||
links: {
|
||||
github: "https://github.com/teableio/teable",
|
||||
website: "https://teable.io/",
|
||||
docs: "https://help.teable.io/",
|
||||
},
|
||||
tags: ["database", "spreadsheet", "low-code", "nocode"],
|
||||
load: () => import("./teable/index").then((m) => m.generate),
|
||||
},
|
||||
];
|
||||
|
@ -13,7 +13,7 @@ export interface Schema {
|
||||
export interface Template {
|
||||
envs: string[];
|
||||
mounts?: {
|
||||
mountPath: string;
|
||||
filePath: string;
|
||||
content?: string;
|
||||
}[];
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
@ -65,6 +65,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
@ -81,6 +81,7 @@ docker service create \
|
||||
--network dokploy-network \
|
||||
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
|
||||
--mount type=bind,source=/etc/dokploy,target=/etc/dokploy \
|
||||
--mount type=volume,source=dokploy-docker-config,target=/root/.docker \
|
||||
--publish published=3000,target=3000,mode=host \
|
||||
--update-parallelism 1 \
|
||||
--update-order stop-first \
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 443 KiB |
18511
pnpm-lock.yaml
18511
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user