Merge pull request #434 from coollabsio/next

v2.9.0
This commit is contained in:
Andras Bacsai 2022-05-31 11:20:16 +02:00 committed by GitHub
commit 53e32c038b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
65 changed files with 2446 additions and 337 deletions

View File

@ -9,6 +9,7 @@ ## Live Demo
(If it is unresponsive, that means someone overloaded the server. 🙃) (If it is unresponsive, that means someone overloaded the server. 🙃)
## Feedback ## Feedback
If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
## How to install ## How to install
@ -62,6 +63,7 @@ ### Applications
- Rust - Rust
- Docker - Docker
- Python - Python
- Deno
### Databases ### Databases

View File

@ -0,0 +1,23 @@
version: '3.5'
services:
${ID}:
container_name: proxy-for-${PORT}
image: traefik:v2.6
command:
- --api.insecure=true
- --entrypoints.web.address=:${PORT}
- --providers.docker=false
- --providers.docker.exposedbydefault=false
- --providers.http.endpoint=http://host.docker.internal:3000/traefik.json?id=${ID}
- --providers.http.pollTimeout=5s
- --log.level=error
ports:
- '${PORT}:${PORT}'
networks:
- ${NETWORK}
networks:
net:
external: false
name: ${NETWORK}

View File

@ -0,0 +1,29 @@
version: '3.8'
services:
proxy:
image: traefik:v2.6
command:
- --api.insecure=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --providers.docker=false
- --providers.docker.exposedbydefault=false
- --providers.http.endpoint=http://host.docker.internal:3000/traefik.json
- --providers.http.pollTimeout=5s
- --log.level=error
ports:
- '80:80'
- '443:443'
- '8080:8080'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
extra_hosts:
- 'host.docker.internal:host-gateway'
networks:
- coolify-infra
networks:
coolify-infra:
attachable: true
name: coolify-infra

View File

@ -39,3 +39,5 @@ volumes:
name: coolify-ssl-certs name: coolify-ssl-certs
coolify-letsencrypt: coolify-letsencrypt:
name: coolify-letsencrypt name: coolify-letsencrypt
coolify-traefik-letsencrypt:
name: coolify-traefik-letsencrypt

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.8.2", "version": "2.9.0",
"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",
@ -30,10 +30,11 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.73", "@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.326", "@sveltejs/adapter-static": "1.0.0-next.31",
"@sveltejs/kit": "1.0.0-next.334",
"@types/js-cookie": "3.0.2", "@types/js-cookie": "3.0.2",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/node": "17.0.31", "@types/node": "17.0.34",
"@types/node-forge": "1.0.2", "@types/node-forge": "1.0.2",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
@ -49,10 +50,10 @@
"postcss": "8.4.13", "postcss": "8.4.13",
"prettier": "2.6.2", "prettier": "2.6.2",
"prettier-plugin-svelte": "2.7.0", "prettier-plugin-svelte": "2.7.0",
"prettier-plugin-tailwindcss": "0.1.10", "prettier-plugin-tailwindcss": "0.1.11",
"prisma": "3.11.1", "prisma": "3.11.1",
"svelte": "3.48.0", "svelte": "3.48.0",
"svelte-check": "2.7.0", "svelte-check": "2.7.1",
"svelte-preprocess": "4.10.6", "svelte-preprocess": "4.10.6",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",
"sveltekit-i18n": "2.2.1", "sveltekit-i18n": "2.2.1",
@ -67,7 +68,7 @@
"@prisma/client": "3.11.1", "@prisma/client": "3.11.1",
"@sentry/node": "6.19.7", "@sentry/node": "6.19.7",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bullmq": "1.81.4", "bullmq": "1.82.2",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.5.0", "cookie": "0.5.0",
"cuid": "2.1.8", "cuid": "2.1.8",

View File

@ -5,17 +5,18 @@ specifiers:
'@prisma/client': 3.11.1 '@prisma/client': 3.11.1
'@sentry/node': 6.19.7 '@sentry/node': 6.19.7
'@sveltejs/adapter-node': 1.0.0-next.73 '@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.326 '@sveltejs/adapter-static': 1.0.0-next.31
'@sveltejs/kit': 1.0.0-next.334
'@types/js-cookie': 3.0.2 '@types/js-cookie': 3.0.2
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/node': 17.0.31 '@types/node': 17.0.34
'@types/node-forge': 1.0.2 '@types/node-forge': 1.0.2
'@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/eslint-plugin': 4.31.1
'@typescript-eslint/parser': 4.31.1 '@typescript-eslint/parser': 4.31.1
'@zerodevx/svelte-toast': 0.7.1 '@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.7 autoprefixer: 10.4.7
bcryptjs: 2.4.3 bcryptjs: 2.4.3
bullmq: 1.81.4 bullmq: 1.82.2
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.5.0 cookie: 0.5.0
cross-env: 7.0.3 cross-env: 7.0.3
@ -43,10 +44,10 @@ specifiers:
postcss: 8.4.13 postcss: 8.4.13
prettier: 2.6.2 prettier: 2.6.2
prettier-plugin-svelte: 2.7.0 prettier-plugin-svelte: 2.7.0
prettier-plugin-tailwindcss: 0.1.10 prettier-plugin-tailwindcss: 0.1.11
prisma: 3.11.1 prisma: 3.11.1
svelte: 3.48.0 svelte: 3.48.0
svelte-check: 2.7.0 svelte-check: 2.7.1
svelte-kit-cookie-session: 2.1.4 svelte-kit-cookie-session: 2.1.4
svelte-preprocess: 4.10.6 svelte-preprocess: 4.10.6
svelte-select: 4.4.7 svelte-select: 4.4.7
@ -63,7 +64,7 @@ dependencies:
'@prisma/client': 3.11.1_prisma@3.11.1 '@prisma/client': 3.11.1_prisma@3.11.1
'@sentry/node': 6.19.7 '@sentry/node': 6.19.7
bcryptjs: 2.4.3 bcryptjs: 2.4.3
bullmq: 1.81.4 bullmq: 1.82.2
compare-versions: 4.1.3 compare-versions: 4.1.3
cookie: 0.5.0 cookie: 0.5.0
cuid: 2.1.8 cuid: 2.1.8
@ -87,10 +88,11 @@ dependencies:
devDependencies: devDependencies:
'@sveltejs/adapter-node': 1.0.0-next.73 '@sveltejs/adapter-node': 1.0.0-next.73
'@sveltejs/kit': 1.0.0-next.326_svelte@3.48.0 '@sveltejs/adapter-static': 1.0.0-next.31
'@sveltejs/kit': 1.0.0-next.334_svelte@3.48.0
'@types/js-cookie': 3.0.2 '@types/js-cookie': 3.0.2
'@types/js-yaml': 4.0.5 '@types/js-yaml': 4.0.5
'@types/node': 17.0.31 '@types/node': 17.0.34
'@types/node-forge': 1.0.2 '@types/node-forge': 1.0.2
'@typescript-eslint/eslint-plugin': 4.31.1_lii63oz3usekbu5ehvrcuwn5jy '@typescript-eslint/eslint-plugin': 4.31.1_lii63oz3usekbu5ehvrcuwn5jy
'@typescript-eslint/parser': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4 '@typescript-eslint/parser': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4
@ -106,15 +108,15 @@ devDependencies:
postcss: 8.4.13 postcss: 8.4.13
prettier: 2.6.2 prettier: 2.6.2
prettier-plugin-svelte: 2.7.0_kkjbqzpydplecjtkxrgomroeru prettier-plugin-svelte: 2.7.0_kkjbqzpydplecjtkxrgomroeru
prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2 prettier-plugin-tailwindcss: 0.1.11_prettier@2.6.2
prisma: 3.11.1 prisma: 3.11.1
svelte: 3.48.0 svelte: 3.48.0
svelte-check: 2.7.0_f2ke6qjyzu5axsjd6yk3u4tn7a svelte-check: 2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a
svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu
svelte-select: 4.4.7 svelte-select: 4.4.7
sveltekit-i18n: 2.2.1_svelte@3.48.0 sveltekit-i18n: 2.2.1_svelte@3.48.0
tailwindcss: 3.0.24_ts-node@10.7.0 tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_l47be6km5p57gglrggidw5gsgm ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa
tslib: 2.4.0 tslib: 2.4.0
typescript: 4.6.4 typescript: 4.6.4
@ -214,6 +216,31 @@ packages:
} }
dev: false dev: false
/@jridgewell/resolve-uri/3.0.7:
resolution:
{
integrity: sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==
}
engines: { node: '>=6.0.0' }
dev: true
/@jridgewell/sourcemap-codec/1.4.13:
resolution:
{
integrity: sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==
}
dev: true
/@jridgewell/trace-mapping/0.3.13:
resolution:
{
integrity: sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==
}
dependencies:
'@jridgewell/resolve-uri': 3.0.7
'@jridgewell/sourcemap-codec': 1.4.13
dev: true
/@nodelib/fs.scandir/2.1.5: /@nodelib/fs.scandir/2.1.5:
resolution: resolution:
{ {
@ -380,12 +407,21 @@ packages:
tiny-glob: 0.2.9 tiny-glob: 0.2.9
dev: true dev: true
/@sveltejs/kit/1.0.0-next.326_svelte@3.48.0: /@sveltejs/adapter-static/1.0.0-next.31:
resolution: resolution:
{ {
integrity: sha512-prJqmXZ2H1wmFfnMw7wDujfbkcA8vuubuqUkpVVmXhfh2+SEzQscPTNwxoE5EJxb5sywtLWEvYx3hv1gPS4Lvg== integrity: sha512-d9RNA/de5ljb+gN8mKA3YfmfJoTbYFdH96NYDD8u4Lu9O/ZnseUxOAcAmD4/LKbLXOY/oYhRpt029xT2owyI3Q==
} }
engines: { node: '>=14.13' } dependencies:
tiny-glob: 0.2.9
dev: true
/@sveltejs/kit/1.0.0-next.334_svelte@3.48.0:
resolution:
{
integrity: sha512-HPMF1oYBfyOG6wfU0Y6F4SID8jphue9yF+PXJqVTDBL5Z2WBG2ogum6MavE8aWhq+g2H6w5y0jNT8+8DO2KTCA==
}
engines: { node: '>=16' }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
svelte: ^3.44.0 svelte: ^3.44.0
@ -495,7 +531,7 @@ packages:
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.1 '@types/http-cache-semantics': 4.0.1
'@types/keyv': 3.1.3 '@types/keyv': 3.1.3
'@types/node': 17.0.31 '@types/node': 17.0.34
'@types/responselike': 1.0.0 '@types/responselike': 1.0.0
dev: false dev: false
@ -533,7 +569,7 @@ packages:
integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==
} }
dependencies: dependencies:
'@types/node': 17.0.31 '@types/node': 17.0.34
dev: false dev: false
/@types/node-forge/1.0.2: /@types/node-forge/1.0.2:
@ -542,13 +578,13 @@ packages:
integrity: sha512-J1OkeZGaW0y9Y7xD49Ja8O82B9l5nZDeoYuGWqIOYPAf9LR+xF23k9ILdzv8dH+2H033fx3D5oiA0GlmicI+sg== integrity: sha512-J1OkeZGaW0y9Y7xD49Ja8O82B9l5nZDeoYuGWqIOYPAf9LR+xF23k9ILdzv8dH+2H033fx3D5oiA0GlmicI+sg==
} }
dependencies: dependencies:
'@types/node': 17.0.31 '@types/node': 17.0.34
dev: true dev: true
/@types/node/17.0.31: /@types/node/17.0.34:
resolution: resolution:
{ {
integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== integrity: sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA==
} }
/@types/pug/2.0.5: /@types/pug/2.0.5:
@ -564,7 +600,7 @@ packages:
integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==
} }
dependencies: dependencies:
'@types/node': 17.0.31 '@types/node': 17.0.34
dev: false dev: false
/@types/sass/1.16.1: /@types/sass/1.16.1:
@ -573,7 +609,7 @@ packages:
integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ==
} }
dependencies: dependencies:
'@types/node': 17.0.31 '@types/node': 17.0.34
dev: true dev: true
/@typescript-eslint/eslint-plugin/4.31.1_lii63oz3usekbu5ehvrcuwn5jy: /@typescript-eslint/eslint-plugin/4.31.1_lii63oz3usekbu5ehvrcuwn5jy:
@ -1689,10 +1725,10 @@ packages:
ieee754: 1.2.1 ieee754: 1.2.1
dev: false dev: false
/bullmq/1.81.4: /bullmq/1.82.2:
resolution: resolution:
{ {
integrity: sha512-sUEWOMKZnWlh1/XNqYAoSwXW6P8nZN7uJiHKZ8XlZCiIxWlEGjFtlugkkiCZ0lsTI2nNRHdxfpn78x9K3L1utQ== integrity: sha512-pDmMl6HmL/7B41ldBK4lnmGUcobkI/n/a0T3d/volMWC0ULxsaZ6R6fDePk23LwH9Fxu4o9Ny+zurCL3vG7lbg==
} }
dependencies: dependencies:
cron-parser: 4.2.1 cron-parser: 4.2.1
@ -4150,7 +4186,7 @@ packages:
dependencies: dependencies:
lilconfig: 2.0.5 lilconfig: 2.0.5
postcss: 8.4.13 postcss: 8.4.13
ts-node: 10.7.0_l47be6km5p57gglrggidw5gsgm ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa
yaml: 1.10.2 yaml: 1.10.2
dev: true dev: true
@ -4218,10 +4254,10 @@ packages:
svelte: 3.48.0 svelte: 3.48.0
dev: true dev: true
/prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2: /prettier-plugin-tailwindcss/0.1.11_prettier@2.6.2:
resolution: resolution:
{ {
integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ== integrity: sha512-a28+1jvpIZQdZ/W97wOXb6VqI762MKE/TxMMuibMEHhyYsSxQA8Ek30KObd5kJI2HF1ldtSYprFayXJXi3pz8Q==
} }
engines: { node: '>=12.17.0' } engines: { node: '>=12.17.0' }
peerDependencies: peerDependencies:
@ -4708,14 +4744,6 @@ packages:
engines: { node: '>=0.10.0' } engines: { node: '>=0.10.0' }
dev: true dev: true
/source-map/0.7.3:
resolution:
{
integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
}
engines: { node: '>= 8' }
dev: true
/sourcemap-codec/1.4.8: /sourcemap-codec/1.4.8:
resolution: resolution:
{ {
@ -4888,21 +4916,21 @@ packages:
engines: { node: '>= 0.4' } engines: { node: '>= 0.4' }
dev: true dev: true
/svelte-check/2.7.0_f2ke6qjyzu5axsjd6yk3u4tn7a: /svelte-check/2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a:
resolution: resolution:
{ {
integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A== integrity: sha512-vHVu2+SQ6ibt77iTQaq2oiOjBgGL48qqcg0ZdEOsP5pPOjgeyR9QbnaEdzdBs9nsVYBc/42haKtzb2uFqS8GVw==
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
svelte: ^3.24.0 svelte: ^3.24.0
dependencies: dependencies:
'@jridgewell/trace-mapping': 0.3.13
chokidar: 3.5.3 chokidar: 3.5.3
fast-glob: 3.2.11 fast-glob: 3.2.11
import-fresh: 3.3.0 import-fresh: 3.3.0
picocolors: 1.0.0 picocolors: 1.0.0
sade: 1.7.4 sade: 1.7.4
source-map: 0.7.3
svelte: 3.48.0 svelte: 3.48.0
svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu
typescript: 4.6.4 typescript: 4.6.4
@ -5143,7 +5171,7 @@ packages:
engines: { node: '>=0.10.0' } engines: { node: '>=0.10.0' }
dev: true dev: true
/ts-node/10.7.0_l47be6km5p57gglrggidw5gsgm: /ts-node/10.7.0_3smuweqyuzdazdnyhhezld6mfa:
resolution: resolution:
{ {
integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==
@ -5165,7 +5193,7 @@ packages:
'@tsconfig/node12': 1.0.9 '@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1 '@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2 '@tsconfig/node16': 1.0.2
'@types/node': 17.0.31 '@types/node': 17.0.34
acorn: 8.5.0 acorn: 8.5.0
acorn-walk: 8.2.0 acorn-walk: 8.2.0
arg: 4.1.3 arg: 4.1.3

View File

@ -0,0 +1,24 @@
-- 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,
"isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true,
"isTraefikUsed" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "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

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Minio" ADD COLUMN "apiFqdn" TEXT;

View File

@ -20,6 +20,7 @@ model Setting {
proxyHash String? proxyHash String?
isAutoUpdateEnabled Boolean @default(false) isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true) isDNSCheckEnabled Boolean @default(true)
isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -334,6 +335,7 @@ model Minio {
rootUser String rootUser String
rootUserPassword String rootUserPassword String
publicPort Int? publicPort Int?
apiFqdn String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -3,7 +3,6 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title>
%svelte.head% %svelte.head%
</head> </head>
<body> <body>

View File

@ -96,12 +96,16 @@ export const getUserDetails = async (
const userId = event?.locals?.session?.data?.userId || null; const userId = event?.locals?.session?.data?.userId || null;
let permission = 'read'; let permission = 'read';
if (teamId && userId) { if (teamId && userId) {
try {
const data = await db.prisma.permission.findFirst({ const data = await db.prisma.permission.findFirst({
where: { teamId, userId }, where: { teamId, userId },
select: { permission: true }, select: { permission: true },
rejectOnNotFound: true rejectOnNotFound: true
}); });
if (data.permission) permission = data.permission; if (data.permission) permission = data.permission;
} catch (error) {
console.log(error);
}
} }
const payload = { const payload = {

View File

@ -0,0 +1,35 @@
<script>
import { onMount, onDestroy } from 'svelte';
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
let timeout;
const progress = tweened(0, {
duration: 2000,
easing: cubicOut
});
onMount(() => {
timeout = setTimeout(() => {
progress.set(0.7);
}, 500);
});
onDestroy(() => {
clearTimeout(timeout);
});
</script>
<div class="progress-bar">
<div class="progress-sliver" style={`--width: ${$progress * 100}%`} />
</div>
<style lang="postcss">
.progress-bar {
height: 0.2rem;
@apply fixed top-0 left-0 right-0;
}
.progress-sliver {
width: var(--width);
@apply h-full bg-coollabs;
}
</style>

View File

@ -219,6 +219,18 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 3000 main: 3000
} }
// },
// {
// name: 'appwrite',
// fancyName: 'AppWrite',
// baseImage: 'appwrite/appwrite',
// images: ['appwrite/influxdb', 'appwrite/telegraf', 'mariadb:10.7', 'redis:6.0-alpine3.12'],
// versions: ['latest', '0.13.0'],
// recommendedVersion: '0.13.0',
// ports: {
// main: 3000
// }
// }
} }
]; ];

View File

@ -51,10 +51,12 @@ export async function isSecretExists({
export async function isDomainConfigured({ export async function isDomainConfigured({
id, id,
fqdn fqdn,
checkOwn = false
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
checkOwn?: boolean;
}): Promise<boolean> { }): Promise<boolean> {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', ''); const nakedDomain = domain.replace('www.', '');
@ -72,12 +74,15 @@ export async function isDomainConfigured({
where: { where: {
OR: [ OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } }, { fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } } { fqdn: { endsWith: `//www.${nakedDomain}` } },
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
], ],
id: { not: id } id: { not: checkOwn ? undefined : id }
}, },
select: { fqdn: true } select: { fqdn: true }
}); });
const coolifyFqdn = await prisma.setting.findFirst({ const coolifyFqdn = await prisma.setting.findFirst({
where: { where: {
OR: [ OR: [

View File

@ -305,6 +305,12 @@ export async function getFreePort() {
select: { mysqlPublicPort: true } select: { mysqlPublicPort: true }
}) })
).map((a) => a.mysqlPublicPort); ).map((a) => a.mysqlPublicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed]; const minioUsed = await (
await prisma.minio.findMany({
where: { publicPort: { not: null } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
} }

View File

@ -1,6 +1,6 @@
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy, startTraefikProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.'; import { getDatabaseImage } from '.';
import { prisma } from './common'; import { prisma } from './common';
import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client'; import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client';
@ -125,7 +125,14 @@ export async function newLocalDestination({
} }
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
} }
if (isCoolifyProxyUsed) await startCoolifyProxy(engine); if (isCoolifyProxyUsed) {
const settings = await prisma.setting.findFirst();
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine);
}
}
return destination.id; return destination.id;
} }
export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>): Promise<void> { export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>): Promise<void> {
@ -133,12 +140,14 @@ export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>):
if (destination.isCoolifyProxyUsed) { if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine); const host = getEngine(destination.engine);
const { network } = destination; const { network } = destination;
const settings = await prisma.setting.findFirst();
const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy';
const { stdout: found } = await asyncExecShell( const { stdout: found } = await asyncExecShell(
`DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=coolify-haproxy --format '{{.}}'` `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'`
); );
if (found) { if (found) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST="${host}" docker network disconnect ${network} coolify-haproxy` `DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}`
); );
await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`); await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`);
} }

View File

@ -360,7 +360,24 @@ export async function updateService({
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } }); return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } });
} }
export async function updateMinioService({
id,
fqdn,
apiFqdn,
exposePort,
name
}: {
id: string;
fqdn: string;
apiFqdn: string;
exposePort?: number;
name: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, exposePort, minio: { update: { apiFqdn } } }
});
}
export async function updateFiderService({ export async function updateFiderService({
id, id,
fqdn, fqdn,
@ -459,7 +476,7 @@ export async function updateWordpress({
}); });
} }
export async function updateMinioService({ export async function updateMinioServicePort({
id, id,
publicPort publicPort
}: { }: {

View File

@ -3,12 +3,22 @@ import { asyncExecShell, getEngine } from '$lib/common';
import got, { type Got, type Response } from 'got'; import got, { type Got, type Response } from 'got';
import * as db from '$lib/database'; import * as db from '$lib/database';
import type { DestinationDocker } from '@prisma/client'; import type { DestinationDocker } from '@prisma/client';
import fs from 'fs/promises';
import yaml from 'js-yaml';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export const defaultTraefikImage = `traefik:v2.6`;
const mainTraefikEndpoint = dev
? 'http://host.docker.internal:3000/webhooks/traefik/main.json'
: 'http://coolify:3000/webhooks/traefik/main.json';
const otherTraefikEndpoint = dev
? 'http://host.docker.internal:3000/webhooks/traefik/other.json'
: 'http://coolify:3000/webhooks/traefik/other.json';
export async function haproxyInstance(): Promise<Got> { export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings(); const { proxyPassword } = await db.listSettings();
@ -98,13 +108,21 @@ export async function checkHAProxy(haproxy?: Got): Promise<void> {
} }
export async function stopTcpHttpProxy( export async function stopTcpHttpProxy(
id: string,
destinationDocker: DestinationDocker, destinationDocker: DestinationDocker,
publicPort: number publicPort: number,
forceName: string = null
): Promise<{ stdout: string; stderr: string } | Error> { ): Promise<{ stdout: string; stderr: string } | Error> {
const { engine } = destinationDocker; const { engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`; const settings = await db.listSettings();
let containerName = `${id}-${publicPort}`;
if (!settings.isTraefikUsed) {
containerName = `haproxy-for-${publicPort}`;
}
if (forceName) containerName = forceName;
const found = await checkContainer(engine, containerName); const found = await checkContainer(engine, containerName);
try { try {
if (found) { if (found) {
return await asyncExecShell( return await asyncExecShell(
@ -115,12 +133,76 @@ export async function stopTcpHttpProxy(
return error; return error;
} }
} }
export async function startTcpProxy( export async function startTraefikTCPProxy(
destinationDocker: DestinationDocker, destinationDocker: DestinationDocker,
id: string, id: string,
publicPort: number, publicPort: number,
privatePort: number, privatePort: number,
volume?: string type?: string
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `${id}-${publicPort}`;
const found = await checkContainer(engine, containerName, true);
let dependentId = id;
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
const foundDependentContainer = await checkContainer(engine, dependentId, true);
try {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
const tcpProxy = {
version: '3.5',
services: {
[`${id}-${publicPort}`]: {
container_name: containerName,
image: 'traefik:v2.6',
command: [
`--entrypoints.tcp.address=:${publicPort}`,
`--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp`,
'--providers.http.pollTimeout=2s',
'--log.level=error'
],
ports: [`${publicPort}:${publicPort}`],
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`],
volumes: ['/var/run/docker.sock:/var/run/docker.sock'],
networks: ['coolify-infra', network]
}
},
networks: {
[network]: {
external: false,
name: network
},
'coolify-infra': {
external: false,
name: 'coolify-infra'
}
}
};
await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d`
);
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
console.log(error);
return error;
}
}
export async function startTcpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
): Promise<{ stdout: string; stderr: string } | Error> { ): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker; const { network, engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
@ -128,7 +210,6 @@ export async function startTcpProxy(
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
const found = await checkContainer(engine, containerName, true); const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true); const foundDependentContainer = await checkContainer(engine, id, true);
try { try {
if (foundDependentContainer && !found) { if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell( const { stdout: Config } = await asyncExecShell(
@ -136,9 +217,7 @@ export async function startTcpProxy(
); );
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell( return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${ `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}`
volume ? `-v ${volume}` : ''
} -d coollabsio/${defaultProxyImageTcp}`
); );
} }
if (!foundDependentContainer && found) { if (!foundDependentContainer && found) {
@ -151,6 +230,75 @@ export async function startTcpProxy(
} }
} }
export async function startTraefikHTTPProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker;
const host = getEngine(engine);
const containerName = `${id}-${publicPort}`;
const found = await checkContainer(engine, containerName, true);
const foundDependentContainer = await checkContainer(engine, id, true);
try {
if (foundDependentContainer && !found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
const tcpProxy = {
version: '3.5',
services: {
[`${id}-${publicPort}`]: {
container_name: containerName,
image: 'traefik:v2.6',
command: [
`--entrypoints.http.address=:${publicPort}`,
`--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`,
'--providers.http.pollTimeout=2s',
'--certificatesresolvers.letsencrypt.acme.httpchallenge=true',
'--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json',
'--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http',
'--log.level=error'
],
ports: [`${publicPort}:${publicPort}`],
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`],
networks: ['coolify-infra', network],
volumes: ['coolify-traefik-letsencrypt:/etc/traefik/acme']
}
},
networks: {
[network]: {
external: false,
name: network
},
'coolify-infra': {
external: false,
name: 'coolify-infra'
}
},
volumes: {
'coolify-traefik-letsencrypt': {}
}
};
await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d`
);
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
}
if (!foundDependentContainer && found) {
return await asyncExecShell(
`DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}`
);
}
} catch (error) {
return error;
}
}
export async function startHttpProxy( export async function startHttpProxy(
destinationDocker: DestinationDocker, destinationDocker: DestinationDocker,
id: string, id: string,
@ -197,10 +345,50 @@ export async function startCoolifyProxy(engine: string): Promise<void> {
`DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}`
); );
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true });
} }
await configureNetworkCoolifyProxy(engine); await configureNetworkCoolifyProxy(engine);
} }
export async function startTraefikProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-proxy', true);
const { id, proxyPassword, proxyUser } = await db.listSettings();
if (!found) {
const { stdout: Config } = await asyncExecShell(
`DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'`
);
const ip = JSON.parse(Config)[0].Gateway;
await asyncExecShell(
`DOCKER_HOST="${host}" docker run --restart always \
--add-host 'host.docker.internal:host-gateway' \
--add-host 'host.docker.internal:${ip}' \
-v coolify-traefik-letsencrypt:/etc/traefik/acme \
-v /var/run/docker.sock:/var/run/docker.sock \
--network coolify-infra \
-p "80:80" \
-p "443:443" \
-p "8080:8080" \
--name coolify-proxy \
-d ${defaultTraefikImage} \
--api.insecure=true \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443 \
--providers.docker=true \
--providers.docker.exposedbydefault=false \
--providers.http.endpoint=${mainTraefikEndpoint} \
--providers.http.pollTimeout=5s \
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
--log.level=error`
);
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true });
}
await configureNetworkTraefikProxy(engine);
}
export async function isContainerExited(engine: string, containerName: string): Promise<boolean> { export async function isContainerExited(engine: string, containerName: string): Promise<boolean> {
let isExited = false; let isExited = false;
const host = getEngine(engine); const host = getEngine(engine);
@ -245,6 +433,21 @@ export async function checkContainer(
return containerFound; return containerFound;
} }
export async function getContainerUsage(engine: string, container: string): Promise<any> {
const host = getEngine(engine);
try {
const { stdout } = await asyncExecShell(
`DOCKER_HOST="${host}" docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"`
);
return JSON.parse(stdout);
} catch (err) {
return {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
}
}
export async function stopCoolifyProxy( export async function stopCoolifyProxy(
engine: string engine: string
): Promise<{ stdout: string; stderr: string } | Error> { ): Promise<{ stdout: string; stderr: string } | Error> {
@ -263,6 +466,24 @@ export async function stopCoolifyProxy(
return error; return error;
} }
} }
export async function stopTraefikProxy(
engine: string
): Promise<{ stdout: string; stderr: string } | Error> {
const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-proxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
const { id } = await db.prisma.setting.findFirst({});
await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } });
try {
if (found) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker stop -t 0 coolify-proxy && docker rm coolify-proxy`
);
}
} catch (error) {
return error;
}
}
export async function configureNetworkCoolifyProxy(engine: string): Promise<void> { export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
@ -279,3 +500,19 @@ export async function configureNetworkCoolifyProxy(engine: string): Promise<void
} }
} }
} }
export async function configureNetworkTraefikProxy(engine: string): Promise<void> {
const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
const { stdout: networks } = await asyncExecShell(
`DOCKER_HOST="${host}" docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
);
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
for (const destination of destinations) {
if (!configuredNetworks.includes(destination.network)) {
await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-proxy`
);
}
}
}

View File

@ -1,4 +1,4 @@
import { ErrorHandler } from '$lib/database'; import { ErrorHandler, prisma } from '$lib/database';
import { configureHAProxy } from '$lib/haproxy/configuration'; import { configureHAProxy } from '$lib/haproxy/configuration';
export default async function (): Promise<void | { export default async function (): Promise<void | {
@ -6,7 +6,10 @@ export default async function (): Promise<void | {
body: { message: string; error: string }; body: { message: string; error: string };
}> { }> {
try { try {
const settings = await prisma.setting.findFirst();
if (!settings.isTraefikUsed) {
return await configureHAProxy(); return await configureHAProxy();
}
} catch (error) { } catch (error) {
return ErrorHandler(error.response?.body || error); return ErrorHandler(error.response?.body || error);
} }

View File

@ -1,5 +1,16 @@
import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database';
import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy'; import {
checkContainer,
startCoolifyProxy,
startHttpProxy,
startTcpProxy,
startTraefikHTTPProxy,
startTraefikProxy,
startTraefikTCPProxy,
stopCoolifyProxy,
stopTcpHttpProxy,
stopTraefikProxy
} from '$lib/haproxy';
export default async function (): Promise<void | { export default async function (): Promise<void | {
status: number; status: number;
@ -7,12 +18,23 @@ export default async function (): Promise<void | {
}> { }> {
try { try {
// Coolify Proxy // Coolify Proxy
const engine = '/var/run/docker.sock';
const settings = await prisma.setting.findFirst();
const localDocker = await prisma.destinationDocker.findFirst({ const localDocker = await prisma.destinationDocker.findFirst({
where: { engine: '/var/run/docker.sock' } where: { engine, network: 'coolify' }
}); });
if (localDocker && localDocker.isCoolifyProxyUsed) { if (localDocker && localDocker.isCoolifyProxyUsed) {
await startCoolifyProxy('/var/run/docker.sock'); if (settings.isTraefikUsed) {
const found = await checkContainer(engine, 'coolify-haproxy');
if (found) await stopCoolifyProxy(engine);
await startTraefikProxy(engine);
} else {
const found = await checkContainer(engine, 'coolify-proxy');
if (found) await stopTraefikProxy(engine);
await startCoolifyProxy(engine);
} }
}
// TCP Proxies // TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({ const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } }, where: { publicPort: { not: null } },
@ -21,10 +43,18 @@ export default async function (): Promise<void | {
for (const database of databasesWithPublicPort) { for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database; const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId) { if (destinationDockerId) {
if (destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database); const { privatePort } = generateDatabaseConfiguration(database);
if (settings.isTraefikUsed) {
await stopTcpHttpProxy(id, destinationDocker, publicPort, `haproxy-for-${publicPort}`);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} else {
await stopTcpHttpProxy(id, destinationDocker, publicPort, `${id}-${publicPort}`);
await startTcpProxy(destinationDocker, id, publicPort, privatePort); await startTcpProxy(destinationDocker, id, publicPort, privatePort);
} }
} }
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({ const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } }, where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } } include: { service: { include: { destinationDocker: true } } }
@ -33,11 +63,25 @@ export default async function (): Promise<void | {
const { service, ftpPublicPort } = ftp; const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) { if (destinationDockerId) {
if (destinationDocker.isCoolifyProxyUsed) {
if (settings.isTraefikUsed) {
await stopTcpHttpProxy(
id,
destinationDocker,
ftpPublicPort,
`haproxy-for-${ftpPublicPort}`
);
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
} else {
await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort, `${id}-${ftpPublicPort}`);
await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22); await startTcpProxy(destinationDocker, `${id}-ftp`, ftpPublicPort, 22);
} }
} }
}
}
// HTTP Proxies // HTTP Proxies
if (!settings.isTraefikUsed) {
const minioInstances = await prisma.minio.findMany({ const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } }, where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } } include: { service: { include: { destinationDocker: true } } }
@ -46,9 +90,13 @@ export default async function (): Promise<void | {
const { service, publicPort } = minio; const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId) { if (destinationDockerId) {
if (destinationDocker.isCoolifyProxyUsed) {
await stopTcpHttpProxy(id, destinationDocker, publicPort, `${id}-${publicPort}`);
await startHttpProxy(destinationDocker, id, publicPort, 9000); await startHttpProxy(destinationDocker, id, publicPort, 9000);
} }
} }
}
}
} catch (error) { } catch (error) {
return ErrorHandler(error.response?.body || error); return ErrorHandler(error.response?.body || error);
} }

View File

@ -1,8 +1,12 @@
import { generateSSLCerts } from '$lib/letsencrypt'; import { generateSSLCerts } from '$lib/letsencrypt';
import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
try { try {
const settings = await prisma.setting.findFirst();
if (!settings.isTraefikUsed) {
return await generateSSLCerts(); return await generateSSLCerts();
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
throw error; throw error;

View File

@ -1,8 +1,12 @@
import { renewSSLCerts } from '$lib/letsencrypt'; import { renewSSLCerts } from '$lib/letsencrypt';
import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
try { try {
const settings = await prisma.setting.findFirst();
if (!settings.isTraefikUsed) {
return await renewSSLCerts(); return await renewSSLCerts();
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
throw error; throw error;

3
src/lib/realtime.ts Normal file
View File

@ -0,0 +1,3 @@
// import ioClient from 'socket.io-client';
// const socket = ioClient('http://localhost:3000');
// export const io = socket;

View File

@ -12,3 +12,14 @@ export const features: Readable<{ latestVersion: string; beta: boolean }> = read
beta: browser && window.localStorage.getItem('beta') === 'true', beta: browser && window.localStorage.getItem('beta') === 'true',
latestVersion: browser && window.localStorage.getItem('latestVersion') latestVersion: browser && window.localStorage.getItem('latestVersion')
}); });
export const isTraefikUsed: Writable<boolean> = writable(false);
export const status: Writable<any> = writable({
application: {
isRunning: false,
isExited: false,
loading: false,
initialLoading: true
}
});

View File

@ -34,23 +34,30 @@
</script> </script>
<script> <script>
export let settings;
import '../tailwind.css'; import '../tailwind.css';
import { SvelteToast, toast } from '@zerodevx/svelte-toast'; import { SvelteToast, toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { fade } from 'svelte/transition';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { asyncSleep } from '$lib/components/common'; import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { dev } from '$app/env'; import { dev } from '$app/env';
import { features } from '$lib/store'; import { features, isTraefikUsed } from '$lib/store';
let isUpdateAvailable = false; import { navigating } from '$app/stores';
import PageLoader from '$lib/components/PageLoader.svelte';
$isTraefikUsed = settings?.isTraefikUsed || false;
let isUpdateAvailable = false;
let updateStatus = { let updateStatus = {
found: false, found: false,
loading: false, loading: false,
success: null success: null
}; };
let latestVersion = 'latest'; let latestVersion = 'latest';
onMount(async () => { onMount(async () => {
if ($session.userId) { if ($session.userId) {
const overrideVersion = $features.latestVersion; const overrideVersion = $features.latestVersion;
@ -129,9 +136,16 @@
<title>Coolify</title> <title>Coolify</title>
{#if !$session.whiteLabeled} {#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
{:else if $session.whiteLabelDetails.icon}
<link rel="icon" href={$session.whiteLabelDetails.icon} />
{/if} {/if}
</svelte:head> </svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $navigating}
<div out:fade={{ delay: 100 }}>
<PageLoader />
</div>
{/if}
{#if $session.userId} {#if $session.userId}
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">

View File

@ -17,7 +17,7 @@
const endpoint = `/applications/${params.id}.json`; const endpoint = `/applications/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
let { application, isRunning, isExited, appId, githubToken, gitlabToken } = await res.json(); let { application, appId, githubToken, gitlabToken } = await res.json();
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
@ -45,13 +45,10 @@
return { return {
props: { props: {
application, application,
isRunning,
isExited,
githubToken, githubToken,
gitlabToken gitlabToken
}, },
stuff: { stuff: {
isRunning,
application, application,
appId appId
} }
@ -67,8 +64,6 @@
<script lang="ts"> <script lang="ts">
export let application; export let application;
export let isRunning;
export let isExited;
export let githubToken; export let githubToken;
export let gitlabToken; export let gitlabToken;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
@ -77,7 +72,7 @@
import Loading from '$lib/components/Loading.svelte'; import Loading from '$lib/components/Loading.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store'; import { gitTokens, status } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { disabledButton } from '$lib/store'; import { disabledButton } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
@ -135,17 +130,31 @@
} }
} }
async function getStatus() { async function getStatus() {
statusInterval = setInterval(async () => { if ($status.application.loading) return;
const data = await get(`/applications/${id}.json`); $status.application.loading = true;
isRunning = data.isRunning; const data = await get(`/applications/${id}/status.json`);
isExited = data.isExited; $status.application.isRunning = data.isRunning;
}, 1000); $status.application.isExited = data.isExited;
$status.application.loading = false;
$status.application.initialLoading = false;
} }
onDestroy(() => { onDestroy(() => {
$status.application.initialLoading = true;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
if (!application.gitSourceId || !application.destinationDockerId || !application.fqdn) {
$status.application.initialLoading = false;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.loading = false;
return;
} else {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => {
await getStatus();
}, 1000);
}
}); });
</script> </script>
@ -153,16 +162,16 @@
{#if loading} {#if loading}
<Loading fullscreen cover /> <Loading fullscreen cover />
{:else} {:else}
{#if isExited} {#if $status.application.isExited}
<a <a
href={!$disabledButton ? `/applications/${id}/logs` : null} href={!$disabledButton ? `/applications/${id}/logs` : null}
class=" icons bg-transparent tooltip-bottom text-sm flex items-center text-red-500 tooltip-red-500" class=" icons tooltip-bottom tooltip-red-500 flex items-center bg-transparent text-sm text-red-500"
data-tooltip="Application exited with an error!" data-tooltip="Application exited with an error!"
sveltekit:prefetch sveltekit:prefetch
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentcolor" stroke="currentcolor"
@ -179,20 +188,43 @@
</svg> </svg>
</a> </a>
{/if} {/if}
{#if isRunning} {#if $status.application.initialLoading}
<button
class="icons tooltip-bottom flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.application.isRunning}
<button <button
on:click={stopApplication} on:click={stopApplication}
title="Stop application" title="Stop application"
type="submit" type="submit"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-red-500" class="icons tooltip-bottom flex items-center space-x-2 bg-transparent text-sm text-red-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? $t('application.stop_application') ? $t('application.stop_application')
: $t('application.permission_denied_stop_application')} : $t('application.permission_denied_stop_application')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -210,14 +242,14 @@
title="Rebuild application" title="Rebuild application"
type="submit" type="submit"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 hover:text-green-500" class="icons tooltip-bottom flex items-center space-x-2 bg-transparent text-sm hover:text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Rebuild application' ? 'Rebuild application'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -239,14 +271,14 @@
title="Build and start application" title="Build and start application"
type="submit" type="submit"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm flex items-center space-x-2 text-green-500" class="icons tooltip-bottom flex items-center space-x-2 bg-transparent text-sm text-green-500"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? 'Build and start application' ? 'Build and start application'
: 'You do not have permission to Build and start application.'} : 'You do not have permission to Build and start application.'}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -261,18 +293,18 @@
</form> </form>
{/if} {/if}
<div class="border border-coolgray-500 h-8" /> <div class="h-8 border border-coolgray-500" />
<a <a
href={!$disabledButton ? `/applications/${id}` : null} href={!$disabledButton ? `/applications/${id}` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-yellow-500 rounded" class="rounded hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`} class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
> >
<button <button
title="Configurations" title="Configurations"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip="Configurations" data-tooltip="Configurations"
> >
<svg <svg
@ -301,19 +333,19 @@
<a <a
href={!$disabledButton ? `/applications/${id}/secrets` : null} href={!$disabledButton ? `/applications/${id}/secrets` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="rounded hover:text-pink-500"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`} class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
> >
<button <button
title="Secret" title="Secret"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip="Secret" data-tooltip="Secret"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -333,19 +365,19 @@
<a <a
href={!$disabledButton ? `/applications/${id}/storage` : null} href={!$disabledButton ? `/applications/${id}/storage` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-pink-500 rounded" class="rounded hover:text-pink-500"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`} class:text-pink-500={$page.url.pathname === `/applications/${id}/storage`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storage`}
> >
<button <button
title="Persistent Storage" title="Persistent Storage"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip="Persistent Storage" data-tooltip="Persistent Storage"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -363,19 +395,19 @@
<a <a
href={!$disabledButton ? `/applications/${id}/previews` : null} href={!$disabledButton ? `/applications/${id}/previews` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-orange-500 rounded" class="rounded hover:text-orange-500"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`} class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
> >
<button <button
title="Previews" title="Previews"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip="Previews" data-tooltip="Previews"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@ -392,18 +424,18 @@
</svg></button </svg></button
></a ></a
> >
<div class="border border-coolgray-500 h-8" /> <div class="h-8 border border-coolgray-500" />
<a <a
href={!$disabledButton && isRunning ? `/applications/${id}/logs` : null} href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-sky-500 rounded" class="rounded hover:text-sky-500"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`} class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
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.logs')} title={$t('application.logs')}
disabled={$disabledButton || !isRunning} disabled={$disabledButton || !$status.application.isRunning}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip={$t('application.logs')} data-tooltip={$t('application.logs')}
> >
<svg <svg
@ -428,14 +460,14 @@
<a <a
href={!$disabledButton ? `/applications/${id}/logs/build` : null} href={!$disabledButton ? `/applications/${id}/logs/build` : null}
sveltekit:prefetch sveltekit:prefetch
class="hover:text-red-500 rounded" class="rounded hover:text-red-500"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`} class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
> >
<button <button
title="Build Logs" title="Build Logs"
disabled={$disabledButton} disabled={$disabledButton}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip="Build Logs" data-tooltip="Build Logs"
> >
<svg <svg
@ -460,7 +492,7 @@
</svg> </svg>
</button></a </button></a
> >
<div class="border border-coolgray-500 h-8" /> <div class="h-8 border border-coolgray-500" />
<button <button
on:click={() => deleteApplication(application.name)} on:click={() => deleteApplication(application.name)}
@ -468,7 +500,7 @@
type="submit" type="submit"
disabled={!$session.isAdmin} disabled={!$session.isAdmin}
class:hover:text-red-500={$session.isAdmin} class:hover:text-red-500={$session.isAdmin}
class="icons bg-transparent tooltip-bottom text-sm" class="icons tooltip-bottom bg-transparent text-sm"
data-tooltip={$session.isAdmin data-tooltip={$session.isAdmin
? $t('application.delete_application') ? $t('application.delete_application')
: $t('application.permission_denied_delete_application')} : $t('application.permission_denied_delete_application')}

View File

@ -52,7 +52,7 @@ export const post: RequestHandler = async (event) => {
exposePort = Number(exposePort); exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) { if (exposePort < 1024 || exposePort > 65535) {
throw { message: `Expose Port needs to be between 1024 and 65535.` }; throw { message: `Exposed Port needs to be between 1024 and 65535.` };
} }
const publicPort = await getPort({ port: exposePort }); const publicPort = await getPort({ port: exposePort });

View File

@ -19,10 +19,13 @@
<script lang="ts"> <script lang="ts">
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { gitTokens } from '$lib/store';
export let application; export let application;
export let appId; export let appId;
$gitTokens.githubToken = null;
import GithubRepositories from './_GithubRepositories.svelte'; import GithubRepositories from './_GithubRepositories.svelte';
import GitlabRepositories from './_GitlabRepositories.svelte'; import GitlabRepositories from './_GitlabRepositories.svelte';
</script> </script>

View File

@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common'; import { asyncExecShell, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { checkContainer, isContainerExited } from '$lib/haproxy'; import { checkContainer, getContainerUsage, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { setDefaultConfiguration } from '$lib/buildPacks/common'; import { setDefaultConfiguration } from '$lib/buildPacks/common';
@ -12,21 +12,14 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
const appId = process.env['COOLIFY_APP_ID']; const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
let githubToken = event.locals.cookies?.githubToken || null; let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null; let gitlabToken = event.locals.cookies?.gitlabToken || null;
try { try {
const application = await db.getApplication({ id, teamId }); const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return { return {
status: 200, status: 200,
body: { body: {
isRunning,
isExited,
application, application,
appId, appId,
githubToken, githubToken,

View File

@ -4,8 +4,7 @@
if (stuff?.application?.id) { if (stuff?.application?.id) {
return { return {
props: { props: {
application: stuff.application, application: stuff.application
isRunning: stuff.isRunning
} }
}; };
} }
@ -36,10 +35,9 @@
baseImages: Array<{ value: string; label: string }>; baseImages: Array<{ value: string; label: string }>;
baseBuildImages: Array<{ value: string; label: string }>; baseBuildImages: Array<{ value: string; label: string }>;
}; };
export let isRunning;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
@ -50,13 +48,22 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { disabledButton } from '$lib/store'; import { disabledButton, status } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
const { id } = $page.params; const { id } = $page.params;
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
let loading = false; let loading = false;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval;
let forceSave = false; let forceSave = false;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
@ -67,6 +74,7 @@
let isNonWWWDomainOK = false; let isNonWWWDomainOK = false;
let isWWWDomainOK = false; let isWWWDomainOK = false;
$: isDisabled = !$session.isAdmin || $status.application.isRunning;
let wsgis = [ let wsgis = [
{ {
value: 'None', value: 'None',
@ -78,15 +86,29 @@
} }
]; ];
function containerClass() { function containerClass() {
if (!$session.isAdmin || isRunning) {
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0'; return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
} }
async function getUsage() {
if (usageLoading) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage.json`);
usage = data.usage;
usageLoading = false;
} }
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
await post(`/applications/${id}.json`, { ...application });
} }
onMount(() => {
domainEl.focus(); domainEl.focus();
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
}); });
async function changeSettings(name) { async function changeSettings(name) {
@ -129,6 +151,7 @@
} }
} }
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
@ -261,6 +284,33 @@
</a> </a>
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Application Usage</div>
<div class="mx-auto">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
<dd class="mt-1 text-xl font-semibold text-white">
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.CPUPerc}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.NetIO}
</dd>
</div>
</dl>
</div>
</div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration --> <!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
@ -354,7 +404,7 @@
value={application.destinationDocker.name} value={application.destinationDocker.name}
id="destination" id="destination"
disabled disabled
class="bg-transparent " class="bg-transparent"
/> />
</div> </div>
</div> </div>
@ -365,10 +415,10 @@
> >
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
isDisabled={!$session.isAdmin || isRunning} {isDisabled}
containerClasses={containerClass()} containerClasses={isDisabled && containerClass()}
id="baseImages" id="baseImages"
showIndicator={!isRunning} showIndicator={!$status.application.isRunning}
items={application.baseImages} items={application.baseImages}
on:select={selectBaseImage} on:select={selectBaseImage}
value={application.baseImage} value={application.baseImage}
@ -386,10 +436,10 @@
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
isDisabled={!$session.isAdmin || isRunning} {isDisabled}
containerClasses={containerClass()} containerClasses={isDisabled && containerClass()}
id="baseBuildImages" id="baseBuildImages"
showIndicator={!isRunning} showIndicator={!$status.application.isRunning}
items={application.baseBuildImages} items={application.baseBuildImages}
on:select={selectBaseBuildImage} on:select={selectBaseBuildImage}
value={application.baseBuildImage} value={application.baseBuildImage}
@ -422,8 +472,8 @@
</div> </div>
<div> <div>
<input <input
readonly={!$session.isAdmin || isRunning} readonly={isDisabled}
disabled={!$session.isAdmin || isRunning} disabled={isDisabled}
bind:this={domainEl} bind:this={domainEl}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
@ -470,12 +520,12 @@
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<Setting <Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isRunning} disabled={$status.application.isRunning}
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')} description={$t('application.ssl_explainer')}
on:click={() => !isRunning && changeSettings('dualCerts')} on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
@ -527,8 +577,8 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label> <label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input <input
readonly={!$session.isAdmin && !isRunning} readonly={!$session.isAdmin && !$status.application.isRunning}
disabled={!$session.isAdmin || isRunning} disabled={isDisabled}
name="exposePort" name="exposePort"
id="exposePort" id="exposePort"
bind:value={application.exposePort} bind:value={application.exposePort}

View File

@ -17,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const destinationDocker = await db.getDestinationByApplicationId({ id, teamId }); const destinationDocker = await db.getDestinationByApplicationId({ id, teamId });
const docker = dockerInstance({ destinationDocker }); const docker = dockerInstance({ destinationDocker });
const listContainers = await docker.engine.listContainers({ const listContainers = await docker.engine.listContainers({
filters: { network: [destinationDocker.network] } filters: { network: [destinationDocker.network], name: [id] }
}); });
const containers = listContainers.filter((container) => { const containers = listContainers.filter((container) => {
return ( return (
@ -30,11 +30,7 @@ export const get: RequestHandler = async (event) => {
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
) )
.filter((container) => { .filter((container) => {
return ( return container.pullmergeRequestId && container.applicationId === id;
container.type !== 'manual' &&
container.type !== 'webhook_commit' &&
container.applicationId === id
);
}); });
return { return {
body: { body: {

View File

@ -31,6 +31,7 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { goto } from '$app/navigation';
const { id } = $page.params; const { id } = $page.params;
async function refreshSecrets() { async function refreshSecrets() {
@ -39,11 +40,18 @@
} }
async function redeploy(container) { async function redeploy(container) {
try { try {
await post(`/applications/${id}/deploy.json`, { const { buildId } = await post(`/applications/${id}/deploy.json`, {
pullmergeRequestId: container.pullmergeRequestId, pullmergeRequestId: container.pullmergeRequestId,
branch: container.branch branch: container.branch
}); });
toast.push('Application redeployed queued.'); toast.push('Application redeployed queued.');
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else {
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, {
replaceState: true
});
}
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} }

View File

@ -0,0 +1,36 @@
import { asyncExecShell, getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, getContainerUsage, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let isRunning = false;
let isExited = false;
try {
const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
[isRunning, isExited] = await Promise.all([
checkContainer(application.destinationDocker.engine, id),
isContainerExited(application.destinationDocker.engine, id)
]);
}
return {
status: 200,
body: {
isRunning,
isExited
},
headers: {}
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,30 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { getContainerUsage } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let usage = {};
try {
const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]);
}
return {
status: 200,
body: {
usage
},
headers: {}
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -49,6 +49,7 @@ export const get: RequestHandler = async (event) => {
where: { userId }, where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } } include: { team: { include: { _count: { select: { users: true } } } } }
}); });
const settings = await db.prisma.setting.findFirst();
return { return {
body: { body: {
teams, teams,
@ -57,7 +58,8 @@ export const get: RequestHandler = async (event) => {
destinationsCount, destinationsCount,
teamsCount, teamsCount,
databasesCount, databasesCount,
servicesCount servicesCount,
settings
} }
}; };
} catch (error) { } catch (error) {

View File

@ -33,10 +33,40 @@
<script lang="ts"> <script lang="ts">
import DatabaseLinks from '$lib/components/DatabaseLinks.svelte'; import DatabaseLinks from '$lib/components/DatabaseLinks.svelte';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { onDestroy, onMount } from 'svelte';
export let database; export let database;
export let settings; export let settings;
export let privatePort; export let privatePort;
export let isRunning; export let isRunning;
const { id } = $page.params;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval;
async function getUsage() {
if (usageLoading) return;
usageLoading = true;
const data = await get(`/databases/${id}/usage.json`);
usage = data.usage;
usageLoading = false;
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
});
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex items-center space-x-2 p-6 text-2xl font-bold">
@ -49,4 +79,31 @@
<DatabaseLinks {database} /> <DatabaseLinks {database} />
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Database Usage</div>
<div class="mx-auto">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
<dd class="mt-1 text-xl font-semibold text-white">
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.CPUPerc}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.NetIO}
</dd>
</div>
</dl>
</div>
</div>
<Databases bind:database {privatePort} {settings} {isRunning} /> <Databases bind:database {privatePort} {settings} {isRunning} />

View File

@ -1,7 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database';
import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import { startTcpProxy, startTraefikTCPProxy, stopTcpHttpProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@ -13,6 +13,7 @@ export const post: RequestHandler = async (event) => {
const publicPort = await getFreePort(); const publicPort = await getFreePort();
try { try {
const settings = await db.listSettings();
await db.setDatabase({ id, isPublic, appendOnly }); await db.setDatabase({ id, isPublic, appendOnly });
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database; const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
@ -21,7 +22,11 @@ export const post: RequestHandler = async (event) => {
if (destinationDockerId) { if (destinationDockerId) {
if (isPublic) { if (isPublic) {
await db.prisma.database.update({ where: { id }, data: { publicPort } }); await db.prisma.database.update({ where: { id }, data: { publicPort } });
if (settings.isTraefikUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} else {
await startTcpProxy(destinationDocker, id, publicPort, privatePort); await startTcpProxy(destinationDocker, id, publicPort, privatePort);
}
} else { } else {
await db.prisma.database.update({ where: { id }, data: { publicPort: null } }); await db.prisma.database.update({ where: { id }, data: { publicPort: null } });
await stopTcpHttpProxy(destinationDocker, oldPublicPort); await stopTcpHttpProxy(destinationDocker, oldPublicPort);

View File

@ -0,0 +1,30 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { getContainerUsage } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let usage = {};
try {
const database = await db.getDatabase({ id, teamId });
if (database.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]);
}
return {
status: 200,
body: {
usage
},
headers: {}
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -26,8 +26,12 @@ export const get: RequestHandler = async (event) => {
// // await saveSshKey(destination); // // await saveSshKey(destination);
// payload.state = await checkContainer(engine, 'coolify-haproxy'); // payload.state = await checkContainer(engine, 'coolify-haproxy');
} else { } else {
let containerName = 'coolify-proxy';
if (!settings.isTraefikUsed) {
containerName = 'coolify-haproxy';
}
payload.state = payload.state =
destination?.engine && (await checkContainer(destination.engine, 'coolify-haproxy')); destination?.engine && (await checkContainer(destination.engine, containerName));
} }
return { return {
status: 200, status: 200,

View File

@ -1,7 +1,12 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; import {
startCoolifyProxy,
startTraefikProxy,
stopCoolifyProxy,
stopTraefikProxy
} from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@ -11,9 +16,16 @@ export const post: RequestHandler = async (event) => {
const { engine } = await event.request.json(); const { engine } = await event.request.json();
try { try {
const settings = await db.prisma.setting.findFirst({});
if (settings?.isTraefikUsed) {
await stopTraefikProxy(engine);
await startTraefikProxy(engine);
} else {
await stopCoolifyProxy(engine); await stopCoolifyProxy(engine);
await startCoolifyProxy(engine); await startCoolifyProxy(engine);
}
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true });
return { return {
status: 200 status: 200
}; };

View File

@ -1,6 +1,12 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; import * as db from '$lib/database';
import {
startCoolifyProxy,
startTraefikProxy,
stopCoolifyProxy,
stopTraefikProxy
} from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@ -8,14 +14,24 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { engine } = await event.request.json(); const { engine } = await event.request.json();
const settings = await db.prisma.setting.findFirst({});
try { try {
if (settings?.isTraefikUsed) {
await startTraefikProxy(engine);
} else {
await startCoolifyProxy(engine); await startCoolifyProxy(engine);
}
return { return {
status: 200 status: 200
}; };
} catch (error) { } catch (error) {
if (settings?.isTraefikUsed) {
await stopTraefikProxy(engine);
} else {
await stopCoolifyProxy(engine); await stopCoolifyProxy(engine);
}
return ErrorHandler(error); return ErrorHandler(error);
} }
}; };

View File

@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { stopCoolifyProxy } from '$lib/haproxy'; import * as db from '$lib/database';
import { stopCoolifyProxy, stopTraefikProxy } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
@ -9,7 +10,13 @@ export const post: RequestHandler = async (event) => {
const { engine } = await event.request.json(); const { engine } = await event.request.json();
try { try {
const settings = await db.prisma.setting.findFirst({});
if (settings?.isTraefikUsed) {
await stopTraefikProxy(engine);
} else {
await stopCoolifyProxy(engine); await stopCoolifyProxy(engine);
}
return { return {
status: 200 status: 200
}; };

View File

@ -23,7 +23,6 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Loading from './applications/[id]/logs/_Loading.svelte';
import Trend from './_Trend.svelte'; import Trend from './_Trend.svelte';
import { session } from '$app/stores'; import { session } from '$app/stores';

View File

@ -30,7 +30,8 @@
value={service.minio.rootUserPassword} value={service.minio.rootUserPassword}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> {#if !service.minio.apiFqdn}
<div class="grid grid-cols-2 items-center px-10">
<label for="publicPort">{$t('forms.api_port')}</label> <label for="publicPort">{$t('forms.api_port')}</label>
<input <input
name="publicPort" name="publicPort"
@ -40,4 +41,5 @@
readonly readonly
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
/> />
</div> </div>
{/if}

View File

@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/env';
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
@ -12,6 +14,8 @@
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import cuid from 'cuid';
import { onMount } from 'svelte';
import Fider from './_Fider.svelte'; import Fider from './_Fider.svelte';
import Ghost from './_Ghost.svelte'; import Ghost from './_Ghost.svelte';
import Hasura from './_Hasura.svelte'; import Hasura from './_Hasura.svelte';
@ -29,9 +33,14 @@
let dualCerts = service.dualCerts; let dualCerts = service.dualCerts;
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
loading = true; loading = true;
try { try {
await post(`/services/${id}/check.json`, { fqdn: service.fqdn }); await post(`/services/${id}/check.json`, {
fqdn: service.fqdn,
otherFqdns: [service.minio.apiFqdn],
exposePort: service.exposePort
});
await post(`/services/${id}/${service.type}.json`, { ...service }); await post(`/services/${id}/${service.type}.json`, { ...service });
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
@ -62,6 +71,12 @@
return errorNotification(error); return errorNotification(error);
} }
} }
onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) {
service.fqdn = `http://${cuid()}.demo.coolify.io`;
await post(`/services/${id}/${service.type}.json`, { ...service });
}
});
</script> </script>
<div class="mx-auto max-w-4xl px-6 pb-12"> <div class="mx-auto max-w-4xl px-6 pb-12">
@ -86,6 +101,14 @@
</div> </div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
{#if service.type === 'minio' && !service.minio.apiFqdn && isRunning}
<div class="text-center">
<span class="font-bold text-red-500">IMPORTANT!</span> There was a small modification with
Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL,
so you could use both through SSL. But this proccess cannot be done automatically, so you have
to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience.
</div>
{/if}
<div class="mt-2 grid grid-cols-2 items-center px-10"> <div class="mt-2 grid grid-cols-2 items-center px-10">
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> <label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label>
<div> <div>
@ -131,6 +154,41 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if service.type === 'minio'}
<div class="grid grid-cols-2 px-10">
<div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Console URL</label>
</div>
<CopyPasswordField
placeholder="eg: https://console.min.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.fqdn}
required
/>
</div>
<div class="grid grid-cols-2 px-10">
<div class="flex-col ">
<label for="apiFqdn" class="pt-2 text-base font-bold text-stone-100">API URL</label>
<Explainer text={$t('application.https_explainer')} />
</div>
<CopyPasswordField
placeholder="eg: https://min.io"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
name="apiFqdn"
id="apiFqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
bind:value={service.minio.apiFqdn}
required
/>
</div>
{:else}
<div class="grid grid-cols-2 px-10"> <div class="grid grid-cols-2 px-10">
<div class="flex-col "> <div class="flex-col ">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" <label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
@ -150,6 +208,8 @@
required required
/> />
</div> </div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<Setting <Setting
disabled={isRunning} disabled={isRunning}

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, exposePort } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
try {
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,519 @@
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 { getServiceMainPort } from '$lib/components/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('n8n');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
variables[secret.name] = secret.value;
});
}
const variables = {
_APP_ENV: 'production',
_APP_VERSION: '',
_APP_LOCALE: '',
_APP_OPTIONS_ABUSE: '',
_APP_OPTIONS_FORCE_HTTPS: '',
_APP_OPENSSL_KEY_V1: '',
_APP_DOMAIN: '',
_APP_DOMAIN_TARGET: '',
_APP_CONSOLE_WHITELIST_ROOT: '',
_APP_CONSOLE_WHITELIST_EMAILS: '',
_APP_CONSOLE_WHITELIST_IPS: '',
_APP_SYSTEM_EMAIL_NAME: '',
_APP_SYSTEM_EMAIL_ADDRESS: '',
_APP_SYSTEM_RESPONSE_FORMAT: '',
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS: '',
_APP_USAGE_STATS: '',
_APP_LOGGING_PROVIDER: '',
_APP_LOGGING_CONFIG: '',
_APP_USAGE_AGGREGATION_INTERVAL: '',
_APP_WORKER_PER_CORE: '',
_APP_REDIS_HOST: '',
_APP_REDIS_PORT: '',
_APP_REDIS_USER: '',
_APP_REDIS_PASS: '',
_APP_DB_HOST: '',
_APP_DB_PORT: '',
_APP_DB_SCHEMA: '',
_APP_DB_USER: '',
_APP_DB_PASS: '',
_APP_DB_ROOT_PASS: '',
_APP_INFLUXDB_HOST: '',
_APP_INFLUXDB_PORT: '',
_APP_STATSD_HOST: '',
_APP_STATSD_PORT: '',
_APP_SMTP_HOST: '',
_APP_SMTP_PORT: '',
_APP_SMTP_SECURE: '',
_APP_SMTP_USERNAME: '',
_APP_SMTP_PASSWORD: '',
_APP_STORAGE_LIMIT: '',
_APP_STORAGE_ANTIVIRUS: '',
_APP_STORAGE_ANTIVIRUS_HOST: '',
_APP_STORAGE_ANTIVIRUS_PORT: '',
_APP_STORAGE_DEVICE: '',
_APP_STORAGE_S3_ACCESS_KEY: '',
_APP_STORAGE_S3_SECRET: '',
_APP_STORAGE_S3_REGION: '',
_APP_STORAGE_S3_BUCKET: '',
_APP_STORAGE_DO_SPACES_ACCESS_KEY: '',
_APP_STORAGE_DO_SPACES_SECRET: '',
_APP_STORAGE_DO_SPACES_REGION: '',
_APP_STORAGE_DO_SPACES_BUCKET: '',
_APP_FUNCTIONS_SIZE_LIMIT: '',
_APP_FUNCTIONS_TIMEOUT: '',
_APP_FUNCTIONS_BUILD_TIMEOUT: '',
_APP_FUNCTIONS_CONTAINERS: '',
_APP_FUNCTIONS_CPUS: '',
_APP_FUNCTIONS_MEMORY: '',
_APP_FUNCTIONS_MEMORY_SWAP: '',
_APP_FUNCTIONS_RUNTIMES: '',
_APP_EXECUTOR_SECRET: '',
_APP_EXECUTOR_RUNTIME_NETWORK: '',
_APP_FUNCTIONS_ENVS: '',
_APP_FUNCTIONS_INACTIVE_THRESHOLD: '',
DOCKERHUB_PULL_USERNAME: '',
DOCKERHUB_PULL_PASSWORD: '',
DOCKERHUB_PULL_EMAIL: '',
_APP_MAINTENANCE_INTERVAL: '',
_APP_MAINTENANCE_RETENTION_EXECUTION: '',
_APP_MAINTENANCE_RETENTION_ABUSE: '',
_APP_MAINTENANCE_RETENTION_AUDIT: ''
};
const config = {
appwrite: {
image: `${image}:${version}`,
volumes: [
`${id}-appwrite-uploads:/storage/uploads`,
`${id}-appwrite-cache:/storage/cache`,
`${id}-appwrite-config:/storage/config`,
`${id}-appwrite-certificates:/storage/certificates`,
`${id}-appwrite-functions:/storage/functions`
],
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE,
_APP_LOCALE: variables._APP_LOCALE,
_APP_CONSOLE_WHITELIST_ROOT: variables._APP_CONSOLE_WHITELIST_ROOT,
_APP_CONSOLE_WHITELIST_EMAILS: variables._APP_CONSOLE_WHITELIST_EMAILS,
_APP_CONSOLE_WHITELIST_IPS: variables._APP_CONSOLE_WHITELIST_IPS,
_APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME,
_APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS,
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS,
_APP_SYSTEM_RESPONSE_FORMAT: variables._APP_SYSTEM_RESPONSE_FORMAT,
_APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE,
_APP_OPTIONS_FORCE_HTTPS: variables._APP_OPTIONS_FORCE_HTTPS,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_DOMAIN: variables._APP_DOMAIN,
_APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_SMTP_HOST: variables._APP_SMTP_HOST,
_APP_SMTP_PORT: variables._APP_SMTP_PORT,
_APP_SMTP_SECURE: variables._APP_SMTP_SECURE,
_APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME,
_APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD,
_APP_USAGE_STATS: variables._APP_USAGE_STATS,
_APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST,
_APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT,
_APP_STORAGE_LIMIT: variables._APP_STORAGE_LIMIT,
_APP_STORAGE_ANTIVIRUS: variables._APP_STORAGE_ANTIVIRUS,
_APP_STORAGE_ANTIVIRUS_HOST: variables._APP_STORAGE_ANTIVIRUS_HOST,
_APP_STORAGE_ANTIVIRUS_PORT: variables._APP_STORAGE_ANTIVIRUS_PORT,
_APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE,
_APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY,
_APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET,
_APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION,
_APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET,
_APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY,
_APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET,
_APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION,
_APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET,
_APP_FUNCTIONS_SIZE_LIMIT: variables._APP_FUNCTIONS_SIZE_LIMIT,
_APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT,
_APP_FUNCTIONS_BUILD_TIMEOUT: variables._APP_FUNCTIONS_BUILD_TIMEOUT,
_APP_FUNCTIONS_CONTAINERS: variables._APP_FUNCTIONS_CONTAINERS,
_APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS,
_APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY,
_APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP,
_APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET,
_APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG,
_APP_STATSD_HOST: variables._APP_STATSD_HOST,
_APP_STATSD_PORT: variables._APP_STATSD_PORT,
_APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL,
_APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION,
_APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE,
_APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT
}
},
appwriteRealtime: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE,
_APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_USAGE_STATS: variables._APP_USAGE_STATS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteExecutor: {
image: `${image}:${version}`,
volumes: [
`${id}-appwrite-functions:/storage/functions`,
`/tmp:/tmp`,
'/var/run/docker.sock:/var/run/docker.sock'
],
environmentVariables: {
DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME,
DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG,
_APP_VERSION: variables._APP_VERSION,
_APP_ENV: variables._APP_ENV,
_APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE,
_APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY,
_APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET,
_APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION,
_APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET,
_APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY,
_APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET,
_APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION,
_APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET,
_APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS,
_APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY,
_APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP,
_APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT,
_APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET,
_APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES,
_APP_FUNCTIONS_INACTIVE_THRESHOLD: variables._APP_FUNCTIONS_INACTIVE_THRESHOLD,
_APP_EXECUTOR_RUNTIME_NETWORK: variables._APP_EXECUTOR_RUNTIME_NETWORK
}
},
appwriteWorkerDatabase: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerBuilds: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerAudits: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerWebhooks: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerDeletes: {
image: `${image}:${version}`,
volumes: [
`${id}-appwrite-uploads:/storage/uploads`,
`${id}-appwrite-cache:/storage/cache`,
`${id}-appwrite-certificates:/storage/certificates`
],
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE,
_APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY,
_APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET,
_APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION,
_APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET,
_APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY,
_APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET,
_APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION,
_APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerCertificates: {
image: `${image}:${version}`,
volumes: [
`${id}-appwrite-config:/storage/config`,
`${id}-appwrite-certificates:/storage/certificates`
],
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteWorkerFunctions: {
image: `${image}:${version}`,
envvironmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT,
_APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET,
_APP_USAGE_STATS: variables._APP_USAGE_STATS,
DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME,
DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD
}
},
appwriteWorkerMails: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME,
_APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_SMTP_HOST: variables._APP_SMTP_HOST,
_APP_SMTP_PORT: variables._APP_SMTP_PORT,
_APP_SMTP_SECURE: variables._APP_SMTP_SECURE,
_APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME,
_APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD,
_APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER,
_APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG
}
},
appwriteMaintenance: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS,
_APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL,
_APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION,
_APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE,
_APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT
}
},
appwriteUsage: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1,
_APP_DB_HOST: variables._APP_DB_HOST,
_APP_DB_PORT: variables._APP_DB_PORT,
_APP_DB_SCHEMA: variables._APP_DB_SCHEMA,
_APP_DB_USER: variables._APP_DB_USER,
_APP_DB_PASS: variables._APP_DB_PASS,
_APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST,
_APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT,
_APP_USAGE_AGGREGATION_INTERVAL: variables._APP_USAGE_AGGREGATION_INTERVAL,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS
}
},
appwriteSchedule: {
image: `${image}:${version}`,
environmentVariables: {
_APP_ENV: variables._APP_ENV,
_APP_REDIS_HOST: variables._APP_REDIS_HOST,
_APP_REDIS_PORT: variables._APP_REDIS_PORT,
_APP_REDIS_USER: variables._APP_REDIS_USER,
_APP_REDIS_PASS: variables._APP_REDIS_PASS
}
},
mariadb: {
image: 'mariadb:10.7',
volumes: [`${id}-appwrite-mariadb:/var/lib/mysql`],
environmentVariables: {
MYSQL_ROOT_PASSWORD: variables._APP_DB_ROOT_PASS,
MYSQL_DATABASE: variables._APP_DB_SCHEMA,
MYSQL_USER: variables._APP_DB_USER,
MYSQL_PASSWORD: variables._APP_DB_PASS
}
},
redis: {
image: 'redis:6.0-alpine3.12',
volumes: [`${id}-appwrite-redis:/data`]
},
influxdb: {
image: 'appwrite/influxdb:1.0.0',
volumes: [`${id}-appwrite-influxdb:/var/lib/influxdb`]
},
telegraf: {
image: 'appwrite/telegraf:1.0.0',
environmentVariables: {
_APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST,
_APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT
}
}
};
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.image,
networks: [network],
volumes: [...config.appwrite.volumes],
environment: config.environmentVariables,
restart: 'always',
labels: makeLabelForServices('appwrite'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
}
},
networks: {
[network]: {
external: true
}
},
volumes: {
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
}
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -0,0 +1,35 @@
import { getUserDetails, removeDestinationDocker } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
try {
const service = await db.getService({ id, teamId });
const { destinationDockerId, destinationDocker, fqdn } = service;
if (destinationDockerId) {
const engine = destinationDocker.engine;
try {
const found = await checkContainer(engine, id);
if (found) {
await removeDestinationDocker({ id, engine });
}
} catch (error) {
console.error(error);
}
}
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};

View File

@ -1,19 +1,56 @@
import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import getPort from 'get-port';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { fqdn } = await event.request.json(); let { fqdn, exposePort, otherFqdns } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns) otherFqdns = otherFqdns.map((fqdn) => fqdn.toLowerCase());
if (exposePort) exposePort = Number(exposePort);
try { try {
const found = await db.isDomainConfigured({ id, fqdn }); let found = await db.isDomainConfigured({ id, fqdn });
if (found) {
throw {
message: t.get('application.domain_already_in_use', {
domain: getDomain(fqdn).replace('www.', '')
})
};
}
if (otherFqdns) {
for (const ofqdn of otherFqdns) {
const domain = getDomain(ofqdn);
const nakedDomain = domain.replace('www.', '');
found = await db.isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true });
if (found) {
throw {
message: t.get('application.domain_already_in_use', {
domain: nakedDomain
})
};
}
}
}
if (exposePort) {
exposePort = Number(exposePort);
if (exposePort < 1024 || exposePort > 65535) {
throw { message: `Exposed Port needs to be between 1024 and 65535.` };
}
const publicPort = await getPort({ port: exposePort });
if (publicPort !== exposePort) {
throw { message: `Port ${exposePort} is already in use.` };
}
}
return { return {
status: found ? 500 : 200, status: found ? 500 : 200,
body: { body: {

View File

@ -30,19 +30,43 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import cuid from 'cuid';
import { browser } from '$app/env';
import ServiceLinks from '$lib/components/ServiceLinks.svelte'; import ServiceLinks from '$lib/components/ServiceLinks.svelte';
import Services from './_Services/_Services.svelte'; import Services from './_Services/_Services.svelte';
import { get } from '$lib/api';
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
export let service; export let service;
export let isRunning; export let isRunning;
export let readOnly; export let readOnly;
export let settings; export let settings;
if (browser && window.location.hostname === 'demo.coolify.io' && !service.fqdn) { const { id } = $page.params;
service.fqdn = `http://${cuid()}.demo.coolify.io`; let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval;
async function getUsage() {
if (usageLoading) return;
usageLoading = true;
const data = await get(`/services/${id}/usage.json`);
usage = data.usage;
usageLoading = false;
} }
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
});
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
@ -52,6 +76,7 @@
</div> </div>
<span class="text-xs">{service.name}</span> <span class="text-xs">{service.name}</span>
</div> </div>
{#if service.fqdn} {#if service.fqdn}
<a <a
href={service.fqdn} href={service.fqdn}
@ -77,5 +102,31 @@
<ServiceLinks {service} /> <ServiceLinks {service} />
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Service Usage</div>
<div class="mx-auto">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
<dd class="mt-1 text-xl font-semibold text-white">
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.CPUPerc}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.NetIO}
</dd>
</div>
</dl>
</div>
</div>
<Services bind:service {isRunning} {readOnly} {settings} /> <Services bind:service {isRunning} {readOnly} {settings} />

View File

@ -9,12 +9,17 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
let { name, fqdn, exposePort } = await event.request.json(); let {
name,
fqdn,
exposePort,
minio: { apiFqdn }
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort); if (exposePort) exposePort = Number(exposePort);
if (apiFqdn) apiFqdn = apiFqdn.toLowerCase();
try { try {
await db.updateService({ id, fqdn, name, exposePort }); await db.updateMinioService({ id, fqdn, apiFqdn, name, exposePort });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -3,7 +3,6 @@ import * as db from '$lib/database';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { startHttpProxy } from '$lib/haproxy';
import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database'; import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
@ -35,7 +34,6 @@ export const post: RequestHandler = async (event) => {
const publicPort = await getFreePort(); const publicPort = await getFreePort();
const consolePort = 9001; const consolePort = 9001;
const apiPort = 9000;
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type); const image = getServiceImage(type);
@ -94,8 +92,7 @@ export const post: RequestHandler = async (event) => {
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`);
await db.updateMinioService({ id, publicPort }); await db.updateMinioServicePort({ id, publicPort });
await startHttpProxy(destinationDocker, id, publicPort, apiPort);
return { return {
status: 200 status: 200
}; };

View File

@ -12,12 +12,7 @@ export const post: RequestHandler = async (event) => {
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { const { destinationDockerId, destinationDocker } = service;
destinationDockerId,
destinationDocker,
fqdn,
minio: { publicPort }
} = service;
await db.updateMinioService({ id, publicPort: null }); await db.updateMinioService({ id, publicPort: null });
if (destinationDockerId) { if (destinationDockerId) {
const engine = destinationDocker.engine; const engine = destinationDocker.engine;
@ -30,11 +25,6 @@ export const post: RequestHandler = async (event) => {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
try {
await stopTcpHttpProxy(destinationDocker, publicPort);
} catch (error) {
console.log(error);
}
} }
return { return {

View File

@ -195,7 +195,6 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
console.log(JSON.stringify(composeFile, null, 2));
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`);
await asyncExecShell( await asyncExecShell(

View File

@ -0,0 +1,30 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { getContainerUsage } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let usage = {};
try {
const service = await db.getService({ id, teamId });
if (service.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]);
}
return {
status: 200,
body: {
usage
},
headers: {}
};
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -3,7 +3,12 @@ import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, generatePassword, getFreePort } from '$lib/database'; import { ErrorHandler, generatePassword, getFreePort } from '$lib/database';
import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; import {
checkContainer,
startTcpProxy,
startTraefikTCPProxy,
stopTcpHttpProxy
} from '$lib/haproxy';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import cuid from 'cuid'; import cuid from 'cuid';
@ -31,12 +36,16 @@ export const post: RequestHandler = async (event) => {
}); });
const { const {
service: { destinationDockerId, destinationDocker }, service: { destinationDockerId, destinationDocker },
ftpPublicPort: oldPublicPort, ftpPublicPort,
ftpUser: user, ftpUser: user,
ftpPassword: savedPassword, ftpPassword: savedPassword,
ftpHostKey, ftpHostKey,
ftpHostKeyPrivate ftpHostKeyPrivate
} = data; } = data;
const { network, engine } = destinationDocker;
const settings = await db.prisma.setting.findFirst();
const host = getEngine(engine);
if (ftpEnabled) {
if (user) ftpUser = user; if (user) ftpUser = user;
if (savedPassword) ftpPassword = decrypt(savedPassword); if (savedPassword) ftpPassword = decrypt(savedPassword);
@ -71,9 +80,7 @@ export const post: RequestHandler = async (event) => {
} else { } else {
await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`);
} }
const { network, engine } = destinationDocker;
const host = getEngine(engine);
if (ftpEnabled) {
await db.prisma.wordpress.update({ await db.prisma.wordpress.update({
where: { serviceId: id }, where: { serviceId: id },
data: { data: {
@ -142,8 +149,15 @@ export const post: RequestHandler = async (event) => {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
); );
}
await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); return {
status: 201,
body: {
publicPort,
ftpUser,
ftpPassword
}
};
} else { } else {
await db.prisma.wordpress.update({ await db.prisma.wordpress.update({
where: { serviceId: id }, where: { serviceId: id },
@ -156,19 +170,7 @@ export const post: RequestHandler = async (event) => {
} catch (error) { } catch (error) {
// //
} }
await stopTcpHttpProxy(destinationDocker, oldPublicPort); await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort);
}
}
if (ftpEnabled) {
return {
status: 201,
body: {
publicPort,
ftpUser,
ftpPassword
}
};
} else {
return { return {
status: 200, status: 200,
body: {} body: {}

View File

@ -72,8 +72,7 @@ export const post: RequestHandler = async (event) => {
minPort, minPort,
maxPort, maxPort,
isAutoUpdateEnabled, isAutoUpdateEnabled,
isDNSCheckEnabled, isDNSCheckEnabled
forceSave
} = await event.request.json(); } = await event.request.json();
try { try {
const { id } = await db.listSettings(); const { id } = await db.listSettings();

View File

@ -37,12 +37,13 @@
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { features } from '$lib/store'; import { features, isTraefikUsed } from '$lib/store';
let isRegistrationEnabled = settings.isRegistrationEnabled; let isRegistrationEnabled = settings.isRegistrationEnabled;
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled;
$isTraefikUsed = settings.isTraefikUsed;
let minPort = settings.minPort; let minPort = settings.minPort;
let maxPort = settings.maxPort; let maxPort = settings.maxPort;
@ -55,7 +56,8 @@
let isFqdnSet = !!settings.fqdn; let isFqdnSet = !!settings.fqdn;
let loading = { let loading = {
save: false, save: false,
remove: false remove: false,
proxyMigration: false
}; };
async function removeFqdn() { async function removeFqdn() {
@ -86,6 +88,7 @@
if (name === 'isDNSCheckEnabled') { if (name === 'isDNSCheckEnabled') {
isDNSCheckEnabled = !isDNSCheckEnabled; isDNSCheckEnabled = !isDNSCheckEnabled;
} }
await post(`/settings.json`, { await post(`/settings.json`, {
isRegistrationEnabled, isRegistrationEnabled,
dualCerts, dualCerts,
@ -156,6 +159,20 @@
function resetView() { function resetView() {
forceSave = false; forceSave = false;
} }
async function migrateProxy(to) {
if (loading.proxyMigration) return;
try {
loading.proxyMigration = true;
await post(`/update.json`, { type: to });
const data = await get(`/settings.json`);
$isTraefikUsed = data.settings.isTraefikUsed;
return toast.push('Proxy migration started, it takes a few seconds.');
} catch ({ error }) {
return errorNotification(error);
} finally {
loading.proxyMigration = false;
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@ -192,6 +209,26 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<!-- <Language /> --> <!-- <Language /> -->
<div class="grid grid-cols-2 items-center">
<div class="flex items-center py-2 pr-8">
<div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base">New Proxy Available!</div>
<Explainer
text="We are changing to <span class='text-sky-500 font-bold'>Traefik</span> as <span class='text-red-500 font-bold'>HAProxy</span> had several problems and uses a LOT of unnecessary memory (<span class='text-sky-500 font-bold'>~20MB</span> vs <span class='text-red-500 font-bold'>~200MB</span>).<br><br>You can switch back to HAProxy if something is not working and <span class='text-yellow-500 font-bold'>please let us know</span>!"
/>
</div>
</div>
<button
disabled={loading.proxyMigration}
class="bg-green-600 text-white hover:bg-green-500"
on:click={() => migrateProxy($isTraefikUsed ? 'haproxy' : 'traefik')}
>{loading.proxyMigration
? 'Migrating...'
: $isTraefikUsed
? 'Switch back to HAProxy'
: 'Migrate to Traefik'}</button
>
</div>
<div class="grid grid-cols-2 items-start"> <div class="grid grid-cols-2 items-start">
<div class="flex-col"> <div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100"> <div class="pt-2 text-base font-bold text-stone-100">

View File

@ -80,10 +80,13 @@
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
{#if !source.githubAppId} {#if !source.githubApp?.installationId}
<form on:submit|preventDefault={newGithubApp} class="py-4"> <form on:submit|preventDefault={newGithubApp} class="py-4">
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">General</div> <div class="title">General</div>
{#if source.apiUrl && source.htmlUrl && source.name}
<button class=" bg-orange-600" type="submit">Create new GitHub App</button>
{/if}
</div> </div>
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
@ -117,11 +120,6 @@
/> />
</div> </div>
</div> </div>
{#if source.apiUrl && source.htmlUrl && source.name}
<div class="text-center">
<button class=" mt-8 bg-orange-600" type="submit">Create new GitHub App</button>
</div>
{/if}
</form> </form>
{:else if source.githubApp?.installationId} {:else if source.githubApp?.installationId}
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">

View File

@ -113,7 +113,7 @@
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !source.gitlabAppId && !source.githubAppId} {#if !source.gitlabAppId && (!source.githubAppId || !source.githubApp?.installationId)}
<div class="flex-col space-y-2 pb-10 text-center"> <div class="flex-col space-y-2 pb-10 text-center">
<div class="text-xl font-bold text-white">Select a provider</div> <div class="text-xl font-bold text-white">Select a provider</div>
<div class="flex justify-center space-x-2"> <div class="flex justify-center space-x-2">

View File

@ -6,6 +6,7 @@ import * as db from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import compare from 'compare-versions'; import compare from 'compare-versions';
import got from 'got'; import got from 'got';
import { checkContainer, startCoolifyProxy, startTraefikProxy } from '$lib/haproxy';
export const get: RequestHandler = async (request) => { export const get: RequestHandler = async (request) => {
try { try {
@ -34,14 +35,14 @@ export const get: RequestHandler = async (request) => {
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { type, latestVersion } = await event.request.json(); const { type, latestVersion } = await event.request.json();
const settings = await db.prisma.setting.findFirst();
if (type === 'update') { if (type === 'update') {
try { try {
if (!dev) { if (!dev) {
const { isAutoUpdateEnabled } = await db.prisma.setting.findFirst();
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell( await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${settings.isAutoUpdateEnabled}' .env`
); );
await asyncExecShell( 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"` `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"`
@ -61,6 +62,44 @@ export const post: RequestHandler = async (event) => {
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }
} else if (type === 'traefik') {
try {
// const found = await checkContainer('/var/run/docker.sock', 'coolify-haproxy');
// if (found) {
// await asyncExecShell(`docker stop -t 0 coolify-haproxy`);
// await asyncExecShell(`docker rm coolify-haproxy`);
// }
// await startTraefikProxy('/var/run/docker.sock');
await db.prisma.setting.update({
where: { id: settings.id },
data: { isTraefikUsed: true }
});
return {
status: 200,
body: {}
};
} catch (error) {
return ErrorHandler(error);
}
} else if (type === 'haproxy') {
try {
// const found = await checkContainer('/var/run/docker.sock', 'coolify-proxy');
// if (found) {
// await asyncExecShell(`docker stop -t 0 coolify-proxy`);
// await asyncExecShell(`docker rm coolify-proxy`);
// }
// await startCoolifyProxy('/var/run/docker.sock');
await db.prisma.setting.update({
where: { id: settings.id },
data: { isTraefikUsed: false }
});
return {
status: 200,
body: {}
};
} catch (error) {
return ErrorHandler(error);
}
} }
return { return {
status: 500 status: 500

View File

@ -0,0 +1,360 @@
import { dev } from '$app/env';
import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database';
import { listServicesWithIncludes } from '$lib/database';
import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts },
traefik
) {
if (isHttps) {
traefik.http.routers[id] = {
entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
middlewares: ['redirect-to-https']
};
traefik.http.services[id] = {
loadbalancer: {
servers: [
{
url: `http://${container}:${port}`
}
]
}
};
if (isDualCerts) {
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
} else {
if (isWWW) {
traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)`,
service: `${id}`,
tls: {
domains: {
main: `${domain}`
}
},
middlewares: ['redirect-to-www']
};
traefik.http.routers[`${id}`].middlewares.push('redirect-to-www');
} else {
traefik.http.routers[`${id}-secure-www`] = {
entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
domains: {
main: `${domain}`
}
},
middlewares: ['redirect-to-non-www']
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${domain}\`)`,
service: `${id}`,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
};
traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www');
}
}
} else {
traefik.http.routers[id] = {
entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
middlewares: []
};
traefik.http.routers[`${id}-secure`] = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`,
service: `${id}`,
tls: {
domains: {
main: `${nakedDomain}`
}
},
middlewares: ['redirect-to-http']
};
traefik.http.services[id] = {
loadbalancer: {
servers: [
{
url: `http://${container}:${port}`
}
]
}
};
if (!isDualCerts) {
if (isWWW) {
traefik.http.routers[`${id}`].middlewares.push('redirect-to-www');
traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www');
} else {
traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www');
traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www');
}
}
}
}
export const get: RequestHandler = async (event) => {
const traefik = {
http: {
routers: {},
services: {},
middlewares: {
'redirect-to-https': {
redirectscheme: {
scheme: 'https'
}
},
'redirect-to-http': {
redirectscheme: {
scheme: 'http'
}
},
'redirect-to-non-www': {
redirectregex: {
regex: '^https?://www\\.(.+)',
replacement: 'http://${1}'
}
},
'redirect-to-www': {
redirectregex: {
regex: '^https?://(?:www\\.)?(.+)',
replacement: 'http://www.${1}'
}
}
}
}
};
const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }
});
const data = {
applications: [],
services: [],
coolify: []
};
for (const application of applications) {
const {
fqdn,
id,
port,
destinationDocker,
destinationDockerId,
settings: { previews, dualCerts }
} = application;
if (destinationDockerId) {
const { engine, network } = destinationDocker;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
if (isRunning) {
data.applications.push({
id,
container: id,
port: port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
data.applications.push({
id: container,
container,
port: port || 3000,
domain: previewDomain,
isRunning,
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
}
}
}
}
}
const services = await listServicesWithIncludes();
for (const service of services) {
const {
fqdn,
id,
type,
destinationDocker,
destinationDockerId,
dualCerts,
plausibleAnalytics
} = service;
if (destinationDockerId) {
const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
if (isRunning) {
// Plausible Analytics custom script
let scriptName = false;
if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') {
scriptName = plausibleAnalytics.scriptName;
}
let container = id;
let otherDomain = null;
let otherNakedDomain = null;
let otherIsHttps = null;
let otherIsWWW = null;
if (type === 'minio' && service.minio.apiFqdn) {
otherDomain = getDomain(service.minio.apiFqdn);
otherNakedDomain = otherDomain.replace(/^www\./, '');
otherIsHttps = service.minio.apiFqdn.startsWith('https://');
otherIsWWW = service.minio.apiFqdn.includes('www.');
}
data.services.push({
id,
container,
type,
otherDomain,
otherNakedDomain,
otherIsHttps,
otherIsWWW,
port,
publicPort,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
scriptName
});
}
}
}
}
}
const { fqdn, dualCerts } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
container: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
});
}
for (const application of data.applications) {
configureMiddleware(application, traefik);
}
for (const service of data.services) {
const { id, scriptName } = service;
configureMiddleware(service, traefik);
if (service.type === 'minio') {
service.id = id + '-minio';
service.container = id;
service.domain = service.otherDomain;
service.nakedDomain = service.otherNakedDomain;
service.isHttps = service.otherIsHttps;
service.isWWW = service.otherIsWWW;
service.port = 9000;
configureMiddleware(service, traefik);
}
if (scriptName) {
traefik.http.middlewares[`${id}-redir`] = {
replacepathregex: {
regex: `/js/${scriptName}`,
replacement: '/js/plausible.js'
}
};
if (traefik.http.routers[id].middlewares.length > 0) {
traefik.http.routers[id].middlewares.push(`${id}-redir`);
} else {
traefik.http.routers[id].middlewares = [`${id}-redir`];
}
}
}
for (const coolify of data.coolify) {
configureMiddleware(coolify, traefik);
}
if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null;
}
if (Object.keys(traefik.http.services).length === 0) {
traefik.http.services = null;
}
return {
status: 200,
body: {
...traefik
}
};
};

View File

@ -0,0 +1,136 @@
import { dev } from '$app/env';
import { getDomain } from '$lib/common';
import * as db from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => {
const id = event.url.searchParams.get('id');
if (id) {
const privatePort = event.url.searchParams.get('privatePort');
const publicPort = event.url.searchParams.get('publicPort');
const type = event.url.searchParams.get('type');
let traefik = {};
if (publicPort && type && privatePort) {
if (type === 'tcp') {
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `HostSNI(\`*\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ address: `${id}:${privatePort}` }]
}
}
}
}
};
} else if (type === 'http') {
const service = await db.prisma.service.findFirst({
where: { id },
include: { minio: true }
});
if (service) {
if (service.type === 'minio') {
if (service?.minio?.apiFqdn) {
const {
minio: { apiFqdn }
} = service;
const domain = getDomain(apiFqdn);
const isHttps = apiFqdn.startsWith('https://');
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `Host(\`${domain}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
}
}
};
if (isHttps) {
if (dev) {
traefik[type].routers[id].tls = {
domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
}
}
} else {
if (service?.fqdn) {
const domain = getDomain(service.fqdn);
const isHttps = service.fqdn.startsWith('https://');
traefik = {
[type]: {
routers: {
[id]: {
entrypoints: [type],
rule: `Host(\`${domain}:${privatePort}\`)`,
service: id
}
},
services: {
[id]: {
loadbalancer: {
servers: [{ url: `http://${id}:${privatePort}` }]
}
}
}
}
};
if (isHttps) {
if (dev) {
traefik[type].routers[id].tls = {
domains: {
main: `${domain}`
}
};
} else {
traefik[type].routers[id].tls = {
certresolver: 'letsencrypt'
};
}
}
}
}
} else {
return {
status: 500
};
}
}
} else {
return {
status: 500
};
}
return {
status: 200,
body: {
...traefik
}
};
}
return {
status: 500
};
};

View File

@ -49,6 +49,9 @@ #svelte .custom-select-wrapper .selectContainer {
@apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm; @apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
} }
#svelte .custom-select-wrapper .spinner {
@apply text-coollabs-100;
}
#svelte .listContainer { #svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200; @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
} }
@ -66,26 +69,6 @@ #svelte .item.active {
select { select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
} }
.svelte-select {
--background: rgb(32 32 32);
--inputColor: white;
--multiItemPadding: 0;
--multiSelectPadding: 0 0.5rem 0 0.5rem;
--border: none;
--placeholderColor: rgb(87 83 78);
--listBackground: rgb(32 32 32);
--itemColor: white;
--itemHoverBG: rgb(107 22 237);
--multiItemBG: rgb(32 32 32);
--multiClearHoverBG: transparent;
--multiClearHoverFill: rgb(239 68 68);
--multiItemActiveBG: transparent;
--multiClearBG: transparent;
--clearSelectFocusColor: white;
--clearSelectHoverColor: rgb(239 68 68);
--multiItemBorderRadius: 0.25rem;
--listShadow: none;
}
label { label {
@apply inline-block w-64 text-xs tracking-tight md:text-sm; @apply inline-block w-64 text-xs tracking-tight md:text-sm;

View File

@ -1,6 +1,5 @@
import preprocess from 'svelte-preprocess'; import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
const config = { const config = {
preprocess: preprocess(), preprocess: preprocess(),
kit: { kit: {
@ -10,6 +9,12 @@ const config = {
}, },
floc: true, floc: true,
vite: { vite: {
proxy: {
'/api/v2': {
target: 'http://localhost:3001/api/v2',
changeOrigin: true
}
},
optimizeDeps: { optimizeDeps: {
exclude: ['svelte-kit-cookie-session'] exclude: ['svelte-kit-cookie-session']
}, },