diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx index ad9a8ad7..0e35fce4 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/services/compose/[composeId].tsx @@ -219,21 +219,17 @@ const Service = ( General - {data?.composeType === "docker-compose" && ( - - Environment - - )} + Environment {!data?.serverId && ( Monitoring )} diff --git a/apps/dokploy/public/templates/evolutionapi.png b/apps/dokploy/public/templates/evolutionapi.png new file mode 100644 index 00000000..bd9b3850 Binary files /dev/null and b/apps/dokploy/public/templates/evolutionapi.png differ diff --git a/apps/dokploy/public/templates/formbricks.png b/apps/dokploy/public/templates/formbricks.png new file mode 100644 index 00000000..2bf1ca1f Binary files /dev/null and b/apps/dokploy/public/templates/formbricks.png differ diff --git a/apps/dokploy/templates/evolutionapi/docker-compose.yml b/apps/dokploy/templates/evolutionapi/docker-compose.yml new file mode 100644 index 00000000..54657e87 --- /dev/null +++ b/apps/dokploy/templates/evolutionapi/docker-compose.yml @@ -0,0 +1,61 @@ +services: + evolution-api: + image: atendai/evolution-api:v2.1.2 + restart: always + volumes: + - evolution-instances:/evolution/instances + networks: + - dokploy-network + environment: + - SERVER_URL=${SERVER_URL} + - AUTHENTICATION_TYPE=${AUTHENTICATION_TYPE} + - AUTHENTICATION_API_KEY=${AUTHENTICATION_API_KEY} + - AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=${AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES} + - LANGUAGE=${LANGUAGE} + - CONFIG_SESSION_PHONE_CLIENT=${CONFIG_SESSION_PHONE_CLIENT} + - CONFIG_SESSION_PHONE_NAME=${CONFIG_SESSION_PHONE_NAME} + - TELEMETRY=${TELEMETRY} + - TELEMETRY_URL=${TELEMETRY_URL} + - DATABASE_ENABLED=${DATABASE_ENABLED} + - DATABASE_PROVIDER=${DATABASE_PROVIDER} + - DATABASE_CONNECTION_URI=${DATABASE_CONNECTION_URI} + - DATABASE_SAVE_DATA_INSTANCE=${DATABASE_SAVE_DATA_INSTANCE} + - DATABASE_SAVE_DATA_NEW_MESSAGE=${DATABASE_SAVE_DATA_NEW_MESSAGE} + - DATABASE_SAVE_MESSAGE_UPDATE=${DATABASE_SAVE_MESSAGE_UPDATE} + - DATABASE_SAVE_DATA_CONTACTS=${DATABASE_SAVE_DATA_CONTACTS} + - DATABASE_SAVE_DATA_CHATS=${DATABASE_SAVE_DATA_CHATS} + - DATABASE_SAVE_DATA_LABELS=${DATABASE_SAVE_DATA_LABELS} + - DATABASE_SAVE_DATA_HISTORIC=${DATABASE_SAVE_DATA_HISTORIC} + - CACHE_REDIS_ENABLED=${CACHE_REDIS_ENABLED} + - CACHE_REDIS_URI=${CACHE_REDIS_URI} + - CACHE_REDIS_PREFIX_KEY=${CACHE_REDIS_PREFIX_KEY} + - CACHE_REDIS_SAVE_INSTANCES=${CACHE_REDIS_SAVE_INSTANCES} + + evolution-postgres: + image: postgres:16-alpine + restart: always + volumes: + - evolution-postgres-data:/var/lib/postgresql/data + networks: + - dokploy-network + environment: + - POSTGRES_DB=${POSTGRES_DATABASE} + - POSTGRES_USER=${POSTGRES_USERNAME} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + + evolution-redis: + image: redis:alpine + restart: always + volumes: + - evolution-redis-data:/data + networks: + - dokploy-network + +networks: + dokploy-network: + external: true + +volumes: + evolution-instances: + evolution-postgres-data: + evolution-redis-data: \ No newline at end of file diff --git a/apps/dokploy/templates/evolutionapi/index.ts b/apps/dokploy/templates/evolutionapi/index.ts new file mode 100644 index 00000000..6ca7a3b6 --- /dev/null +++ b/apps/dokploy/templates/evolutionapi/index.ts @@ -0,0 +1,59 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generatePassword, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const apiKey = generateBase64(64); + const postgresPassword = generatePassword(); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 8080, + serviceName: "evolution-api", + }, + ]; + + const envs = [ + `SERVER_URL=https://${mainDomain}`, + "AUTHENTICATION_TYPE=apikey", + `AUTHENTICATION_API_KEY=${apiKey}`, + "AUTHENTICATION_EXPOSE_IN_FETCH_INSTANCES=true", + + "LANGUAGE=en", + "CONFIG_SESSION_PHONE_CLIENT=Evolution API", + "CONFIG_SESSION_PHONE_NAME=Chrome", + "TELEMETRY=false", + "TELEMETRY_URL=", + + "POSTGRES_DATABASE=evolution", + "POSTGRES_USERNAME=postgresql", + `POSTGRES_PASSWORD=${postgresPassword}`, + "DATABASE_ENABLED=true", + "DATABASE_PROVIDER=postgresql", + `DATABASE_CONNECTION_URI=postgres://postgresql:${postgresPassword}@evolution-postgres:5432/evolution`, + "DATABASE_SAVE_DATA_INSTANCE=true", + "DATABASE_SAVE_DATA_NEW_MESSAGE=true", + "DATABASE_SAVE_MESSAGE_UPDATE=true", + "DATABASE_SAVE_DATA_CONTACTS=true", + "DATABASE_SAVE_DATA_CHATS=true", + "DATABASE_SAVE_DATA_LABELS=true", + "DATABASE_SAVE_DATA_HISTORIC=true", + + "CACHE_REDIS_ENABLED=true", + "CACHE_REDIS_URI=redis://evolution-redis:6379", + "CACHE_REDIS_PREFIX_KEY=evolution", + "CACHE_REDIS_SAVE_INSTANCES=true", + ]; + + return { + domains, + envs, + }; +} diff --git a/apps/dokploy/templates/formbricks/docker-compose.yml b/apps/dokploy/templates/formbricks/docker-compose.yml new file mode 100644 index 00000000..558f8b4d --- /dev/null +++ b/apps/dokploy/templates/formbricks/docker-compose.yml @@ -0,0 +1,38 @@ +x-environment: &environment + environment: + WEBAPP_URL: ${WEBAPP_URL} + NEXTAUTH_URL: ${NEXTAUTH_URL} + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + ENCRYPTION_KEY: ${ENCRYPTION_KEY} + CRON_SECRET: ${CRON_SECRET} + EMAIL_VERIFICATION_DISABLED: 1 + PASSWORD_RESET_DISABLED: 1 + S3_FORCE_PATH_STYLE: 0 + +services: + postgres: + restart: always + image: pgvector/pgvector:pg17 + volumes: + - postgres:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + networks: + - dokploy-network + + formbricks: + restart: always + image: ghcr.io/formbricks/formbricks:v3.1.3 + depends_on: + - postgres + ports: + - 3000 + volumes: + - ../files/uploads:/home/nextjs/apps/web/uploads/ + <<: *environment + +volumes: + postgres: + driver: local + uploads: diff --git a/apps/dokploy/templates/formbricks/index.ts b/apps/dokploy/templates/formbricks/index.ts new file mode 100644 index 00000000..fc179f49 --- /dev/null +++ b/apps/dokploy/templates/formbricks/index.ts @@ -0,0 +1,38 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateBase64, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const mainDomain = generateRandomDomain(schema); + const secretBase = generateBase64(64); + const encryptionKey = generateBase64(48); + const cronSecret = generateBase64(32); + + const domains: DomainSchema[] = [ + { + host: mainDomain, + port: 3000, + serviceName: "formbricks", + }, + ]; + + const envs = [ + `WEBAPP_URL=http://${mainDomain}`, + `NEXTAUTH_URL=http://${mainDomain}`, + `NEXTAUTH_SECRET=${secretBase}`, + `ENCRYPTION_KEY=${encryptionKey}`, + `CRON_SECRET=${cronSecret}`, + ]; + + const mounts: Template["mounts"] = []; + + return { + envs, + mounts, + domains, + }; +} diff --git a/apps/dokploy/templates/listmonk/docker-compose.yml b/apps/dokploy/templates/listmonk/docker-compose.yml index 725d0a09..17b09f41 100644 --- a/apps/dokploy/templates/listmonk/docker-compose.yml +++ b/apps/dokploy/templates/listmonk/docker-compose.yml @@ -1,6 +1,6 @@ services: db: - image: postgres:13 + image: postgres:17-alpine ports: - 5432 networks: @@ -19,7 +19,7 @@ services: - listmonk-data:/var/lib/postgresql/data setup: - image: listmonk/listmonk:v3.0.0 + image: listmonk/listmonk:v4.1.0 networks: - dokploy-network volumes: @@ -35,7 +35,7 @@ services: app: restart: unless-stopped - image: listmonk/listmonk:v3.0.0 + image: listmonk/listmonk:v4.1.0 environment: - TZ=Etc/UTC depends_on: diff --git a/apps/dokploy/templates/listmonk/index.ts b/apps/dokploy/templates/listmonk/index.ts index 725659ca..2a25efca 100644 --- a/apps/dokploy/templates/listmonk/index.ts +++ b/apps/dokploy/templates/listmonk/index.ts @@ -2,13 +2,11 @@ import { type DomainSchema, type Schema, type Template, - generatePassword, generateRandomDomain, } from "../utils"; export function generate(schema: Schema): Template { const randomDomain = generateRandomDomain(schema); - const adminPassword = generatePassword(32); const domains: DomainSchema[] = [ { @@ -19,7 +17,7 @@ export function generate(schema: Schema): Template { ]; const envs = [ - `# login with admin:${adminPassword}`, + "# visit the page to setup your super admin user", "# check config.toml in Advanced / Volumes for more options", ]; @@ -29,9 +27,6 @@ export function generate(schema: Schema): Template { content: `[app] address = "0.0.0.0:9000" -admin_username = "admin" -admin_password = "${adminPassword}" - [db] host = "db" port = 5432 diff --git a/apps/dokploy/templates/teable/docker-compose.yml b/apps/dokploy/templates/teable/docker-compose.yml index b96b677c..959de21c 100644 --- a/apps/dokploy/templates/teable/docker-compose.yml +++ b/apps/dokploy/templates/teable/docker-compose.yml @@ -2,7 +2,7 @@ version: "3.9" services: teable: - image: ghcr.io/teableio/teable:1.3.1-alpha-build.460 + image: ghcr.io/teableio/teable:latest restart: always volumes: - teable-data:/app/.assets diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 98bbdbc5..f8dac16e 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1239,6 +1239,21 @@ export const templates: TemplateData[] = [ tags: ["matrix", "communication"], load: () => import("./conduit/index").then((m) => m.generate), }, + { + id: "evolutionapi", + name: "Evolution API", + version: "v2.1.2", + description: + "Evolution API is a robust platform dedicated to empowering small businesses with limited resources, going beyond a simple messaging solution via WhatsApp.", + logo: "evolutionapi.png", + links: { + github: "https://github.com/EvolutionAPI/evolution-api", + docs: "https://doc.evolution-api.com/v2/en/get-started/introduction", + website: "https://evolution-api.com/opensource-whatsapp-api/", + }, + tags: ["api", "whatsapp", "messaging"], + load: () => import("./evolutionapi/index").then((m) => m.generate), + }, { id: "conduwuit", name: "Conduwuit", @@ -1438,4 +1453,19 @@ export const templates: TemplateData[] = [ tags: ["sharing", "shortener", "url"], load: () => import("./shlink/index").then((m) => m.generate), }, + { + id: "formbricks", + name: "Formbricks", + version: "v3.1.3", + description: + "Formbricks is an open-source survey and form platform for collecting user data.", + logo: "formbricks.png", + links: { + github: "https://github.com/formbricks/formbricks", + website: "https://formbricks.com/", + docs: "https://formbricks.com/docs", + }, + tags: ["forms", "analytics"], + load: () => import("./formbricks/index").then((m) => m.generate), + }, ]; diff --git a/packages/server/src/services/compose.ts b/packages/server/src/services/compose.ts index a0492314..39bf423f 100644 --- a/packages/server/src/services/compose.ts +++ b/packages/server/src/services/compose.ts @@ -558,6 +558,17 @@ export const stopCompose = async (composeId: string) => { } } + if (compose.composeType === "stack") { + if (compose.serverId) { + await execAsyncRemote( + compose.serverId, + `docker stack rm ${compose.appName}`, + ); + } else { + await execAsync(`docker stack rm ${compose.appName}`); + } + } + await updateCompose(composeId, { composeStatus: "idle", }); diff --git a/packages/server/src/utils/builders/compose.ts b/packages/server/src/utils/builders/compose.ts index ae181fc3..af560728 100644 --- a/packages/server/src/utils/builders/compose.ts +++ b/packages/server/src/utils/builders/compose.ts @@ -2,6 +2,7 @@ import { createWriteStream, existsSync, mkdirSync, + readFileSync, writeFileSync, } from "node:fs"; import { dirname, join } from "node:path"; @@ -12,8 +13,12 @@ import { writeDomainsToCompose, writeDomainsToComposeRemote, } from "../docker/domain"; -import { encodeBase64, prepareEnvironmentVariables } from "../docker/utils"; -import { execAsyncRemote } from "../process/execAsync"; +import { + encodeBase64, + getEnviromentVariablesObject, + prepareEnvironmentVariables, +} from "../docker/utils"; +import { execAsync, execAsyncRemote } from "../process/execAsync"; import { spawnAsync } from "../process/spawnAsync"; export type ComposeNested = InferResultType< @@ -30,12 +35,12 @@ export const buildCompose = async (compose: ComposeNested, logPath: string) => { createEnvFile(compose); const logContent = ` -App Name: ${appName} -Build Compose 🐳 -Detected: ${mounts.length} mounts 📂 -Command: docker ${command} -Source Type: docker ${sourceType} ✅ -Compose Type: ${composeType} ✅`; + App Name: ${appName} + Build Compose 🐳 + Detected: ${mounts.length} mounts 📂 + Command: docker ${command} + Source Type: docker ${sourceType} ✅ + Compose Type: ${composeType} ✅`; const logBox = boxen(logContent, { padding: { left: 1, @@ -46,7 +51,6 @@ Compose Type: ${composeType} ✅`; borderStyle: "double", }); writeStream.write(`\n${logBox}\n`); - const projectPath = join(COMPOSE_PATH, compose.appName, "code"); await spawnAsync( @@ -62,6 +66,9 @@ Compose Type: ${composeType} ✅`; env: { NODE_ENV: process.env.NODE_ENV, PATH: process.env.PATH, + ...(composeType === "stack" && { + ...getEnviromentVariablesObject(compose.env, compose.project.env), + }), }, }, ); @@ -85,6 +92,7 @@ export const getBuildComposeCommand = async ( const command = createCommand(compose); const envCommand = getCreateEnvFileCommand(compose); const projectPath = join(COMPOSE_PATH, compose.appName, "code"); + const exportEnvCommand = getExportEnvCommand(compose); const newCompose = await writeDomainsToComposeRemote( compose, @@ -120,6 +128,8 @@ Compose Type: ${composeType} ✅`; cd "${projectPath}"; + ${exportEnvCommand} + docker ${command.split(" ").join(" ")} >> "${logPath}" 2>&1 || { echo "Error: ❌ Docker command failed" >> "${logPath}"; exit 1; } echo "Docker Compose Deployed: ✅" >> "${logPath}" @@ -153,9 +163,7 @@ export const createCommand = (compose: ComposeNested) => { sourceType === "raw" ? "docker-compose.yml" : compose.composePath; let command = ""; - if (composeType === "docker-compose") { - command = `compose -p ${appName} -f ${path} up -d --build --remove-orphans`; - } else if (composeType === "stack") { + if (composeType === "stack") { command = `stack deploy -c ${path} ${appName} --prune`; } @@ -219,3 +227,17 @@ touch ${envFilePath}; echo "${encodedContent}" | base64 -d > "${envFilePath}"; `; }; + +const getExportEnvCommand = (compose: ComposeNested) => { + if (compose.composeType !== "stack") return ""; + + const envVars = getEnviromentVariablesObject( + compose.env, + compose.project.env, + ); + const exports = Object.entries(envVars) + .map(([key, value]) => `export ${key}=${JSON.stringify(value)}`) + .join("\n"); + + return exports ? `\n# Export environment variables\n${exports}\n` : ""; +}; diff --git a/packages/server/src/utils/docker/utils.ts b/packages/server/src/utils/docker/utils.ts index 64e98306..062e0722 100644 --- a/packages/server/src/utils/docker/utils.ts +++ b/packages/server/src/utils/docker/utils.ts @@ -278,12 +278,15 @@ export const prepareEnvironmentVariables = ( return resolvedVars; }; -export const prepareBuildArgs = (input: string | null) => { - const pairs = (input ?? "").split("\n"); +export const getEnviromentVariablesObject = ( + input: string | null, + projectEnv?: string | null, +) => { + const envs = prepareEnvironmentVariables(input, projectEnv); const jsonObject: Record = {}; - for (const pair of pairs) { + for (const pair of envs) { const [key, value] = pair.split("="); if (key && value) { jsonObject[key] = value;