import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; import { ServiceStartStop } from '../../routes/api/v1/services/types'; import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { defaultServiceConfigurations } from '../services'; export async function startService(request: FastifyRequest) { try { const { type } = request.params if (type === 'plausibleanalytics') { return await startPlausibleAnalyticsService(request) } if (type === 'nocodb') { return await startNocodbService(request) } if (type === 'minio') { return await startMinioService(request) } if (type === 'vscodeserver') { return await startVscodeService(request) } if (type === 'wordpress') { return await startWordpressService(request) } if (type === 'vaultwarden') { return await startVaultwardenService(request) } if (type === 'languagetool') { return await startLanguageToolService(request) } if (type === 'n8n') { return await startN8nService(request) } if (type === 'uptimekuma') { return await startUptimekumaService(request) } if (type === 'ghost') { return await startGhostService(request) } if (type === 'meilisearch') { return await startMeilisearchService(request) } if (type === 'umami') { return await startUmamiService(request) } if (type === 'hasura') { return await startHasuraService(request) } if (type === 'fider') { return await startFiderService(request) } if (type === 'moodle') { return await startMoodleService(request) } if (type === 'appwrite') { return await startAppWriteService(request) } if (type === 'glitchTip') { return await startGlitchTipService(request) } if (type === 'searxng') { return await startSearXNGService(request) } throw `Service type ${type} not supported.` } catch (error) { throw { status: 500, message: error?.message || error } } } export async function stopService(request: FastifyRequest) { try { return await stopServiceContainers(request) } catch (error) { throw { status: 500, message: error?.message || error } } } async function startPlausibleAnalyticsService(request: FastifyRequest) { try { const { id } = request.params const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, fqdn, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, plausibleAnalytics: { id: plausibleDbId, username, email, password, postgresqlDatabase, postgresqlPassword, postgresqlUser, secretKeyBase } } = service; const image = getServiceImage(type); const config = { plausibleAnalytics: { image: `${image}:${version}`, environmentVariables: { ADMIN_USER_EMAIL: email, ADMIN_USER_NAME: username, ADMIN_USER_PWD: password, BASE_URL: fqdn, SECRET_KEY_BASE: secretKeyBase, DISABLE_AUTH: 'false', DISABLE_REGISTRATION: 'true', DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, CLICKHOUSE_DATABASE_URL: `http://${id}-clickhouse:8123/plausible` } }, postgresql: { volume: `${plausibleDbId}-postgresql-data:/bitnami/postgresql/`, image: 'bitnami/postgresql:13.2.0', environmentVariables: { POSTGRESQL_PASSWORD: postgresqlPassword, POSTGRESQL_USERNAME: postgresqlUser, POSTGRESQL_DATABASE: postgresqlDatabase } }, clickhouse: { volume: `${plausibleDbId}-clickhouse-data:/var/lib/clickhouse`, image: 'yandex/clickhouse-server:21.3.2.5', environmentVariables: {}, ulimits: { nofile: { soft: 262144, hard: 262144 } } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.plausibleAnalytics.environmentVariables[secret.name] = secret.value; }); } const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('plausibleanalytics'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const clickhouseConfigXml = ` warning true `; const clickhouseUserConfigXml = ` 0 0 `; const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'; const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'; await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml); await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml); await fs.writeFile(`${workdir}/init.query`, initQuery); await fs.writeFile(`${workdir}/init-db.sh`, initScript); const Dockerfile = ` FROM ${config.clickhouse.image} COPY ./clickhouse-config.xml /etc/clickhouse-server/users.d/logging.xml COPY ./clickhouse-user-config.xml /etc/clickhouse-server/config.d/logging.xml COPY ./init.query /docker-entrypoint-initdb.d/init.query COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.plausibleAnalytics.image, volumes, command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', environment: config.plausibleAnalytics.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`, `${id}-clickhouse`], labels: makeLabelForServices('plausibleAnalytics'), ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { container_name: `${id}-postgresql`, image: config.postgresql.image, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], ...defaultComposeConfiguration(network), }, [`${id}-clickhouse`]: { build: workdir, container_name: `${id}-clickhouse`, environment: config.clickhouse.environmentVariables, volumes: [config.clickhouse.volume], ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.postgresql.volume.split(':')[0]]: { name: config.postgresql.volume.split(':')[0] }, [config.clickhouse.volume.split(':')[0]]: { name: config.clickhouse.volume.split(':')[0] } } }; 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 startNocodbService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('nocodb'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-nc:/usr/app/data`, environmentVariables: {} }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, volumes, environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startMinioService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, fqdn, destinationDockerId, destinationDocker, persistentStorage, exposePort, minio: { rootUser, rootUserPassword }, serviceSecret } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('minio'); const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) const publicPort = await getFreePublicPort(id, dockerId); const consolePort = 9001; const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-minio-data:/data`, environmentVariables: { MINIO_ROOT_USER: rootUser, MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_BROWSER_REDIRECT_URL: fqdn } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, command: `server /data --console-address ":${consolePort}"`, environment: config.environmentVariables, volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('minio'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await startServiceContainers(destinationDocker.id, composeFileDestination) await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } async function startVscodeService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, vscodeserver: { password } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('vscodeserver'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-vscodeserver-data:/home/coder`, environmentVariables: { PASSWORD: password } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, environment: config.environmentVariables, volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vscodeServer'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await startServiceContainers(destinationDocker.id, composeFileDestination) const changePermissionOn = persistentStorage.map((p) => p.path); if (changePermissionOn.length > 0) { await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( ' ' )}` }) } return {} } catch ({ status, message }) { return errorHandler({ status, message }) } } async function startWordpressService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { arch, type, version, destinationDockerId, serviceSecret, destinationDocker, persistentStorage, exposePort, wordpress: { mysqlDatabase, mysqlHost, mysqlPort, mysqlUser, mysqlPassword, extraConfig, mysqlRootUser, mysqlRootUserPassword, ownMysql } } = service; const network = destinationDockerId && destinationDocker.network; const image = getServiceImage(type); const port = getServiceMainPort('wordpress'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const config = { wordpress: { image: `${image}:${version}`, volume: `${id}-wordpress-data:/var/www/html`, environmentVariables: { WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, WORDPRESS_DB_USER: mysqlUser, WORDPRESS_DB_PASSWORD: mysqlPassword, WORDPRESS_DB_NAME: mysqlDatabase, WORDPRESS_CONFIG_EXTRA: extraConfig } }, mysql: { image: `bitnami/mysql:5.7`, volume: `${id}-mysql-data:/bitnami/mysql/data`, environmentVariables: { MYSQL_ROOT_PASSWORD: mysqlRootUserPassword, MYSQL_ROOT_USER: mysqlRootUser, MYSQL_USER: mysqlUser, MYSQL_PASSWORD: mysqlPassword, MYSQL_DATABASE: mysqlDatabase } } }; 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; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.wordpress.image, environment: config.wordpress.environmentVariables, volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('wordpress'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; if (!ownMysql) { composeFile.services[id].depends_on = [`${id}-mysql`]; composeFile.services[`${id}-mysql`] = { container_name: `${id}-mysql`, image: config.mysql.image, volumes: [config.mysql.volume], environment: config.mysql.environmentVariables, ...defaultComposeConfiguration(network), }; composeFile.volumes[config.mysql.volume.split(':')[0]] = { name: config.mysql.volume.split(':')[0] }; } 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 startVaultwardenService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('vaultwarden'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-vaultwarden-data:/data/`, environmentVariables: {} }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, environment: config.environmentVariables, volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startLanguageToolService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('languagetool'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-ngrams:/ngrams`, environmentVariables: {} }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes, labels: makeLabelForServices('languagetool'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startN8nService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('n8n'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-n8n:/root/.n8n`, environmentVariables: { WEBHOOK_URL: `${service.fqdn}` } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, volumes, environment: config.environmentVariables, labels: makeLabelForServices('n8n'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startUptimekumaService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('uptimekuma'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-uptimekuma:/app/data`, environmentVariables: {} }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, volumes, environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startGhostService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, fqdn, ghost: { defaultEmail, defaultPassword, mariadbRootUser, mariadbRootUserPassword, mariadbDatabase, mariadbPassword, mariadbUser } } = service; const network = destinationDockerId && destinationDocker.network; const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const domain = getDomain(fqdn); const port = getServiceMainPort('ghost'); const isHttps = fqdn.startsWith('https://'); const config = { ghost: { image: `${image}:${version}`, volume: `${id}-ghost:/bitnami/ghost`, environmentVariables: { url: fqdn, GHOST_HOST: domain, GHOST_ENABLE_HTTPS: isHttps ? 'yes' : 'no', GHOST_EMAIL: defaultEmail, GHOST_PASSWORD: defaultPassword, GHOST_DATABASE_HOST: `${id}-mariadb`, GHOST_DATABASE_USER: mariadbUser, GHOST_DATABASE_PASSWORD: mariadbPassword, GHOST_DATABASE_NAME: mariadbDatabase, GHOST_DATABASE_PORT_NUMBER: 3306 } }, mariadb: { image: `bitnami/mariadb:latest`, volume: `${id}-mariadb:/bitnami/mariadb`, environmentVariables: { MARIADB_USER: mariadbUser, MARIADB_PASSWORD: mariadbPassword, MARIADB_DATABASE: mariadbDatabase, MARIADB_ROOT_USER: mariadbRootUser, MARIADB_ROOT_PASSWORD: mariadbRootUserPassword } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.ghost.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.ghost.image, volumes, environment: config.ghost.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), depends_on: [`${id}-mariadb`], ...defaultComposeConfiguration(network), }, [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, volumes: [config.mariadb.volume], environment: config.mariadb.environmentVariables, ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.mariadb.volume.split(':')[0]]: { name: config.mariadb.volume.split(':')[0] } } }; 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 startMeilisearchService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { meiliSearch: { masterKey } } = service; const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('meilisearch'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { image: `${image}:${version}`, volume: `${id}-datams:/data.ms`, environmentVariables: { MEILI_MASTER_KEY: masterKey } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.image, environment: config.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes, labels: makeLabelForServices('meilisearch'), ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: volumeMounts }; 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 startUmamiService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, umami: { umamiAdminPassword, postgresqlUser, postgresqlPassword, postgresqlDatabase, hashSalt } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('umami'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { umami: { image: `${image}:${version}`, environmentVariables: { DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, DATABASE_TYPE: 'postgresql', HASH_SALT: hashSalt } }, postgresql: { image: 'postgres:12-alpine', volume: `${id}-postgresql-data:/var/lib/postgresql/data`, environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, POSTGRES_DB: postgresqlDatabase } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.umami.environmentVariables[secret.name] = secret.value; }); } const initDbSQL = ` drop table if exists event; drop table if exists pageview; drop table if exists session; drop table if exists website; drop table if exists account; create table account ( user_id serial primary key, username varchar(255) unique not null, password varchar(60) not null, is_admin bool not null default false, created_at timestamp with time zone default current_timestamp, updated_at timestamp with time zone default current_timestamp ); create table website ( website_id serial primary key, website_uuid uuid unique not null, user_id int not null references account(user_id) on delete cascade, name varchar(100) not null, domain varchar(500), share_id varchar(64) unique, created_at timestamp with time zone default current_timestamp ); create table session ( session_id serial primary key, session_uuid uuid unique not null, website_id int not null references website(website_id) on delete cascade, created_at timestamp with time zone default current_timestamp, hostname varchar(100), browser varchar(20), os varchar(20), device varchar(20), screen varchar(11), language varchar(35), country char(2) ); create table pageview ( view_id serial primary key, website_id int not null references website(website_id) on delete cascade, session_id int not null references session(session_id) on delete cascade, created_at timestamp with time zone default current_timestamp, url varchar(500) not null, referrer varchar(500) ); create table event ( event_id serial primary key, website_id int not null references website(website_id) on delete cascade, session_id int not null references session(session_id) on delete cascade, created_at timestamp with time zone default current_timestamp, url varchar(500) not null, event_type varchar(50) not null, event_value varchar(50) not null ); create index website_user_id_idx on website(user_id); create index session_created_at_idx on session(created_at); create index session_website_id_idx on session(website_id); create index pageview_created_at_idx on pageview(created_at); create index pageview_website_id_idx on pageview(website_id); create index pageview_session_id_idx on pageview(session_id); create index pageview_website_id_created_at_idx on pageview(website_id, created_at); create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at); create index event_created_at_idx on event(created_at); create index event_website_id_idx on event(website_id); create index event_session_id_idx on event(session_id); insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( umamiAdminPassword, 10 )}', true);`; await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL); const Dockerfile = ` FROM ${config.postgresql.image} COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.umami.image, environment: config.umami.environmentVariables, volumes, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('umami'), depends_on: [`${id}-postgresql`], ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { build: workdir, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.postgresql.volume.split(':')[0]]: { name: config.postgresql.volume.split(':')[0] } } }; 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 startHasuraService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, persistentStorage, serviceSecret, exposePort, hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('hasura'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { hasura: { image: `${image}:${version}`, environmentVariables: { HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}` } }, postgresql: { image: 'postgres:12-alpine', volume: `${id}-postgresql-data:/var/lib/postgresql/data`, environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, POSTGRES_DB: postgresqlDatabase } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.hasura.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.hasura.image, environment: config.hasura.environmentVariables, volumes, labels: makeLabelForServices('hasura'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.postgresql.volume.split(':')[0]]: { name: config.postgresql.volume.split(':')[0] } } }; 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 startFiderService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, fqdn, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, fider: { postgresqlUser, postgresqlPassword, postgresqlDatabase, jwtSecret, emailNoreply, emailMailgunApiKey, emailMailgunDomain, emailMailgunRegion, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpEnableStartTls } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('fider'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { fider: { image: `${image}:${version}`, environmentVariables: { BASE_URL: fqdn, DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, EMAIL_NOREPLY: emailNoreply, EMAIL_MAILGUN_API: emailMailgunApiKey, EMAIL_MAILGUN_REGION: emailMailgunRegion, EMAIL_MAILGUN_DOMAIN: emailMailgunDomain, EMAIL_SMTP_HOST: emailSmtpHost, EMAIL_SMTP_PORT: emailSmtpPort, EMAIL_SMTP_USER: emailSmtpUser, EMAIL_SMTP_PASSWORD: emailSmtpPassword, EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls } }, postgresql: { image: 'postgres:12-alpine', volume: `${id}-postgresql-data:/var/lib/postgresql/data`, environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, POSTGRES_DB: postgresqlDatabase } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.fider.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.fider.image, environment: config.fider.environmentVariables, volumes, labels: makeLabelForServices('fider'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`], ...defaultComposeConfiguration(network), }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.postgresql.volume.split(':')[0]]: { name: config.postgresql.volume.split(':')[0] } } }; 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 startAppWriteService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; 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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(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", ...defaultComposeConfiguration(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` ], ...defaultComposeConfiguration(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 ], ...defaultComposeConfiguration(network), } dockerCompose[`${id}-influxdb`] = { "image": "appwrite/influxdb:1.5.0", container_name: `${id}-influxdb`, "volumes": [ `${id}-influxdb:/var/lib/influxdb:rw` ], ...defaultComposeConfiguration(network), } dockerCompose[`${id}-telegraf`] = { "image": "appwrite/telegraf:1.4.0", container_name: `${id}-telegraf`, "environment": [ `_APP_INFLUXDB_HOST=${id}-influxdb`, "_APP_INFLUXDB_PORT=8086", ], ...defaultComposeConfiguration(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} build --no-cache` }) 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; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, fqdn, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, moodle: { defaultUsername, defaultPassword, defaultEmail, mariadbRootUser, mariadbRootUserPassword, mariadbDatabase, mariadbPassword, mariadbUser } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('moodle'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { moodle: { image: `${image}:${version}`, volume: `${id}-data:/bitnami/moodle`, environmentVariables: { MOODLE_USERNAME: defaultUsername, MOODLE_PASSWORD: defaultPassword, MOODLE_EMAIL: defaultEmail, MOODLE_DATABASE_HOST: `${id}-mariadb`, MOODLE_DATABASE_USER: mariadbUser, MOODLE_DATABASE_PASSWORD: mariadbPassword, MOODLE_DATABASE_NAME: mariadbDatabase, MOODLE_REVERSEPROXY: 'yes' } }, mariadb: { image: 'bitnami/mariadb:latest', volume: `${id}-mariadb-data:/bitnami/mariadb`, environmentVariables: { MARIADB_USER: mariadbUser, MARIADB_PASSWORD: mariadbPassword, MARIADB_DATABASE: mariadbDatabase, MARIADB_ROOT_USER: mariadbRootUser, MARIADB_ROOT_PASSWORD: mariadbRootUserPassword } } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.moodle.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.moodle.image, environment: config.moodle.environmentVariables, networks: [network], volumes, restart: 'always', labels: makeLabelForServices('moodle'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), deploy: { restart_policy: { condition: 'on-failure', delay: '5s', max_attempts: 3, window: '120s' } }, depends_on: [`${id}-mariadb`] }, [`${id}-mariadb`]: { container_name: `${id}-mariadb`, image: config.mariadb.image, environment: config.mariadb.environmentVariables, networks: [network], volumes: [], restart: 'always', deploy: { restart_policy: { condition: 'on-failure', delay: '5s', max_attempts: 3, window: '120s' } }, depends_on: [] } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.mariadb.volume.split(':')[0]]: { name: config.mariadb.volume.split(':')[0] } } }; 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 startGlitchTipService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, fqdn, destinationDockerId, destinationDocker, serviceSecret, persistentStorage, exposePort, glitchTip: { postgresqlDatabase, postgresqlPassword, postgresqlUser, secretKeyBase, defaultEmail, defaultUsername, defaultPassword, defaultFromEmail, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpUseTls, emailSmtpUseSsl, emailBackend, mailgunApiKey, sendgridApiKey, enableOpenUserRegistration, } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('glitchTip'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { glitchTip: { image: `${image}:${version}`, environmentVariables: { PORT: port, GLITCHTIP_DOMAIN: fqdn, SECRET_KEY: secretKeyBase, DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, REDIS_URL: `redis://${id}-redis:6379/0`, DEFAULT_FROM_EMAIL: defaultFromEmail, EMAIL_HOST: emailSmtpHost, EMAIL_PORT: emailSmtpPort, EMAIL_HOST_USER: emailSmtpUser, EMAIL_HOST_PASSWORD: emailSmtpPassword, EMAIL_USE_TLS: emailSmtpUseTls, EMAIL_USE_SSL: emailSmtpUseSsl, EMAIL_BACKEND: emailBackend, MAILGUN_API_KEY: mailgunApiKey, SENDGRID_API_KEY: sendgridApiKey, ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration, DJANGO_SUPERUSER_EMAIL: defaultEmail, DJANGO_SUPERUSER_USERNAME: defaultUsername, DJANGO_SUPERUSER_PASSWORD: defaultPassword, } }, postgresql: { image: 'postgres:14-alpine', volume: `${id}-postgresql-data:/var/lib/postgresql/data`, environmentVariables: { POSTGRES_USER: postgresqlUser, POSTGRES_PASSWORD: postgresqlPassword, POSTGRES_DB: postgresqlDatabase } }, redis: { image: 'redis:7-alpine', volume: `${id}-redis-data:/data`, } }; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.glitchTip.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { container_name: id, image: config.glitchTip.image, environment: config.glitchTip.environmentVariables, volumes, labels: makeLabelForServices('glitchTip'), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), depends_on: [`${id}-postgresql`, `${id}-redis`], ...defaultComposeConfiguration(network), }, [`${id}-worker`]: { container_name: `${id}-worker`, image: config.glitchTip.image, command: './bin/run-celery-with-beat.sh', environment: config.glitchTip.environmentVariables, depends_on: [`${id}-postgresql`, `${id}-redis`], ...defaultComposeConfiguration(network), }, [`${id}-setup`]: { container_name: `${id}-setup`, image: config.glitchTip.image, command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"', environment: config.glitchTip.environmentVariables, networks: [network], restart: "no", depends_on: [`${id}-postgresql`, `${id}-redis`] }, [`${id}-postgresql`]: { image: config.postgresql.image, container_name: `${id}-postgresql`, environment: config.postgresql.environmentVariables, volumes: [config.postgresql.volume], ...defaultComposeConfiguration(network), }, [`${id}-redis`]: { image: config.redis.image, container_name: `${id}-redis`, volumes: [config.redis.volume], ...defaultComposeConfiguration(network), } }, networks: { [network]: { external: true } }, volumes: { ...volumeMounts, [config.postgresql.volume.split(':')[0]]: { name: config.postgresql.volume.split(':')[0] }, [config.redis.volume.split(':')[0]]: { name: config.redis.volume.split(':')[0] } } }; 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 startSearXNGService(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user.teamId; const service = await getServiceFromDB({ id, teamId }); const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn, searxng: { secretKey, redisPassword } } = service; const network = destinationDockerId && destinationDocker.network; const port = getServiceMainPort('searxng'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); const config = { searxng: { image: `${image}:${version}`, volume: `${id}-searxng:/etc/searxng`, environmentVariables: { SEARXNG_BASE_URL: `${fqdn}` }, }, redis: { image: 'redis:7-alpine', } }; const settingsYml = ` # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings use_default_settings: true server: secret_key: ${secretKey} limiter: true image_proxy: true ui: static_use_hash: true redis: url: redis://:${redisPassword}@${id}-redis:6379/0` const Dockerfile = ` FROM ${config.searxng.image} COPY ./settings.yml /etc/searxng/settings.yml`; if (serviceSecret.length > 0) { serviceSecret.forEach((secret) => { config.searxng.environmentVariables[secret.name] = secret.value; }); } const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) const composeFile: ComposeFile = { version: '3.8', services: { [id]: { build: workdir, container_name: id, volumes, environment: config.searxng.environmentVariables, ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('searxng'), cap_drop: ['ALL'], cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE'], depends_on: [`${id}-redis`], ...defaultComposeConfiguration(network), }, [`${id}-redis`]: { container_name: `${id}-redis`, image: config.redis.image, command: `redis-server --requirepass ${redisPassword} --save "" --appendonly "no"`, labels: makeLabelForServices('searxng'), cap_drop: ['ALL'], cap_add: ['SETGID', 'SETUID', 'DAC_OVERRIDE'], ...defaultComposeConfiguration(network), }, }, networks: { [network]: { external: true } }, volumes: volumeMounts }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/settings.yml`, settingsYml); await startServiceContainers(destinationDocker.id, composeFileDestination) return {} } catch ({ status, message }) { return errorHandler({ status, message }) } }