Files
coolify/apps/api/src/lib/services/handlers.ts
2022-08-29 15:29:00 +02:00

2227 lines
86 KiB
TypeScript

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<ServiceStartStop>) {
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<ServiceStartStop>) {
try {
return await stopServiceContainers(request)
} catch (error) {
throw { status: 500, message: error?.message || error }
}
}
async function startPlausibleAnalyticsService(request: FastifyRequest<ServiceStartStop>) {
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 = `
<yandex>
<logger>
<level>warning</level>
<console>true</console>
</logger>
<!-- Stop all the unnecessary logging -->
<query_thread_log remove="remove"/>
<query_log remove="remove"/>
<text_log remove="remove"/>
<trace_log remove="remove"/>
<metric_log remove="remove"/>
<asynchronous_metric_log remove="remove"/>
<session_log remove="remove"/>
<part_log remove="remove"/>
</yandex>`;
const clickhouseUserConfigXml = `
<yandex>
<profiles>
<default>
<log_queries>0</log_queries>
<log_query_threads>0</log_query_threads>
</default>
</profiles>
</yandex>`;
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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<ServiceStartStop>) {
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 })
}
}