diff --git a/.gitpod.Dockerfile b/.gitpod.Dockerfile new file mode 100644 index 000000000..62de37209 --- /dev/null +++ b/.gitpod.Dockerfile @@ -0,0 +1,2 @@ +FROM gitpod/workspace-node:2022-06-20-19-54-55 +RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack) \ No newline at end of file diff --git a/.gitpod.yml b/.gitpod.yml index d46244e67..2cd6c8113 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,7 +1,8 @@ # This configuration file was automatically generated by Gitpod. # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) # and commit this file to your remote git repository to share the goodness with others. -image: gitpod/workspace-node:2022-06-20-19-54-55 +image: + file: .gitpod.Dockerfile tasks: - init: pnpm install && pnpm db:push && pnpm db:seed command: pnpm dev diff --git a/README.md b/README.md index 40d7ac2dc..475d00d1d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,108 @@ # Coolify -An open-source & self-hostable Heroku / Netlify alternative -(ARM support is in beta). +An open-source & self-hostable Heroku / Netlify alternative. + +## Live Demo + +https://demo.coolify.io/ + +(If it is unresponsive, that means someone overloaded the server. 😄) + +## Feedback + +If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! + +--- + +## How to install + +For more details goto the [docs](https://docs.coollabs.io/coolify/installation.html). + +Installation is automated with the following command: + +```bash +wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh +``` + +If you would like no questions during installation: + +```bash +wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f +``` + +--- + +## Features + +### Git Sources + +Self-hosted versions also! + + + + +### Destinations + +Deploy your resource to: + +- Local Docker Engine +- Remote Docker Engine + +### Applications + + + + + + + + + + + + + + + + + + +### Databases + + + + + + + + +### Services +- [Appwrite](https://appwrite.io) +- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) +- [Ghost](https://ghost.org) +- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) +- [NocoDB](https://nocodb.com) +- [VSCode Server](https://github.com/cdr/code-server) +- [MinIO](https://min.io) +- [VaultWarden](https://github.com/dani-garcia/vaultwarden) +- [LanguageTool](https://languagetool.org) +- [n8n](https://n8n.io) +- [Uptime Kuma](https://github.com/louislam/uptime-kuma) +- [MeiliSearch](https://github.com/meilisearch/meilisearch) +- [Umami](https://github.com/mikecao/umami) +- [Fider](https://fider.io) +- [Hasura](https://hasura.io) + +## Migration from v1 + +A fresh installation is necessary. v2 and v3 are not compatible with v1. + +## Support + +- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) +- Telegram: [@andrasbacsai](https://t.me/andrasbacsai) +- Email: [andras@coollabs.io](mailto:andras@coollabs.io) +- Discord: [Invitation](https://discord.gg/6rDM4fkymF) ## Financial Contributors @@ -24,127 +125,4 @@ Support this project with your organization. Your logo will show up here with a - - ---- - -## Live Demo - -https://demo.coolify.io/ - -(If it is unresponsive, that means someone overloaded the server. 😄) - -## Feedback - -If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! - ---- - -## How to install - -Installation is automated with the following command: - -```bash -wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -``` - -If you would like no questions during installation: - -```bash -wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f -``` - -For more details goto the [docs](https://docs.coollabs.io/coolify/installation). - ---- - -## Features - -### Git Sources - -You can use the following Git Sources to be auto-deployed to your Coolify instance! (Self-hosted versions are also supported.) - - - - -### Destinations - -You can deploy your applications to the following destinations: - -- Local Docker Engine -- Remote Docker Engine - -### Applications - -Predefined build packs to cover the basic needs to deploy applications. - -If you have an advanced use case, you can use the Docker build pack that allows you to deploy your application based on your custom Dockerfile. - - - - - - - - - - - - - - - - - -If you have a new build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! - -### Databases - -One-click database is ready to be used internally or shared over the internet: - - - - - - - - -If you have a new database you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! - - -### Services - -You quickly need to host a self-hostable, open-source service? You can do it with a few clicks! -- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) -- [Ghost](https://ghost.org) -- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) -- [NocoDB](https://nocodb.com) -- [VSCode Server](https://github.com/cdr/code-server) -- [MinIO](https://min.io) -- [VaultWarden](https://github.com/dani-garcia/vaultwarden) -- [LanguageTool](https://languagetool.org) -- [n8n](https://n8n.io) -- [Uptime Kuma](https://github.com/louislam/uptime-kuma) -- [MeiliSearch](https://github.com/meilisearch/meilisearch) -- [Umami](https://github.com/mikecao/umami) -- [Fider](https://fider.io) -- [Hasura](https://hasura.io) - - -If you have a new service you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! - -## Migration from v1 - -A fresh installation is necessary. v2 and v3 are not compatible with v1. - -## Support - -- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) -- Telegram: [@andrasbacsai](https://t.me/andrasbacsai) -- Email: [andras@coollabs.io](mailto:andras@coollabs.io) -- Discord: [Invitation](https://discord.gg/xhBCC7eGKw) - - -## License - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text. + diff --git a/apps/api/prisma/migrations/20220815133844_appwrite/migration.sql b/apps/api/prisma/migrations/20220815133844_appwrite/migration.sql new file mode 100644 index 000000000..ec61dc434 --- /dev/null +++ b/apps/api/prisma/migrations/20220815133844_appwrite/migration.sql @@ -0,0 +1,22 @@ +-- CreateTable +CREATE TABLE "Appwrite" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "opensslKeyV1" TEXT NOT NULL, + "executorSecret" TEXT NOT NULL, + "redisPassword" TEXT NOT NULL, + "mariadbHost" TEXT, + "mariadbPort" INTEGER NOT NULL DEFAULT 3306, + "mariadbUser" TEXT NOT NULL, + "mariadbPassword" TEXT NOT NULL, + "mariadbRootUser" TEXT NOT NULL, + "mariadbRootUserPassword" TEXT NOT NULL, + "mariadbDatabase" TEXT NOT NULL, + "mariadbPublicPort" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Appwrite_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Appwrite_serviceId_key" ON "Appwrite"("serviceId"); diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 7d50b63b8..4877c6ffb 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -312,30 +312,33 @@ model DatabaseSettings { } model Service { - id String @id @default(cuid()) + id String @id @default(cuid()) name String fqdn String? exposePort Int? - dualCerts Boolean @default(false) + dualCerts Boolean @default(false) type String? version String? destinationDockerId String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) - fider Fider? - ghost Ghost? - hasura Hasura? - meiliSearch MeiliSearch? - minio Minio? - moodle Moodle? - plausibleAnalytics PlausibleAnalytics? - persistentStorage ServicePersistentStorage[] - serviceSecret ServiceSecret[] - umami Umami? - vscodeserver Vscodeserver? - wordpress Wordpress? - teams Team[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + + fider Fider? + ghost Ghost? + hasura Hasura? + meiliSearch MeiliSearch? + minio Minio? + moodle Moodle? + plausibleAnalytics PlausibleAnalytics? + persistentStorage ServicePersistentStorage[] + serviceSecret ServiceSecret[] + umami Umami? + vscodeserver Vscodeserver? + wordpress Wordpress? + appwrite Appwrite? + + teams Team[] } model PlausibleAnalytics { @@ -491,3 +494,22 @@ model Moodle { updatedAt DateTime @updatedAt service Service @relation(fields: [serviceId], references: [id]) } + +model Appwrite { + id String @id @default(cuid()) + serviceId String @unique + opensslKeyV1 String + executorSecret String + redisPassword String + mariadbHost String? + mariadbPort Int @default(3306) + mariadbUser String + mariadbPassword String + mariadbRootUser String + mariadbRootUserPassword String + mariadbDatabase String + mariadbPublicPort Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + service Service @relation(fields: [serviceId], references: [id]) +} diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 9782067d1..ff87ebe7c 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -152,6 +152,13 @@ import * as buildpacks from '../lib/buildPacks'; .createHash('sha256') .update( JSON.stringify({ + pythonWSGI, + pythonModule, + pythonVariable, + deploymentType, + denoOptions, + baseImage, + baseBuildImage, buildPack, port, exposePort, @@ -291,6 +298,7 @@ import * as buildpacks from '../lib/buildPacks'; } }; }); + console.log({port}) const composeFile = { version: '3.8', services: { diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 62aa271c1..6684664d6 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -252,6 +252,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st label: 'python:3.7-slim-bullseye' } ]; + const herokuVersions = [ + { + value: 'heroku/builder:22', + label: 'heroku/builder:22' + }, + { + value: 'heroku/buildpacks:20', + label: 'heroku/buildpacks:20' + }, + { + value: 'heroku/builder-classic:22', + label: 'heroku/builder-classic:22' + }, + ] let payload: any = { baseImage: null, baseBuildImage: null, @@ -299,6 +313,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st payload.baseBuildImage = 'node:18'; payload.baseBuildImages = nodeVersions; } + if (buildPack === 'heroku') { + payload.baseImage = 'heroku/buildpacks:20'; + payload.baseImages = herokuVersions; + + } return payload; } diff --git a/apps/api/src/lib/buildPacks/heroku.ts b/apps/api/src/lib/buildPacks/heroku.ts index 1b382547c..3efdeaf6a 100644 --- a/apps/api/src/lib/buildPacks/heroku.ts +++ b/apps/api/src/lib/buildPacks/heroku.ts @@ -2,14 +2,14 @@ import { executeDockerCmd, prisma } from "../common" import { saveBuildLog } from "./common"; export default async function (data: any): Promise { + const { buildId, applicationId, tag, dockerId, debug, workdir } = data try { - const { buildId, applicationId, tag, dockerId, debug, workdir } = data + await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); const { stdout } = await executeDockerCmd({ dockerId, command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20` }) - if (debug) { const array = stdout.split('\n') for (const line of array) { @@ -24,6 +24,16 @@ export default async function (data: any): Promise { } await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); } catch (error) { + const array = error.stdout.split('\n') + for (const line of array) { + if (line !== '\n') { + await saveBuildLog({ + line: `${line.replace('\n', '')}`, + buildId, + applicationId + }); + } + } throw error; } } diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 369b3f2b5..d67125db3 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; import * as serviceFields from './serviceFields' -export const version = '3.3.4'; +export const version = '3.4.0'; export const isDev = process.env.NODE_ENV === 'development'; const algorithm = 'aes-256-ctr'; @@ -78,6 +78,8 @@ export const include: any = { umami: true, hasura: true, fider: true, + moodle: true, + appwrite: true }; export const uniqueName = (): string => uniqueNamesGenerator(customConfig); @@ -258,8 +260,8 @@ export const supportedServiceTypesAndVersions = [ fancyName: 'Hasura', baseImage: 'hasura/graphql-engine', images: ['postgres:12-alpine'], - versions: ['latest', 'v2.8.4', 'v2.5.1'], - recommendedVersion: 'v2.8.4', + versions: ['latest', 'v2.10.0', 'v2.5.1'], + recommendedVersion: 'v2.10.0', ports: { main: 8080 } @@ -275,6 +277,17 @@ export const supportedServiceTypesAndVersions = [ main: 3000 } }, + { + name: 'appwrite', + fancyName: 'Appwrite', + baseImage: 'appwrite/appwrite', + images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], + versions: ['latest', '0.15.3'], + recommendedVersion: '0.15.3', + ports: { + main: 80 + } + } // { // name: 'moodle', // fancyName: 'Moodle', @@ -579,6 +592,11 @@ export async function executeDockerCmd({ dockerId, command }: { dockerId: string } else { engine = 'unix:///var/run/docker.sock' } + if (process.env.CODESANDBOX_HOST) { + if (command.startsWith('docker compose')) { + command = command.replace(/docker compose/gi, 'docker-compose') + } + } return await asyncExecShell( `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` ); @@ -590,6 +608,11 @@ export async function startTraefikProxy(id: string): Promise { const { id: settingsId, ipv4, ipv6 } = await listSettings(); if (!found) { + const { stdout: coolifyNetwork } = await executeDockerCmd({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` }) + + if (!coolifyNetwork) { + await executeDockerCmd({ dockerId: id, command: `docker network create --attachable coolify-infra` }) + } const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }) const ip = JSON.parse(Config)[0].Gateway; let traefikUrl = mainTraefikEndpoint @@ -873,11 +896,11 @@ export function generateDatabaseConfiguration(database: any, arch: string): } if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/var/lib/postgresql`; - configuration.environmentVariables = { - POSTGRES_PASSWORD: dbUserPassword, - POSTGRES_USER: dbUser, - POSTGRES_DB: defaultDatabase - } + configuration.environmentVariables = { + POSTGRES_PASSWORD: dbUserPassword, + POSTGRES_USER: dbUser, + POSTGRES_DB: defaultDatabase + } } return configuration } else if (type === 'redis') { @@ -1528,6 +1551,35 @@ export async function configureServiceType({ } } }); + } else if (type === 'appwrite') { + const opensslKeyV1 = encrypt(generatePassword()); + const executorSecret = encrypt(generatePassword()); + const redisPassword = encrypt(generatePassword()); + const mariadbHost = `${id}-mariadb` + const mariadbUser = cuid(); + const mariadbPassword = encrypt(generatePassword()); + const mariadbDatabase = 'appwrite'; + const mariadbRootUser = cuid(); + const mariadbRootUserPassword = encrypt(generatePassword()); + await prisma.service.update({ + where: { id }, + data: { + type, + appwrite: { + create: { + opensslKeyV1, + executorSecret, + redisPassword, + mariadbHost, + mariadbUser, + mariadbPassword, + mariadbDatabase, + mariadbRootUser, + mariadbRootUserPassword + } + } + } + }); } else { await prisma.service.update({ where: { id }, @@ -1539,6 +1591,7 @@ export async function configureServiceType({ } export async function removeService({ id }: { id: string }): Promise { + await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.fider.deleteMany({ where: { serviceId: id } }); @@ -1549,8 +1602,8 @@ export async function removeService({ id }: { id: string }): Promise { await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } }); - await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); - + await prisma.moodle.deleteMany({ where: { serviceId: id } }); + await prisma.appwrite.deleteMany({ where: { serviceId: id } }); await prisma.service.delete({ where: { id } }); } @@ -1615,9 +1668,9 @@ export const getServiceMainPort = (service: string) => { export function makeLabelForServices(type) { return [ 'coolify.managed=true', - `coolify.version = ${version} `, + `coolify.version = ${version}`, `coolify.type = service`, - `coolify.service.type = ${type} ` + `coolify.service.type = ${type}` ]; } export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index db2dbc3e7..aa6a29cfd 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -71,7 +71,7 @@ export async function removeContainer({ }): Promise { try { const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) - + console.log(id) if (JSON.parse(stdout).Running) { await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) diff --git a/apps/api/src/lib/serviceFields.ts b/apps/api/src/lib/serviceFields.ts index 9ddf37ba3..be8c34c4f 100644 --- a/apps/api/src/lib/serviceFields.ts +++ b/apps/api/src/lib/serviceFields.ts @@ -469,6 +469,87 @@ export const moodle = [{ isBoolean: false, isEncrypted: true }, +{ + name: 'mariadbDatabase', + isEditable: true, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}] + +export const appwrite = [{ + name: 'opensslKeyV1', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'executorSecret', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'redisPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'mariadbHost', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'mariadbPort', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'mariadbUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'mariadbPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, +{ + name: 'mariadbRootUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}, +{ + name: 'mariadbRootUserPassword', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: true +}, { name: 'mariadbDatabase', isEditable: true, diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts new file mode 100644 index 000000000..c5d315ff0 --- /dev/null +++ b/apps/api/src/lib/services.ts @@ -0,0 +1,35 @@ +import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common"; + +export async function defaultServiceConfigurations({ id, teamId }) { + const service = await getServiceFromDB({ id, teamId }); + const { destinationDockerId, destinationDocker, type, serviceSecret } = service; + + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort(type); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + + const image = getServiceImage(type); + let secrets = []; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + secrets.push(`${secret.name}=${secret.value}`); + }); + } + return { ...service, network, port, workdir, image, secrets } +} + +export function defaultServiceComposeConfiguration(network: string): any { + return { + networks: [network], + restart: 'always', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '10s', + max_attempts: 10, + window: '120s' + } + } + } +} \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 5d3bcac51..96c740440 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -3,8 +3,7 @@ import type { FastifyRequest } from 'fastify'; import { FastifyReply } from 'fastify'; import yaml from 'js-yaml'; import fs from 'fs/promises'; -import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, isARM, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; -import { checkContainer } from '../../../../lib/docker'; +import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index a2c87092e..bfd8a9352 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -2,13 +2,14 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; -import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import cuid from 'cuid'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; +import { defaultServiceComposeConfiguration, defaultServiceConfigurations } from '../../../../lib/services'; // async function startServiceNew(request: FastifyRequest) { // try { @@ -134,8 +135,7 @@ import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServ // // config.services[id].environment = environmentVariables // const composeFileDestination = `${workdir}/docker-compose.yaml`; // // await fs.writeFile(composeFileDestination, yaml.dump(config)); -// // await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); -// // await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); +// await startServiceContainers(destinationDocker.id, composeFileDestination) // return {} // } catch ({ status, message }) { // return errorHandler({ status, message }) @@ -588,6 +588,9 @@ export async function startService(request: FastifyRequest) { if (type === 'moodle') { return await startMoodleService(request) } + if (type === 'appwrite') { + return await startAppWriteService(request) + } throw `Service type ${type} not supported.` } catch (error) { throw { status: 500, message: error?.message || error } @@ -595,53 +598,54 @@ export async function startService(request: FastifyRequest) { } export async function stopService(request: FastifyRequest) { try { - const { type } = request.params - if (type === 'plausibleanalytics') { - return await stopPlausibleAnalyticsService(request) - } - if (type === 'nocodb') { - return await stopNocodbService(request) - } - if (type === 'minio') { - return await stopMinioService(request) - } - if (type === 'vscodeserver') { - return await stopVscodeService(request) - } - if (type === 'wordpress') { - return await stopWordpressService(request) - } - if (type === 'vaultwarden') { - return await stopVaultwardenService(request) - } - if (type === 'languagetool') { - return await stopLanguageToolService(request) - } - if (type === 'n8n') { - return await stopN8nService(request) - } - if (type === 'uptimekuma') { - return await stopUptimekumaService(request) - } - if (type === 'ghost') { - return await stopGhostService(request) - } - if (type === 'meilisearch') { - return await stopMeilisearchService(request) - } - if (type === 'umami') { - return await stopUmamiService(request) - } - if (type === 'hasura') { - return await stopHasuraService(request) - } - if (type === 'fider') { - return await stopFiderService(request) - } - if (type === 'moodle') { - return await stopMoodleService(request) - } - throw `Service type ${type} not supported.` + return await stopServiceContainers(request) + // const { type } = request.params + // if (type === 'plausibleanalytics') { + // return await stopPlausibleAnalyticsService(request) + // } + // if (type === 'nocodb') { + // return await stopNocodbService(request) + // } + // if (type === 'minio') { + // return await stopMinioService(request) + // } + // if (type === 'vscodeserver') { + // return await stopVscodeService(request) + // } + // if (type === 'wordpress') { + // return await stopWordpressService(request) + // } + // if (type === 'vaultwarden') { + // return await stopVaultwardenService(request) + // } + // if (type === 'languagetool') { + // return await stopLanguageToolService(request) + // } + // if (type === 'n8n') { + // return await stopN8nService(request) + // } + // if (type === 'uptimekuma') { + // return await stopUptimekumaService(request) + // } + // if (type === 'ghost') { + // return await stopGhostService(request) + // } + // if (type === 'meilisearch') { + // return await stopMeilisearchService(request) + // } + // if (type === 'umami') { + // return await stopUmamiService(request) + // } + // if (type === 'hasura') { + // return await stopHasuraService(request) + // } + // if (type === 'fider') { + // return await stopFiderService(request) + // } + // if (type === 'moodle') { + // return await stopMoodleService(request) + // } + // throw `Service type ${type} not supported.` } catch (error) { throw { status: 500, message: error?.message || error } } @@ -798,52 +802,25 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; volumes, command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', - networks: [network], environment: config.plausibleAnalytics.environmentVariables, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`, `${id}-clickhouse`], labels: makeLabelForServices('plausibleAnalytics'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '10s', - max_attempts: 5, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), }, [`${id}-postgresql`]: { container_name: `${id}-postgresql`, image: config.postgresql.image, - networks: [network], environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '10s', - max_attempts: 5, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), }, [`${id}-clickhouse`]: { build: workdir, container_name: `${id}-clickhouse`, - networks: [network], environment: config.clickhouse.environmentVariables, volumes: [config.clickhouse.volume], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '10s', - max_attempts: 5, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -863,36 +840,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function stopPlausibleAnalyticsService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker, fqdn } = service; - if (destinationDockerId) { - const engine = destinationDocker.engine; - - let found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` }); - if (found) { - await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id }); - } - found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-clickhouse` }); - if (found) { - await removeContainer({ id: `${id}-clickhouse`, dockerId: destinationDocker.id }); - } - } - + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -929,20 +877,11 @@ async function startNocodbService(request: FastifyRequest) { [id]: { container_name: id, image: config.image, - networks: [network], volumes, environment: config.environmentVariables, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -954,25 +893,7 @@ async function startNocodbService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function stopNocodbService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker, fqdn } = service; - if (destinationDockerId) { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -1029,19 +950,10 @@ async function startMinioService(request: FastifyRequest) { image: config.image, command: `server /data --console-address ":${consolePort}"`, environment: config.environmentVariables, - networks: [network], volumes, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('minio'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1053,32 +965,13 @@ async function startMinioService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopMinioService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - await prisma.minio.update({ where: { serviceId: id }, data: { publicPort: null } }) - if (destinationDockerId) { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startVscodeService(request: FastifyRequest) { try { @@ -1123,19 +1016,10 @@ async function startVscodeService(request: FastifyRequest) { container_name: id, image: config.image, environment: config.environmentVariables, - networks: [network], volumes, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vscodeServer'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1148,8 +1032,7 @@ async function startVscodeService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) const changePermissionOn = persistentStorage.map((p) => p.path); if (changePermissionOn.length > 0) { @@ -1164,23 +1047,6 @@ async function startVscodeService(request: FastifyRequest) { return errorHandler({ status, message }) } } -async function stopVscodeService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startWordpressService(request: FastifyRequest) { try { @@ -1188,6 +1054,7 @@ async function startWordpressService(request: FastifyRequest) const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { + arch, type, version, destinationDockerId, @@ -1237,6 +1104,10 @@ async function startWordpressService(request: FastifyRequest) } } }; + if (isARM(arch)) { + config.mysql.image = 'mysql:5.7' + config.mysql.volume = `${id}-mysql-data:/var/lib/mysql` + } if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.wordpress.environmentVariables[secret.name] = secret.value; @@ -1253,18 +1124,9 @@ async function startWordpressService(request: FastifyRequest) image: config.wordpress.image, environment: config.wordpress.environmentVariables, volumes, - networks: [network], - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('wordpress'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1281,16 +1143,7 @@ async function startWordpressService(request: FastifyRequest) image: config.mysql.image, volumes: [config.mysql.volume], environment: config.mysql.environmentVariables, - networks: [network], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), }; composeFile.volumes[config.mysql.volume.split(':')[0]] = { @@ -1300,61 +1153,13 @@ async function startWordpressService(request: FastifyRequest) const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopWordpressService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - destinationDockerId, - destinationDocker, - wordpress: { ftpEnabled } - } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mysql` }); - if (found) { - await removeContainer({ id: `${id}-mysql`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - try { - if (ftpEnabled) { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-ftp` }); - if (found) { - await removeContainer({ id: `${id}-ftp`, dockerId: destinationDocker.id }); - } - await prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpEnabled: false } - }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startVaultwardenService(request: FastifyRequest) { try { @@ -1388,19 +1193,10 @@ async function startVaultwardenService(request: FastifyRequest container_name: id, image: config.image, environment: config.environmentVariables, - networks: [network], volumes, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1413,35 +1209,13 @@ async function startVaultwardenService(request: FastifyRequest const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopVaultwardenService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startLanguageToolService(request: FastifyRequest) { try { @@ -1474,20 +1248,11 @@ async function startLanguageToolService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startN8nService(request: FastifyRequest) { try { @@ -1562,20 +1305,11 @@ async function startN8nService(request: FastifyRequest) { [id]: { container_name: id, image: config.image, - networks: [network], volumes, environment: config.environmentVariables, - restart: 'always', labels: makeLabelForServices('n8n'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1588,35 +1322,13 @@ async function startN8nService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopN8nService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startUptimekumaService(request: FastifyRequest) { try { @@ -1648,20 +1360,11 @@ async function startUptimekumaService(request: FastifyRequest) [id]: { container_name: id, image: config.image, - networks: [network], volumes, environment: config.environmentVariables, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1674,35 +1377,13 @@ async function startUptimekumaService(request: FastifyRequest) const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopUptimekumaService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startGhostService(request: FastifyRequest) { try { @@ -1777,37 +1458,19 @@ async function startGhostService(request: FastifyRequest) { [id]: { container_name: id, image: config.ghost.image, - networks: [network], volumes, environment: config.ghost.environmentVariables, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), depends_on: [`${id}-mariadb`], - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), }, [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, - networks: [network], volumes: [config.mariadb.volume], environment: config.mariadb.environmentVariables, - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1825,39 +1488,13 @@ async function startGhostService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopGhostService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - let found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` }); - if (found) { - await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startMeilisearchService(request: FastifyRequest) { try { @@ -1895,20 +1532,11 @@ async function startMeilisearchService(request: FastifyRequest [id]: { container_name: id, image: config.image, - networks: [network], environment: config.environmentVariables, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes, labels: makeLabelForServices('meilisearch'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -1920,29 +1548,7 @@ async function startMeilisearchService(request: FastifyRequest }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function stopMeilisearchService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -2092,36 +1698,18 @@ async function startUmamiService(request: FastifyRequest) { container_name: id, image: config.umami.image, environment: config.umami.environmentVariables, - networks: [network], volumes, - restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('umami'), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [`${id}-postgresql`] + depends_on: [`${id}-postgresql`], + ...defaultServiceComposeConfiguration(network), }, [`${id}-postgresql`]: { build: workdir, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - networks: [network], volumes: [config.postgresql.volume], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -2138,37 +1726,7 @@ async function startUmamiService(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function stopUmamiService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` }); - if (found) { - await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) @@ -2227,36 +1785,18 @@ async function startHasuraService(request: FastifyRequest) { container_name: id, image: config.hasura.image, environment: config.hasura.environmentVariables, - networks: [network], volumes, - restart: 'always', labels: makeLabelForServices('hasura'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [`${id}-postgresql`] + depends_on: [`${id}-postgresql`], + ...defaultServiceComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - networks: [network], volumes: [config.postgresql.volume], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -2274,43 +1814,13 @@ async function startHasuraService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopHasuraService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` }); - if (found) { - await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} async function startFiderService(request: FastifyRequest) { try { @@ -2388,36 +1898,18 @@ async function startFiderService(request: FastifyRequest) { container_name: id, image: config.fider.image, environment: config.fider.environmentVariables, - networks: [network], volumes, - restart: 'always', labels: makeLabelForServices('fider'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [`${id}-postgresql`] + depends_on: [`${id}-postgresql`], + ...defaultServiceComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, - networks: [network], volumes: [config.postgresql.volume], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - } + ...defaultServiceComposeConfiguration(network), } }, networks: { @@ -2435,44 +1927,525 @@ async function startFiderService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopFiderService(request: FastifyRequest) { + +async function startAppWriteService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); + const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) + + let isStatsEnabled = false + if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) { + isStatsEnabled = true + } + const { + opensslKeyV1, + executorSecret, + mariadbHost, + mariadbPort, + mariadbUser, + mariadbPassword, + mariadbRootUser, + mariadbRootUserPassword, + mariadbDatabase + } = appwrite; + + const dockerCompose = { + [id]: { + image: `${image}:${version}`, + container_name: id, + labels: makeLabelForServices('appwrite'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + "volumes": [ + `${id}-uploads:/storage/uploads:rw`, + `${id}-cache:/storage/cache:rw`, + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + `${id}-functions:/storage/functions:rw` + ], + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + "_APP_LOCALE=en", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + `_APP_STATSD_HOST=${id}-telegraf`, + "_APP_STATSD_PORT=8125", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-realtime`]: { + image: `${image}:${version}`, + container_name: `${id}-realtime`, + entrypoint: "realtime", + labels: makeLabelForServices('appwrite'), + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-audits`]: { + + image: `${image}:${version}`, + container_name: `${id}-worker-audits`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-audits", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-webhooks`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-webhooks`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-webhooks", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-deletes`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-deletes`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-deletes", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "volumes": [ + `${id}-uploads:/storage/uploads:rw`, + `${id}-cache:/storage/cache:rw`, + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + `${id}-functions:/storage/functions:rw`, + `${id}-builds:/storage/builds:rw`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-databases`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-databases`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-databases", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-builds`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-builds`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-builds", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-certificates`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-certificates`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-certificates", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "volumes": [ + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-functions`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-functions`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-functions", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + `${id}-executor` + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-executor`]: { + image: `${image}:${version}`, + container_name: `${id}-executor`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "executor", + "stop_signal": "SIGINT", + "volumes": [ + `${id}-functions:/storage/functions:rw`, + `${id}-builds:/storage/builds:rw`, + "/var/run/docker.sock:/var/run/docker.sock", + "/tmp:/tmp:rw" + ], + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + `${id}` + ], + "environment": [ + "_APP_ENV=production", + `_APP_EXECUTOR_SECRET=${executorSecret}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-mails`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-mails`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-mails", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-worker-messaging`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-messaging`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-messaging", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-maintenance`]: { + image: `${image}:${version}`, + container_name: `${id}-maintenance`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "maintenance", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-schedule`]: { + image: `${image}:${version}`, + container_name: `${id}-schedule`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "schedule", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-mariadb`]: { + "image": "mariadb:10.7", + container_name: `${id}-mariadb`, + labels: makeLabelForServices('appwrite'), + "volumes": [ + `${id}-mariadb:/var/lib/mysql:rw` + ], + "environment": [ + `MYSQL_ROOT_USER=${mariadbRootUser}`, + `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, + `MYSQL_USER=${mariadbUser}`, + `MYSQL_PASSWORD=${mariadbPassword}`, + `MYSQL_DATABASE=${mariadbDatabase}` + ], + "command": "mysqld --innodb-flush-method=fsync", + ...defaultServiceComposeConfiguration(network), + }, + [`${id}-redis`]: { + "image": "redis:6.2-alpine", + container_name: `${id}-redis`, + "command": `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, + "volumes": [ + `${id}-redis:/data:rw` + ], + ...defaultServiceComposeConfiguration(network), + }, + + }; + if (isStatsEnabled) { + dockerCompose[id].depends_on.push(`${id}-influxdb`); + dockerCompose[`${id}-usage`] = { + image: `${image}:${version}`, + container_name: `${id}-usage`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "usage", + "depends_on": [ + `${id}-mariadb`, + `${id}-influxdb`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultServiceComposeConfiguration(network), } - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-postgresql` }); - if (found) { - await removeContainer({ id: `${id}-postgresql`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); + dockerCompose[`${id}-influxdb`] = { + "image": "appwrite/influxdb:1.5.0", + container_name: `${id}-influxdb`, + "volumes": [ + `${id}-influxdb:/var/lib/influxdb:rw` + ], + ...defaultServiceComposeConfiguration(network), + } + dockerCompose[`${id}-telegraf`] = { + "image": "appwrite/telegraf:1.4.0", + container_name: `${id}-telegraf`, + "environment": [ + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + ], + ...defaultServiceComposeConfiguration(network), } } + + const composeFile: any = { + version: '3.8', + services: dockerCompose, + networks: { + [network]: { + external: true + } + }, + volumes: { + [`${id}-uploads`]: { + name: `${id}-uploads` + }, + [`${id}-cache`]: { + name: `${id}-cache` + }, + [`${id}-config`]: { + name: `${id}-config` + }, + [`${id}-certificates`]: { + name: `${id}-certificates` + }, + [`${id}-functions`]: { + name: `${id}-functions` + }, + [`${id}-builds`]: { + name: `${id}-builds` + }, + [`${id}-mariadb`]: { + name: `${id}-mariadb` + }, + [`${id}-redis`]: { + name: `${id}-redis` + }, + [`${id}-influxdb`]: { + name: `${id}-influxdb` + } + } + + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } - +async function startServiceContainers(dockerId, composeFileDestination) { + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) + await asyncSleep(1000); + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) +} +async function stopServiceContainers(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const { destinationDockerId } = await getServiceFromDB({ id, teamId }); + if (destinationDockerId) { + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker stop -t 0` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker rm --force` + }) + return {} + } + throw { status: 500, message: 'Could not stop containers.' } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} async function startMoodleService(request: FastifyRequest) { try { const { id } = request.params; @@ -2593,44 +2566,13 @@ async function startMoodleService(request: FastifyRequest) { const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } -async function stopMoodleService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId, destinationDocker } = service; - if (destinationDockerId) { - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: id }); - if (found) { - await removeContainer({ id, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - try { - const found = await checkContainer({ dockerId: destinationDocker.id, container: `${id}-mariadb` }); - if (found) { - await removeContainer({ id: `${id}-mariadb`, dockerId: destinationDocker.id }); - } - } catch (error) { - console.error(error); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - export async function activatePlausibleUsers(request: FastifyRequest, reply: FastifyReply) { try { diff --git a/apps/ui/src/lib/common.ts b/apps/ui/src/lib/common.ts index 79c1598b5..3834422e5 100644 --- a/apps/ui/src/lib/common.ts +++ b/apps/ui/src/lib/common.ts @@ -131,8 +131,8 @@ export const supportedServiceTypesAndVersions = [ fancyName: 'Hasura', baseImage: 'hasura/graphql-engine', images: ['postgres:12-alpine'], - versions: ['latest', 'v2.5.1'], - recommendedVersion: 'v2.5.1', + versions: ['latest', 'v2.10.0', 'v2.5.1'], + recommendedVersion: 'v2.10.0', ports: { main: 8080 } @@ -148,6 +148,17 @@ export const supportedServiceTypesAndVersions = [ main: 3000 } }, + { + name: 'appwrite', + fancyName: 'Appwrite', + baseImage: 'appwrite/appwrite', + images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], + versions: ['latest', '0.15.3'], + recommendedVersion: '0.15.3', + ports: { + main: 80 + } + } // { // name: 'moodle', // fancyName: 'Moodle', @@ -218,7 +229,7 @@ export const staticDeployments = [ 'astro', 'eleventy' ]; -export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel']; +export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku']; export function generateRemoteEngine(destination: any) { diff --git a/apps/ui/src/lib/components/Toast.svelte b/apps/ui/src/lib/components/Toast.svelte index 282bc159f..b0586a51f 100644 --- a/apps/ui/src/lib/components/Toast.svelte +++ b/apps/ui/src/lib/components/Toast.svelte @@ -5,16 +5,17 @@
dispatch('click')} on:mouseover={() => dispatch('pause')} on:focus={() => dispatch('pause')} on:mouseout={() => dispatch('resume')} on:blur={() => dispatch('resume')} - class="alert shadow-lg text-white rounded" + class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer" class:bg-coollabs={type === 'success'} class:alert-error={type === 'error'} class:alert-info={type === 'info'} > - + {/if}
diff --git a/apps/ui/src/lib/components/Toasts.svelte b/apps/ui/src/lib/components/Toasts.svelte index 2e53c6287..1c7c8cbc0 100644 --- a/apps/ui/src/lib/components/Toasts.svelte +++ b/apps/ui/src/lib/components/Toasts.svelte @@ -2,7 +2,7 @@ import { fade } from 'svelte/transition'; import Toast from './Toast.svelte'; - import { pauseToast, resumeToast, toasts } from '$lib/store'; + import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store'; {#if $toasts} @@ -12,7 +12,8 @@ resumeToast(toast.id)} - on:pause={() => pauseToast(toast.id)}>{@html toast.message} pauseToast(toast.id)} + on:click={() => dismissToast(toast.id)}>{@html toast.message} {/each} diff --git a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte index 379b775ce..0980063c6 100644 --- a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte +++ b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte @@ -38,4 +38,6 @@ {:else if application.buildPack?.toLowerCase() === 'laravel'} +{:else if application.buildPack?.toLowerCase() === 'heroku'} + {/if} diff --git a/apps/ui/src/lib/components/svg/applications/Heroku.svelte b/apps/ui/src/lib/components/svg/applications/Heroku.svelte new file mode 100644 index 000000000..dff845bc2 --- /dev/null +++ b/apps/ui/src/lib/components/svg/applications/Heroku.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/apps/ui/src/lib/components/svg/applications/index.ts b/apps/ui/src/lib/components/svg/applications/index.ts index 12b712f4c..ee2236357 100644 --- a/apps/ui/src/lib/components/svg/applications/index.ts +++ b/apps/ui/src/lib/components/svg/applications/index.ts @@ -16,4 +16,4 @@ export { default as Astro } from './Astro.svelte'; export { default as Eleventy } from './Eleventy.svelte'; export { default as Deno } from './Deno.svelte'; export { default as Laravel } from './Laravel.svelte'; - +export { default as Heroku } from './Heroku.svelte'; diff --git a/apps/ui/src/lib/components/svg/services/Appwrite.svelte b/apps/ui/src/lib/components/svg/services/Appwrite.svelte new file mode 100644 index 000000000..fbabd5168 --- /dev/null +++ b/apps/ui/src/lib/components/svg/services/Appwrite.svelte @@ -0,0 +1,21 @@ + + + + + + diff --git a/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte b/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte index 00e1bcf6e..c5f537736 100644 --- a/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte +++ b/apps/ui/src/lib/components/svg/services/ServiceIcons.svelte @@ -32,6 +32,8 @@ {:else if type === 'fider'} +{:else if type === 'appwrite'} + {:else if type === 'moodle'} {/if} diff --git a/apps/ui/src/lib/components/svg/services/index.ts b/apps/ui/src/lib/components/svg/services/index.ts index 007ded1ce..1e47a5def 100644 --- a/apps/ui/src/lib/components/svg/services/index.ts +++ b/apps/ui/src/lib/components/svg/services/index.ts @@ -1,17 +1,18 @@ //@ts-nocheck export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte'; -export { default as NocoDb } from './NocoDB.svelte'; -export { default as MinIo } from './MinIO.svelte'; -export { default as VsCodeServer } from './VSCodeServer.svelte'; -export { default as Wordpress } from './Wordpress.svelte'; -export { default as VaultWarden } from './VaultWarden.svelte'; -export { default as LanguageTool } from './LanguageTool.svelte'; -export { default as N8n } from './N8n.svelte'; -export { default as UptimeKuma } from './UptimeKuma.svelte'; -export { default as Ghost } from './Ghost.svelte'; -export { default as MeiliSearch } from './MeiliSearch.svelte'; -export { default as Umami } from './Umami.svelte'; -export { default as Hasura } from './Hasura.svelte'; -export { default as Fider } from './Fider.svelte'; -export { default as Moodle } from './Moodle.svelte'; +export { default as NocoDb } from './NocoDB.svelte'; +export { default as MinIo } from './MinIO.svelte'; +export { default as VsCodeServer } from './VSCodeServer.svelte'; +export { default as Wordpress } from './Wordpress.svelte'; +export { default as VaultWarden } from './VaultWarden.svelte'; +export { default as LanguageTool } from './LanguageTool.svelte'; +export { default as N8n } from './N8n.svelte'; +export { default as UptimeKuma } from './UptimeKuma.svelte'; +export { default as Ghost } from './Ghost.svelte'; +export { default as MeiliSearch } from './MeiliSearch.svelte'; +export { default as Umami } from './Umami.svelte'; +export { default as Hasura } from './Hasura.svelte'; +export { default as Fider } from './Fider.svelte'; +export { default as Appwrite } from './Appwrite.svelte'; +export { default as Moodle } from './Moodle.svelte'; diff --git a/apps/ui/src/lib/templates.ts b/apps/ui/src/lib/templates.ts index e2c023032..c4ec05ad6 100644 --- a/apps/ui/src/lib/templates.ts +++ b/apps/ui/src/lib/templates.ts @@ -170,6 +170,16 @@ export function findBuildPack(pack: string, packageManager = 'npm') { port: 80 }; } + if (pack === 'heroku') { + return { + ...metaData, + installCommand: null, + buildCommand: null, + startCommand: null, + publishDirectory: null, + port: 5000 + }; + } return { name: 'node', fancyName: 'Node.js', @@ -187,119 +197,137 @@ export const buildPacks = [ name: 'node', fancyName: 'Node.js', hoverColor: 'hover:bg-green-700', - color: 'bg-green-700' + color: 'bg-green-700', + isCoolifyBuildPack: true, }, { name: 'static', fancyName: 'Static', hoverColor: 'hover:bg-orange-700', - color: 'bg-orange-700' + color: 'bg-orange-700', + isCoolifyBuildPack: true, }, { name: 'php', fancyName: 'PHP', hoverColor: 'hover:bg-indigo-700', - color: 'bg-indigo-700' + color: 'bg-indigo-700', + isCoolifyBuildPack: true, }, { name: 'laravel', fancyName: 'Laravel', hoverColor: 'hover:bg-indigo-700', - color: 'bg-indigo-700' + color: 'bg-indigo-700', + isCoolifyBuildPack: true, }, { name: 'docker', fancyName: 'Docker', hoverColor: 'hover:bg-sky-700', - color: 'bg-sky-700' + color: 'bg-sky-700', + isCoolifyBuildPack: true, }, { name: 'svelte', fancyName: 'Svelte', hoverColor: 'hover:bg-orange-700', - color: 'bg-orange-700' + color: 'bg-orange-700', + isCoolifyBuildPack: true, }, { name: 'vuejs', fancyName: 'VueJS', hoverColor: 'hover:bg-green-700', - color: 'bg-green-700' + color: 'bg-green-700', + isCoolifyBuildPack: true, }, { name: 'nuxtjs', fancyName: 'NuxtJS', hoverColor: 'hover:bg-green-700', - color: 'bg-green-700' + color: 'bg-green-700', + isCoolifyBuildPack: true, }, { name: 'gatsby', fancyName: 'Gatsby', hoverColor: 'hover:bg-blue-700', - color: 'bg-blue-700' + color: 'bg-blue-700', + isCoolifyBuildPack: true, }, { name: 'astro', fancyName: 'Astro', hoverColor: 'hover:bg-pink-700', - color: 'bg-pink-700' + color: 'bg-pink-700', + isCoolifyBuildPack: true, }, { name: 'eleventy', fancyName: 'Eleventy', hoverColor: 'hover:bg-red-700', - color: 'bg-red-700' + color: 'bg-red-700', + isCoolifyBuildPack: true, }, { name: 'react', fancyName: 'React', hoverColor: 'hover:bg-blue-700', - color: 'bg-blue-700' + color: 'bg-blue-700', + isCoolifyBuildPack: true, }, { name: 'preact', fancyName: 'Preact', hoverColor: 'hover:bg-blue-700', - color: 'bg-blue-700' + color: 'bg-blue-700', + isCoolifyBuildPack: true, }, { name: 'nextjs', fancyName: 'NextJS', hoverColor: 'hover:bg-blue-700', - color: 'bg-blue-700' + color: 'bg-blue-700', + isCoolifyBuildPack: true, }, { name: 'nestjs', fancyName: 'NestJS', hoverColor: 'hover:bg-red-700', - color: 'bg-red-700' + color: 'bg-red-700', + isCoolifyBuildPack: true, }, { name: 'rust', fancyName: 'Rust', hoverColor: 'hover:bg-pink-700', - color: 'bg-pink-700' + color: 'bg-pink-700', + isCoolifyBuildPack: true, }, { name: 'python', fancyName: 'Python', hoverColor: 'hover:bg-green-700', - color: 'bg-green-700' + color: 'bg-green-700', + isCoolifyBuildPack: true, }, { name: 'deno', fancyName: 'Deno', hoverColor: 'hover:bg-green-700', - color: 'bg-green-700' - } - // }, - // { - // name: 'heroku', - // fancyName: 'Heroku Buildpack', - // hoverColor: 'hover:bg-indigo-700', - // color: 'bg-indigo-700' - // } + color: 'bg-green-700', + isCoolifyBuildPack: true, + }, + { + name: 'heroku', + fancyName: 'Heroku', + hoverColor: 'hover:bg-purple-700', + color: 'bg-purple-700', + isHerokuBuildPack: true, + } ]; export const scanningTemplates = { '@sveltejs/kit': { diff --git a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte index 7330bbbd0..e187d95bd 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte @@ -30,7 +30,6 @@ import { page } from '$app/stores'; import { get } from '$lib/api'; import { appSession } from '$lib/store'; - import { browser } from '$app/env'; import { t } from '$lib/translations'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates'; import { errorNotification } from '$lib/common'; @@ -263,11 +262,27 @@ {:else} -
- {#each buildPacks as buildPack} + + +
+
Coolify Buildpacks
+
+ {#each buildPacks.filter(bp => bp.isCoolifyBuildPack === true) as buildPack} +
+ +
+ {/each} +
+
+ +
+
Heroku
+
+ {#each buildPacks.filter(bp => bp.isHerokuBuildPack === true) as buildPack}
{/each}
+
{/if} diff --git a/apps/ui/src/routes/applications/[id]/configuration/destination.svelte b/apps/ui/src/routes/applications/[id]/configuration/destination.svelte index 2bfe98a5c..2f0efe2d3 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/destination.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/destination.svelte @@ -31,6 +31,7 @@ import { t } from '$lib/translations'; import { appSession } from '$lib/store'; import { errorNotification } from '$lib/common'; + import { onMount } from 'svelte'; const { id } = $page.params; const from = $page.url.searchParams.get('from'); @@ -55,6 +56,11 @@ return errorNotification(error); } } + onMount(async () => { + if (destinations.length === 1) { + await handleSubmit(destinations[0].id); + } + });
@@ -65,7 +71,9 @@
{#if !destinations || ownDestinations.length === 0}
-
{$t('application.configuration.no_configurable_destination')}
+
+ {$t('application.configuration.no_configurable_destination')} +
- + {#if isDisabled} + + {:else} + + {/if}
+ {#if isDisabled} + + {:else} + {/if}
{/if} - {#if application.buildPack !== 'laravel'} + {#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}