diff --git a/package.json b/package.json
index 820aaca49..d1bb03ba4 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
- "version": "2.1.1",
+ "version": "2.2.0",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev",
diff --git a/prisma/migrations/20220327180323_ghost/migration.sql b/prisma/migrations/20220327180323_ghost/migration.sql
new file mode 100644
index 000000000..3c1cec36f
--- /dev/null
+++ b/prisma/migrations/20220327180323_ghost/migration.sql
@@ -0,0 +1,19 @@
+-- CreateTable
+CREATE TABLE "Ghost" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "defaultEmail" TEXT NOT NULL,
+ "defaultPassword" TEXT NOT NULL,
+ "mariadbUser" TEXT NOT NULL,
+ "mariadbPassword" TEXT NOT NULL,
+ "mariadbRootUser" TEXT NOT NULL,
+ "mariadbRootUserPassword" TEXT NOT NULL,
+ "mariadbDatabase" TEXT,
+ "mariadbPublicPort" INTEGER,
+ "serviceId" TEXT NOT NULL,
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" DATETIME NOT NULL,
+ CONSTRAINT "Ghost_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Ghost_serviceId_key" ON "Ghost"("serviceId");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 95310d2aa..a849ae36f 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -278,6 +278,7 @@ model Service {
minio Minio?
vscodeserver Vscodeserver?
wordpress Wordpress?
+ ghost Ghost?
serviceSecret ServiceSecret[]
}
@@ -332,3 +333,19 @@ model Wordpress {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
+
+model Ghost {
+ id String @id @default(cuid())
+ defaultEmail String
+ defaultPassword String
+ mariadbUser String
+ mariadbPassword String
+ mariadbRootUser String
+ mariadbRootUserPassword String
+ mariadbDatabase String?
+ mariadbPublicPort Int?
+ serviceId String @unique
+ service Service @relation(fields: [serviceId], references: [id])
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
diff --git a/src/app.d.ts b/src/app.d.ts
index d69042fe8..2ebfc021b 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -8,9 +8,11 @@ declare namespace App {
interface Platform {}
interface Session extends SessionData {}
interface Stuff {
+ service: any;
application: any;
isRunning: boolean;
appId: string;
+ readOnly: boolean;
}
}
diff --git a/src/lib/components/svg/services/Ghost.svelte b/src/lib/components/svg/services/Ghost.svelte
new file mode 100644
index 000000000..567ab69bc
--- /dev/null
+++ b/src/lib/components/svg/services/Ghost.svelte
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/lib/components/svg/services/N8n.svelte b/src/lib/components/svg/services/N8n.svelte
new file mode 100644
index 000000000..04b2e8216
--- /dev/null
+++ b/src/lib/components/svg/services/N8n.svelte
@@ -0,0 +1,24 @@
+
+
+
diff --git a/src/lib/components/svg/services/UptimeKuma.svelte b/src/lib/components/svg/services/UptimeKuma.svelte
new file mode 100644
index 000000000..e043ea54b
--- /dev/null
+++ b/src/lib/components/svg/services/UptimeKuma.svelte
@@ -0,0 +1,159 @@
+
+
+
diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts
index 4638e483e..e89105f05 100644
--- a/src/lib/database/common.ts
+++ b/src/lib/database/common.ts
@@ -107,6 +107,7 @@ export const supportedServiceTypesAndVersions = [
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
+ images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest'],
ports: {
main: 8000
@@ -143,6 +144,7 @@ export const supportedServiceTypesAndVersions = [
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
+ images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
ports: {
main: 80
@@ -165,6 +167,34 @@ export const supportedServiceTypesAndVersions = [
ports: {
main: 8010
}
+ },
+ {
+ name: 'n8n',
+ fancyName: 'n8n',
+ baseImage: 'n8nio/n8n',
+ versions: ['latest'],
+ ports: {
+ main: 5678
+ }
+ },
+ {
+ name: 'uptimekuma',
+ fancyName: 'Uptime Kuma',
+ baseImage: 'louislam/uptime-kuma',
+ versions: ['latest'],
+ ports: {
+ main: 3001
+ }
+ },
+ {
+ name: 'ghost',
+ fancyName: 'Ghost',
+ baseImage: 'bitnami/ghost',
+ images: ['bitnami/mariadb'],
+ versions: ['latest'],
+ ports: {
+ main: 2368
+ }
}
];
@@ -189,6 +219,13 @@ export function getServiceImage(type) {
}
return '';
}
+export function getServiceImages(type) {
+ const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
+ if (found) {
+ return found.images;
+ }
+ return [];
+}
export function generateDatabaseConfiguration(database) {
const {
id,
diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts
index ea0be0a83..a5c2ee9ea 100644
--- a/src/lib/database/services.ts
+++ b/src/lib/database/services.ts
@@ -1,3 +1,4 @@
+import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import cuid from 'cuid';
import { generatePassword } from '.';
@@ -20,6 +21,7 @@ export async function getService({ id, teamId }) {
minio: true,
vscodeserver: true,
wordpress: true,
+ ghost: true,
serviceSecret: true
}
});
@@ -43,12 +45,18 @@ export async function getService({ id, teamId }) {
if (body.wordpress?.mysqlRootUserPassword)
body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword);
+ if (body.ghost?.mariadbPassword) body.ghost.mariadbPassword = decrypt(body.ghost.mariadbPassword);
+ if (body.ghost?.mariadbRootUserPassword)
+ body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
+ if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
+
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
+
return { ...body };
}
@@ -119,6 +127,44 @@ export async function configureServiceType({ id, type }) {
type
}
});
+ } else if (type === 'n8n') {
+ await prisma.service.update({
+ where: { id },
+ data: {
+ type
+ }
+ });
+ } else if (type === 'uptimekuma') {
+ await prisma.service.update({
+ where: { id },
+ data: {
+ type
+ }
+ });
+ } else if (type === 'ghost') {
+ const defaultEmail = `${cuid()}@coolify.io`;
+ const defaultPassword = encrypt(generatePassword());
+ const mariadbUser = cuid();
+ const mariadbPassword = encrypt(generatePassword());
+ const mariadbRootUser = cuid();
+ const mariadbRootUserPassword = encrypt(generatePassword());
+
+ await prisma.service.update({
+ where: { id },
+ data: {
+ type,
+ ghost: {
+ create: {
+ defaultEmail,
+ defaultPassword,
+ mariadbUser,
+ mariadbPassword,
+ mariadbRootUser,
+ mariadbRootUserPassword
+ }
+ }
+ }
+ });
}
}
export async function setServiceVersion({ id, version }) {
@@ -139,7 +185,7 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } });
}
-export async function updateNocoDbOrMinioService({ id, fqdn, name }) {
+export async function updateService({ id, fqdn, name }) {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateLanguageToolService({ id, fqdn, name }) {
@@ -160,8 +206,15 @@ export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConf
export async function updateMinioService({ id, publicPort }) {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
}
+export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
+ return await prisma.service.update({
+ where: { id },
+ data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
+ });
+}
export async function removeService({ id }) {
+ await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts
index e85f539de..eb74aa252 100644
--- a/src/lib/letsencrypt/index.ts
+++ b/src/lib/letsencrypt/index.ts
@@ -104,32 +104,34 @@ export async function generateSSLCerts() {
});
for (const application of applications) {
try {
- const {
- fqdn,
- id,
- destinationDocker: { engine, network },
- settings: { previews }
- } = application;
- const isRunning = await checkContainer(engine, id);
- const domain = getDomain(fqdn);
- const isHttps = fqdn.startsWith('https://');
- if (isRunning) {
- if (isHttps) ssls.push({ domain, id, isCoolify: false });
- }
- if (previews) {
- const host = getEngine(engine);
- const { stdout } = await asyncExecShell(
- `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
- );
- const containers = stdout
- .trim()
- .split('\n')
- .filter((a) => a)
- .map((c) => c.replace(/"/g, ''));
- if (containers.length > 0) {
- for (const container of containers) {
- let previewDomain = `${container.split('-')[1]}.${domain}`;
- if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
+ if (application.fqdn && application.destinationDockerId) {
+ const {
+ fqdn,
+ id,
+ destinationDocker: { engine, network },
+ settings: { previews }
+ } = application;
+ const isRunning = await checkContainer(engine, id);
+ const domain = getDomain(fqdn);
+ const isHttps = fqdn.startsWith('https://');
+ if (isRunning) {
+ if (isHttps) ssls.push({ domain, id, isCoolify: false });
+ }
+ if (previews) {
+ const host = getEngine(engine);
+ const { stdout } = await asyncExecShell(
+ `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
+ );
+ const containers = stdout
+ .trim()
+ .split('\n')
+ .filter((a) => a)
+ .map((c) => c.replace(/"/g, ''));
+ if (containers.length > 0) {
+ for (const container of containers) {
+ let previewDomain = `${container.split('-')[1]}.${domain}`;
+ if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
+ }
}
}
}
@@ -143,26 +145,29 @@ export async function generateSSLCerts() {
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
- wordpress: true
+ wordpress: true,
+ ghost: true
},
orderBy: { createdAt: 'desc' }
});
for (const service of services) {
try {
- const {
- fqdn,
- id,
- type,
- destinationDocker: { engine }
- } = service;
- const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
- if (found) {
- const domain = getDomain(fqdn);
- const isHttps = fqdn.startsWith('https://');
- const isRunning = await checkContainer(engine, id);
- if (isRunning) {
- if (isHttps) ssls.push({ domain, id, isCoolify: false });
+ if (service.fqdn && service.destinationDockerId) {
+ const {
+ fqdn,
+ id,
+ type,
+ destinationDocker: { engine }
+ } = service;
+ const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
+ if (found) {
+ const domain = getDomain(fqdn);
+ const isHttps = fqdn.startsWith('https://');
+ const isRunning = await checkContainer(engine, id);
+ if (isRunning) {
+ if (isHttps) ssls.push({ domain, id, isCoolify: false });
+ }
}
}
} catch (error) {
diff --git a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte
index 4afbea65c..06e58be58 100644
--- a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte
+++ b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte
@@ -1,6 +1,6 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte
index 896953cc4..6a54d9b98 100644
--- a/src/routes/services/[id]/_Services/_Services.svelte
+++ b/src/routes/services/[id]/_Services/_Services.svelte
@@ -10,6 +10,7 @@
import Setting from '$lib/components/Setting.svelte';
import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast';
+ import Ghost from './_Ghost.svelte';
import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import VsCodeServer from './_VSCodeServer.svelte';
@@ -142,6 +143,8 @@
{:else if service.type === 'wordpress'}
+ {:else if service.type === 'ghost'}
+
{/if}
diff --git a/src/routes/services/[id]/__layout.svelte b/src/routes/services/[id]/__layout.svelte
index fffce0007..362f89786 100644
--- a/src/routes/services/[id]/__layout.svelte
+++ b/src/routes/services/[id]/__layout.svelte
@@ -35,6 +35,7 @@
}
if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true;
if (service.wordpress?.mysqlDatabase) readOnly = true;
+ if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true;
return {
props: {
diff --git a/src/routes/services/[id]/configuration/type.svelte b/src/routes/services/[id]/configuration/type.svelte
index 0dec45c5f..bf8575065 100644
--- a/src/routes/services/[id]/configuration/type.svelte
+++ b/src/routes/services/[id]/configuration/type.svelte
@@ -38,6 +38,9 @@
import { post } from '$lib/api';
import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
+ import N8n from '$lib/components/svg/services/N8n.svelte';
+ import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
+ import Ghost from '$lib/components/svg/services/Ghost.svelte';
const { id } = $page.params;
const from = $page.url.searchParams.get('from');
@@ -77,6 +80,12 @@
{:else if type.name === 'languagetool'}
+ {:else if type.name === 'n8n'}
+
+ {:else if type.name === 'uptimekuma'}
+
+ {:else if type.name === 'ghost'}
+
{/if}{type.fancyName}
diff --git a/src/routes/services/[id]/ghost/index.json.ts b/src/routes/services/[id]/ghost/index.json.ts
new file mode 100644
index 000000000..81ca8ade1
--- /dev/null
+++ b/src/routes/services/[id]/ghost/index.json.ts
@@ -0,0 +1,23 @@
+import { getUserDetails } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+ const { id } = event.params;
+
+ let {
+ name,
+ fqdn,
+ ghost: { mariadbDatabase }
+ } = await event.request.json();
+ if (fqdn) fqdn = fqdn.toLowerCase();
+ try {
+ await db.updateGhostService({ id, fqdn, name, mariadbDatabase });
+ return { status: 201 };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/ghost/start.json.ts b/src/routes/services/[id]/ghost/start.json.ts
new file mode 100644
index 000000000..4fd9e7fa4
--- /dev/null
+++ b/src/routes/services/[id]/ghost/start.json.ts
@@ -0,0 +1,133 @@
+import {
+ asyncExecShell,
+ createDirectories,
+ getDomain,
+ getEngine,
+ getUserDetails
+} from '$lib/common';
+import * as db from '$lib/database';
+import { promises as fs } from 'fs';
+import yaml from 'js-yaml';
+import type { RequestHandler } from '@sveltejs/kit';
+import { ErrorHandler, getServiceImage } from '$lib/database';
+import { makeLabelForServices } from '$lib/buildPacks/common';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const {
+ type,
+ version,
+ destinationDockerId,
+ destinationDocker,
+ serviceSecret,
+ fqdn,
+ ghost: {
+ defaultEmail,
+ defaultPassword,
+ mariadbRootUser,
+ mariadbRootUserPassword,
+ mariadbDatabase,
+ mariadbPassword,
+ mariadbUser
+ }
+ } = service;
+ const network = destinationDockerId && destinationDocker.network;
+ const host = getEngine(destinationDocker.engine);
+
+ const { workdir } = await createDirectories({ repository: type, buildId: id });
+ const image = getServiceImage(type);
+ const domain = getDomain(fqdn);
+ const config = {
+ ghost: {
+ image: `${image}:${version}`,
+ volume: `${id}-ghost:/bitnami/ghost`,
+ environmentVariables: {
+ GHOST_HOST: domain,
+ 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 composeFile = {
+ version: '3.8',
+ services: {
+ [id]: {
+ container_name: id,
+ image: config.ghost.image,
+ networks: [network],
+ volumes: [config.ghost.volume],
+ environment: config.ghost.environmentVariables,
+ restart: 'always',
+ labels: makeLabelForServices('ghost'),
+ depends_on: [`${id}-mariadb`]
+ },
+ [`${id}-mariadb`]: {
+ container_name: `${id}-mariadb`,
+ image: config.mariadb.image,
+ networks: [network],
+ volumes: [config.mariadb.volume],
+ environment: config.mariadb.environmentVariables,
+ restart: 'always'
+ }
+ },
+ networks: {
+ [network]: {
+ external: true
+ }
+ },
+ volumes: {
+ [config.ghost.volume.split(':')[0]]: {
+ name: config.ghost.volume.split(':')[0]
+ },
+ [config.mariadb.volume.split(':')[0]]: {
+ name: config.mariadb.volume.split(':')[0]
+ }
+ }
+ };
+ const composeFileDestination = `${workdir}/docker-compose.yaml`;
+ await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
+
+ try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/ghost/stop.json.ts b/src/routes/services/[id]/ghost/stop.json.ts
new file mode 100644
index 000000000..478f5b946
--- /dev/null
+++ b/src/routes/services/[id]/ghost/stop.json.ts
@@ -0,0 +1,39 @@
+import { getUserDetails, removeDestinationDocker } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import { checkContainer } from '$lib/haproxy';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const { destinationDockerId, destinationDocker, fqdn } = service;
+ if (destinationDockerId) {
+ const engine = destinationDocker.engine;
+
+ try {
+ let found = await checkContainer(engine, id);
+ if (found) {
+ await removeDestinationDocker({ id, engine });
+ }
+ found = await checkContainer(engine, `${id}-mariadb`);
+ if (found) {
+ await removeDestinationDocker({ id: `${id}-mariadb`, engine });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/index.json.ts b/src/routes/services/[id]/index.json.ts
index d712c1c72..676ff6405 100644
--- a/src/routes/services/[id]/index.json.ts
+++ b/src/routes/services/[id]/index.json.ts
@@ -4,7 +4,8 @@ import {
generateDatabaseConfiguration,
getServiceImage,
getVersions,
- ErrorHandler
+ ErrorHandler,
+ getServiceImages
} from '$lib/database';
import { dockerInstance } from '$lib/docker';
import type { RequestHandler } from '@sveltejs/kit';
@@ -23,7 +24,13 @@ export const get: RequestHandler = async (event) => {
const host = getEngine(destinationDocker.engine);
const docker = dockerInstance({ destinationDocker });
const baseImage = getServiceImage(type);
+ const images = getServiceImages(type);
docker.engine.pull(`${baseImage}:${version}`);
+ if (images?.length > 0) {
+ for (const image of images) {
+ docker.engine.pull(`${image}:latest`);
+ }
+ }
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}`
diff --git a/src/routes/services/[id]/index.svelte b/src/routes/services/[id]/index.svelte
index 573ec26ac..ada5c0d9e 100644
--- a/src/routes/services/[id]/index.svelte
+++ b/src/routes/services/[id]/index.svelte
@@ -39,6 +39,9 @@
import cuid from 'cuid';
import { browser } from '$app/env';
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
+ import N8n from '$lib/components/svg/services/N8n.svelte';
+ import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
+ import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let service;
export let isRunning;
@@ -109,6 +112,18 @@
+ {:else if service.type === 'n8n'}
+
+
+
+ {:else if service.type === 'uptimekuma'}
+
+
+
+ {:else if service.type === 'ghost'}
+
+
+
{/if}
diff --git a/src/routes/services/[id]/languagetool/start.json.ts b/src/routes/services/[id]/languagetool/start.json.ts
index 4666fea79..a6a81d475 100644
--- a/src/routes/services/[id]/languagetool/start.json.ts
+++ b/src/routes/services/[id]/languagetool/start.json.ts
@@ -41,7 +41,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
environment: config.environmentVariables,
restart: 'always',
- volumes: [`${id}-ngrams:/ngrams`],
+ volumes: [config.volume],
labels: makeLabelForServices('languagetool')
}
},
@@ -51,20 +51,20 @@ export const post: RequestHandler = async (event) => {
}
},
volumes: {
- [`${id}-ngrams`]: {
- external: true
+ [config.volume.split(':')[0]]: {
+ name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
- try {
- await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}-ngrams`);
- } catch (error) {
- console.log(error);
- }
try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
diff --git a/src/routes/services/[id]/minio/index.json.ts b/src/routes/services/[id]/minio/index.json.ts
index be5c0525f..d717502c5 100644
--- a/src/routes/services/[id]/minio/index.json.ts
+++ b/src/routes/services/[id]/minio/index.json.ts
@@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase();
try {
- await db.updateNocoDbOrMinioService({ id, fqdn, name });
+ await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
diff --git a/src/routes/services/[id]/minio/start.json.ts b/src/routes/services/[id]/minio/start.json.ts
index 99665dfca..bcffd9e8f 100644
--- a/src/routes/services/[id]/minio/start.json.ts
+++ b/src/routes/services/[id]/minio/start.json.ts
@@ -76,19 +76,13 @@ export const post: RequestHandler = async (event) => {
},
volumes: {
[config.volume.split(':')[0]]: {
- external: true
+ name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
- try {
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
- );
- } catch (error) {
- console.log(error);
- }
+
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort });
diff --git a/src/routes/services/[id]/n8n/index.json.ts b/src/routes/services/[id]/n8n/index.json.ts
new file mode 100644
index 000000000..5ec3fa69a
--- /dev/null
+++ b/src/routes/services/[id]/n8n/index.json.ts
@@ -0,0 +1,20 @@
+import { getUserDetails } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+ const { id } = event.params;
+
+ let { name, fqdn } = await event.request.json();
+ if (fqdn) fqdn = fqdn.toLowerCase();
+
+ try {
+ await db.updateService({ id, fqdn, name });
+ return { status: 201 };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/n8n/start.json.ts b/src/routes/services/[id]/n8n/start.json.ts
new file mode 100644
index 000000000..4a04917c7
--- /dev/null
+++ b/src/routes/services/[id]/n8n/start.json.ts
@@ -0,0 +1,77 @@
+import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
+import * as db from '$lib/database';
+import { promises as fs } from 'fs';
+import yaml from 'js-yaml';
+import type { RequestHandler } from '@sveltejs/kit';
+import { ErrorHandler, getServiceImage } from '$lib/database';
+import { makeLabelForServices } from '$lib/buildPacks/common';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
+ const network = destinationDockerId && destinationDocker.network;
+ const host = getEngine(destinationDocker.engine);
+
+ const { workdir } = await createDirectories({ repository: type, buildId: id });
+ const image = getServiceImage(type);
+
+ const config = {
+ image: `${image}:${version}`,
+ volume: `${id}-n8n:/root/.n8n`,
+ environmentVariables: {}
+ };
+ if (serviceSecret.length > 0) {
+ serviceSecret.forEach((secret) => {
+ config.environmentVariables[secret.name] = secret.value;
+ });
+ }
+ const composeFile = {
+ version: '3.8',
+ services: {
+ [id]: {
+ container_name: id,
+ image: config.image,
+ networks: [network],
+ volumes: [config.volume],
+ environment: config.environmentVariables,
+ restart: 'always',
+ labels: makeLabelForServices('n8n')
+ }
+ },
+ networks: {
+ [network]: {
+ external: true
+ }
+ },
+ volumes: {
+ [config.volume.split(':')[0]]: {
+ name: config.volume.split(':')[0]
+ }
+ }
+ };
+ const composeFileDestination = `${workdir}/docker-compose.yaml`;
+ await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
+
+ try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/n8n/stop.json.ts b/src/routes/services/[id]/n8n/stop.json.ts
new file mode 100644
index 000000000..c604e1cc3
--- /dev/null
+++ b/src/routes/services/[id]/n8n/stop.json.ts
@@ -0,0 +1,35 @@
+import { getUserDetails, removeDestinationDocker } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import { checkContainer } from '$lib/haproxy';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const { destinationDockerId, destinationDocker, fqdn } = service;
+ if (destinationDockerId) {
+ const engine = destinationDocker.engine;
+
+ try {
+ const found = await checkContainer(engine, id);
+ if (found) {
+ await removeDestinationDocker({ id, engine });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/nocodb/index.json.ts b/src/routes/services/[id]/nocodb/index.json.ts
index bc9f1a0d1..5ec3fa69a 100644
--- a/src/routes/services/[id]/nocodb/index.json.ts
+++ b/src/routes/services/[id]/nocodb/index.json.ts
@@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => {
if (fqdn) fqdn = fqdn.toLowerCase();
try {
- await db.updateNocoDbOrMinioService({ id, fqdn, name });
+ await db.updateService({ id, fqdn, name });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
diff --git a/src/routes/services/[id]/nocodb/start.json.ts b/src/routes/services/[id]/nocodb/start.json.ts
index 1088bf817..ebc59ce97 100644
--- a/src/routes/services/[id]/nocodb/start.json.ts
+++ b/src/routes/services/[id]/nocodb/start.json.ts
@@ -52,6 +52,11 @@ export const post: RequestHandler = async (event) => {
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts
index 4b635264b..836bd0178 100644
--- a/src/routes/services/[id]/plausibleanalytics/start.json.ts
+++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts
@@ -158,29 +158,21 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
- external: true
+ name: config.postgresql.volume.split(':')[0]
},
[config.clickhouse.volume.split(':')[0]]: {
- external: true
+ name: config.clickhouse.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
- try {
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.postgresql.volume.split(':')[0]}`
- );
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.clickhouse.volume.split(':')[0]}`
- );
- } catch (error) {
- console.log(error);
+ if (version === 'latest') {
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
}
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d`
);
-
return {
status: 200
};
diff --git a/src/routes/services/[id]/uptimekuma/index.json.ts b/src/routes/services/[id]/uptimekuma/index.json.ts
new file mode 100644
index 000000000..5ec3fa69a
--- /dev/null
+++ b/src/routes/services/[id]/uptimekuma/index.json.ts
@@ -0,0 +1,20 @@
+import { getUserDetails } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+ const { id } = event.params;
+
+ let { name, fqdn } = await event.request.json();
+ if (fqdn) fqdn = fqdn.toLowerCase();
+
+ try {
+ await db.updateService({ id, fqdn, name });
+ return { status: 201 };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/uptimekuma/start.json.ts b/src/routes/services/[id]/uptimekuma/start.json.ts
new file mode 100644
index 000000000..845873bb0
--- /dev/null
+++ b/src/routes/services/[id]/uptimekuma/start.json.ts
@@ -0,0 +1,77 @@
+import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common';
+import * as db from '$lib/database';
+import { promises as fs } from 'fs';
+import yaml from 'js-yaml';
+import type { RequestHandler } from '@sveltejs/kit';
+import { ErrorHandler, getServiceImage } from '$lib/database';
+import { makeLabelForServices } from '$lib/buildPacks/common';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
+ const network = destinationDockerId && destinationDocker.network;
+ const host = getEngine(destinationDocker.engine);
+
+ 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 composeFile = {
+ version: '3.8',
+ services: {
+ [id]: {
+ container_name: id,
+ image: config.image,
+ networks: [network],
+ volumes: [config.volume],
+ environment: config.environmentVariables,
+ restart: 'always',
+ labels: makeLabelForServices('uptimekuma')
+ }
+ },
+ networks: {
+ [network]: {
+ external: true
+ }
+ },
+ volumes: {
+ [config.volume.split(':')[0]]: {
+ name: config.volume.split(':')[0]
+ }
+ }
+ };
+ const composeFileDestination = `${workdir}/docker-compose.yaml`;
+ await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
+
+ try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/uptimekuma/stop.json.ts b/src/routes/services/[id]/uptimekuma/stop.json.ts
new file mode 100644
index 000000000..c604e1cc3
--- /dev/null
+++ b/src/routes/services/[id]/uptimekuma/stop.json.ts
@@ -0,0 +1,35 @@
+import { getUserDetails, removeDestinationDocker } from '$lib/common';
+import * as db from '$lib/database';
+import { ErrorHandler } from '$lib/database';
+import { checkContainer } from '$lib/haproxy';
+import type { RequestHandler } from '@sveltejs/kit';
+
+export const post: RequestHandler = async (event) => {
+ const { teamId, status, body } = await getUserDetails(event);
+ if (status === 401) return { status, body };
+
+ const { id } = event.params;
+
+ try {
+ const service = await db.getService({ id, teamId });
+ const { destinationDockerId, destinationDocker, fqdn } = service;
+ if (destinationDockerId) {
+ const engine = destinationDocker.engine;
+
+ try {
+ const found = await checkContainer(engine, id);
+ if (found) {
+ await removeDestinationDocker({ id, engine });
+ }
+ } catch (error) {
+ console.error(error);
+ }
+ }
+
+ return {
+ status: 200
+ };
+ } catch (error) {
+ return ErrorHandler(error);
+ }
+};
diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts
index 703a71473..f977adda3 100644
--- a/src/routes/services/[id]/vaultwarden/start.json.ts
+++ b/src/routes/services/[id]/vaultwarden/start.json.ts
@@ -52,20 +52,18 @@ export const post: RequestHandler = async (event) => {
},
volumes: {
[config.volume.split(':')[0]]: {
- external: true
+ name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
- );
- } catch (error) {
- console.log(error);
- }
- try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts
index be43cb2a7..6f51fcf2a 100644
--- a/src/routes/services/[id]/vscodeserver/start.json.ts
+++ b/src/routes/services/[id]/vscodeserver/start.json.ts
@@ -61,29 +61,20 @@ export const post: RequestHandler = async (event) => {
},
volumes: {
[config.volume.split(':')[0]]: {
- external: true
+ name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
- try {
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}`
- );
- } catch (error) {
- console.log(error);
- }
-
- try {
- await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
- return {
- status: 200
- };
- } catch (error) {
- return ErrorHandler(error);
+ if (version === 'latest') {
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
}
+ await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
+ return {
+ status: 200
+ };
} catch (error) {
return ErrorHandler(error);
}
diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts
index d1685d8b1..98e7d6aab 100644
--- a/src/routes/services/[id]/wordpress/start.json.ts
+++ b/src/routes/services/[id]/wordpress/start.json.ts
@@ -72,6 +72,7 @@ export const post: RequestHandler = async (event) => {
container_name: id,
image: config.wordpress.image,
environment: config.wordpress.environmentVariables,
+ volumes: [config.wordpress.volume],
networks: [network],
restart: 'always',
depends_on: [`${id}-mysql`],
@@ -80,6 +81,7 @@ export const post: RequestHandler = async (event) => {
[`${id}-mysql`]: {
container_name: `${id}-mysql`,
image: config.mysql.image,
+ volumes: [config.mysql.volume],
environment: config.mysql.environmentVariables,
networks: [network],
restart: 'always'
@@ -91,29 +93,22 @@ export const post: RequestHandler = async (event) => {
}
},
volumes: {
- [config.mysql.volume.split(':')[0]]: {
- external: true
- },
[config.wordpress.volume.split(':')[0]]: {
- external: true
+ name: config.wordpress.volume.split(':')[0]
+ },
+ [config.mysql.volume.split(':')[0]]: {
+ name: config.mysql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
-
- try {
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.mysql.volume.split(':')[0]}`
- );
- await asyncExecShell(
- `DOCKER_HOST=${host} docker volume create ${config.wordpress.volume.split(':')[0]}`
- );
- } catch (error) {
- console.log(error);
- }
-
try {
+ if (version === 'latest') {
+ await asyncExecShell(
+ `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`
+ );
+ }
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte
index a4d34cd5f..2b5584270 100644
--- a/src/routes/services/index.svelte
+++ b/src/routes/services/index.svelte
@@ -8,6 +8,9 @@
import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte';
import { post } from '$lib/api';
import { goto } from '$app/navigation';
+ import N8n from '$lib/components/svg/services/N8n.svelte';
+ import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte';
+ import Ghost from '$lib/components/svg/services/Ghost.svelte';
export let services;
async function newService() {
@@ -58,6 +61,12 @@
{:else if service.type === 'languagetool'}
+ {:else if service.type === 'n8n'}
+
+ {:else if service.type === 'uptimekuma'}
+
+ {:else if service.type === 'ghost'}
+
{/if}
{service.name}
diff --git a/src/tailwind.css b/src/tailwind.css
index 1a30a3ac0..1f357acfb 100644
--- a/src/tailwind.css
+++ b/src/tailwind.css
@@ -37,6 +37,29 @@ textarea {
@apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm;
}
+#svelte .custom-select-wrapper .selectContainer.disabled input {
+ @apply placeholder:text-stone-600;
+}
+
+#svelte .custom-select-wrapper .selectContainer input {
+ @apply text-white;
+}
+
+#svelte .custom-select-wrapper .selectContainer {
+ @apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 text-xs font-bold tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
+}
+
+#svelte .listContainer {
+ @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
+}
+
+#svelte .item.hover {
+ @apply bg-coolgray-100 text-white;
+}
+#svelte .item.active {
+ @apply bg-coollabs text-white;
+}
+
select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
}
diff --git a/static/ghost.png b/static/ghost.png
new file mode 100644
index 000000000..f4e300654
Binary files /dev/null and b/static/ghost.png differ