commit
5ccea1cfcc
@ -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
|
183
CONTRIBUTING.md
183
CONTRIBUTING.md
@ -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
|
|
||||||
|
@ -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",
|
||||||
|
17
prisma/migrations/20220425071132_umami/migration.sql
Normal file
17
prisma/migrations/20220425071132_umami/migration.sql
Normal 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");
|
@ -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;
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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) => {
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
85
src/lib/components/svg/services/Umami.svelte
Normal file
85
src/lib/components/svg/services/Umami.svelte
Normal 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>
|
@ -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
|
||||||
|
@ -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 } });
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
42
src/lib/queues/autoUpdater.ts
Normal file
42
src/lib/queues/autoUpdater.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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}`);
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
29
src/routes/services/[id]/_Services/_Umami.svelte
Normal file
29
src/routes/services/[id]/_Services/_Umami.svelte
Normal 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>
|
@ -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>
|
||||||
|
21
src/routes/services/[id]/umami/index.json.ts
Normal file
21
src/routes/services/[id]/umami/index.json.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
214
src/routes/services/[id]/umami/start.json.ts
Normal file
214
src/routes/services/[id]/umami/start.json.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
42
src/routes/services/[id]/umami/stop.json.ts
Normal file
42
src/routes/services/[id]/umami/stop.json.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
@ -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}
|
||||||
|
@ -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 } });
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
Loading…
Reference in New Issue
Block a user