Merge pull request #312 from Dokploy/canary

v0.6.0
This commit is contained in:
Mauricio Siu
2024-08-02 10:47:43 -06:00
committed by GitHub
28 changed files with 9440 additions and 33 deletions

1
.gitignore vendored
View File

@@ -35,6 +35,7 @@ yarn-error.log*
# Editor # Editor
.vscode .vscode
.idea
# Misc # Misc
.DS_Store .DS_Store

View File

@@ -59,7 +59,7 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Premium Supporters 🥇 ### Premium Supporters 🥇
<div style="display: flex; gap: 30px; flex-wrap: wrap;"> <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="Supafort.com" 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> </div>
<!-- Elite Contributors 🥈 --> <!-- Elite Contributors 🥈 -->
@@ -69,13 +69,13 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Supporting Members 🥉 ### Supporting Members 🥉
<div style="display: flex; gap: 30px; flex-wrap: wrap;"> <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> </div>
### Community Backers 🤝 ### Community Backers 🤝
<div style="display: flex; gap: 30px; flex-wrap: wrap;"> <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="Steamsets.com"/></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> </div>
#### Organizations: #### Organizations:

View File

@@ -32,7 +32,8 @@ The following templates are available:
- **Metabase**: Open Source Business Intelligence - **Metabase**: Open Source Business Intelligence
- **Grafana**: Open Source Dashboard for your metrics - **Grafana**: Open Source Dashboard for your metrics
- **Wordpress**: Open Source Content Management System - **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
@@ -42,4 +43,4 @@ We accept contributions to upload new templates to the dokploy repository.
Make sure to follow the guidelines for creating a template: 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) [Steps to create your own template](https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#templates)

View File

@@ -78,7 +78,7 @@ test("Should not touch config without host", () => {
expect(originalConfig).toEqual(config); expect(originalConfig).toEqual(config);
}); });
test("Should remove web-secure if https rollback to http", () => { test("Should remove websecure if https rollback to http", () => {
const originalConfig: FileConfig = loadOrCreateConfig("dokploy"); const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
updateServerTraefik( updateServerTraefik(

View File

@@ -40,6 +40,7 @@ const baseApp: ApplicationNested = {
placementSwarm: null, placementSwarm: null,
ports: [], ports: [],
projectId: "", projectId: "",
publishDirectory: null,
redirects: [], redirects: [],
refreshToken: "", refreshToken: "",
registry: null, registry: null,
@@ -54,6 +55,7 @@ const baseApp: ApplicationNested = {
title: null, title: null,
updateConfigSwarm: null, updateConfigSwarm: null,
username: null, username: null,
dockerContextPath: null,
}; };
const baseDomain: Domain = { const baseDomain: Domain = {

View File

@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { import {
Form, Form,
FormControl, FormControl,
FormDescription,
FormField, FormField,
FormItem, FormItem,
FormLabel, FormLabel,
@@ -23,6 +24,7 @@ enum BuildType {
heroku_buildpacks = "heroku_buildpacks", heroku_buildpacks = "heroku_buildpacks",
paketo_buildpacks = "paketo_buildpacks", paketo_buildpacks = "paketo_buildpacks",
nixpacks = "nixpacks", nixpacks = "nixpacks",
static = "static",
} }
const mySchema = z.discriminatedUnion("buildType", [ const mySchema = z.discriminatedUnion("buildType", [
@@ -34,6 +36,7 @@ const mySchema = z.discriminatedUnion("buildType", [
invalid_type_error: "Dockerfile path is required", invalid_type_error: "Dockerfile path is required",
}) })
.min(1, "Dockerfile required"), .min(1, "Dockerfile required"),
dockerContextPath: z.string().nullable().default(""),
}), }),
z.object({ z.object({
buildType: z.literal("heroku_buildpacks"), buildType: z.literal("heroku_buildpacks"),
@@ -43,6 +46,10 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal("nixpacks"), 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"); const buildType = form.watch("buildType");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
// TODO: refactor this
if (data.buildType === "dockerfile") { if (data.buildType === "dockerfile") {
form.reset({ form.reset({
buildType: data.buildType, buildType: data.buildType,
...(data.buildType && { ...(data.buildType && {
dockerfile: data.dockerfile || "", dockerfile: data.dockerfile || "",
dockerContextPath: data.dockerContextPath || "",
}), }),
}); });
} else { } else {
form.reset({ form.reset({
buildType: data.buildType, buildType: data.buildType,
publishDirectory: data.publishDirectory || undefined,
}); });
} }
} }
@@ -93,7 +101,11 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId,
buildType: data.buildType, buildType: data.buildType,
publishDirectory:
data.buildType === "nixpacks" ? data.publishDirectory : null,
dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null, dockerfile: data.buildType === "dockerfile" ? data.dockerfile : null,
dockerContextPath:
data.buildType === "dockerfile" ? data.dockerContextPath : null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -171,6 +183,12 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
Paketo Buildpacks Paketo Buildpacks
</FormLabel> </FormLabel>
</FormItem> </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> </RadioGroup>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -179,16 +197,71 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
}} }}
/> />
{buildType === "dockerfile" && ( {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 <FormField
control={form.control} control={form.control}
name="dockerfile" name="publishDirectory"
render={({ field }) => { render={({ field }) => {
return ( return (
<FormItem> <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> <FormControl>
<Input <Input
placeholder={"Path of your docker file"} placeholder={"Publish Directory"}
{...field} {...field}
value={field.value ?? ""} value={field.value ?? ""}
/> />

View File

@@ -114,7 +114,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
<FormItem> <FormItem>
<FormLabel>Password</FormLabel> <FormLabel>Password</FormLabel>
<FormControl> <FormControl>
<Input placeholder="Password" {...field} /> <Input placeholder="Password" {...field} type="password" />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "publishDirectory" text;

View File

@@ -0,0 +1 @@
ALTER TYPE "buildType" ADD VALUE 'static';

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "dockerContextPath" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -190,6 +190,27 @@
"when": 1721979220929, "when": 1721979220929,
"tag": "0026_known_dormammu", "tag": "0026_known_dormammu",
"breakpoints": true "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
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.5.1", "version": "v0.6.0",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

View File

@@ -191,6 +191,8 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
buildType: input.buildType, buildType: input.buildType,
dockerfile: input.dockerfile, dockerfile: input.dockerfile,
publishDirectory: input.publishDirectory,
dockerContextPath: input.dockerContextPath,
}); });
return true; return true;

View File

@@ -35,6 +35,7 @@ export const buildType = pgEnum("buildType", [
"heroku_buildpacks", "heroku_buildpacks",
"paketo_buildpacks", "paketo_buildpacks",
"nixpacks", "nixpacks",
"static",
]); ]);
// TODO: refactor this types // TODO: refactor this types
@@ -140,6 +141,7 @@ export const applications = pgTable("application", {
}, },
), ),
dockerfile: text("dockerfile"), dockerfile: text("dockerfile"),
dockerContextPath: text("dockerContextPath"),
// Drop // Drop
dropBuildPath: text("dropBuildPath"), dropBuildPath: text("dropBuildPath"),
// Docker swarm json // Docker swarm json
@@ -157,6 +159,7 @@ export const applications = pgTable("application", {
.notNull() .notNull()
.default("idle"), .default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"), buildType: buildType("buildType").notNull().default("nixpacks"),
publishDirectory: text("publishDirectory"),
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
@@ -315,7 +318,9 @@ const createSchema = createInsertSchema(applications, {
"heroku_buildpacks", "heroku_buildpacks",
"paketo_buildpacks", "paketo_buildpacks",
"nixpacks", "nixpacks",
"static",
]), ]),
publishDirectory: z.string().optional(),
owner: z.string(), owner: z.string(),
healthCheckSwarm: HealthCheckSwarmSchema.nullable(), healthCheckSwarm: HealthCheckSwarmSchema.nullable(),
restartPolicySwarm: RestartPolicySwarmSchema.nullable(), restartPolicySwarm: RestartPolicySwarmSchema.nullable(),
@@ -352,8 +357,10 @@ export const apiSaveBuildType = createSchema
applicationId: true, applicationId: true,
buildType: true, buildType: true,
dockerfile: true, dockerfile: true,
dockerContextPath: true,
}) })
.required(); .required()
.merge(createSchema.pick({ publishDirectory: true }));
export const apiSaveGithubProvider = createSchema export const apiSaveGithubProvider = createSchema
.pick({ .pick({

View File

@@ -1,7 +1,10 @@
import type { WriteStream } from "node:fs"; import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@/server/utils/docker/utils"; import { prepareEnvironmentVariables } from "@/server/utils/docker/utils";
import type { ApplicationNested } from "."; import type { ApplicationNested } from ".";
import { getBuildAppDirectory } from "../filesystem/directory"; import {
getBuildAppDirectory,
getDockerContextPath,
} from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
import { createEnvFile } from "./utils"; import { createEnvFile } from "./utils";
@@ -9,22 +12,30 @@ export const buildCustomDocker = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { appName, env, buildArgs } = application; const { appName, env, publishDirectory, buildArgs } = application;
const dockerFilePath = getBuildAppDirectory(application); const dockerFilePath = getBuildAppDirectory(application);
try { try {
const image = `${appName}`; const image = `${appName}`;
const contextPath = const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(buildArgs); const args = prepareEnvironmentVariables(buildArgs);
const dockerContextPath = getDockerContextPath(application);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."]; const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); 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( await spawnAsync(
"docker", "docker",
commandArgs, commandArgs,
@@ -34,7 +45,7 @@ export const buildCustomDocker = async (
} }
}, },
{ {
cwd: contextPath, cwd: dockerContextPath || defaultContextPath,
}, },
); );
} catch (error) { } catch (error) {

View File

@@ -15,6 +15,7 @@ import { buildCustomDocker } from "./docker-file";
import { buildHeroku } from "./heroku"; import { buildHeroku } from "./heroku";
import { buildNixpacks } from "./nixpacks"; import { buildNixpacks } from "./nixpacks";
import { buildPaketo } from "./paketo"; import { buildPaketo } from "./paketo";
import { buildStatic } from "./static";
// NIXPACKS codeDirectory = where is the path of the code directory // NIXPACKS codeDirectory = where is the path of the code directory
// HEROKU 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); await buildPaketo(application, writeStream);
} else if (buildType === "dockerfile") { } else if (buildType === "dockerfile") {
await buildCustomDocker(application, writeStream); await buildCustomDocker(application, writeStream);
} else if (buildType === "static") {
await buildStatic(application, writeStream);
} }
if (application.registryId) { if (application.registryId) {

View File

@@ -1,18 +1,28 @@
import type { WriteStream } from "node:fs"; 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 type { ApplicationNested } from ".";
import { prepareEnvironmentVariables } from "../docker/utils"; import { prepareEnvironmentVariables } from "../docker/utils";
import { getBuildAppDirectory } from "../filesystem/directory"; import { getBuildAppDirectory } from "../filesystem/directory";
import { spawnAsync } from "../process/spawnAsync"; import { spawnAsync } from "../process/spawnAsync";
// TODO: integrate in the vps sudo chown -R $(whoami) ~/.docker
export const buildNixpacks = async ( export const buildNixpacks = async (
application: ApplicationNested, application: ApplicationNested,
writeStream: WriteStream, writeStream: WriteStream,
) => { ) => {
const { env, appName } = application; const { env, appName, publishDirectory } = application;
const buildAppDirectory = getBuildAppDirectory(application);
const buildAppDirectory = getBuildAppDirectory(application);
const buildContainerId = `${appName}-${nanoid(10)}`;
const envVariables = prepareEnvironmentVariables(env); const envVariables = prepareEnvironmentVariables(env);
const writeToStream = (data: string) => {
if (writeStream.writable) {
writeStream.write(data);
}
};
try { try {
const args = ["build", buildAppDirectory, "--name", appName]; const args = ["build", buildAppDirectory, "--name", appName];
@@ -20,13 +30,44 @@ export const buildNixpacks = async (
args.push("--env", env); args.push("--env", env);
} }
await spawnAsync("nixpacks", args, (data) => { if (publishDirectory) {
if (writeStream.writable) { /* No need for any start command, since we'll use nginx later on */
writeStream.write(data); 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; return true;
} catch (e) { } catch (e) {
await spawnAsync("docker", ["rm", buildContainerId], writeToStream);
throw e; throw e;
} }
}; };

View 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;
}
};

View File

@@ -242,12 +242,7 @@ export const generateConfigContainer = (application: ApplicationNested) => {
? { ? {
RestartPolicy: restartPolicySwarm, RestartPolicy: restartPolicySwarm,
} }
: { : {}),
// if no restartPolicySwarm provided use default
RestartPolicy: {
Condition: "on-failure",
},
}),
...(placementSwarm ...(placementSwarm
? { ? {
Placement: placementSwarm, Placement: placementSwarm,

View File

@@ -89,3 +89,12 @@ export const getBuildAppDirectory = (application: Application) => {
return path.join(APPLICATIONS_PATH, appName, "code", buildPath ?? ""); 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);
};

View File

@@ -25,7 +25,7 @@ export const updateServerTraefik = (
if (admin?.certificateType === "letsencrypt") { if (admin?.certificateType === "letsencrypt") {
config.http.routers[`${appName}-router-app-secure`] = { config.http.routers[`${appName}-router-app-secure`] = {
...currentRouterConfig, ...currentRouterConfig,
entryPoints: ["web-secure"], entryPoints: ["websecure"],
tls: { certResolver: "letsencrypt" }, tls: { certResolver: "letsencrypt" },
}; };

View 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: {}

View 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,
};
}

View File

@@ -393,4 +393,19 @@ export const templates: TemplateData[] = [
tags: ["media system"], tags: ["media system"],
load: () => import("./jellyfin/index").then((m) => m.generate), 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),
},
]; ];