Merge pull request #370 from coollabsio/next

v2.5.2
This commit is contained in:
Andras Bacsai 2022-04-25 17:49:07 +02:00 committed by GitHub
commit 5ccea1cfcc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 788 additions and 116 deletions

View File

@ -2,5 +2,7 @@ COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234 COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN= COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker" COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED="false" COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=false

View File

@ -12,9 +12,6 @@ ## 🙋 Want to help?
- [🧑‍💻 Develop your own ideas](#developer-contribution) - [🧑‍💻 Develop your own ideas](#developer-contribution)
- [🌐 Translate the project](#translation) - [🌐 Translate the project](#translation)
- [📄 Help sorting out the issues](#help-sorting-out-the-issues)
- [🎯 Test Pull Requests](#test-pull-requests)
- [✒️ Help with the documentation](#help-with-the-documentation)
## 👋 Introduction ## 👋 Introduction
@ -60,6 +57,7 @@ ### Technical skills required
- **Languages**: Node.js / Javascript / Typescript - **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: Svelte / SvelteKit - **Framework JS/TS**: Svelte / SvelteKit
- **Database ORM**: Prisma.io - **Database ORM**: Prisma.io
- **Docker Engine**
### Database migrations ### Database migrations
@ -83,50 +81,159 @@ # How to add new services
## Backend ## Backend
I use MinIO as an example. There are 5 steps you should make on the backend side.
You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties: 1. Create Prisma / database schema for the new service.
2. Add supported versions of the service.
3. Update global functions.
4. Create API endpoints.
5. Define automatically generated variables.
1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. > I will use [Umami](https://umami.is/) as an example service.
Basic services only require updating the URL(fqdn) and the name of the service. ### Create Prisma / database schema for the new service.
2. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service. You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB.
- To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma).
Example JSON: - Add new model with the new service name.
- Make a relationshup with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
```js If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command.
> You must restart the running development environment to be able to use the new model
> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode.
### Add supported versions
Supported versions are hardcoded into Coolify (for now).
You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON:
```js
{ {
// Name used to identify the service in Coolify // Name used to identify the service internally
name: 'minio', name: 'umami',
// Fancier name to show to the user // Fancier name to show to the user
fancyName: 'MinIO', fancyName: 'Umami',
// Docker base image for the service // Docker base image for the service
baseImage: 'minio/minio', baseImage: 'ghcr.io/mikecao/umami',
// Optional: If there is any dependent image, you should list it here
images: [],
// Usable tags // Usable tags
versions: ['latest'], versions: ['postgresql-latest'],
// Which tag is the recommended // Which tag is the recommended
recommendedVersion: 'latest', recommendedVersion: 'postgresql-latest',
// Application's default port, MinIO listens on 9001 (and 9000, more details later on) // Application's default port, Umami listens on 3000
ports: { ports: {
main: 9001 main: 3000
} }
}, }
``` ```
- You need to define a compose file as `const composeFile: ComposeFile` found in [src/routes/services/[id]/minio/start.json.ts](src/routes/services/[id]/minio/start.json.ts) ### Update global functions
**IMPORTANT:** It should contain `all the default environment variables` that are required for the service to function correctly and `all the volumes to persist data` in restarts. 1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required.
- You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts)) ```js
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true // This line!
};
```
3. `stop.json.ts` A stop endpoint that stops the service. 2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable).
It needs to stop all the services by their container name and proxies (if applicable). ```js
[...]
else if (type === 'umami') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt,
}
}
}
});
}
```
4. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function. 3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
```js
if (body.umami?.postgresqlPassword)
body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
```
4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts)
### Create API endpoints.
You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder.
#### `index.json.ts`:
It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this:
```js
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);
}
};
```
If it's necessary, you can create your own database update function, specifically for the new service.
#### `start.json.ts`
It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine.
You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts))
#### `stop.json.ts`
It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database.
## Frontend ## Frontend
@ -134,7 +241,11 @@ ## Frontend
SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
2. You need to include it the logo at [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` and [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with a link to the docs/main site of the service. 2. You need to include it the logo at
- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places,
- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service
- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`.
3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte). 3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte).
@ -164,19 +275,3 @@ ### Adding a language
1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`). 1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`).
2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`). 2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`).
3. Have fun translating! 3. Have fun translating!
### Additionnal pull requests steps
Please add the emoji 🌐 to your pull request title to indicate that it is a translation.
## 📄 Help sorting out the issues
ToDo
## 🎯 Test Pull Requests
ToDo
## ✒️ Help with the documentation
ToDo

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.5.1", "version": "2.5.2",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",

View File

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "Umami" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"umamiAdminPassword" TEXT NOT NULL,
"hashSalt" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId");

View File

@ -0,0 +1,22 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Setting" (
"id" TEXT NOT NULL PRIMARY KEY,
"fqdn" TEXT,
"isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"minPort" INTEGER NOT NULL DEFAULT 9000,
"maxPort" INTEGER NOT NULL DEFAULT 9100,
"proxyPassword" TEXT NOT NULL,
"proxyUser" TEXT NOT NULL,
"proxyHash" TEXT,
"isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -18,6 +18,7 @@ model Setting {
proxyPassword String proxyPassword String
proxyUser String proxyUser String
proxyHash String? proxyHash String?
isAutoUpdateEnabled Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -91,9 +92,9 @@ model Application {
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?
pythonVariable String? pythonVariable String?
dockerFileLocation String? dockerFileLocation String?
denoMainFile String? denoMainFile String?
denoOptions String? denoOptions String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
@ -301,6 +302,7 @@ model Service {
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
meiliSearch MeiliSearch? meiliSearch MeiliSearch?
persistentStorage ServicePersistentStorage[] persistentStorage ServicePersistentStorage[]
umami Umami?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@ -385,3 +387,17 @@ model MeiliSearch {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model Umami {
id String @id @default(cuid())
serviceId String @unique
postgresqlUser String
postgresqlPassword String
postgresqlDatabase String
postgresqlPublicPort Int?
umamiAdminPassword String
hashSalt String
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@ -50,6 +50,20 @@ async function main() {
} }
}); });
} }
// Set auto-update based on env variable
const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true';
const settings = await prisma.setting.findFirst({});
if (settings) {
await prisma.setting.update({
where: {
id: settings.id
},
data: {
isAutoUpdateEnabled
}
});
}
} }
main() main()
.catch((e) => { .catch((e) => {

View File

@ -26,7 +26,7 @@ try {
initialScope: { initialScope: {
tags: { tags: {
appId: process.env['COOLIFY_APP_ID'], appId: process.env['COOLIFY_APP_ID'],
'os.arch': os.arch(), 'os.arch': getOsArch(),
'os.platform': os.platform(), 'os.platform': os.platform(),
'os.release': os.release() 'os.release': os.release()
} }
@ -175,3 +175,7 @@ export function generateTimestamp(): string {
export function getDomain(domain: string): string { export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');
} }
export function getOsArch() {
return os.arch();
}

View File

@ -6,6 +6,7 @@
import N8n from './svg/services/N8n.svelte'; import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte'; import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte'; import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import Umami from './svg/services/Umami.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte'; import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte'; import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte'; import VsCodeServer from './svg/services/VSCodeServer.svelte';
@ -52,4 +53,8 @@
<a href="https://ghost.org" target="_blank"> <a href="https://ghost.org" target="_blank">
<Ghost /> <Ghost />
</a> </a>
{:else if service.type === 'umami'}
<a href="https://umami.is" target="_blank">
<Umami />
</a>
{/if} {/if}

View File

@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 7700 main: 7700
} }
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
} }
]; ];

View File

@ -0,0 +1,85 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
version="1.0"
xmlns="http://www.w3.org/2000/svg"
width="856.000000pt"
height="856.000000pt"
viewBox="0 0 856.000000 856.000000"
preserveAspectRatio="xMidYMid meet"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
>
<metadata> Created by potrace 1.11, written by Peter Selinger 2001-2013 </metadata>
<g
transform="translate(0.000000,856.000000) scale(0.100000,-0.100000)"
fill="currentColor"
stroke="none"
>
<path
d="M4027 8163 c-2 -2 -28 -5 -58 -7 -50 -4 -94 -9 -179 -22 -19 -2 -48
-6 -65 -9 -47 -6 -236 -44 -280 -55 -22 -6 -49 -12 -60 -15 -34 -6 -58 -13
-130 -36 -38 -13 -72 -23 -75 -24 -29 -6 -194 -66 -264 -96 -49 -22 -95 -39
-102 -39 -7 0 -19 -7 -28 -15 -8 -9 -18 -15 -21 -14 -7 1 -197 -92 -205 -101
-3 -3 -21 -13 -40 -24 -79 -42 -123 -69 -226 -137 -94 -62 -246 -173 -280
-204 -6 -5 -29 -25 -52 -43 -136 -111 -329 -305 -457 -462 -21 -25 -41 -47
-44 -50 -4 -3 -22 -26 -39 -52 -18 -25 -38 -52 -45 -60 -34 -35 -207 -308
-259 -408 -13 -25 -25 -47 -28 -50 -11 -11 -121 -250 -159 -346 -42 -105 -114
-321 -126 -374 l-7 -30 -263 0 c-245 0 -268 -2 -321 -21 -94 -35 -171 -122
-191 -216 -9 -39 -8 -852 0 -938 9 -87 16 -150 23 -195 3 -19 6 -48 8 -65 3
-29 14 -97 22 -140 3 -11 7 -36 10 -55 3 -19 9 -51 14 -70 5 -19 11 -46 14
-60 29 -138 104 -401 145 -505 5 -11 23 -58 42 -105 18 -47 42 -105 52 -130
11 -25 21 -49 22 -55 3 -10 109 -224 164 -330 18 -33 50 -89 71 -124 22 -34
40 -64 40 -66 0 -8 104 -161 114 -167 6 -4 7 -8 3 -8 -4 0 4 -12 18 -27 14
-15 25 -32 25 -36 0 -5 6 -14 13 -21 6 -7 21 -25 32 -41 11 -15 34 -44 50 -64
17 -21 41 -52 55 -70 13 -18 33 -43 45 -56 11 -13 42 -49 70 -81 100 -118 359
-369 483 -469 34 -27 62 -53 62 -57 0 -5 6 -8 13 -8 7 0 19 -9 27 -20 8 -11
19 -20 26 -20 6 0 19 -9 29 -20 10 -11 22 -20 27 -20 5 0 23 -13 41 -30 18
-16 37 -30 44 -30 6 0 13 -4 15 -8 3 -8 186 -132 194 -132 2 0 27 -15 56 -34
132 -83 377 -207 558 -280 36 -15 74 -31 85 -36 62 -26 220 -81 320 -109 79
-23 191 -53 214 -57 14 -3 28 -7 31 -9 4 -2 20 -7 36 -9 16 -3 40 -8 54 -11
14 -3 36 -8 50 -11 14 -2 36 -7 50 -10 13 -3 40 -8 60 -10 19 -2 46 -7 60 -10
54 -10 171 -25 320 -40 90 -9 613 -12 636 -4 11 5 28 4 37 -1 9 -6 17 -6 17
-1 0 4 10 8 23 9 29 0 154 12 192 18 17 3 46 7 65 9 70 10 131 20 183 32 16 3
38 7 50 9 45 7 165 36 252 60 50 14 100 28 112 30 12 3 34 10 48 15 14 5 25 7
25 4 0 -4 6 -2 13 3 6 6 30 16 52 22 22 7 47 15 55 18 8 4 17 7 20 7 10 2 179
68 240 94 96 40 342 159 395 191 17 10 53 30 80 46 28 15 81 47 118 71 37 24
72 44 76 44 5 0 11 3 13 8 2 4 30 25 63 47 33 22 62 42 65 45 3 3 50 38 105
79 55 40 105 79 110 85 6 6 24 22 40 34 85 65 465 430 465 447 0 3 8 13 18 23
9 10 35 40 57 66 22 27 47 56 55 65 8 9 42 52 74 96 32 44 71 96 85 115 140
183 358 576 461 830 12 30 28 69 36 85 24 56 123 355 117 355 -3 0 -1 6 5 13
6 6 14 30 18 52 10 48 9 46 17 65 5 13 37 155 52 230 9 42 35 195 40 231 34
235 40 357 40 804 l0 420 -24 44 c-46 87 -143 157 -231 166 -19 2 -144 4 -276
4 l-242 1 -36 118 c-21 64 -46 139 -56 166 -11 27 -20 52 -20 57 0 5 -11 33
-25 63 -14 30 -25 58 -25 61 0 18 -152 329 -162 333 -5 2 -8 10 -8 18 0 8 -4
14 -10 14 -5 0 -9 3 -8 8 3 9 -40 82 -128 217 -63 97 -98 145 -187 259 -133
171 -380 420 -559 564 -71 56 -132 102 -138 102 -5 0 -10 3 -10 8 0 4 -25 23
-55 42 -30 19 -55 38 -55 43 0 4 -6 7 -13 7 -7 0 -22 8 -33 18 -11 9 -37 26
-59 37 -21 11 -44 25 -50 30 -41 37 -413 220 -540 266 -27 9 -61 22 -75 27
-14 5 -28 10 -32 11 -4 1 -28 10 -53 21 -25 11 -46 19 -48 18 -2 -1 -109 29
-137 40 -13 4 -32 9 -65 16 -5 1 -16 5 -22 9 -7 5 -13 6 -13 3 0 -2 -15 0 -32
5 -18 5 -44 11 -58 14 -14 3 -36 7 -50 10 -14 3 -50 9 -80 15 -30 6 -64 12
-75 14 -11 2 -45 6 -75 10 -30 4 -71 9 -90 12 -19 3 -53 6 -75 7 -22 1 -44 5
-50 8 -11 7 -542 9 -548 2z m57 -404 c7 10 436 8 511 -3 22 -3 60 -8 85 -11
25 -2 56 -6 70 -9 14 -2 43 -7 65 -10 38 -5 58 -9 115 -21 14 -3 34 -7 45 -9
11 -2 58 -14 105 -26 47 -12 92 -23 100 -25 35 -7 279 -94 308 -109 17 -9 34
-16 37 -16 3 1 20 -6 38 -14 17 -8 68 -31 112 -51 44 -20 82 -35 84 -35 2 1 7
-3 10 -8 3 -5 43 -28 88 -51 45 -23 87 -48 93 -56 7 -8 17 -15 22 -15 12 0
192 -121 196 -132 2 -4 8 -8 13 -8 10 0 119 -86 220 -172 102 -87 256 -244
349 -357 25 -30 53 -63 63 -73 9 -10 17 -22 17 -28 0 -5 3 -10 8 -10 4 0 25
-27 46 -60 22 -33 43 -60 48 -60 4 0 8 -5 8 -11 0 -6 11 -25 25 -43 14 -18 25
-38 25 -44 0 -7 4 -12 8 -12 5 0 16 -15 25 -32 9 -18 30 -55 47 -83 46 -77
161 -305 154 -305 -4 0 -2 -6 4 -12 6 -7 23 -47 40 -88 16 -41 33 -84 37 -95
5 -11 9 -22 10 -25 0 -3 11 -36 24 -73 13 -38 21 -70 19 -73 -3 -2 -1386 -3
-3075 -2 l-3071 3 38 110 c47 137 117 301 182 425 62 118 167 295 191 320 9
11 17 22 17 25 0 7 39 63 58 83 6 7 26 35 44 60 18 26 37 52 43 57 6 6 34 37
61 70 48 59 271 286 329 335 17 14 53 43 80 65 28 22 52 42 55 45 3 3 21 17
40 30 19 14 40 28 45 32 40 32 105 78 109 78 3 0 28 16 55 35 26 19 53 35 58
35 5 0 18 8 29 18 17 15 53 35 216 119 118 60 412 176 422 166 3 -4 6 -2 6 4
0 6 12 13 28 16 15 3 52 12 82 21 30 9 63 19 73 21 10 2 27 7 37 10 10 3 29 8
42 10 13 3 48 10 78 16 30 7 61 12 68 12 6 0 12 4 12 9 0 5 5 6 10 3 6 -4 34
-2 63 4 51 11 71 13 197 26 36 4 67 9 69 11 2 2 10 -1 17 -7 8 -6 14 -7 18 0z"
/>
</g>
</svg>

View File

@ -154,6 +154,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string; POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string; POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string; POSTGRESQL_DATABASE: string;
@ -220,6 +221,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
return { return {
privatePort: 5432, privatePort: 5432,
environmentVariables: { environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
POSTGRESQL_PASSWORD: dbUserPassword, POSTGRESQL_PASSWORD: dbUserPassword,
POSTGRESQL_USERNAME: dbUser, POSTGRESQL_USERNAME: dbUser,
POSTGRESQL_DATABASE: defaultDatabase POSTGRESQL_DATABASE: defaultDatabase

View File

@ -1,9 +1,27 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Service } from '@prisma/client'; import type { Minio, Prisma, Service } from '@prisma/client';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
import { prisma } from './common'; import { prisma } from './common';
const include: Prisma.ServiceInclude = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true
};
export async function listServicesWithIncludes() {
return await prisma.service.findMany({
include,
orderBy: { createdAt: 'desc' }
});
}
export async function listServices(teamId: string): Promise<Service[]> { export async function listServices(teamId: string): Promise<Service[]> {
if (teamId === '0') { if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } }); return await prisma.service.findMany({ include: { teams: true } });
@ -30,35 +48,21 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
if (teamId === '0') { if (teamId === '0') {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id }, where: { id },
include: { include
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
}); });
} else { } else {
body = await prisma.service.findFirst({ body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } }, where: { id, teams: { some: { id: teamId } } },
include: { include
destinationDocker: true,
plausibleAnalytics: true,
minio: true,
vscodeserver: true,
wordpress: true,
ghost: true,
serviceSecret: true,
meiliSearch: true,
persistentStorage: true
}
}); });
} }
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
if (body.plausibleAnalytics?.postgresqlPassword) if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword = decrypt(
body.plausibleAnalytics.postgresqlPassword body.plausibleAnalytics.postgresqlPassword
@ -85,15 +89,14 @@ export async function getService({ id, teamId }: { id: string; teamId: string })
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey); if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) { if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value); if (body.umami?.postgresqlPassword)
return s; body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword);
}); if (body.umami?.umamiAdminPassword)
} body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword);
if (body.wordpress?.ftpPassword) { if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt);
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
const settings = await prisma.setting.findFirst(); const settings = await prisma.setting.findFirst();
return { ...body, settings }; return { ...body, settings };
@ -219,6 +222,27 @@ export async function configureServiceType({
meiliSearch: { create: { masterKey } } meiliSearch: { create: { masterKey } }
} }
}); });
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
} }
} }
@ -375,6 +399,7 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });

View File

@ -6,6 +6,7 @@ import crypto from 'crypto';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { listServicesWithIncludes } from '$lib/database';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@ -208,17 +209,7 @@ export async function configureHAProxy(): Promise<void> {
} }
} }
} }
const services = await db.prisma.service.findMany({ const services = await listServicesWithIncludes();
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true
}
});
for (const service of services) { for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;

View File

@ -7,6 +7,7 @@ import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import { listServicesWithIncludes } from '$lib/database';
export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> { export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try { try {
@ -145,18 +146,7 @@ export async function generateSSLCerts(): Promise<void> {
console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`); console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`);
} }
} }
const services = await db.prisma.service.findMany({ const services = await listServicesWithIncludes();
include: {
destinationDocker: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true
},
orderBy: { createdAt: 'desc' }
});
for (const service of services) { for (const service of services) {
try { try {

View File

@ -302,7 +302,9 @@
"registration_allowed": "Registration allowed?", "registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.", "registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings", "coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page." "credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running."
}, },
"team": { "team": {
"pending_invitations": "Pending invitations", "pending_invitations": "Pending invitations",

View File

@ -0,0 +1,42 @@
import { prisma } from '$lib/database';
import { buildQueue } from '.';
import got from 'got';
import { asyncExecShell, version } from '$lib/common';
import compare from 'compare-versions';
import { dev } from '$app/env';
export default async function (): Promise<void> {
try {
const currentVersion = version;
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
const versions = await got
.get(
`https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}`
)
.json();
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compare(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = await buildQueue.getActiveCount();
if (activeCount === 0) {
if (!dev) {
await buildQueue.pause();
console.log(`Updating Coolify to ${latestVersion}.`);
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"`
);
} else {
await buildQueue.pause();
console.log('Updating (not really in dev mode).');
}
}
}
}
} catch (error) {
await buildQueue.resume();
console.log(error);
}
}

View File

@ -10,6 +10,7 @@ import proxy from './proxy';
import proxyTcpHttp from './proxyTcpHttp'; import proxyTcpHttp from './proxyTcpHttp';
import ssl from './ssl'; import ssl from './ssl';
import sslrenewal from './sslrenewal'; import sslrenewal from './sslrenewal';
import autoUpdater from './autoUpdater';
import { asyncExecShell, saveBuildLog } from '$lib/common'; import { asyncExecShell, saveBuildLog } from '$lib/common';
@ -34,19 +35,22 @@ const cron = async (): Promise<void> => {
new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions); new QueueScheduler('ssl', connectionOptions);
new QueueScheduler('sslRenew', connectionOptions); new QueueScheduler('sslRenew', connectionOptions);
new QueueScheduler('autoUpdater', connectionOptions);
const queue = { const queue = {
proxy: new Queue('proxy', { ...connectionOptions }), proxy: new Queue('proxy', { ...connectionOptions }),
proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }), proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }),
cleanup: new Queue('cleanup', { ...connectionOptions }), cleanup: new Queue('cleanup', { ...connectionOptions }),
ssl: new Queue('ssl', { ...connectionOptions }), ssl: new Queue('ssl', { ...connectionOptions }),
sslRenew: new Queue('sslRenew', { ...connectionOptions }) sslRenew: new Queue('sslRenew', { ...connectionOptions }),
autoUpdater: new Queue('autoUpdater', { ...connectionOptions })
}; };
await queue.proxy.drain(); await queue.proxy.drain();
await queue.proxyTcpHttp.drain(); await queue.proxyTcpHttp.drain();
await queue.cleanup.drain(); await queue.cleanup.drain();
await queue.ssl.drain(); await queue.ssl.drain();
await queue.sslRenew.drain(); await queue.sslRenew.drain();
await queue.autoUpdater.drain();
new Worker( new Worker(
'proxy', 'proxy',
@ -98,11 +102,22 @@ const cron = async (): Promise<void> => {
} }
); );
new Worker(
'autoUpdater',
async () => {
await autoUpdater();
},
{
...connectionOptions
}
);
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } }); await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
}; };
cron().catch((error) => { cron().catch((error) => {
console.log('cron failed to start'); console.log('cron failed to start');
@ -115,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job)
concurrency: 1, concurrency: 1,
...connectionOptions ...connectionOptions
}); });
buildQueue.resume().catch((err) => {
console.log('Build queue failed to resume!', err);
});
buildWorker.on('completed', async (job: Bullmq.Job) => { buildWorker.on('completed', async (job: Bullmq.Job) => {
try { try {
@ -123,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
setTimeout(async () => { setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234); }, 1234);
console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`; const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
@ -139,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
setTimeout(async () => { setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234); }, 1234);
console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);

View File

@ -401,10 +401,10 @@
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
> >
<button <button
title={$t('application.build_logs')} title={$t('application.logs')}
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons bg-transparent tooltip-bottom text-sm"
data-tooltip={$t('application.build_logs')} data-tooltip={$t('application.logs')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -16,6 +16,7 @@
import MeiliSearch from './_MeiliSearch.svelte'; import MeiliSearch from './_MeiliSearch.svelte';
import MinIo from './_MinIO.svelte'; import MinIo from './_MinIO.svelte';
import PlausibleAnalytics from './_PlausibleAnalytics.svelte'; import PlausibleAnalytics from './_PlausibleAnalytics.svelte';
import Umami from './_Umami.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
import Wordpress from './_Wordpress.svelte'; import Wordpress from './_Wordpress.svelte';
@ -169,6 +170,8 @@
<Ghost bind:service {readOnly} /> <Ghost bind:service {readOnly} />
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
<MeiliSearch bind:service /> <MeiliSearch bind:service />
{:else if service.type === 'umami'}
<Umami bind:service />
{/if} {/if}
</div> </div>
</form> </form>

View File

@ -0,0 +1,29 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
export let service;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Umami</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="adminUser">Admin User</label>
<input name="adminUser" id="adminUser" placeholder="admin" value="admin" disabled readonly />
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="umamiAdminPassword">Initial Admin Password</label>
<CopyPasswordField
isPasswordField
name="umamiAdminPassword"
id="umamiAdminPassword"
placeholder="admin"
value={service.umami.umamiAdminPassword}
disabled
readonly
/>
<Explainer
text="It could be changed in Umami. <br>This is just the password set initially after the first start."
/>
</div>

View File

@ -43,6 +43,7 @@
import Ghost from '$lib/components/svg/services/Ghost.svelte'; import Ghost from '$lib/components/svg/services/Ghost.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import Umami from '$lib/components/svg/services/Umami.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@ -90,6 +91,8 @@
<Ghost isAbsolute /> <Ghost isAbsolute />
{:else if type.name === 'meilisearch'} {:else if type.name === 'meilisearch'}
<MeiliSearch isAbsolute /> <MeiliSearch isAbsolute />
{:else if type.name === 'umami'}
<Umami isAbsolute />
{/if}{type.fancyName} {/if}{type.fancyName}
</button> </button>
</form> </form>

View File

@ -0,0 +1,21 @@
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);
}
};

View File

@ -0,0 +1,214 @@
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';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';
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: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } =
await db.getService({ id, teamId });
const {
type,
version,
destinationDockerId,
destinationDocker,
serviceSecret,
umami: {
umamiAdminPassword,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
hashSalt
}
} = 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 = {
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 composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.umami.image,
environment: config.umami.environmentVariables,
networks: [network],
volumes: [],
restart: 'always',
labels: makeLabelForServices('umami'),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
},
depends_on: [`${id}-postgresql`]
},
[`${id}-postgresql`]: {
build: workdir,
container_name: `${id}-postgresql`,
environment: config.postgresql.environmentVariables,
networks: [network],
volumes: [config.postgresql.volume],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.postgresql.volume.split(':')[0]]: {
name: config.postgresql.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
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) {
console.log(error);
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,42 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, stopTcpHttpProxy } 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 } = 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);
}
try {
const found = await checkContainer(engine, `${id}-postgresql`);
if (found) {
await removeDestinationDocker({ id: `${id}-postgresql`, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -15,6 +15,7 @@
import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte';
import { session } from '$app/stores'; import { session } from '$app/stores';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import Umami from '$lib/components/svg/services/Umami.svelte';
export let services; export let services;
async function newService() { async function newService() {
@ -86,6 +87,8 @@
<Ghost isAbsolute /> <Ghost isAbsolute />
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute /> <MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{/if} {/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}
@ -133,6 +136,8 @@
<Ghost isAbsolute /> <Ghost isAbsolute />
{:else if service.type === 'meilisearch'} {:else if service.type === 'meilisearch'}
<MeiliSearch isAbsolute /> <MeiliSearch isAbsolute />
{:else if service.type === 'umami'}
<Umami isAbsolute />
{/if} {/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}

View File

@ -64,10 +64,14 @@ export const post: RequestHandler = async (event) => {
}; };
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort } = await event.request.json(); const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort, isAutoUpdateEnabled } =
await event.request.json();
try { try {
const { id } = await db.listSettings(); const { id } = await db.listSettings();
await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled, dualCerts } }); await db.prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled }
});
if (fqdn) { if (fqdn) {
await db.prisma.setting.update({ where: { id }, data: { fqdn } }); await db.prisma.setting.update({ where: { id }, data: { fqdn } });
} }

View File

@ -40,10 +40,9 @@
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Language from './_Language.svelte';
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let minPort = settings.minPort; let minPort = settings.minPort;
let maxPort = settings.maxPort; let maxPort = settings.maxPort;
@ -76,7 +75,10 @@
if (name === 'dualCerts') { if (name === 'dualCerts') {
dualCerts = !dualCerts; dualCerts = !dualCerts;
} }
await post(`/settings.json`, { isRegistrationEnabled, dualCerts }); if (name === 'isAutoUpdateEnabled') {
isAutoUpdateEnabled = !isAutoUpdateEnabled;
}
await post(`/settings.json`, { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled });
return toast.push(t.get('application.settings_saved')); return toast.push(t.get('application.settings_saved'));
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
@ -192,6 +194,16 @@
on:click={() => changeSettings('isRegistrationEnabled')} on:click={() => changeSettings('isRegistrationEnabled')}
/> />
</div> </div>
{#if browser && (window.location.hostname === 'staging.coolify.io' || window.location.hostname === 'localhost')}
<div class="grid grid-cols-2 items-center">
<Setting
bind:setting={isAutoUpdateEnabled}
title={$t('setting.auto_update_enabled')}
description={$t('setting.auto_update_enabled_explainer')}
on:click={() => changeSettings('isAutoUpdateEnabled')}
/>
</div>
{/if}
</div> </div>
</form> </form>
<div class="flex space-x-1 pt-6 font-bold"> <div class="flex space-x-1 pt-6 font-bold">