diff --git a/api/app.js b/api/app.js
index d1b42fd35..f1fe5aed5 100644
--- a/api/app.js
+++ b/api/app.js
@@ -12,6 +12,8 @@ module.exports = async function (fastify, opts) {
server.register(require('./routes/v1/application/deploy'), { prefix: '/application/deploy' })
server.register(require('./routes/v1/application/deploy/logs'), { prefix: '/application/deploy/logs' })
server.register(require('./routes/v1/databases'), { prefix: '/databases' })
+ server.register(require('./routes/v1/services'), { prefix: '/services' })
+ server.register(require('./routes/v1/services/deploy'), { prefix: '/services/deploy' })
server.register(require('./routes/v1/server'), { prefix: '/server' })
})
// Public routes
diff --git a/api/libs/applications/configuration.js b/api/libs/applications/configuration.js
index 53b913feb..134599a76 100644
--- a/api/libs/applications/configuration.js
+++ b/api/libs/applications/configuration.js
@@ -2,7 +2,7 @@ const { uniqueNamesGenerator, adjectives, colors, animals } = require('unique-na
const cuid = require('cuid')
const crypto = require('crypto')
const { docker } = require('../docker')
-const { execShellAsync } = require('../common')
+const { execShellAsync, baseServiceConfiguration } = require('../common')
function getUniq () {
return uniqueNamesGenerator({ dictionaries: [adjectives, animals, colors], length: 2 })
@@ -15,25 +15,6 @@ function setDefaultConfiguration (configuration) {
const shaBase = JSON.stringify({ repository: configuration.repository })
const sha256 = crypto.createHash('sha256').update(shaBase).digest('hex')
- const baseServiceConfiguration = {
- replicas: 1,
- restart_policy: {
- condition: 'any',
- max_attempts: 3
- },
- update_config: {
- parallelism: 1,
- delay: '10s',
- order: 'start-first'
- },
- rollback_config: {
- parallelism: 1,
- delay: '10s',
- order: 'start-first',
- failure_action: 'rollback'
- }
- }
-
configuration.build.container.name = sha256.slice(0, 15)
configuration.general.nickname = nickname
@@ -133,4 +114,4 @@ async function precheckDeployment ({ services, configuration }) {
forceUpdate
}
}
-module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment }
+module.exports = { setDefaultConfiguration, updateServiceLabels, precheckDeployment, baseServiceConfiguration }
diff --git a/api/libs/common.js b/api/libs/common.js
index 6026af14b..0225e8224 100644
--- a/api/libs/common.js
+++ b/api/libs/common.js
@@ -6,6 +6,24 @@ const User = require('../models/User')
const algorithm = 'aes-256-cbc'
const key = process.env.SECRETS_ENCRYPTION_KEY
+const baseServiceConfiguration = {
+ replicas: 1,
+ restart_policy: {
+ condition: 'any',
+ max_attempts: 3
+ },
+ update_config: {
+ parallelism: 1,
+ delay: '10s',
+ order: 'start-first'
+ },
+ rollback_config: {
+ parallelism: 1,
+ delay: '10s',
+ order: 'start-first',
+ failure_action: 'rollback'
+ }
+}
function delay (t) {
return new Promise(function (resolve) {
setTimeout(function () {
@@ -94,5 +112,6 @@ module.exports = {
checkImageAvailable,
encryptData,
decryptData,
- verifyUserId
+ verifyUserId,
+ baseServiceConfiguration
}
diff --git a/api/libs/services/plausible/index.js b/api/libs/services/plausible/index.js
new file mode 100644
index 000000000..7e8c61fcc
--- /dev/null
+++ b/api/libs/services/plausible/index.js
@@ -0,0 +1,185 @@
+const { execShellAsync, cleanupTmp, baseServiceConfiguration } = require('../../common')
+const yaml = require('js-yaml')
+const fs = require('fs').promises
+const generator = require('generate-password')
+const { docker } = require('../../docker')
+
+async function plausible ({ email, userName, userPassword, baseURL, traefikURL }) {
+ const deployId = 'plausible'
+ const workdir = '/tmp/plausible'
+ const secretKey = generator.generate({ length: 64, numbers: true, strict: true })
+ const generateEnvsPostgres = {
+ POSTGRESQL_PASSWORD: generator.generate({ length: 24, numbers: true, strict: true }),
+ POSTGRESQL_USERNAME: generator.generate({ length: 10, numbers: true, strict: true }),
+ POSTGRESQL_DATABASE: 'plausible'
+ }
+
+ const secrets = [
+ { name: 'ADMIN_USER_EMAIL', value: email },
+ { name: 'ADMIN_USER_NAME', value: userName },
+ { name: 'ADMIN_USER_PWD', value: userPassword },
+ { name: 'BASE_URL', value: baseURL },
+ { name: 'SECRET_KEY_BASE', value: secretKey },
+ { name: 'DISABLE_AUTH', value: 'false' },
+ { name: 'DISABLE_REGISTRATION', value: 'true' },
+ { name: 'DATABASE_URL', value: `postgresql://${generateEnvsPostgres.POSTGRESQL_USERNAME}:${generateEnvsPostgres.POSTGRESQL_PASSWORD}@plausible_db:5432/${generateEnvsPostgres.POSTGRESQL_DATABASE}` },
+ { name: 'CLICKHOUSE_DATABASE_URL', value: 'http://plausible_events_db:8123/plausible' }
+ ]
+
+ const generateEnvsClickhouse = {}
+ for (const secret of secrets) generateEnvsClickhouse[secret.name] = secret.value
+
+ const clickhouseConfigXml = `
+
+
+ warning
+ true
+
+
+
+
+
+
+
+
+
+ `
+ const clickhouseUserConfigXml = `
+
+
+
+ 0
+ 0
+
+
+ `
+
+ const clickhouseConfigs = [
+ { source: 'plausible-clickhouse-user-config.xml', target: '/etc/clickhouse-server/users.d/logging.xml' },
+ { source: 'plausible-clickhouse-config.xml', target: '/etc/clickhouse-server/config.d/logging.xml' },
+ { source: 'plausible-init.query', target: '/docker-entrypoint-initdb.d/init.query' },
+ { source: 'plausible-init-db.sh', target: '/docker-entrypoint-initdb.d/init-db.sh' }
+ ]
+
+ const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'
+ const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'
+ await execShellAsync(`mkdir -p ${workdir}`)
+ 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 stack = {
+ version: '3.8',
+ services: {
+ [deployId]: {
+ image: 'plausible/analytics:latest',
+ command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"',
+ networks: [`${docker.network}`],
+ volumes: [`${deployId}-postgres-data:/var/lib/postgresql/data`],
+ environment: generateEnvsClickhouse,
+ deploy: {
+ ...baseServiceConfiguration,
+ labels: [
+ 'managedBy=coolify',
+ 'type=service',
+ 'serviceName=plausible',
+ 'configuration=' + JSON.stringify({ email, userName, userPassword, baseURL, secretKey, generateEnvsPostgres, generateEnvsClickhouse }),
+ 'traefik.enable=true',
+ 'traefik.http.services.' +
+ deployId +
+ '.loadbalancer.server.port=8000',
+ 'traefik.http.routers.' +
+ deployId +
+ '.entrypoints=websecure',
+ 'traefik.http.routers.' +
+ deployId +
+ '.rule=Host(`' +
+ traefikURL +
+ '`) && PathPrefix(`/`)',
+ 'traefik.http.routers.' +
+ deployId +
+ '.tls.certresolver=letsencrypt',
+ 'traefik.http.routers.' +
+ deployId +
+ '.middlewares=global-compress'
+ ]
+ }
+ },
+ plausible_db: {
+ image: 'bitnami/postgresql:13.2.0',
+ networks: [`${docker.network}`],
+ environment: generateEnvsPostgres,
+ deploy: {
+ ...baseServiceConfiguration,
+ labels: [
+ 'managedBy=coolify',
+ 'type=service',
+ 'serviceName=plausible'
+ ]
+ }
+ },
+ plausible_events_db: {
+ image: 'yandex/clickhouse-server:21.3.2.5',
+ networks: [`${docker.network}`],
+ volumes: [`${deployId}-clickhouse-data:/var/lib/clickhouse`],
+ ulimits: {
+ nofile: {
+ soft: 262144,
+ hard: 262144
+ }
+ },
+ configs: [...clickhouseConfigs],
+ deploy: {
+ ...baseServiceConfiguration,
+ labels: [
+ 'managedBy=coolify',
+ 'type=service',
+ 'serviceName=plausible'
+ ]
+ }
+ }
+ },
+ networks: {
+ [`${docker.network}`]: {
+ external: true
+ }
+ },
+ volumes: {
+ [`${deployId}-clickhouse-data`]: {
+ external: true
+ },
+ [`${deployId}-postgres-data`]: {
+ external: true
+ }
+ },
+ configs: {
+ 'plausible-clickhouse-user-config.xml': {
+ file: `${workdir}/clickhouse-user-config.xml`
+ },
+ 'plausible-clickhouse-config.xml': {
+ file: `${workdir}/clickhouse-config.xml`
+ },
+ 'plausible-init.query': {
+ file: `${workdir}/init.query`
+ },
+ 'plausible-init-db.sh': {
+ file: `${workdir}/init-db.sh`
+ }
+ }
+ }
+ await fs.writeFile(`${workdir}/stack.yml`, yaml.dump(stack))
+ await execShellAsync('docker stack rm plausible')
+ await execShellAsync(
+ `cat ${workdir}/stack.yml | docker stack deploy --prune -c - ${deployId}`
+ )
+ cleanupTmp(workdir)
+}
+
+async function activateAdminUser () {
+ const { POSTGRESQL_USERNAME, POSTGRESQL_PASSWORD, POSTGRESQL_DATABASE } = JSON.parse(JSON.parse((await execShellAsync('docker service inspect plausible_plausible --format=\'{{json .Spec.Labels.configuration}}\'')))).generateEnvsPostgres
+ const containers = (await execShellAsync('docker ps -a --format=\'{{json .Names}}\'')).replace(/"/g, '').trim().split('\n')
+ const postgresDB = containers.find(container => container.startsWith('plausible_plausible_db'))
+ await execShellAsync(`docker exec ${postgresDB} psql -H postgresql://${POSTGRESQL_USERNAME}:${POSTGRESQL_PASSWORD}@localhost:5432/${POSTGRESQL_DATABASE} -c "UPDATE users SET email_verified = true;"`)
+}
+
+module.exports = { plausible, activateAdminUser }
diff --git a/api/routes/v1/dashboard/index.js b/api/routes/v1/dashboard/index.js
index 4205a66d8..acc5cf6ab 100644
--- a/api/routes/v1/dashboard/index.js
+++ b/api/routes/v1/dashboard/index.js
@@ -23,9 +23,10 @@ module.exports = async function (fastify) {
}
])
const serverLogs = await ServerLog.find()
- const services = await docker.engine.listServices()
- let applications = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
- let databases = services.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
+ const dockerServices = await docker.engine.listServices()
+ let applications = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'application' && r.Spec.Labels.configuration)
+ let databases = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && r.Spec.Labels.configuration)
+ let services = dockerServices.filter(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.configuration)
applications = applications.map(r => {
if (JSON.parse(r.Spec.Labels.configuration)) {
const configuration = JSON.parse(r.Spec.Labels.configuration)
@@ -41,6 +42,11 @@ module.exports = async function (fastify) {
r.Spec.Labels.configuration = configuration
return r
})
+ services = services.map(r => {
+ const configuration = r.Spec.Labels.configuration ? JSON.parse(r.Spec.Labels.configuration) : null
+ r.Spec.Labels.configuration = configuration
+ return r
+ })
applications = [...new Map(applications.map(item => [item.Spec.Labels.configuration.publish.domain + item.Spec.Labels.configuration.publish.path, item])).values()]
return {
serverLogs,
@@ -49,6 +55,9 @@ module.exports = async function (fastify) {
},
databases: {
deployed: databases
+ },
+ services: {
+ deployed: services
}
}
} catch (error) {
diff --git a/api/routes/v1/databases/index.js b/api/routes/v1/databases/index.js
index 9cdac684f..da0f34506 100644
--- a/api/routes/v1/databases/index.js
+++ b/api/routes/v1/databases/index.js
@@ -18,13 +18,15 @@ module.exports = async function (fastify) {
const database = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'database' && JSON.parse(r.Spec.Labels.configuration).general.deployId === deployId)
if (database) {
const jsonEnvs = {}
- for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
- const s = d.split('=')
- jsonEnvs[s[0]] = s[1]
+ if (database.Spec.TaskTemplate.ContainerSpec.Env) {
+ for (const d of database.Spec.TaskTemplate.ContainerSpec.Env) {
+ const s = d.split('=')
+ jsonEnvs[s[0]] = s[1]
+ }
}
const payload = {
config: JSON.parse(database.Spec.Labels.configuration),
- envs: jsonEnvs
+ envs: jsonEnvs || null
}
reply.code(200).send(payload)
} else {
@@ -39,7 +41,7 @@ module.exports = async function (fastify) {
body: {
type: 'object',
properties: {
- type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb'] }
+ type: { type: 'string', enum: ['mongodb', 'postgresql', 'mysql', 'couchdb', 'clickhouse'] }
},
required: ['type']
}
@@ -82,9 +84,11 @@ module.exports = async function (fastify) {
name: nickname
}
}
+ await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
let generateEnvs = {}
let image = null
let volume = null
+ let ulimits = {}
if (type === 'mongodb') {
generateEnvs = {
MONGODB_ROOT_PASSWORD: passwords[0],
@@ -119,6 +123,15 @@ module.exports = async function (fastify) {
}
image = 'bitnami/mysql:8.0'
volume = `${configuration.general.deployId}-${type}-data:/bitnami/mysql/data`
+ } else if (type === 'clickhouse') {
+ image = 'yandex/clickhouse-server'
+ volume = `${configuration.general.deployId}-${type}-data:/var/lib/clickhouse`
+ ulimits = {
+ nofile: {
+ soft: 262144,
+ hard: 262144
+ }
+ }
}
const stack = {
@@ -129,6 +142,7 @@ module.exports = async function (fastify) {
networks: [`${docker.network}`],
environment: generateEnvs,
volumes: [volume],
+ ulimits,
deploy: {
replicas: 1,
update_config: {
@@ -160,12 +174,12 @@ module.exports = async function (fastify) {
}
}
}
- await execShellAsync(`mkdir -p ${configuration.general.workdir}`)
await fs.writeFile(`${configuration.general.workdir}/stack.yml`, yaml.dump(stack))
await execShellAsync(
- `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
+ `cat ${configuration.general.workdir}/stack.yml | docker stack deploy -c - ${configuration.general.deployId}`
)
} catch (error) {
+ console.log(error)
await saveServerLog(error)
throw new Error(error)
}
diff --git a/api/routes/v1/services/deploy.js b/api/routes/v1/services/deploy.js
new file mode 100644
index 000000000..481bf8b9b
--- /dev/null
+++ b/api/routes/v1/services/deploy.js
@@ -0,0 +1,15 @@
+const { plausible, activateAdminUser } = require('../../../libs/services/plausible')
+
+module.exports = async function (fastify) {
+ fastify.post('/plausible', async (request, reply) => {
+ let { email, userName, userPassword, baseURL } = request.body
+ const traefikURL = baseURL
+ baseURL = `https://${baseURL}`
+ await plausible({ email, userName, userPassword, baseURL, traefikURL })
+ return {}
+ })
+ fastify.patch('/plausible/activate', async (request, reply) => {
+ await activateAdminUser()
+ return 'OK'
+ })
+}
diff --git a/api/routes/v1/services/index.js b/api/routes/v1/services/index.js
new file mode 100644
index 000000000..b9a5501f1
--- /dev/null
+++ b/api/routes/v1/services/index.js
@@ -0,0 +1,27 @@
+const { execShellAsync } = require('../../../libs/common')
+const { docker } = require('../../../libs/docker')
+
+module.exports = async function (fastify) {
+ fastify.get('/:serviceName', async (request, reply) => {
+ const { serviceName } = request.params
+ try {
+ const service = (await docker.engine.listServices()).find(r => r.Spec.Labels.managedBy === 'coolify' && r.Spec.Labels.type === 'service' && r.Spec.Labels.serviceName === serviceName && r.Spec.Name === `${serviceName}_${serviceName}`)
+ if (service) {
+ const payload = {
+ config: JSON.parse(service.Spec.Labels.configuration)
+ }
+ reply.code(200).send(payload)
+ } else {
+ throw new Error()
+ }
+ } catch (error) {
+ console.log(error)
+ throw new Error('No service found?')
+ }
+ })
+ fastify.delete('/:serviceName', async (request, reply) => {
+ const { serviceName } = request.params
+ await execShellAsync(`docker stack rm ${serviceName}`)
+ reply.code(200).send({})
+ })
+}
diff --git a/package.json b/package.json
index c34c051ad..e8eeb15ef 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source, hassle-free, self-hostable Heroku & Netlify alternative.",
- "version": "1.0.8",
+ "version": "1.0.9",
"license": "AGPL-3.0",
"scripts": {
"lint": "standard",
@@ -18,7 +18,7 @@
"dependencies": {
"@iarna/toml": "^2.2.5",
"@roxi/routify": "^2.15.1",
- "@zerodevx/svelte-toast": "^0.2.1",
+ "@zerodevx/svelte-toast": "^0.2.2",
"ajv": "^8.1.0",
"axios": "^0.21.1",
"commander": "^7.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a1706c14f..7f8173149 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3,7 +3,7 @@ lockfileVersion: 5.3
specifiers:
'@iarna/toml': ^2.2.5
'@roxi/routify': ^2.15.1
- '@zerodevx/svelte-toast': ^0.2.1
+ '@zerodevx/svelte-toast': ^0.2.2
ajv: ^8.1.0
axios: ^0.21.1
commander: ^7.2.0
@@ -45,7 +45,7 @@ specifiers:
dependencies:
'@iarna/toml': 2.2.5
'@roxi/routify': 2.15.1
- '@zerodevx/svelte-toast': 0.2.1
+ '@zerodevx/svelte-toast': 0.2.2
ajv: 8.1.0
axios: 0.21.1
commander: 7.2.0
@@ -563,8 +563,8 @@ packages:
resolution: {integrity: sha512-b+zB8A2so8eCE0JsxjL24J7vdGl8rzPQ09hZNhystm+KqSbKcAej1A+Hbva1rCMmTTqA+hFnUSDc5kouEo0JzA==}
dev: true
- /@zerodevx/svelte-toast/0.2.1:
- resolution: {integrity: sha512-3yOusE+/xDaVNxkBJwbxDZea5ePQ77B15tbHv6ZlSYtlJu0u0PDhGMu8eoI+SmcCt4j+2sf0A1uS9+LcBIqUgg==}
+ /@zerodevx/svelte-toast/0.2.2:
+ resolution: {integrity: sha512-zriB7tSY54OEbRDqJ1NbHBv5Z83tWKhqqW7a+z8HMtZeR49zZUMLISFXmY7B8tMwzO6auB3A5dxuFyqB9+TZkQ==}
dev: false
/abab/2.0.5:
diff --git a/src/App.svelte b/src/App.svelte
index 8fe87ef35..77a76d6b3 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -31,7 +31,7 @@
@apply bg-warmGray-700 !important;
}
:global(input) {
- @apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none !important;
+ @apply text-sm rounded py-2 px-6 font-bold bg-warmGray-800 text-white transition duration-150 outline-none border border-transparent !important;
}
:global(input:hover) {
@apply bg-warmGray-700 !important;
diff --git a/src/components/Databases/Configuration/Configuration.svelte b/src/components/Databases/Configuration/Configuration.svelte
index cb453de45..0ba93ee52 100644
--- a/src/components/Databases/Configuration/Configuration.svelte
+++ b/src/components/Databases/Configuration/Configuration.svelte
@@ -58,6 +58,13 @@
>
Couchdb
+
{#if type}
@@ -81,6 +88,8 @@
class:hover:bg-orange-500="{type === 'mysql'}"
class:bg-red-600="{type === 'couchdb'}"
class:hover:bg-red-500="{type === 'couchdb'}"
+ class:bg-yellow-500="{type === 'clickhouse'}"
+ class:hover:bg-yellow-400="{type === 'clickhouse'}"
class="button p-2 w-32 text-white"
on:click="{deploy}">Deploy
diff --git a/src/components/Databases/SVGs/Clickhouse.svelte b/src/components/Databases/SVGs/Clickhouse.svelte
new file mode 100644
index 000000000..b8cd28933
--- /dev/null
+++ b/src/components/Databases/SVGs/Clickhouse.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/components/PasswordField.svelte b/src/components/PasswordField.svelte
new file mode 100644
index 000000000..7d59299b9
--- /dev/null
+++ b/src/components/PasswordField.svelte
@@ -0,0 +1,54 @@
+
+
+
+
+
+ {#if showPassword}
+
+ {:else}
+
+ {/if}
+
+
diff --git a/src/components/Services/Plausible.svelte b/src/components/Services/Plausible.svelte
new file mode 100644
index 000000000..fe74ab85a
--- /dev/null
+++ b/src/components/Services/Plausible.svelte
@@ -0,0 +1,82 @@
+
+
+{#if loading}
+
+{:else}
+
+
+
+
General
+
+
+
+
+
+
+
+
+
+
+
PostgreSQL
+
+
+
+
+
+{/if}
diff --git a/src/index.css b/src/index.css
index 2b0db5aea..68f14a23a 100644
--- a/src/index.css
+++ b/src/index.css
@@ -34,7 +34,7 @@ body {
font-family: 'Inter';
font-size: 16px;
font-weight: 600;
- white-space: normal;
+ white-space: normal;
}
[role~="tooltip"][data-microtip-position|="bottom"]::before {
diff --git a/src/pages/_layout.svelte b/src/pages/_layout.svelte
index f354764a4..ee6cd9f2d 100644
--- a/src/pages/_layout.svelte
+++ b/src/pages/_layout.svelte
@@ -145,7 +145,7 @@
+
+
+