Compare commits

...

78 Commits

Author SHA1 Message Date
Andras Bacsai
d93506a18c Merge pull request #543 from coollabsio/next
v3.5.0
2022-08-17 11:16:28 +02:00
Andras Bacsai
0bb77a671b ui: typing 2022-08-17 11:06:51 +02:00
Andras Bacsai
028f883499 fix: autoUpdater & cleanupStorage jobs 2022-08-17 11:03:44 +02:00
Andras Bacsai
727133e28b feat: custom dns servers 2022-08-17 10:43:57 +02:00
Andras Bacsai
1bd08cb2db fix: bots 2022-08-17 10:18:38 +02:00
Andras Bacsai
bac55cd90d fix: bot deployments 2022-08-16 16:03:09 +02:00
Andras Bacsai
9b51936131 feat: deploy bots (no domains) 2022-08-16 15:49:33 +02:00
Andras Bacsai
692665d0da fix: dns button ui 2022-08-16 14:46:24 +02:00
Andras Bacsai
14cdf33473 Merge pull request #540 from coollabsio/next
v3.4.0
2022-08-16 14:42:12 +02:00
Andras Bacsai
17f4f83ad6 fix discord invite 2022-08-16 14:37:08 +02:00
Andras Bacsai
c981606da7 update README 2022-08-16 14:35:21 +02:00
Andras Bacsai
e21d1bffe8 fix: heroku icon 2022-08-16 14:29:06 +02:00
Andras Bacsai
32706092f3 fix: heroku icon 2022-08-16 14:10:44 +02:00
Andras Bacsai
a7fe446550 feat: Heroku deployments 2022-08-16 13:58:37 +02:00
Andras Bacsai
bb01cfb330 remove console.log 2022-08-16 11:08:23 +00:00
Andras Bacsai
2590efba4b fix: services import 2022-08-16 09:52:47 +00:00
Andras Bacsai
186c5897a0 fix: autoimport + readme 2022-08-16 09:40:07 +00:00
Andras Bacsai
6150a008d5 fixes 2022-08-16 09:38:34 +00:00
Andras Bacsai
fb2a86ba3f fix ui 2022-08-16 09:13:58 +00:00
Andras Bacsai
91fa762985 fix: appwrite 2022-08-16 09:13:22 +00:00
Andras Bacsai
8b5c7c94cd feat: Appwrite service 2022-08-15 14:58:10 +00:00
Andras Bacsai
3dd2a059bf fix: gitpod conf and heroku buildpacks 2022-08-15 11:23:21 +00:00
Andras Bacsai
2f724ffba2 fix: create coolify-infra, if it does not exists 2022-08-15 10:28:57 +00:00
Andras Bacsai
0586ec3f42 fix: dashboard ui 2022-08-15 10:28:40 +00:00
Andras Bacsai
c2c9d992e7 fix: replace docker compose with docker-compose on CSB 2022-08-15 09:27:54 +00:00
Andras Bacsai
ce9aa636c4 Merge pull request #537 from coollabsio/next
v3.3.4
2022-08-15 11:20:54 +02:00
Andras Bacsai
68d06dcd19 chore: version++ 2022-08-15 09:17:48 +00:00
Andras Bacsai
03bf93eb12 fix: loading indicator 2022-08-15 09:17:31 +00:00
Andras Bacsai
163eabb76c fix: make it public button 2022-08-15 09:15:42 +00:00
Andras Bacsai
bed1bb2429 Merge pull request #536 from coollabsio/next
v3.3.3
2022-08-14 22:04:28 +02:00
Andras Bacsai
bc802b6f19 fix: postgresql on ARM 2022-08-14 20:02:18 +00:00
Andras Bacsai
9b67a253f1 fix: decryption errors 2022-08-12 19:39:03 +00:00
Andras Bacsai
29dc5a8bb4 revert debug 2022-08-12 19:29:53 +00:00
Andras Bacsai
b63e516274 debug 2022-08-12 19:21:45 +00:00
Andras Bacsai
db9d5a0fb0 Merge branch 'next' of https://github.com/coollabsio/coolify into next 2022-08-12 19:07:45 +00:00
Andras Bacsai
3f5e6faac5 Merge branch 'main' of https://github.com/coollabsio/coolify into next 2022-08-12 19:07:42 +00:00
Andras Bacsai
eef6b95e24 debug more 2022-08-12 19:06:46 +00:00
Andras Bacsai
96b76aea6b debug: fider 2022-08-12 19:00:39 +00:00
Andras Bacsai
0745a12e7d Merge pull request #534 from coollabsio/next
v3.3.2
2022-08-12 18:32:12 +02:00
Andras Bacsai
645d5e19db ui fixes on dashboard 2022-08-12 16:19:56 +00:00
Andras Bacsai
45e2c7bd03 fix: debounce dashboard status requests 2022-08-12 16:14:37 +00:00
Andras Bacsai
9001f34fab Merge pull request #533 from coollabsio/next
v3.3.1
2022-08-12 16:31:09 +02:00
Andras Bacsai
4d3dd2052f fix: empty buildpack icons 2022-08-12 16:30:00 +02:00
Andras Bacsai
ff8849a907 Merge pull request #532 from coollabsio/next
v3.3.0
2022-08-12 16:22:57 +02:00
Andras Bacsai
adecdc8788 Merge branch 'main' of https://github.com/coollabsio/coolify into next 2022-08-12 14:22:10 +00:00
Andras Bacsai
36cbf7f0fb fixes 2022-08-12 16:20:18 +02:00
Andras Bacsai
1262f6b11b feat: new dashboard 2022-08-12 16:09:52 +02:00
Andras Bacsai
edbc34e7e5 fix: Enterprise GH link 2022-08-12 12:52:07 +02:00
Andras Bacsai
e2b4ec47b0 feat: mongodb arm support 2022-08-12 12:13:52 +02:00
Andras Bacsai
e9565c3602 fix: !isARM to isARM 2022-08-12 11:54:06 +02:00
Andras Bacsai
8d3ca92fe9 feat: Databases on ARM 2022-08-12 11:48:38 +02:00
Andras Bacsai
272b2bb65d Merge pull request #531 from coollabsio/next
v3.2.3
2022-08-12 10:46:50 +02:00
Andras Bacsai
5d5a478cd1 fix: cleanup prisma engine if there is more than 1 2022-08-12 10:36:51 +02:00
Andras Bacsai
ddd09412cd fix: Secrets 2022-08-12 10:18:05 +02:00
Andras Bacsai
d6cfc2624f fix: toast 2022-08-12 10:08:48 +02:00
Andras Bacsai
e92d0914c2 fix: cleanup stucked prisma-engines 2022-08-12 09:38:11 +02:00
Andras Bacsai
0a44867240 chore: version++ 2022-08-12 09:38:02 +02:00
Andras Bacsai
bb210085b0 Merge pull request #529 from coollabsio/next
v3.2.2
2022-08-11 18:39:04 +02:00
Andras Bacsai
bfa5fb6f16 Update handlers.ts 2022-08-11 16:35:29 +00:00
Andras Bacsai
168eec3fe0 fix: coolify-network on verification 2022-08-11 16:32:37 +00:00
Andras Bacsai
4a2696a58e Merge branch 'main' of https://github.com/coollabsio/coolify into next 2022-08-11 16:29:38 +00:00
Andras Bacsai
6d2b453361 fix schema 2022-08-11 16:34:39 +02:00
Andras Bacsai
61c3f57f4a Merge pull request #527 from coollabsio/next
v3.2.1
2022-08-11 16:33:05 +02:00
Andras Bacsai
95d29363aa fix schema 2022-08-11 16:27:46 +02:00
Andras Bacsai
d431de8ea5 fix: schema 2022-08-11 16:24:59 +02:00
Andras Bacsai
518fdf676a fix: white-labeled env 2022-08-11 16:22:07 +02:00
Andras Bacsai
b4d658cea1 fix typo 2022-08-11 16:18:42 +02:00
Andras Bacsai
3f7cd317d6 feat: init heroku buildpacks 2022-08-11 16:17:07 +02:00
Andras Bacsai
de4c195641 ui: fix 2022-08-11 15:23:37 +02:00
Andras Bacsai
45be1cb49a heroku buildpack init 2022-08-11 13:20:34 +00:00
Andras Bacsai
7dd19c322d Add pack to Coolify image 2022-08-11 13:04:34 +00:00
Andras Bacsai
fb299cdf4c chore 2022-08-11 12:45:36 +00:00
Andras Bacsai
2b1c46e1ab chore: version++ 2022-08-11 12:26:19 +00:00
Andras Bacsai
5d43ea551a fix: only remove coolify managed containers 2022-08-11 12:08:44 +00:00
Andras Bacsai
e861c3fc0f update 2022-08-11 09:37:06 +00:00
Andras Bacsai
5590acd182 Update api.ts 2022-08-11 09:34:52 +00:00
Andras Bacsai
6b42f4dc5b fix: follow/cancel buttons 2022-08-11 09:30:18 +00:00
Andras Bacsai
9771bea60a Update Toasts.svelte 2022-08-11 09:07:21 +00:00
97 changed files with 2757 additions and 1875 deletions

2
.gitpod.Dockerfile vendored Normal file
View File

@@ -0,0 +1,2 @@
FROM gitpod/workspace-node:2022-06-20-19-54-55
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)

View File

@@ -1,7 +1,8 @@
# This configuration file was automatically generated by Gitpod. # This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
# and commit this file to your remote git repository to share the goodness with others. # and commit this file to your remote git repository to share the goodness with others.
image: gitpod/workspace-node:2022-06-20-19-54-55 image:
file: .gitpod.Dockerfile
tasks: tasks:
- init: pnpm install && pnpm db:push && pnpm db:seed - init: pnpm install && pnpm db:push && pnpm db:seed
command: pnpm dev command: pnpm dev

View File

@@ -23,7 +23,7 @@ ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/ COPY --from=coollabsio/prisma-engine:3.15 /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl psmisc
RUN curl -sL https://unpkg.com/@pnpm/self-installer | node RUN curl -sL https://unpkg.com/@pnpm/self-installer | node
RUN mkdir -p ~/.docker/cli-plugins/ RUN mkdir -p ~/.docker/cli-plugins/
@@ -33,6 +33,8 @@ RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.7.0 -o ~/.docker/cli-plugins/docker-compose RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.7.0 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
COPY --from=build /app/apps/api/build/ . COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/apps/ui/build/ ./public COPY --from=build /app/apps/ui/build/ ./public
COPY --from=build /app/apps/api/prisma/ ./prisma COPY --from=build /app/apps/api/prisma/ ./prisma

228
README.md
View File

@@ -1,7 +1,108 @@
# Coolify # Coolify
An open-source & self-hostable Heroku / Netlify alternative An open-source & self-hostable Heroku / Netlify alternative.
(ARM support is in beta).
## Live Demo
https://demo.coolify.io/
(If it is unresponsive, that means someone overloaded the server. 😄)
## 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!
---
## How to install
For more details goto the [docs](https://docs.coollabs.io/coolify/installation.html).
Installation is automated with the following command:
```bash
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh
```
If you would like no questions during installation:
```bash
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f
```
---
## Features
### Git Sources
Self-hosted versions also!
<a href="https://github.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/github.com"></a>
<a href="https://gitlab.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/gitlab.com"></a>
### Destinations
Deploy your resource to:
- Local Docker Engine
- Remote Docker Engine
### Applications
<a href="https://heroku.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/heroku.com"></a>
<a href="https://html5.org/">
<svg style="width:40px;height:40px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" ><g clip-path="url(#HTML5_Clip0_4)" ><path d="M30.216 0L27.6454 28.7967L16.0907 32L4.56783 28.8012L2 0H30.216Z" fill="#E44D26" /><path d="M16.108 29.5515L25.4447 26.963L27.6415 2.35497H16.108V29.5515Z" fill="#F16529" /><path d="M11.1109 9.4197H16.108V5.88731H7.25053L7.33509 6.83499L8.20327 16.5692H16.108V13.0369H11.4338L11.1109 9.4197Z" fill="#EBEBEB" /><path d="M11.907 18.3354H8.36111L8.856 23.8818L16.0917 25.8904L16.108 25.8859V22.2108L16.0925 22.2149L12.1585 21.1527L11.907 18.3354Z" fill="#EBEBEB" /><path d="M16.0958 16.5692H20.4455L20.0354 21.1504L16.0958 22.2138V25.8887L23.3373 23.8817L23.3904 23.285L24.2205 13.9855L24.3067 13.0369H16.0958V16.5692Z" fill="white" /><path d="M16.0958 9.41105V9.41969H24.6281L24.6989 8.62572L24.8599 6.83499L24.9444 5.88731H16.0958V9.41105Z" fill="white" /></g><defs><clipPath id="HTML5_Clip0_4"><rect width="32" height="32" fill="white" /></clipPath></defs></svg></a>
<a href="https://nodejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nodejs.org"></a>
<a href="https://vuejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/vuejs.org"></a>
<a href="https://nuxtjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nuxtjs.org"></a>
<a href="https://nextjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nextjs.org"></a>
<a href="https://reactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/reactjs.org"></a>
<a href="https://preactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/preactjs.org"></a>
<a href="https://gatsbyjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/gatsbyjs.org"></a>
<a href="https://svelte.dev"><img style="width:40px;height:40px" src="https://icon.horse/icon/svelte.dev"></a>
<a href="https://php.net"><img style="width:40px;height:40px" src="https://icon.horse/icon/php.net"></a>
<a href="https://laravel.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/laravel.com"></a>
<a href="https://python.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/python.org"></a>
<a href="https://deno.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/deno.com"></a>
<a href="https://docker.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/docker.com"></a>
### Databases
<a href="https://mongodb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mongodb.org"></a>
<a href="https://mariadb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mariadb.org"></a>
<a href="https://mysql.com"><svg style="width:40px;height:40px" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 25.6 25.6" ><path d="M179.076 94.886c-3.568-.1-6.336.268-8.656 1.25-.668.27-1.74.27-1.828 1.116.357.355.4.936.713 1.428.535.893 1.473 2.096 2.32 2.72l2.855 2.053c1.74 1.07 3.703 1.695 5.398 2.766.982.625 1.963 1.428 2.945 2.098.5.357.803.938 1.428 1.16v-.135c-.312-.4-.402-.98-.713-1.428l-1.34-1.293c-1.293-1.74-2.9-3.258-4.64-4.506-1.428-.982-4.55-2.32-5.13-3.97l-.088-.1c.98-.1 2.14-.447 3.078-.715 1.518-.4 2.9-.312 4.46-.713l2.143-.625v-.4c-.803-.803-1.383-1.874-2.23-2.632-2.275-1.963-4.775-3.882-7.363-5.488-1.383-.892-3.168-1.473-4.64-2.23-.537-.268-1.428-.402-1.74-.848-.805-.98-1.25-2.275-1.83-3.436l-3.658-7.763c-.803-1.74-1.295-3.48-2.275-5.086-4.596-7.585-9.594-12.18-17.268-16.687-1.65-.937-3.613-1.34-5.7-1.83l-3.346-.18c-.715-.312-1.428-1.16-2.053-1.562-2.543-1.606-9.102-5.086-10.977-.5-1.205 2.9 1.785 5.755 2.8 7.228.76 1.026 1.74 2.186 2.277 3.346.3.758.4 1.562.713 2.365.713 1.963 1.383 4.15 2.32 5.98.5.937 1.025 1.92 1.65 2.767.357.5.982.714 1.115 1.517-.625.893-.668 2.23-1.025 3.347-1.607 5.042-.982 11.288 1.293 15 .715 1.115 2.4 3.57 4.686 2.632 2.008-.803 1.56-3.346 2.14-5.577.135-.535.045-.892.312-1.25v.1l1.83 3.703c1.383 2.186 3.793 4.462 5.8 5.98 1.07.803 1.918 2.187 3.256 2.677v-.135h-.088c-.268-.4-.67-.58-1.027-.892-.803-.803-1.695-1.785-2.32-2.677-1.873-2.498-3.523-5.265-4.996-8.12-.715-1.383-1.34-2.9-1.918-4.283-.27-.536-.27-1.34-.715-1.606-.67.98-1.65 1.83-2.143 3.034-.848 1.918-.936 4.283-1.248 6.737-.18.045-.1 0-.18.1-1.426-.356-1.918-1.83-2.453-3.078-1.338-3.168-1.562-8.254-.402-11.913.312-.937 1.652-3.882 1.117-4.774-.27-.848-1.16-1.338-1.652-2.008-.58-.848-1.203-1.918-1.605-2.855-1.07-2.5-1.605-5.265-2.766-7.764-.537-1.16-1.473-2.365-2.232-3.435-.848-1.205-1.783-2.053-2.453-3.48-.223-.5-.535-1.294-.178-1.83.088-.357.268-.5.623-.58.58-.5 2.232.134 2.812.4 1.65.67 3.033 1.294 4.416 2.23.625.446 1.295 1.294 2.098 1.518h.938c1.428.312 3.033.1 4.37.5 2.365.76 4.506 1.874 6.426 3.08 5.844 3.703 10.664 8.968 13.92 15.26.535 1.026.758 1.963 1.25 3.034.938 2.187 2.098 4.417 3.033 6.56.938 2.097 1.83 4.24 3.168 5.98.67.937 3.346 1.427 4.55 1.918.893.4 2.275.76 3.08 1.25 1.516.937 3.033 2.008 4.46 3.034.713.534 2.945 1.65 3.078 2.54zm-45.5-38.772a7.09 7.09 0 0 0-1.828.223v.1h.088c.357.714.982 1.205 1.428 1.83l1.027 2.142.088-.1c.625-.446.938-1.16.938-2.23-.268-.312-.312-.625-.535-.937-.268-.446-.848-.67-1.206-1.026z" transform="matrix(.390229 0 0 .38781 -46.300037 -16.856717)" fill-rule="evenodd" fill="#00678c" /></svg></a>
<a href="https://postgresql.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/postgresql.org"></a>
<a href="https://couchdb.apache.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/couchdb.apache.org"></a>
<a href="https://redis.io"><svg style="width:40px;height:40px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ><defs ><path id="a" d="m45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" /><path id="b" d="m45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z" /></defs ><g transform="matrix(.848327 0 0 .848327 -7.883573 -9.449691)" ><use fill="#a41e11" xlink:href="#a" /><path d="m45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14s12.35 4.852 14.31 5.582 2.037 1.31.024 2.36z" fill="#d82c20" /><use fill="#a41e11" xlink:href="#a" y="-6.218" /><use fill="#d82c20" xlink:href="#b" /><path d="m45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.815s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" fill="#a41e11" /><use fill="#d82c20" xlink:href="#b" y="-6.449" /><g fill="#fff" ><path d="m29.096 20.712-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zm-6.296 3.912 8.74-1.342-2.64 3.872z" /><ellipse cx="20.444" cy="21.402" rx="4.672" ry="1.811" /></g ><path d="m42.132 21.138-5.17 2.042-.004-4.087z" fill="#7a0c00" /><path d="m36.963 23.18-.56.22-5.166-2.042 5.723-2.264z" fill="#ad2115" /></g ></svg ></a>
### Services
- [Appwrite](https://appwrite.io)
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress)
- [Ghost](https://ghost.org)
- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics)
- [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io)
- [VaultWarden](https://github.com/dani-garcia/vaultwarden)
- [LanguageTool](https://languagetool.org)
- [n8n](https://n8n.io)
- [Uptime Kuma](https://github.com/louislam/uptime-kuma)
- [MeiliSearch](https://github.com/meilisearch/meilisearch)
- [Umami](https://github.com/mikecao/umami)
- [Fider](https://fider.io)
- [Hasura](https://hasura.io)
## Migration from v1
A fresh installation is necessary. v2 and v3 are not compatible with v1.
## Support
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.gg/6rDM4fkymF)
## Financial Contributors ## Financial Contributors
@@ -25,126 +126,3 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
---
## Live Demo
https://demo.coolify.io/
(If it is unresponsive, that means someone overloaded the server. 😄)
## 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!
---
## How to install
Installation is automated with the following command:
```bash
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh
```
If you would like no questions during installation:
```bash
wget -q https://get.coollabs.io/coolify/install.sh -O install.sh; sudo bash ./install.sh -f
```
For more details goto the [docs](https://docs.coollabs.io/coolify/installation).
---
## Features
### Git Sources
You can use the following Git Sources to be auto-deployed to your Coolify instance! (Self-hosted versions are also supported.)
<a href="https://github.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/github.com"></a>
<a href="https://gitlab.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/gitlab.com"></a>
### Destinations
You can deploy your applications to the following destinations:
- Local Docker Engine
- Remote Docker Engine
### Applications
Predefined build packs to cover the basic needs to deploy applications.
If you have an advanced use case, you can use the Docker build pack that allows you to deploy your application based on your custom Dockerfile.
<a href="https://html5.org/">
<svg style="width:40px;height:40px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" ><g clip-path="url(#HTML5_Clip0_4)" ><path d="M30.216 0L27.6454 28.7967L16.0907 32L4.56783 28.8012L2 0H30.216Z" fill="#E44D26" /><path d="M16.108 29.5515L25.4447 26.963L27.6415 2.35497H16.108V29.5515Z" fill="#F16529" /><path d="M11.1109 9.4197H16.108V5.88731H7.25053L7.33509 6.83499L8.20327 16.5692H16.108V13.0369H11.4338L11.1109 9.4197Z" fill="#EBEBEB" /><path d="M11.907 18.3354H8.36111L8.856 23.8818L16.0917 25.8904L16.108 25.8859V22.2108L16.0925 22.2149L12.1585 21.1527L11.907 18.3354Z" fill="#EBEBEB" /><path d="M16.0958 16.5692H20.4455L20.0354 21.1504L16.0958 22.2138V25.8887L23.3373 23.8817L23.3904 23.285L24.2205 13.9855L24.3067 13.0369H16.0958V16.5692Z" fill="white" /><path d="M16.0958 9.41105V9.41969H24.6281L24.6989 8.62572L24.8599 6.83499L24.9444 5.88731H16.0958V9.41105Z" fill="white" /></g><defs><clipPath id="HTML5_Clip0_4"><rect width="32" height="32" fill="white" /></clipPath></defs></svg></a>
<a href="https://nodejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nodejs.org"></a>
<a href="https://vuejs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/vuejs.org"></a>
<a href="https://nuxtjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nuxtjs.org"></a>
<a href="https://nextjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/nextjs.org"></a>
<a href="https://reactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/reactjs.org"></a>
<a href="https://preactjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/preactjs.org"></a>
<a href="https://gatsbyjs.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/gatsbyjs.org"></a>
<a href="https://svelte.dev"><img style="width:40px;height:40px" src="https://icon.horse/icon/svelte.dev"></a>
<a href="https://php.net"><img style="width:40px;height:40px" src="https://icon.horse/icon/php.net"></a>
<a href="https://laravel.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/laravel.com"></a>
<a href="https://python.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/python.org"></a>
<a href="https://deno.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/deno.com"></a>
<a href="https://docker.com"><img style="width:40px;height:40px" src="https://icon.horse/icon/docker.com"></a>
If you have a new build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
### Databases
One-click database is ready to be used internally or shared over the internet:
<a href="https://mongodb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mongodb.org"></a>
<a href="https://mariadb.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/mariadb.org"></a>
<a href="https://mysql.com"><svg style="width:40px;height:40px" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 25.6 25.6" ><path d="M179.076 94.886c-3.568-.1-6.336.268-8.656 1.25-.668.27-1.74.27-1.828 1.116.357.355.4.936.713 1.428.535.893 1.473 2.096 2.32 2.72l2.855 2.053c1.74 1.07 3.703 1.695 5.398 2.766.982.625 1.963 1.428 2.945 2.098.5.357.803.938 1.428 1.16v-.135c-.312-.4-.402-.98-.713-1.428l-1.34-1.293c-1.293-1.74-2.9-3.258-4.64-4.506-1.428-.982-4.55-2.32-5.13-3.97l-.088-.1c.98-.1 2.14-.447 3.078-.715 1.518-.4 2.9-.312 4.46-.713l2.143-.625v-.4c-.803-.803-1.383-1.874-2.23-2.632-2.275-1.963-4.775-3.882-7.363-5.488-1.383-.892-3.168-1.473-4.64-2.23-.537-.268-1.428-.402-1.74-.848-.805-.98-1.25-2.275-1.83-3.436l-3.658-7.763c-.803-1.74-1.295-3.48-2.275-5.086-4.596-7.585-9.594-12.18-17.268-16.687-1.65-.937-3.613-1.34-5.7-1.83l-3.346-.18c-.715-.312-1.428-1.16-2.053-1.562-2.543-1.606-9.102-5.086-10.977-.5-1.205 2.9 1.785 5.755 2.8 7.228.76 1.026 1.74 2.186 2.277 3.346.3.758.4 1.562.713 2.365.713 1.963 1.383 4.15 2.32 5.98.5.937 1.025 1.92 1.65 2.767.357.5.982.714 1.115 1.517-.625.893-.668 2.23-1.025 3.347-1.607 5.042-.982 11.288 1.293 15 .715 1.115 2.4 3.57 4.686 2.632 2.008-.803 1.56-3.346 2.14-5.577.135-.535.045-.892.312-1.25v.1l1.83 3.703c1.383 2.186 3.793 4.462 5.8 5.98 1.07.803 1.918 2.187 3.256 2.677v-.135h-.088c-.268-.4-.67-.58-1.027-.892-.803-.803-1.695-1.785-2.32-2.677-1.873-2.498-3.523-5.265-4.996-8.12-.715-1.383-1.34-2.9-1.918-4.283-.27-.536-.27-1.34-.715-1.606-.67.98-1.65 1.83-2.143 3.034-.848 1.918-.936 4.283-1.248 6.737-.18.045-.1 0-.18.1-1.426-.356-1.918-1.83-2.453-3.078-1.338-3.168-1.562-8.254-.402-11.913.312-.937 1.652-3.882 1.117-4.774-.27-.848-1.16-1.338-1.652-2.008-.58-.848-1.203-1.918-1.605-2.855-1.07-2.5-1.605-5.265-2.766-7.764-.537-1.16-1.473-2.365-2.232-3.435-.848-1.205-1.783-2.053-2.453-3.48-.223-.5-.535-1.294-.178-1.83.088-.357.268-.5.623-.58.58-.5 2.232.134 2.812.4 1.65.67 3.033 1.294 4.416 2.23.625.446 1.295 1.294 2.098 1.518h.938c1.428.312 3.033.1 4.37.5 2.365.76 4.506 1.874 6.426 3.08 5.844 3.703 10.664 8.968 13.92 15.26.535 1.026.758 1.963 1.25 3.034.938 2.187 2.098 4.417 3.033 6.56.938 2.097 1.83 4.24 3.168 5.98.67.937 3.346 1.427 4.55 1.918.893.4 2.275.76 3.08 1.25 1.516.937 3.033 2.008 4.46 3.034.713.534 2.945 1.65 3.078 2.54zm-45.5-38.772a7.09 7.09 0 0 0-1.828.223v.1h.088c.357.714.982 1.205 1.428 1.83l1.027 2.142.088-.1c.625-.446.938-1.16.938-2.23-.268-.312-.312-.625-.535-.937-.268-.446-.848-.67-1.206-1.026z" transform="matrix(.390229 0 0 .38781 -46.300037 -16.856717)" fill-rule="evenodd" fill="#00678c" /></svg></a>
<a href="https://postgresql.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/postgresql.org"></a>
<a href="https://couchdb.apache.org"><img style="width:40px;height:40px" src="https://icon.horse/icon/couchdb.apache.org"></a>
<a href="https://redis.io"><svg style="width:40px;height:40px" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ><defs ><path id="a" d="m45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" /><path id="b" d="m45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z" /></defs ><g transform="matrix(.848327 0 0 .848327 -7.883573 -9.449691)" ><use fill="#a41e11" xlink:href="#a" /><path d="m45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14s12.35 4.852 14.31 5.582 2.037 1.31.024 2.36z" fill="#d82c20" /><use fill="#a41e11" xlink:href="#a" y="-6.218" /><use fill="#d82c20" xlink:href="#b" /><path d="m45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.815s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z" fill="#a41e11" /><use fill="#d82c20" xlink:href="#b" y="-6.449" /><g fill="#fff" ><path d="m29.096 20.712-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zm-6.296 3.912 8.74-1.342-2.64 3.872z" /><ellipse cx="20.444" cy="21.402" rx="4.672" ry="1.811" /></g ><path d="m42.132 21.138-5.17 2.042-.004-4.087z" fill="#7a0c00" /><path d="m36.963 23.18-.56.22-5.166-2.042 5.723-2.264z" fill="#ad2115" /></g ></svg ></a>
If you have a new database you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
### Services
You quickly need to host a self-hostable, open-source service? You can do it with a few clicks!
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress)
- [Ghost](https://ghost.org)
- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics)
- [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io)
- [VaultWarden](https://github.com/dani-garcia/vaultwarden)
- [LanguageTool](https://languagetool.org)
- [n8n](https://n8n.io)
- [Uptime Kuma](https://github.com/louislam/uptime-kuma)
- [MeiliSearch](https://github.com/meilisearch/meilisearch)
- [Umami](https://github.com/mikecao/umami)
- [Fider](https://fider.io)
- [Hasura](https://hasura.io)
If you have a new service you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community!
## Migration from v1
A fresh installation is necessary. v2 and v3 are not compatible with v1.
## Support
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://discord.gg/xhBCC7eGKw)
## License
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Please see the [LICENSE](/LICENSE) file in our repository for the full text.

9
apps/api/.env.example Normal file
View File

@@ -0,0 +1,9 @@
COOLIFY_APP_ID=
COOLIFY_SECRET_KEY="12341234123412341234123412341234 - 32 long"
COOLIFY_DATABASE_URL=
COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON=docker
COOLIFY_WHITE_LABELED=false
COOLIFY_WHITE_LABELED_ICON=
COOLIFY_AUTO_UPDATE=

View File

@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "Appwrite" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"opensslKeyV1" TEXT NOT NULL,
"executorSecret" TEXT NOT NULL,
"redisPassword" TEXT NOT NULL,
"mariadbHost" TEXT,
"mariadbPort" INTEGER NOT NULL DEFAULT 3306,
"mariadbUser" TEXT NOT NULL,
"mariadbPassword" TEXT NOT NULL,
"mariadbRootUser" TEXT NOT NULL,
"mariadbRootUserPassword" TEXT NOT NULL,
"mariadbDatabase" TEXT NOT NULL,
"mariadbPublicPort" INTEGER,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Appwrite_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Appwrite_serviceId_key" ON "Appwrite"("serviceId");

View File

@@ -0,0 +1,20 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Setting" ADD COLUMN "DNSServers" 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)
DNSServers String?
isTraefikUsed Boolean @default(true) isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -124,6 +125,7 @@ model ApplicationSettings {
debug Boolean @default(false) debug Boolean @default(false)
previews Boolean @default(false) previews Boolean @default(false)
autodeploy Boolean @default(true) autodeploy Boolean @default(true)
isBot Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -312,30 +314,33 @@ model DatabaseSettings {
} }
model Service { model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
exposePort Int? exposePort Int?
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
destinationDockerId String? destinationDockerId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
fider Fider?
ghost Ghost? fider Fider?
hasura Hasura? ghost Ghost?
meiliSearch MeiliSearch? hasura Hasura?
minio Minio? meiliSearch MeiliSearch?
moodle Moodle? minio Minio?
plausibleAnalytics PlausibleAnalytics? moodle Moodle?
persistentStorage ServicePersistentStorage[] plausibleAnalytics PlausibleAnalytics?
serviceSecret ServiceSecret[] persistentStorage ServicePersistentStorage[]
umami Umami? serviceSecret ServiceSecret[]
vscodeserver Vscodeserver? umami Umami?
wordpress Wordpress? vscodeserver Vscodeserver?
teams Team[] wordpress Wordpress?
appwrite Appwrite?
teams Team[]
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@@ -491,3 +496,22 @@ model Moodle {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
} }
model Appwrite {
id String @id @default(cuid())
serviceId String @unique
opensslKeyV1 String
executorSecret String
redisPassword String
mariadbHost String?
mariadbPort Int @default(3306)
mariadbUser String
mariadbPassword String
mariadbRootUser String
mariadbRootUserPassword String
mariadbDatabase String
mariadbPublicPort Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
}

View File

@@ -5,8 +5,10 @@ import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, isDev, listSettings, prisma } from './lib/common'; import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common';
import { scheduler } from './lib/scheduler'; import { scheduler } from './lib/scheduler';
import axios from 'axios';
import compareVersions from 'compare-versions';
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
@@ -16,9 +18,9 @@ declare module 'fastify' {
COOLIFY_DATABASE_URL: string, COOLIFY_DATABASE_URL: string,
COOLIFY_SENTRY_DSN: string, COOLIFY_SENTRY_DSN: string,
COOLIFY_IS_ON: string, COOLIFY_IS_ON: string,
COOLIFY_WHITE_LABELED: boolean, COOLIFY_WHITE_LABELED: string,
COOLIFY_WHITE_LABELED_ICON: string | null, COOLIFY_WHITE_LABELED_ICON: string | null,
COOLIFY_AUTO_UPDATE: boolean, COOLIFY_AUTO_UPDATE: string,
}; };
} }
} }
@@ -52,16 +54,16 @@ const schema = {
default: 'docker' default: 'docker'
}, },
COOLIFY_WHITE_LABELED: { COOLIFY_WHITE_LABELED: {
type: 'boolean', type: 'string',
default: false default: 'false'
}, },
COOLIFY_WHITE_LABELED_ICON: { COOLIFY_WHITE_LABELED_ICON: {
type: 'string', type: 'string',
default: null default: null
}, },
COOLIFY_AUTO_UPDATE: { COOLIFY_AUTO_UPDATE: {
type: 'boolean', type: 'string',
default: false default: 'false'
}, },
} }
@@ -104,6 +106,7 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
await initServer(); await initServer();
await scheduler.start('deployApplication'); await scheduler.start('deployApplication');
await scheduler.start('cleanupStorage'); await scheduler.start('cleanupStorage');
await scheduler.start('cleanupPrismaEngines');
await scheduler.start('checkProxies'); await scheduler.start('checkProxies');
// Check if no build is running // Check if no build is running
@@ -112,18 +115,32 @@ fastify.listen({ port, host }, async (err: any, address: any) => {
setInterval(async () => { setInterval(async () => {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) { if (isAutoUpdateEnabled) {
if (scheduler.workers.has('deployApplication')) { const currentVersion = version;
scheduler.workers.get('deployApplication').postMessage("status:autoUpdater"); const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage("status:autoUpdater");
}
} }
} }
}, 60000 * 15) }, isDev ? 5000 : 60000 * 15)
// Cleanup storage // Cleanup storage
setInterval(async () => { setInterval(async () => {
if (scheduler.workers.has('deployApplication')) { if (scheduler.workers.has('deployApplication')) {
scheduler.workers.get('deployApplication').postMessage("status:cleanupStorage"); scheduler.workers.get('deployApplication').postMessage("status:cleanupStorage");
} }
}, 60000 * 10) }, isDev ? 5000 : 60000 * 10)
scheduler.on('worker deleted', async (name) => { scheduler.on('worker deleted', async (name) => {
if (name === 'autoUpdater' || name === 'cleanupStorage') { if (name === 'autoUpdater' || name === 'cleanupStorage') {

View File

@@ -1,90 +1,96 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd } from '../lib/common'; import { prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, executeDockerCmd, listSettings } from '../lib/common';
import { checkContainer } from '../lib/docker'; import { checkContainer } from '../lib/docker';
(async () => { (async () => {
if (parentPort) { if (parentPort) {
// Coolify Proxy local try {
const engine = '/var/run/docker.sock'; const { arch } = await listSettings();
const localDocker = await prisma.destinationDocker.findFirst({ // Coolify Proxy local
where: { engine, network: 'coolify' } const engine = '/var/run/docker.sock';
}); const localDocker = await prisma.destinationDocker.findFirst({
if (localDocker && localDocker.isCoolifyProxyUsed) { where: { engine, network: 'coolify' }
// Remove HAProxy });
const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' }); if (localDocker && localDocker.isCoolifyProxyUsed) {
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
})
}
await startTraefikProxy(localDocker.id);
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database);
// Remove HAProxy // Remove HAProxy
const found = await checkContainer({ const found = await checkContainer({ dockerId: localDocker.id, container: 'coolify-haproxy' });
dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
});
if (found) { if (found) {
await executeDockerCmd({ await executeDockerCmd({
dockerId: localDocker.id, dockerId: localDocker.id,
command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}` command: `docker stop -t 0 coolify-haproxy && docker rm coolify-haproxy`
}) })
} }
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); await startTraefikProxy(localDocker.id);
}
} // TCP Proxies
} const databasesWithPublicPort = await prisma.database.findMany({
const wordpressWithFtp = await prisma.wordpress.findMany({ where: { publicPort: { not: null } },
where: { ftpPublicPort: { not: null } }, include: { settings: true, destinationDocker: true }
include: { service: { include: { destinationDocker: true } } } });
}); for (const database of databasesWithPublicPort) {
for (const ftp of wordpressWithFtp) { const { destinationDockerId, destinationDocker, publicPort, id } = database;
const { service, ftpPublicPort } = ftp; if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { destinationDockerId, destinationDocker, id } = service; const { privatePort } = generateDatabaseConfiguration(database, arch);
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { // Remove HAProxy
// Remove HAProxy const found = await checkContainer({
const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` }); dockerId: localDocker.id, container: `haproxy-for-${publicPort}`
if (found) { });
await executeDockerCmd({ if (found) {
dockerId: localDocker.id, await executeDockerCmd({
command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}` dockerId: localDocker.id,
}) command: `docker stop -t 0 haproxy-for-${publicPort} && docker rm haproxy-for-${publicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
} }
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
} }
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: `haproxy-for-${ftpPublicPort}` });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 haproxy -for-${ftpPublicPort} && docker rm haproxy-for-${ftpPublicPort}`
})
}
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
} finally {
await prisma.$disconnect();
} }
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// Remove HAProxy
const found = await checkContainer({ dockerId: localDocker.id, container: `${id}-${publicPort}` });
if (found) {
await executeDockerCmd({
dockerId: localDocker.id,
command: `docker stop -t 0 ${id}-${publicPort} && docker rm ${id}-${publicPort} `
})
}
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
await prisma.$disconnect();
} else process.exit(0); } else process.exit(0);
})(); })();

View File

@@ -0,0 +1,19 @@
import { parentPort } from 'node:worker_threads';
import { asyncExecShell, isDev, prisma } from '../lib/common';
(async () => {
if (parentPort) {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 10m`)
}
} catch (error) {
console.log(error);
} finally {
await prisma.$disconnect();
}
}
} else process.exit(0);
})();

View File

@@ -152,6 +152,13 @@ import * as buildpacks from '../lib/buildPacks';
.createHash('sha256') .createHash('sha256')
.update( .update(
JSON.stringify({ JSON.stringify({
pythonWSGI,
pythonModule,
pythonVariable,
deploymentType,
denoOptions,
baseImage,
baseBuildImage,
buildPack, buildPack,
port, port,
exposePort, exposePort,
@@ -192,9 +199,9 @@ import * as buildpacks from '../lib/buildPacks';
} catch (error) { } catch (error) {
// //
} }
// if (!imageFound || deployNeeded) { await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (true) { if (!imageFound || deployNeeded) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); // if (true) {
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
@@ -248,11 +255,11 @@ import * as buildpacks from '../lib/buildPacks';
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (pullmergeRequestId) { if (pullmergeRequestId) {
if (secret.isPRMRSecret) { if (secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`); envs.push(`${secret.name}='${secret.value}'`);
} }
} else { } else {
if (!secret.isPRMRSecret) { if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`); envs.push(`${secret.name}='${secret.value}'`);
} }
} }
}); });
@@ -303,6 +310,7 @@ import * as buildpacks from '../lib/buildPacks';
labels, labels,
depends_on: [], depends_on: [],
restart: 'always', restart: 'always',
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: { // logging: {
// driver: 'fluentd', // driver: 'fluentd',

View File

@@ -252,6 +252,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
label: 'python:3.7-slim-bullseye' label: 'python:3.7-slim-bullseye'
} }
]; ];
const herokuVersions = [
{
value: 'heroku/builder:22',
label: 'heroku/builder:22'
},
{
value: 'heroku/buildpacks:20',
label: 'heroku/buildpacks:20'
},
{
value: 'heroku/builder-classic:22',
label: 'heroku/builder-classic:22'
},
]
let payload: any = { let payload: any = {
baseImage: null, baseImage: null,
baseBuildImage: null, baseBuildImage: null,
@@ -299,6 +313,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
if (buildPack === 'heroku') {
payload.baseImage = 'heroku/buildpacks:20';
payload.baseImages = herokuVersions;
}
return payload; return payload;
} }
@@ -523,7 +542,7 @@ export async function buildImage({
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (debug) { if (debug) {
await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment. You will see the full build log when the build is finished!\n###############`, buildId, applicationId }); await saveBuildLog({ line: `\n###############\nIMPORTANT: Due to some issues during implementing Remote Docker Engine, the builds logs are not streamed at the moment - but will be soon! You will see the full build log when the build is finished!\n###############`, buildId, applicationId });
} }
if (!debug && isCache) { if (!debug && isCache) {
await saveBuildLog({ await saveBuildLog({

View File

@@ -0,0 +1,39 @@
import { executeDockerCmd, prisma } from "../common"
import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir } = data
try {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
const { stdout } = await executeDockerCmd({
dockerId,
command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20`
})
if (debug) {
const array = stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
} catch (error) {
const array = error.stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
throw error;
}
}

View File

@@ -15,6 +15,7 @@ import eleventy from './static';
import python from './python'; import python from './python';
import deno from './deno'; import deno from './deno';
import laravel from './laravel'; import laravel from './laravel';
import heroku from './heroku';
export { export {
node, node,
@@ -33,5 +34,6 @@ export {
eleventy, eleventy,
python, python,
deno, deno,
laravel laravel,
heroku
}; };

View File

@@ -17,7 +17,7 @@ import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
import * as serviceFields from './serviceFields' import * as serviceFields from './serviceFields'
export const version = '3.2.0'; export const version = '3.5.0';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
@@ -38,8 +38,8 @@ export function getAPIUrl() {
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '')
return newURL return newURL
} }
if (process.env.CODESANDBOX_HOST) { if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`
} }
return isDev ? 'http://localhost:3001' : 'http://localhost:3000'; return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
} }
@@ -50,8 +50,8 @@ export function getUIUrl() {
const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '')
return newURL return newURL
} }
if (process.env.CODESANDBOX_HOST) { if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/,'3000')}` return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`
} }
return 'http://localhost:3000'; return 'http://localhost:3000';
} }
@@ -78,6 +78,8 @@ export const include: any = {
umami: true, umami: true,
hasura: true, hasura: true,
fider: true, fider: true,
moodle: true,
appwrite: true
}; };
export const uniqueName = (): string => uniqueNamesGenerator(customConfig); export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
@@ -96,17 +98,23 @@ export const base64Decode = (text: string): string => {
}; };
export const decrypt = (hashString: string) => { export const decrypt = (hashString: string) => {
if (hashString) { if (hashString) {
const hash = JSON.parse(hashString); try {
const decipher = crypto.createDecipheriv( const hash = JSON.parse(hashString);
algorithm, const decipher = crypto.createDecipheriv(
process.env['COOLIFY_SECRET_KEY'], algorithm,
Buffer.from(hash.iv, 'hex') process.env['COOLIFY_SECRET_KEY'],
); Buffer.from(hash.iv, 'hex')
const decrpyted = Buffer.concat([ );
decipher.update(Buffer.from(hash.content, 'hex')), const decrpyted = Buffer.concat([
decipher.final() decipher.update(Buffer.from(hash.content, 'hex')),
]); decipher.final()
return decrpyted.toString(); ]);
return decrpyted.toString();
} catch (error) {
console.log({ decryptionError: error.message })
return hashString
}
} }
}; };
export const encrypt = (text: string) => { export const encrypt = (text: string) => {
@@ -252,8 +260,8 @@ export const supportedServiceTypesAndVersions = [
fancyName: 'Hasura', fancyName: 'Hasura',
baseImage: 'hasura/graphql-engine', baseImage: 'hasura/graphql-engine',
images: ['postgres:12-alpine'], images: ['postgres:12-alpine'],
versions: ['latest', 'v2.8.4', 'v2.5.1'], versions: ['latest', 'v2.10.0', 'v2.5.1'],
recommendedVersion: 'v2.8.4', recommendedVersion: 'v2.10.0',
ports: { ports: {
main: 8080 main: 8080
} }
@@ -269,6 +277,17 @@ export const supportedServiceTypesAndVersions = [
main: 3000 main: 3000
} }
}, },
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'],
recommendedVersion: '0.15.3',
ports: {
main: 80
}
}
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',
@@ -288,6 +307,10 @@ export async function checkDoubleBranch(branch: string, projectId: number): Prom
} }
export async function isDNSValid(hostname: any, domain: string): Promise<any> { export async function isDNSValid(hostname: any, domain: string): Promise<any> {
const { isIP } = await import('is-ip'); const { isIP } = await import('is-ip');
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
}
let resolves = []; let resolves = [];
try { try {
if (isIP(hostname)) { if (isIP(hostname)) {
@@ -301,7 +324,6 @@ export async function isDNSValid(hostname: any, domain: string): Promise<any> {
try { try {
let ipDomainFound = false; let ipDomainFound = false;
dns.setServers(['1.1.1.1', '8.8.8.8']);
const dnsResolve = await dns.resolve4(domain); const dnsResolve = await dns.resolve4(domain);
if (dnsResolve.length > 0) { if (dnsResolve.length > 0) {
for (const ip of dnsResolve) { for (const ip of dnsResolve) {
@@ -393,7 +415,12 @@ export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): P
const { isIP } = await import('is-ip'); const { isIP } = await import('is-ip');
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`; const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
dns.setServers(['1.1.1.1', '8.8.8.8']);
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
}
let resolves = []; let resolves = [];
try { try {
if (isIP(hostname)) { if (isIP(hostname)) {
@@ -460,28 +487,50 @@ export const supportedDatabaseTypesAndVersions = [
name: 'mongodb', name: 'mongodb',
fancyName: 'MongoDB', fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb', baseImage: 'bitnami/mongodb',
versions: ['5.0', '4.4', '4.2'] baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2']
},
{
name: 'mysql',
fancyName: 'MySQL',
baseImage: 'bitnami/mysql',
baseImageARM: 'mysql',
versions: ['8.0', '5.7'],
versionsARM: ['8.0', '5.7']
}, },
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{ {
name: 'mariadb', name: 'mariadb',
fancyName: 'MariaDB', fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb', baseImage: 'bitnami/mariadb',
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] baseImageARM: 'mariadb',
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
}, },
{ {
name: 'postgresql', name: 'postgresql',
fancyName: 'PostgreSQL', fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql', baseImage: 'bitnami/postgresql',
versions: ['14.4.0', '13.6.0', '12.10.0', '11.15.0', '10.20.0'] baseImageARM: 'postgres',
versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22']
}, },
{ {
name: 'redis', name: 'redis',
fancyName: 'Redis', fancyName: 'Redis',
baseImage: 'bitnami/redis', baseImage: 'bitnami/redis',
versions: ['7.0', '6.2', '6.0', '5.0'] baseImageARM: 'redis',
versions: ['7.0', '6.2', '6.0', '5.0'],
versionsARM: ['7.0', '6.2', '6.0', '5.0']
}, },
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.2'] } {
name: 'couchdb',
fancyName: 'CouchDB',
baseImage: 'bitnami/couchdb',
baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1']
}
]; ];
export async function getFreeSSHLocalPort(id: string): Promise<number> { export async function getFreeSSHLocalPort(id: string): Promise<number> {
@@ -551,6 +600,11 @@ export async function executeDockerCmd({ dockerId, command }: { dockerId: string
} else { } else {
engine = 'unix:///var/run/docker.sock' engine = 'unix:///var/run/docker.sock'
} }
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
}
}
return await asyncExecShell( return await asyncExecShell(
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}` `DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
); );
@@ -562,6 +616,11 @@ export async function startTraefikProxy(id: string): Promise<void> {
const { id: settingsId, ipv4, ipv6 } = await listSettings(); const { id: settingsId, ipv4, ipv6 } = await listSettings();
if (!found) { if (!found) {
const { stdout: coolifyNetwork } = await executeDockerCmd({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` })
if (!coolifyNetwork) {
await executeDockerCmd({ dockerId: id, command: `docker network create --attachable coolify-infra` })
}
const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }) const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` })
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
let traefikUrl = mainTraefikEndpoint let traefikUrl = mainTraefikEndpoint
@@ -674,10 +733,11 @@ export function generatePassword(length = 24, symbols = false): string {
}); });
} }
export function generateDatabaseConfiguration(database: any): export function generateDatabaseConfiguration(database: any, arch: string):
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
@@ -691,16 +751,20 @@ export function generateDatabaseConfiguration(database: any):
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
MONGODB_ROOT_USER: string; MONGO_INITDB_ROOT_USERNAME?: string;
MONGODB_ROOT_PASSWORD: string; MONGO_INITDB_ROOT_PASSWORD?: string;
MONGODB_ROOT_USER?: string;
MONGODB_ROOT_PASSWORD?: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
@@ -714,6 +778,7 @@ export function generateDatabaseConfiguration(database: any):
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
@@ -726,6 +791,19 @@ export function generateDatabaseConfiguration(database: any):
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRES_USER: string;
POSTGRES_PASSWORD: string;
POSTGRES_DB: string;
};
}
| {
volume: string;
image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
@@ -736,6 +814,7 @@ export function generateDatabaseConfiguration(database: any):
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
@@ -754,9 +833,9 @@ export function generateDatabaseConfiguration(database: any):
type, type,
settings: { appendOnly } settings: { appendOnly }
} = database; } = database;
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type, arch);
if (type === 'mysql') { if (type === 'mysql') {
return { const configuration = {
privatePort: 3306, privatePort: 3306,
environmentVariables: { environmentVariables: {
MYSQL_USER: dbUser, MYSQL_USER: dbUser,
@@ -768,9 +847,13 @@ export function generateDatabaseConfiguration(database: any):
image: `${baseImage}:${version}`, image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mysql/data`, volume: `${id}-${type}-data:/bitnami/mysql/data`,
ulimits: {} ulimits: {}
}; }
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration
} else if (type === 'mariadb') { } else if (type === 'mariadb') {
return { const configuration = {
privatePort: 3306, privatePort: 3306,
environmentVariables: { environmentVariables: {
MARIADB_ROOT_USER: rootUser, MARIADB_ROOT_USER: rootUser,
@@ -783,8 +866,12 @@ export function generateDatabaseConfiguration(database: any):
volume: `${id}-${type}-data:/bitnami/mariadb`, volume: `${id}-${type}-data:/bitnami/mariadb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration
} else if (type === 'mongodb') { } else if (type === 'mongodb') {
return { const configuration = {
privatePort: 27017, privatePort: 27017,
environmentVariables: { environmentVariables: {
MONGODB_ROOT_USER: rootUser, MONGODB_ROOT_USER: rootUser,
@@ -794,8 +881,16 @@ export function generateDatabaseConfiguration(database: any):
volume: `${id}-${type}-data:/bitnami/mongodb`, volume: `${id}-${type}-data:/bitnami/mongodb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) {
configuration.environmentVariables = {
MONGO_INITDB_ROOT_USERNAME: rootUser,
MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
}
configuration.volume = `${id}-${type}-data:/data/db`;
}
return configuration
} else if (type === 'postgresql') { } else if (type === 'postgresql') {
return { const configuration = {
privatePort: 5432, privatePort: 5432,
environmentVariables: { environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword, POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
@@ -806,10 +901,20 @@ export function generateDatabaseConfiguration(database: any):
image: `${baseImage}:${version}`, image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/postgresql`, volume: `${id}-${type}-data:/bitnami/postgresql`,
ulimits: {} ulimits: {}
}; }
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser,
POSTGRES_DB: defaultDatabase
}
}
return configuration
} else if (type === 'redis') { } else if (type === 'redis') {
return { const configuration = {
privatePort: 6379, privatePort: 6379,
command: undefined,
environmentVariables: { environmentVariables: {
REDIS_PASSWORD: dbUserPassword, REDIS_PASSWORD: dbUserPassword,
REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no' REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no'
@@ -818,8 +923,13 @@ export function generateDatabaseConfiguration(database: any):
volume: `${id}-${type}-data:/bitnami/redis/data`, volume: `${id}-${type}-data:/bitnami/redis/data`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/data`;
configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no'} --requirepass ${dbUserPassword}`;
}
return configuration
} else if (type === 'couchdb') { } else if (type === 'couchdb') {
return { const configuration = {
privatePort: 5984, privatePort: 5984,
environmentVariables: { environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword, COUCHDB_PASSWORD: dbUserPassword,
@@ -829,20 +939,35 @@ export function generateDatabaseConfiguration(database: any):
volume: `${id}-${type}-data:/bitnami/couchdb`, volume: `${id}-${type}-data:/bitnami/couchdb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) {
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
}
return configuration
} }
} }
export function isARM(arch: string) {
export function getDatabaseImage(type: string): string { if (arch === 'arm' || arch === 'arm64') {
return true
}
return false
}
export function getDatabaseImage(type: string, arch: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
if (isARM(arch)) {
return found.baseImageARM || found.baseImage
}
return found.baseImage; return found.baseImage;
} }
return ''; return '';
} }
export function getDatabaseVersions(type: string): string[] { export function getDatabaseVersions(type: string, arch: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
if (isARM(arch)) {
return found.versionsARM || found.versions
}
return found.versions; return found.versions;
} }
return []; return [];
@@ -1143,7 +1268,6 @@ export async function startTraefikTCPProxy(
} }
traefikUrl = `${ip}/webhooks/traefik/other.json` traefikUrl = `${ip}/webhooks/traefik/other.json`
} }
console.log(traefikUrl)
const tcpProxy = { const tcpProxy = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1208,6 +1332,7 @@ export async function getServiceFromDB({ id, teamId }: { id: string; teamId: str
return s; return s;
}); });
} }
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) } body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) }
return { ...body, settings }; return { ...body, settings };
} }
@@ -1434,6 +1559,35 @@ export async function configureServiceType({
} }
} }
}); });
} else if (type === 'appwrite') {
const opensslKeyV1 = encrypt(generatePassword());
const executorSecret = encrypt(generatePassword());
const redisPassword = encrypt(generatePassword());
const mariadbHost = `${id}-mariadb`
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbDatabase = 'appwrite';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
appwrite: {
create: {
opensslKeyV1,
executorSecret,
redisPassword,
mariadbHost,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else { } else {
await prisma.service.update({ await prisma.service.update({
where: { id }, where: { id },
@@ -1445,6 +1599,7 @@ export async function configureServiceType({
} }
export async function removeService({ id }: { id: string }): Promise<void> { export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.fider.deleteMany({ where: { serviceId: id } }); await prisma.fider.deleteMany({ where: { serviceId: id } });
@@ -1455,8 +1610,8 @@ export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } }); await prisma.wordpress.deleteMany({ where: { serviceId: id } });
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
await prisma.service.delete({ where: { id } }); await prisma.service.delete({ where: { id } });
} }
@@ -1521,9 +1676,9 @@ export const getServiceMainPort = (service: string) => {
export function makeLabelForServices(type) { export function makeLabelForServices(type) {
return [ return [
'coolify.managed=true', 'coolify.managed=true',
`coolify.version = ${version} `, `coolify.version = ${version}`,
`coolify.type = service`, `coolify.type = service`,
`coolify.service.type = ${type} ` `coolify.service.type = ${type}`
]; ];
} }
export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) { export function errorHandler({ status = 500, message = 'Unknown error.' }: { status: number, message: string | any }) {
@@ -1614,7 +1769,7 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
return return
} }
try { try {
await executeDockerCmd({ dockerId, command: `docker container prune -f` }) await executeDockerCmd({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` })
} catch (error) { } catch (error) {
//console.log(error); //console.log(error);
} }
@@ -1637,7 +1792,7 @@ export function persistentVolumes(id, persistentStorage, config) {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
let volumes = [ ...persistentVolume] let volumes = [...persistentVolume]
if (config.volume) volumes = [config.volume, ...volumes] if (config.volume) volumes = [config.volume, ...volumes]
const composeVolumes = volumes.length > 0 && volumes.map((volume) => { const composeVolumes = volumes.length > 0 && volumes.map((volume) => {

View File

@@ -71,7 +71,7 @@ export async function removeContainer({
}): Promise<void> { }): Promise<void> {
try { try {
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
console.log(id)
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) await executeDockerCmd({ dockerId, command: `docker rm ${id}` })

View File

@@ -20,7 +20,6 @@ const options: any = {
} }
if (message.caller === 'cleanupStorage') { if (message.caller === 'cleanupStorage') {
if (!scheduler.workers.has('cleanupStorage')) { if (!scheduler.workers.has('cleanupStorage')) {
await scheduler.stop('deployApplication');
await scheduler.run('cleanupStorage') await scheduler.run('cleanupStorage')
} }
} }
@@ -35,6 +34,10 @@ const options: any = {
{ {
name: 'cleanupStorage', name: 'cleanupStorage',
}, },
{
name: 'cleanupPrismaEngines',
interval: '1m'
},
{ {
name: 'checkProxies', name: 'checkProxies',
interval: '10s' interval: '10s'

View File

@@ -326,7 +326,7 @@ export const fider = [{
isBoolean: false, isBoolean: false,
isEncrypted: true isEncrypted: true
}, { }, {
name: 'postgreslUser', name: 'postgresqlUser',
isEditable: false, isEditable: false,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,
@@ -477,3 +477,84 @@ export const moodle = [{
isBoolean: false, isBoolean: false,
isEncrypted: false isEncrypted: false
}] }]
export const appwrite = [{
name: 'opensslKeyV1',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'executorSecret',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'redisPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbHost',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbPort',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbRootUser',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
},
{
name: 'mariadbRootUserPassword',
isEditable: false,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: true
},
{
name: 'mariadbDatabase',
isEditable: true,
isLowerCase: false,
isNumber: false,
isBoolean: false,
isEncrypted: false
}]

View File

@@ -0,0 +1,35 @@
import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common";
export async function defaultServiceConfigurations({ id, teamId }) {
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, type, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort(type);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
let secrets = [];
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
secrets.push(`${secret.name}=${secret.value}`);
});
}
return { ...service, network, port, workdir, image, secrets }
}
export function defaultServiceComposeConfiguration(network: string): any {
return {
networks: [network],
restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '10s',
max_attempts: 10,
window: '120s'
}
}
}
}

View File

@@ -5,7 +5,7 @@ import axios from 'axios';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkDomainsIsValidInDNS, checkDoubleBranch, decrypt, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, getFreeExposedPort, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker'; import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { scheduler } from '../../../../lib/scheduler'; import { scheduler } from '../../../../lib/scheduler';
@@ -18,7 +18,7 @@ export async function listApplications(request: FastifyRequest) {
const { teamId } = request.user const { teamId } = request.user
const applications = await prisma.application.findMany({ const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true, destinationDocker: true } include: { teams: true, destinationDocker: true, settings: true }
}); });
const settings = await prisma.setting.findFirst() const settings = await prisma.setting.findFirst()
return { return {
@@ -90,10 +90,11 @@ export async function getApplication(request: FastifyRequest<OnlyId>) {
const { teamId } = request.user const { teamId } = request.user
const appId = process.env['COOLIFY_APP_ID']; const appId = process.env['COOLIFY_APP_ID'];
const application: any = await getApplicationFromDB(id, teamId); const application: any = await getApplicationFromDB(id, teamId);
const settings = await listSettings();
return { return {
application, application,
appId appId,
settings
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -275,7 +276,7 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) { export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot } = request.body
const isDouble = await checkDoubleBranch(branch, projectId); const isDouble = await checkDoubleBranch(branch, projectId);
if (isDouble && autodeploy) { if (isDouble && autodeploy) {
await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } }) await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
@@ -283,7 +284,7 @@ export async function saveApplicationSettings(request: FastifyRequest<SaveApplic
} }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { settings: { update: { debug, previews, dualCerts, autodeploy } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
return reply.code(201).send(); return reply.code(201).send();

View File

@@ -25,7 +25,7 @@ export interface SaveApplication extends OnlyId {
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; }; Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };

View File

@@ -4,7 +4,6 @@ import { FastifyReply } from 'fastify';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { ComposeFile, createDirectories, decrypt, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker';
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types'; import { GetDatabaseLogs, OnlyId, SaveDatabase, SaveDatabaseDestination, SaveDatabaseSettings, SaveVersion } from '../../../../types';
@@ -93,14 +92,15 @@ export async function getDatabase(request: FastifyRequest<OnlyId>) {
if (!database) { if (!database) {
throw { status: 404, message: 'Database not found.' } throw { status: 404, message: 'Database not found.' }
} }
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const configuration = generateDatabaseConfiguration(database); const configuration = generateDatabaseConfiguration(database, arch);
const settings = await listSettings(); const settings = await listSettings();
return { return {
privatePort: configuration?.privatePort, privatePort: configuration?.privatePort,
database, database,
versions: await getDatabaseVersions(database.type), versions: await getDatabaseVersions(database.type, arch),
settings settings
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -137,8 +137,10 @@ export async function getVersions(request: FastifyRequest<OnlyId>) {
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
const { arch } = await listSettings();
const versions = getDatabaseVersions(type, arch);
return { return {
versions: supportedDatabaseTypesAndVersions.find((name) => name.name === type).versions versions
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -219,6 +221,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { const {
@@ -228,8 +231,8 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
publicPort, publicPort,
settings: { isPublic } settings: { isPublic }
} = database; } = database;
const { privatePort, environmentVariables, image, volume, ulimits } = const { privatePort, command, environmentVariables, image, volume, ulimits } =
generateDatabaseConfiguration(database); generateDatabaseConfiguration(database, arch);
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const volumeName = volume.split(':')[0]; const volumeName = volume.split(':')[0];
@@ -243,6 +246,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
[id]: { [id]: {
container_name: id, container_name: id,
image, image,
command,
networks: [network], networks: [network],
environment: environmentVariables, environment: environmentVariables,
volumes: [volume], volumes: [volume],
@@ -270,6 +274,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
} }
} }
}; };
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
try { try {
@@ -282,6 +287,7 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
return {}; return {};
} catch (error) { } catch (error) {
console.log(error)
throw { throw {
error error
}; };
@@ -440,11 +446,12 @@ export async function saveDatabaseSettings(request: FastifyRequest<SaveDatabaseS
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true } include: { destinationDocker: true, settings: true }
}); });
const { arch } = await listSettings();
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword); if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database; const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database;
const { privatePort } = generateDatabaseConfiguration(database); const { privatePort } = generateDatabaseConfiguration(database, arch);
if (destinationDockerId) { if (destinationDockerId) {
if (isPublic) { if (isPublic) {

View File

@@ -60,7 +60,7 @@ export async function getDestination(request: FastifyRequest<OnlyId>) {
throw { status: 404, message: `Destination not found.` }; throw { status: 404, message: `Destination not found.` };
} }
const settings = await listSettings(); const settings = await listSettings();
let payload = { const payload = {
destination, destination,
settings settings
}; };
@@ -218,7 +218,11 @@ export async function verifyRemoteDockerEngine(request: FastifyRequest, reply: F
if (!stdout) { if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`); await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
} }
const { stdout:coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
if (!coolifyNetwork) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
}
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } }) await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send() return reply.code(201).send()

View File

@@ -4,7 +4,7 @@ import axios from 'axios';
import compare from 'compare-versions'; import compare from 'compare-versions';
import cuid from 'cuid'; import cuid from 'cuid';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, prisma, uniqueName, version } from '../../../lib/common'; import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.'; import type { Login, Update } from '.';
@@ -96,34 +96,22 @@ export async function showDashboard(request: FastifyRequest) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const applicationsCount = await prisma.application.count({ const applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true }
});
const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const sourcesCount = await prisma.gitSource.count({ const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const destinationsCount = await prisma.destinationDocker.count({ const settings = await listSettings();
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teamsCount = await prisma.permission.count({ where: { userId } });
const databasesCount = await prisma.database.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const servicesCount = await prisma.service.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teams = await prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }
});
return { return {
teams, applications,
applicationsCount, databases,
sourcesCount, services,
destinationsCount, settings,
teamsCount,
databasesCount,
servicesCount,
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

File diff suppressed because it is too large Load Diff

View File

@@ -33,12 +33,13 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
minPort, minPort,
maxPort, maxPort,
isAutoUpdateEnabled, isAutoUpdateEnabled,
isDNSCheckEnabled isDNSCheckEnabled,
DNSServers
} = request.body } = request.body
const { id } = await listSettings(); const { id } = await listSettings();
await prisma.setting.update({ await prisma.setting.update({
where: { id }, where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled } data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers }
}); });
if (fqdn) { if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } }); await prisma.setting.update({ where: { id }, data: { fqdn } });
@@ -54,6 +55,10 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply: FastifyReply) { export async function deleteDomain(request: FastifyRequest<DeleteDomain>, reply: FastifyReply) {
try { try {
const { fqdn } = request.body const { fqdn } = request.body
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
}
let ip; let ip;
try { try {
ip = await dns.resolve(fqdn); ip = await dns.resolve(fqdn);

View File

@@ -8,7 +8,8 @@ export interface SaveSettings {
minPort: number, minPort: number,
maxPort: number, maxPort: number,
isAutoUpdateEnabled: boolean, isAutoUpdateEnabled: boolean,
isDNSCheckEnabled: boolean isDNSCheckEnabled: boolean,
DNSServers: string
} }
} }
export interface DeleteDomain { export interface DeleteDomain {

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm init svelte
# create a new project in my-app
npm init svelte my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@@ -24,7 +24,7 @@ export function getWebhookUrl(type: string) {
} }
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
const newURL = CODESANDBOX_HOST.replace(/\$PORT/,'3001') const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}`
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`
} }

View File

@@ -131,8 +131,8 @@ export const supportedServiceTypesAndVersions = [
fancyName: 'Hasura', fancyName: 'Hasura',
baseImage: 'hasura/graphql-engine', baseImage: 'hasura/graphql-engine',
images: ['postgres:12-alpine'], images: ['postgres:12-alpine'],
versions: ['latest', 'v2.5.1'], versions: ['latest', 'v2.10.0', 'v2.5.1'],
recommendedVersion: 'v2.5.1', recommendedVersion: 'v2.10.0',
ports: { ports: {
main: 8080 main: 8080
} }
@@ -148,6 +148,17 @@ export const supportedServiceTypesAndVersions = [
main: 3000 main: 3000
} }
}, },
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'],
recommendedVersion: '0.15.3',
ports: {
main: 80
}
}
// { // {
// name: 'moodle', // name: 'moodle',
// fancyName: 'Moodle', // fancyName: 'Moodle',
@@ -218,7 +229,7 @@ export const staticDeployments = [
'astro', 'astro',
'eleventy' 'eleventy'
]; ];
export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel']; export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku'];
export function generateRemoteEngine(destination: any) { export function generateRemoteEngine(destination: any) {
@@ -232,35 +243,6 @@ export function changeQueryParams(buildId: string) {
return history.pushState(null, null, '?' + queryParams.toString()); return history.pushState(null, null, '?' + queryParams.toString());
} }
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0', '4.4', '4.2']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{
name: 'mariadb',
fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb',
versions: ['10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
},
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2', '6.0', '5.0']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
export const getServiceMainPort = (service: string) => { export const getServiceMainPort = (service: string) => {
const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service); const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service);
if (serviceType) { if (serviceType) {

View File

@@ -1,14 +1,21 @@
<script> <script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let type = 'info'; export let type = 'info';
</script> </script>
<div <div
class="alert shadow-lg text-white rounded" on:click={() => dispatch('click')}
class:alert-success={type === 'success'} on:mouseover={() => dispatch('pause')}
on:focus={() => dispatch('pause')}
on:mouseout={() => dispatch('resume')}
on:blur={() => dispatch('resume')}
class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer"
class:bg-coollabs={type === 'success'}
class:alert-error={type === 'error'} class:alert-error={type === 'error'}
class:alert-info={type === 'info'} class:alert-info={type === 'info'}
> >
<!-- {#if type === 'success'} {#if type === 'success'}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="stroke-current flex-shrink-0 h-6 w-6" class="stroke-current flex-shrink-0 h-6 w-6"
@@ -47,6 +54,6 @@
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg /></svg
> >
{/if} --> {/if}
<slot /> <slot />
</div> </div>

View File

@@ -2,14 +2,19 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import Toast from './Toast.svelte'; import Toast from './Toast.svelte';
import { toasts } from '$lib/store'; import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
</script> </script>
{#if $toasts} {#if $toasts}
<section> <section>
<article class="toast toast-bottom toast-end rounded-none" role="alert" transition:fade> <article class="toast toast-top toast-end rounded-none" role="alert" transition:fade>
{#each $toasts as toast (toast.id)} {#each $toasts as toast (toast.id)}
<Toast type={toast.type}>{toast.message}</Toast> <Toast
type={toast.type}
on:resume={() => resumeToast(toast.id)}
on:pause={() => pauseToast(toast.id)}
on:click={() => dismissToast(toast.id)}>{@html toast.message}</Toast
>
{/each} {/each}
</article> </article>
</section> </section>

View File

@@ -83,7 +83,7 @@
disabled={updateStatus.success === false} disabled={updateStatus.success === false}
on:click={update} on:click={update}
class="icons tooltip tooltip-right tooltip-primary bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" class="icons tooltip tooltip-right tooltip-primary bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105"
data-tip="Update available!" data-tip="Update Available!"
> >
{#if updateStatus.loading} {#if updateStatus.loading}
<svg <svg

View File

@@ -22,11 +22,10 @@
usage: false, usage: false,
cleanup: false cleanup: false
}; };
import { addToast, appSession } from '$lib/store'; import { appSession } from '$lib/store';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { get, post } from '$lib/api'; import { get } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Trend from './Trend.svelte';
async function getStatus() { async function getStatus() {
if (loading.usage) return; if (loading.usage) return;
loading.usage = true; loading.usage = true;
@@ -49,118 +48,66 @@
return errorNotification(error); return errorNotification(error);
} }
}); });
let warning = {
memory: false,
cpu: false,
disk: false
};
let trends = {
memory: 'stable',
cpu: 'stable',
disk: 'stable'
};
async function manuallyCleanupStorage() {
try {
loading.cleanup = true
await post('/internal/cleanup', {});
return addToast({
message: "Cleanup done.",
type:"success"
})
} catch(error) {
return errorNotification(error);
} finally {
loading.cleanup = false
}
}
</script> </script>
{#if $appSession.teamId === '0'} <div class="pb-4">
<div class="px-6 text-2xl font-bold">Server Usage</div> <div class="title">Hardware Details</div>
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <div class="text-center p-8 ">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div>
<dt class="truncate text-sm font-medium text-white">Total Memory</dt> <div class="stat w-64">
<dd class="mt-1 text-3xl font-semibold text-white"> <div class="stat-title">Total Memory</div>
{(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span> <div class="stat-value">
</dd> {(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used Memory</div>
<div class="stat-value">
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</div>
</div>
<div class="stat w-64">
<div class="stat-title">Free Memory</div>
<div class="stat-value">
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
</div>
</div>
</div> </div>
<div class="py-10">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Used Memory</dt> <div class="stat-title">Total CPUs</div>
<dd class="mt-1 text-3xl font-semibold text-white "> <div class="stat-value">
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span> {usage?.cpu.count}
</dd> </div>
</div>
<div class="stat w-64">
<div class="stat-title">CPU Usage</div>
<div class="stat-value">
{usage?.cpu.usage}<span class="text-sm">%</span>
</div>
</div>
<div class="stat w-64">
<div class="stat-title">Load Average (5,10,30mins)</div>
<div class="stat-value">{usage?.cpu.load}</div>
</div>
</div> </div>
<div>
<div <div class="stat w-64">
class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left" <div class="stat-title">Total Disk</div>
class:bg-red-500={warning.memory} <div class="stat-value">
> {usage?.disk.totalGb}<span class="text-sm">GB</span>
<dt class="truncate text-sm font-medium text-white">Free Memory</dt> </div>
<dd class="mt-1 text-3xl font-semibold text-white"> </div>
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span> <div class="stat w-64">
{#if !warning.memory} <div class="stat-title">Used Disk</div>
<Trend trend={trends.memory} /> <div class="stat-value">
{/if} {usage?.disk.usedGb}<span class="text-sm">GB</span>
</dd> </div>
</div>
<div class="stat w-64">
<div class="stat-title">Free Disk</div>
<div class="stat-value">{usage?.disk.freePercentage}<span class="text-sm">%</span></div>
</div>
</div> </div>
</dl> </div>
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> </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">Total CPUs</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.cpu.count}
</dd>
</div>
<div
class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"
class:bg-red-500={warning.cpu}
>
<dt class="truncate text-sm font-medium text-white">CPU Usage</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.cpu.usage}<span class="text-sm">%</span>
{#if !warning.cpu}
<Trend trend={trends.cpu} />
{/if}
</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">Load Average (5/10/30mins)</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.cpu.load.join('/')}
</dd>
</div>
</dl>
<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="truncate text-sm font-medium text-white">Total Disk</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.disk.totalGb}<span class="text-sm">GB</span>
</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 Disk</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.disk.usedGb}<span class="text-sm">GB</span>
</dd>
<button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
>Cleanup Storage</button
>
</div>
<div
class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"
class:bg-red-500={warning.disk}
>
<dt class="truncate text-sm font-medium text-white">Free Disk</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.disk.freePercentage}<span class="text-sm">%</span>
{#if !warning.disk}
<Trend trend={trends.disk} />
{/if}
</dd>
</div>
</dl>
<div class="px-6 pt-20 text-2xl font-bold">Resources</div>
{/if}

View File

@@ -0,0 +1,43 @@
<script lang="ts">
import * as Icons from '$lib/components/svg/applications';
export let application: any;
export let isAbsolute = true;
</script>
{#if application.buildPack?.toLowerCase() === 'rust'}
<Icons.Rust {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'node'}
<Icons.Nodejs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'react'}
<Icons.React {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'svelte'}
<Icons.Svelte {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'vuejs'}
<Icons.Vuejs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'php'}
<Icons.Php {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'python'}
<Icons.Python {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'static'}
<Icons.Static {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nestjs'}
<Icons.Nestjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nuxtjs'}
<Icons.Nuxtjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nextjs'}
<Icons.Nextjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'gatsby'}
<Icons.Gatsby {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'docker'}
<Icons.Docker {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'astro'}
<Icons.Astro {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'eleventy'}
<Icons.Eleventy {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'deno'}
<Icons.Deno {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'laravel'}
<Icons.Laravel {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'heroku'}
<Icons.Heroku {isAbsolute} />
{/if}

View File

@@ -1,5 +1,9 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-6 h-14 w-14" class={isAbsolute ? 'absolute top-0 left-0 -m-6 h-14 w-14' : 'mx-auto w-8 h-8'}
viewBox="0 0 256 256" viewBox="0 0 256 256"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,5 +1,11 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="text-white-500 absolute top-0 left-0 -m-4 h-10 w-10" class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 text-white-500'
: 'mx-auto w-8 h-8 text-white-500'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,4 +1,11 @@
<svg viewBox="0 0 128 128" class="absolute top-0 left-0 -m-8 h-16 w-16"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
viewBox="0 0 128 128"
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-8 h-8'}
>
<g <g
><path ><path
fill-rule="evenodd" fill-rule="evenodd"

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,4 +1,11 @@
<svg viewBox="0 0 128 128" class="absolute top-0 left-0 -m-8 h-16 w-16"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
viewBox="0 0 128 128"
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-8 h-8'}
>
<path fill="transparent" d="M18 0h92v128H18z" /><path <path fill="transparent" d="M18 0h92v128H18z" /><path
d="M55.3 36.3h.4c1.1 0 1.5.9 1.5 2.3v41.8c0 1.8-.4 3-1.6 3l-4.8-.1c-1.2 0-1.6-1-1.6-3V45.5l-2.1.5c-1 0-1.5-.8-1.5-2.2v-3c0-1.2.4-2 1.4-2.2l8.3-2.2zm16 36.1l.1 3 .6 1.3.6.6.8.1h2.2c1 0 1.7.8 1.7 2v1.9c0 1.2-.6 2-1.8 2h-3.2l-2.3-.1c-.7-.2-1.4-.5-2.2-1a5.7 5.7 0 01-2-1.9c-.4-.8-.8-1.9-1-3.2-.4-1.4-.5-3-.5-4.8v-16h-1.5c-1.1 0-1.6-1-1.6-2.4v-1.7c0-1.4.5-2.3 1.6-2.3h1.5v-.1l.6-12.3c0-1.5.5-2.5 1.6-2.5h3.1c1.2 0 1.6 1 1.6 2.5v12.3h3.6c1.1 0 1.6 1 1.6 2.4V54c0 1.4-.5 2.3-1.6 2.3h-3.6v16.2zm9.4 15.7c0-2 .3-3.2 1.5-3.2.2 0 .4 0 1.4.4l1.1.3.2-.1.4-.7c.3-.6.4-1.6.4-3l-.6-3.3L79 52.7v-.9c0-1.2.3-2 1.2-2H84c.5 0 1 .3 1.3.6.3.4.5.9.6 1.6l3.4 18 2.6-17.8c.1-.8.3-1.3.6-1.7.3-.4.8-.6 1.3-.6h2.6c1 0 1.4.8 1.4 2l-.1.8L92 82.2c-.5 2.5-1 4.4-1.5 5.7a6.6 6.6 0 01-2 3c-.9.6-1.9.9-3.1.9h-.3c-2 0-3.3-.6-4.1-1.7-.3-.4-.4-1-.4-2zM31.3 38.8l8.2-2.1h.5c1 0 1.4.8 1.4 2.2v41.9c0 1.8-.4 2.9-1.6 2.9h-4.7c-1.2 0-1.6-1.1-1.6-3v-35l-2 .6c-1.2 0-1.6-.9-1.6-2.2v-3c0-1.2.4-2 1.4-2.3z" d="M55.3 36.3h.4c1.1 0 1.5.9 1.5 2.3v41.8c0 1.8-.4 3-1.6 3l-4.8-.1c-1.2 0-1.6-1-1.6-3V45.5l-2.1.5c-1 0-1.5-.8-1.5-2.2v-3c0-1.2.4-2 1.4-2.2l8.3-2.2zm16 36.1l.1 3 .6 1.3.6.6.8.1h2.2c1 0 1.7.8 1.7 2v1.9c0 1.2-.6 2-1.8 2h-3.2l-2.3-.1c-.7-.2-1.4-.5-2.2-1a5.7 5.7 0 01-2-1.9c-.4-.8-.8-1.9-1-3.2-.4-1.4-.5-3-.5-4.8v-16h-1.5c-1.1 0-1.6-1-1.6-2.4v-1.7c0-1.4.5-2.3 1.6-2.3h1.5v-.1l.6-12.3c0-1.5.5-2.5 1.6-2.5h3.1c1.2 0 1.6 1 1.6 2.5v12.3h3.6c1.1 0 1.6 1 1.6 2.4V54c0 1.4-.5 2.3-1.6 2.3h-3.6v16.2zm9.4 15.7c0-2 .3-3.2 1.5-3.2.2 0 .4 0 1.4.4l1.1.3.2-.1.4-.7c.3-.6.4-1.6.4-3l-.6-3.3L79 52.7v-.9c0-1.2.3-2 1.2-2H84c.5 0 1 .3 1.3.6.3.4.5.9.6 1.6l3.4 18 2.6-17.8c.1-.8.3-1.3.6-1.7.3-.4.8-.6 1.3-.6h2.6c1 0 1.4.8 1.4 2l-.1.8L92 82.2c-.5 2.5-1 4.4-1.5 5.7a6.6 6.6 0 01-2 3c-.9.6-1.9.9-3.1.9h-.3c-2 0-3.3-.6-4.1-1.7-.3-.4-.4-1-.4-2zM31.3 38.8l8.2-2.1h.5c1 0 1.4.8 1.4 2.2v41.9c0 1.8-.4 2.9-1.6 2.9h-4.7c-1.2 0-1.6-1.1-1.6-3v-35l-2 .6c-1.2 0-1.6-.9-1.6-2.2v-3c0-1.2.4-2 1.4-2.3z"
fill="#FFF" fill="#FFF"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,4 +1,11 @@
<svg class="absolute top-0 left-0 -m-4 h-10 w-10" viewBox="0 0 128 128"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
viewBox="0 0 128 128"
>
<path <path
fill="#64328B" fill="#64328B"
d="M64,0C28.7,0,0,28.7,0,64v0c0,35.3,28.7,64,64,64s64-28.7,64-64v0C128,28.7,99.3,0,64,0z M13.2,64L64,114.8 C35.9,114.8,13.2,92.1,13.2,64z M75.4,113.5l-60.9-61C19.7,30,39.9,13.2,64,13.2c16.6,0,31.3,7.9,40.5,20.2l-7.5,7.2 C89.7,30.2,77.7,23.5,64,23.5c-17.6,0-32.5,11.2-38.1,26.8C33.1,57,75.4,98.8,78.1,102c12.7-4.7,22.3-15.5,25.4-28.9H81.9v-9.4 l33,0.2C114.8,88.2,98,108.4,75.4,113.5z" d="M64,0C28.7,0,0,28.7,0,64v0c0,35.3,28.7,64,64,64s64-28.7,64-64v0C128,28.7,99.3,0,64,0z M13.2,64L64,114.8 C35.9,114.8,13.2,92.1,13.2,64z M75.4,113.5l-60.9-61C19.7,30,39.9,13.2,64,13.2c16.6,0,31.3,7.9,40.5,20.2l-7.5,7.2 C89.7,30.2,77.7,23.5,64,23.5c-17.6,0-32.5,11.2-38.1,26.8C33.1,57,75.4,98.8,78.1,102c12.7-4.7,22.3-15.5,25.4-28.9H81.9v-9.4 l33,0.2C114.8,88.2,98,108.4,75.4,113.5z"

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 593 B

View File

@@ -0,0 +1,15 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8 '}
viewBox="0 0 72 80"
>
<path
xmlns="http://www.w3.org/2000/svg"
fill="#430098"
d="M64.8,0 L7.2,0 C3.224,0 0,3.224 0,7.2 L0,72.8 C0,76.776 3.224,80 7.2,80 L64.8,80 C68.776,80 72,76.776 72,72.8 L72,7.2 C72,3.224 68.776,0 64.8,0 Z M18,68 L18,52 L27,60 L18,68 Z M46,68 L46,44.11 C45.961,42.243 45.062,40 41,40 C32.866,40 23.742,44.091 23.651,44.132 L18,46.692 L18,12 L26,12 L26,34.711 C29.994,33.411 35.577,32 41,32 C45.945,32 48.905,33.944 50.517,35.575 C53.958,39.055 54.005,43.488 54.0002258,44 L54.0002258,68 L46,68 Z M48,25 L40,25 C43.144,20.875 45.118,16.534 46,12 L54,12 C53.46,16.544 51.618,20.9 48,25 Z"
/>
</svg>

View File

@@ -1,5 +1,9 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-4 h-10 w-10" class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
viewBox="0 0 50 52" viewBox="0 0 50 52"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><title>Logomark</title><path ><title>Logomark</title><path

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -1,4 +1,13 @@
<svg class="absolute top-0 left-0 -m-4 h-10 w-10 fill-current text-blue-500" viewBox="0 0 128 128"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 fill-current text-blue-500'
: 'mx-auto w-8 h-8 fill-current text-blue-500'}
viewBox="0 0 128 128"
>
<path <path
d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.2 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.3V41.8h7.5v42.8z" d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.2 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.3V41.8h7.5v42.8z"
/> />

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 442 B

View File

@@ -1,5 +1,9 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-4 h-10 w-10 text-green-500" class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10 text-green-500' : 'mx-auto w-8 h-8 text-green-500'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
aria-hidden="true" aria-hidden="true"
focusable="false" focusable="false"

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,5 +1,9 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-4 h-10 w-10" class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400" viewBox="0 0 400 400"
> >

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,4 +1,13 @@
<svg viewBox="0 0 128 128" class="absolute top-0 left-0 -m-6 h-14 w-14 text-white"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
viewBox="0 0 128 128"
class={isAbsolute
? 'absolute top-0 left-0 -m-6 h-14 w-14 text-white'
: 'mx-auto w-8 h-8 text-white'}
>
<path <path
fill="#6181B6" fill="#6181B6"
d="M64 33.039c-33.74 0-61.094 13.862-61.094 30.961s27.354 30.961 61.094 30.961 61.094-13.862 61.094-30.961-27.354-30.961-61.094-30.961zm-15.897 36.993c-1.458 1.364-3.077 1.927-4.86 2.507-1.783.581-4.052.461-6.811.461h-6.253l-1.733 10h-7.301l6.515-34h14.04c4.224 0 7.305 1.215 9.242 3.432 1.937 2.217 2.519 5.364 1.747 9.337-.319 1.637-.856 3.159-1.614 4.515-.759 1.357-1.75 2.624-2.972 3.748zm21.311 2.968l2.881-14.42c.328-1.688.208-2.942-.361-3.555-.57-.614-1.782-1.025-3.635-1.025h-5.79l-3.731 19h-7.244l6.515-33h7.244l-1.732 9h6.453c4.061 0 6.861.815 8.402 2.231s2.003 3.356 1.387 6.528l-3.031 15.241h-7.358zm40.259-11.178c-.318 1.637-.856 3.133-1.613 4.488-.758 1.357-1.748 2.598-2.971 3.722-1.458 1.364-3.078 1.927-4.86 2.507-1.782.581-4.053.461-6.812.461h-6.253l-1.732 10h-7.301l6.514-34h14.041c4.224 0 7.305 1.215 9.241 3.432 1.935 2.217 2.518 5.418 1.746 9.39zM95.919 54h-5.001l-2.727 14h4.442c2.942 0 5.136-.29 6.576-1.4 1.442-1.108 2.413-2.828 2.918-5.421.484-2.491.264-4.434-.66-5.458-.925-1.024-2.774-1.721-5.548-1.721zM38.934 54h-5.002l-2.727 14h4.441c2.943 0 5.136-.29 6.577-1.4 1.441-1.108 2.413-2.828 2.917-5.421.484-2.491.264-4.434-.66-5.458s-2.772-1.721-5.546-1.721z" d="M64 33.039c-33.74 0-61.094 13.862-61.094 30.961s27.354 30.961 61.094 30.961 61.094-13.862 61.094-30.961-27.354-30.961-61.094-30.961zm-15.897 36.993c-1.458 1.364-3.077 1.927-4.86 2.507-1.783.581-4.052.461-6.811.461h-6.253l-1.733 10h-7.301l6.515-34h14.04c4.224 0 7.305 1.215 9.242 3.432 1.937 2.217 2.519 5.364 1.747 9.337-.319 1.637-.856 3.159-1.614 4.515-.759 1.357-1.75 2.624-2.972 3.748zm21.311 2.968l2.881-14.42c.328-1.688.208-2.942-.361-3.555-.57-.614-1.782-1.025-3.635-1.025h-5.79l-3.731 19h-7.244l6.515-33h7.244l-1.732 9h6.453c4.061 0 6.861.815 8.402 2.231s2.003 3.356 1.387 6.528l-3.031 15.241h-7.358zm40.259-11.178c-.318 1.637-.856 3.133-1.613 4.488-.758 1.357-1.748 2.598-2.971 3.722-1.458 1.364-3.078 1.927-4.86 2.507-1.782.581-4.053.461-6.812.461h-6.253l-1.732 10h-7.301l6.514-34h14.041c4.224 0 7.305 1.215 9.241 3.432 1.935 2.217 2.518 5.418 1.746 9.39zM95.919 54h-5.001l-2.727 14h4.442c2.942 0 5.136-.29 6.576-1.4 1.442-1.108 2.413-2.828 2.918-5.421.484-2.491.264-4.434-.66-5.458-.925-1.024-2.774-1.721-5.548-1.721zM38.934 54h-5.002l-2.727 14h4.441c2.943 0 5.136-.29 6.577-1.4 1.441-1.108 2.413-2.828 2.917-5.421.484-2.491.264-4.434-.66-5.458s-2.772-1.721-5.546-1.721z"

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,4 +1,11 @@
<svg class="absolute top-0 left-0 -m-6 h-14 w-14" viewBox="0 0 128 128"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-6 h-14 w-14' : 'mx-auto w-8 h-8'}
viewBox="0 0 128 128"
>
<linearGradient <linearGradient
id="a" id="a"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,4 +1,13 @@
<svg class="absolute top-0 left-0 -m-4 h-10 w-10 text-blue-500" viewBox="0 0 128 128"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 text-blue-500'
: 'mx-auto w-8 h-8 text-blue-500'}
viewBox="0 0 128 128"
>
<g fill="#61DAFB" <g fill="#61DAFB"
><circle cx="64" cy="64" r="11.4" /><path ><circle cx="64" cy="64" r="11.4" /><path
d="M107.3 45.2c-2.2-.8-4.5-1.6-6.9-2.3.6-2.4 1.1-4.8 1.5-7.1 2.1-13.2-.2-22.5-6.6-26.1-1.9-1.1-4-1.6-6.4-1.6-7 0-15.9 5.2-24.9 13.9-9-8.7-17.9-13.9-24.9-13.9-2.4 0-4.5.5-6.4 1.6-6.4 3.7-8.7 13-6.6 26.1.4 2.3.9 4.7 1.5 7.1-2.4.7-4.7 1.4-6.9 2.3-12.5 4.8-19.3 11.4-19.3 18.8s6.9 14 19.3 18.8c2.2.8 4.5 1.6 6.9 2.3-.6 2.4-1.1 4.8-1.5 7.1-2.1 13.2.2 22.5 6.6 26.1 1.9 1.1 4 1.6 6.4 1.6 7.1 0 16-5.2 24.9-13.9 9 8.7 17.9 13.9 24.9 13.9 2.4 0 4.5-.5 6.4-1.6 6.4-3.7 8.7-13 6.6-26.1-.4-2.3-.9-4.7-1.5-7.1 2.4-.7 4.7-1.4 6.9-2.3 12.5-4.8 19.3-11.4 19.3-18.8s-6.8-14-19.3-18.8zm-14.8-30.5c4.1 2.4 5.5 9.8 3.8 20.3-.3 2.1-.8 4.3-1.4 6.6-5.2-1.2-10.7-2-16.5-2.5-3.4-4.8-6.9-9.1-10.4-13 7.4-7.3 14.9-12.3 21-12.3 1.3 0 2.5.3 3.5.9zm-11.2 59.3c-1.8 3.2-3.9 6.4-6.1 9.6-3.7.3-7.4.4-11.2.4-3.9 0-7.6-.1-11.2-.4-2.2-3.2-4.2-6.4-6-9.6-1.9-3.3-3.7-6.7-5.3-10 1.6-3.3 3.4-6.7 5.3-10 1.8-3.2 3.9-6.4 6.1-9.6 3.7-.3 7.4-.4 11.2-.4 3.9 0 7.6.1 11.2.4 2.2 3.2 4.2 6.4 6 9.6 1.9 3.3 3.7 6.7 5.3 10-1.7 3.3-3.4 6.6-5.3 10zm8.3-3.3c1.5 3.5 2.7 6.9 3.8 10.3-3.4.8-7 1.4-10.8 1.9 1.2-1.9 2.5-3.9 3.6-6 1.2-2.1 2.3-4.2 3.4-6.2zm-25.6 27.1c-2.4-2.6-4.7-5.4-6.9-8.3 2.3.1 4.6.2 6.9.2 2.3 0 4.6-.1 6.9-.2-2.2 2.9-4.5 5.7-6.9 8.3zm-18.6-15c-3.8-.5-7.4-1.1-10.8-1.9 1.1-3.3 2.3-6.8 3.8-10.3 1.1 2 2.2 4.1 3.4 6.1 1.2 2.2 2.4 4.1 3.6 6.1zm-7-25.5c-1.5-3.5-2.7-6.9-3.8-10.3 3.4-.8 7-1.4 10.8-1.9-1.2 1.9-2.5 3.9-3.6 6-1.2 2.1-2.3 4.2-3.4 6.2zm25.6-27.1c2.4 2.6 4.7 5.4 6.9 8.3-2.3-.1-4.6-.2-6.9-.2-2.3 0-4.6.1-6.9.2 2.2-2.9 4.5-5.7 6.9-8.3zm22.2 21l-3.6-6c3.8.5 7.4 1.1 10.8 1.9-1.1 3.3-2.3 6.8-3.8 10.3-1.1-2.1-2.2-4.2-3.4-6.2zm-54.5-16.2c-1.7-10.5-.3-17.9 3.8-20.3 1-.6 2.2-.9 3.5-.9 6 0 13.5 4.9 21 12.3-3.5 3.8-7 8.2-10.4 13-5.8.5-11.3 1.4-16.5 2.5-.6-2.3-1-4.5-1.4-6.6zm-24.7 29c0-4.7 5.7-9.7 15.7-13.4 2-.8 4.2-1.5 6.4-2.1 1.6 5 3.6 10.3 6 15.6-2.4 5.3-4.5 10.5-6 15.5-13.8-4-22.1-10-22.1-15.6zm28.5 49.3c-4.1-2.4-5.5-9.8-3.8-20.3.3-2.1.8-4.3 1.4-6.6 5.2 1.2 10.7 2 16.5 2.5 3.4 4.8 6.9 9.1 10.4 13-7.4 7.3-14.9 12.3-21 12.3-1.3 0-2.5-.3-3.5-.9zm60.8-20.3c1.7 10.5.3 17.9-3.8 20.3-1 .6-2.2.9-3.5.9-6 0-13.5-4.9-21-12.3 3.5-3.8 7-8.2 10.4-13 5.8-.5 11.3-1.4 16.5-2.5.6 2.3 1 4.5 1.4 6.6zm9-15.6c-2 .8-4.2 1.5-6.4 2.1-1.6-5-3.6-10.3-6-15.6 2.4-5.3 4.5-10.5 6-15.5 13.8 4 22.1 10 22.1 15.6 0 4.7-5.8 9.7-15.7 13.4z" d="M107.3 45.2c-2.2-.8-4.5-1.6-6.9-2.3.6-2.4 1.1-4.8 1.5-7.1 2.1-13.2-.2-22.5-6.6-26.1-1.9-1.1-4-1.6-6.4-1.6-7 0-15.9 5.2-24.9 13.9-9-8.7-17.9-13.9-24.9-13.9-2.4 0-4.5.5-6.4 1.6-6.4 3.7-8.7 13-6.6 26.1.4 2.3.9 4.7 1.5 7.1-2.4.7-4.7 1.4-6.9 2.3-12.5 4.8-19.3 11.4-19.3 18.8s6.9 14 19.3 18.8c2.2.8 4.5 1.6 6.9 2.3-.6 2.4-1.1 4.8-1.5 7.1-2.1 13.2.2 22.5 6.6 26.1 1.9 1.1 4 1.6 6.4 1.6 7.1 0 16-5.2 24.9-13.9 9 8.7 17.9 13.9 24.9 13.9 2.4 0 4.5-.5 6.4-1.6 6.4-3.7 8.7-13 6.6-26.1-.4-2.3-.9-4.7-1.5-7.1 2.4-.7 4.7-1.4 6.9-2.3 12.5-4.8 19.3-11.4 19.3-18.8s-6.8-14-19.3-18.8zm-14.8-30.5c4.1 2.4 5.5 9.8 3.8 20.3-.3 2.1-.8 4.3-1.4 6.6-5.2-1.2-10.7-2-16.5-2.5-3.4-4.8-6.9-9.1-10.4-13 7.4-7.3 14.9-12.3 21-12.3 1.3 0 2.5.3 3.5.9zm-11.2 59.3c-1.8 3.2-3.9 6.4-6.1 9.6-3.7.3-7.4.4-11.2.4-3.9 0-7.6-.1-11.2-.4-2.2-3.2-4.2-6.4-6-9.6-1.9-3.3-3.7-6.7-5.3-10 1.6-3.3 3.4-6.7 5.3-10 1.8-3.2 3.9-6.4 6.1-9.6 3.7-.3 7.4-.4 11.2-.4 3.9 0 7.6.1 11.2.4 2.2 3.2 4.2 6.4 6 9.6 1.9 3.3 3.7 6.7 5.3 10-1.7 3.3-3.4 6.6-5.3 10zm8.3-3.3c1.5 3.5 2.7 6.9 3.8 10.3-3.4.8-7 1.4-10.8 1.9 1.2-1.9 2.5-3.9 3.6-6 1.2-2.1 2.3-4.2 3.4-6.2zm-25.6 27.1c-2.4-2.6-4.7-5.4-6.9-8.3 2.3.1 4.6.2 6.9.2 2.3 0 4.6-.1 6.9-.2-2.2 2.9-4.5 5.7-6.9 8.3zm-18.6-15c-3.8-.5-7.4-1.1-10.8-1.9 1.1-3.3 2.3-6.8 3.8-10.3 1.1 2 2.2 4.1 3.4 6.1 1.2 2.2 2.4 4.1 3.6 6.1zm-7-25.5c-1.5-3.5-2.7-6.9-3.8-10.3 3.4-.8 7-1.4 10.8-1.9-1.2 1.9-2.5 3.9-3.6 6-1.2 2.1-2.3 4.2-3.4 6.2zm25.6-27.1c2.4 2.6 4.7 5.4 6.9 8.3-2.3-.1-4.6-.2-6.9-.2-2.3 0-4.6.1-6.9.2 2.2-2.9 4.5-5.7 6.9-8.3zm22.2 21l-3.6-6c3.8.5 7.4 1.1 10.8 1.9-1.1 3.3-2.3 6.8-3.8 10.3-1.1-2.1-2.2-4.2-3.4-6.2zm-54.5-16.2c-1.7-10.5-.3-17.9 3.8-20.3 1-.6 2.2-.9 3.5-.9 6 0 13.5 4.9 21 12.3-3.5 3.8-7 8.2-10.4 13-5.8.5-11.3 1.4-16.5 2.5-.6-2.3-1-4.5-1.4-6.6zm-24.7 29c0-4.7 5.7-9.7 15.7-13.4 2-.8 4.2-1.5 6.4-2.1 1.6 5 3.6 10.3 6 15.6-2.4 5.3-4.5 10.5-6 15.5-13.8-4-22.1-10-22.1-15.6zm28.5 49.3c-4.1-2.4-5.5-9.8-3.8-20.3.3-2.1.8-4.3 1.4-6.6 5.2 1.2 10.7 2 16.5 2.5 3.4 4.8 6.9 9.1 10.4 13-7.4 7.3-14.9 12.3-21 12.3-1.3 0-2.5-.3-3.5-.9zm60.8-20.3c1.7 10.5.3 17.9-3.8 20.3-1 .6-2.2.9-3.5.9-6 0-13.5-4.9-21-12.3 3.5-3.8 7-8.2 10.4-13 5.8-.5 11.3-1.4 16.5-2.5.6 2.3 1 4.5 1.4 6.6zm9-15.6c-2 .8-4.2 1.5-6.4 2.1-1.6-5-3.6-10.3-6-15.6 2.4-5.3 4.5-10.5 6-15.5 13.8 4 22.1 10 22.1 15.6 0 4.7-5.8 9.7-15.7 13.4z"

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -1,5 +1,11 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-4 h-10 w-10 text-white" class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 text-white'
: 'mx-auto w-8 h-8 text-white'}
viewBox="0 0 32 32" viewBox="0 0 32 32"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,5 +1,9 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg <svg
class="absolute top-0 left-0 -m-4 h-10 w-10" class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,4 +1,13 @@
<svg class="absolute top-0 left-0 -m-4 h-10 w-10 text-green-500" viewBox="0 0 128 128"> <script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 text-green-500'
: 'mx-auto w-8 h-8 text-green-500'}
viewBox="0 0 128 128"
>
<path <path
d="m-2.3125e-8 8.9337 49.854 0.1586 14.167 24.47 14.432-24.47 49.547-0.1577-63.834 110.14zm126.98 0.6374-24.36 0.0207-38.476 66.052-38.453-66.052-24.749-0.0194 63.211 107.89zm-25.149-0.008-22.745 0.16758l-15.053 24.647-14.817-24.647-22.794-0.1679 37.731 64.476zM25.997 9.3929l23.002 0.0087M25.997 9.3929l23.002 0.0087" d="m-2.3125e-8 8.9337 49.854 0.1586 14.167 24.47 14.432-24.47 49.547-0.1577-63.834 110.14zm126.98 0.6374-24.36 0.0207-38.476 66.052-38.453-66.052-24.749-0.0194 63.211 107.89zm-25.149-0.008-22.745 0.16758l-15.053 24.647-14.817-24.647-22.794-0.1679 37.731 64.476zM25.997 9.3929l23.002 0.0087M25.997 9.3929l23.002 0.0087"
fill="none" fill="none"

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 794 B

View File

@@ -0,0 +1,19 @@
//@ts-nocheck
export { default as Rust } from './Rust.svelte';
export { default as Nodejs } from './Nodejs.svelte';
export { default as React } from './React.svelte';
export { default as Svelte } from './Svelte.svelte';
export { default as Vuejs } from './Vuejs.svelte';
export { default as Php } from './PHP.svelte';
export { default as Python } from './Python.svelte';
export { default as Static } from './Static.svelte';
export { default as Nestjs } from './Nestjs.svelte';
export { default as Nuxtjs } from './Nuxtjs.svelte';
export { default as Nextjs } from './Nextjs.svelte';
export { default as Gatsby } from './Gatsby.svelte';
export { default as Docker } from './Docker.svelte';
export { default as Astro } from './Astro.svelte';
export { default as Eleventy } from './Eleventy.svelte';
export { default as Deno } from './Deno.svelte';
export { default as Laravel } from './Laravel.svelte';
export { default as Heroku } from './Heroku.svelte';

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import * as Icons from '$lib/components/svg/databases';
export let type: any;
export let isAbsolute = false;
</script>
{#if type === 'mysql'}
<Icons.MySQL {isAbsolute} />
{:else if type === 'postgresql'}
<Icons.PostgreSQL {isAbsolute} />
{:else if type === 'mongodb'}
<Icons.MongoDB {isAbsolute} />
{:else if type === 'mariadb'}
<Icons.MariaDB {isAbsolute} />
{:else if type === 'redis'}
<Icons.Redis {isAbsolute} />
{:else if type === 'couchdb'}
<Icons.CouchDB {isAbsolute} />
{/if}

View File

@@ -0,0 +1,10 @@
//@ts-nocheck
export { default as Clickhouse } from './Clickhouse.svelte';
export { default as CouchDB } from './CouchDB.svelte';
export { default as MariaDB } from './MariaDB.svelte';
export { default as MongoDB } from './MongoDB.svelte';
export { default as MySQL } from './MySQL.svelte';
export { default as PostgreSQL } from './PostgreSQL.svelte';
export { default as Redis } from './Redis.svelte';

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
<script lang="ts">
export let type: string;
export let isAbsolute = true;
import * as Icons from '$lib/components/svg/services';
</script>
{#if type === 'plausibleanalytics'}
<Icons.PlausibleAnalytics {isAbsolute} />
{:else if type === 'nocodb'}
<Icons.NocoDb {isAbsolute} />
{:else if type === 'minio'}
<Icons.MinIo {isAbsolute} />
{:else if type === 'vscodeserver'}
<Icons.VsCodeServer {isAbsolute} />
{:else if type === 'wordpress'}
<Icons.Wordpress {isAbsolute} />
{:else if type === 'vaultwarden'}
<Icons.VaultWarden {isAbsolute} />
{:else if type === 'languagetool'}
<Icons.LanguageTool {isAbsolute} />
{:else if type === 'n8n'}
<Icons.N8n {isAbsolute} />
{:else if type === 'uptimekuma'}
<Icons.UptimeKuma {isAbsolute} />
{:else if type === 'ghost'}
<Icons.Ghost {isAbsolute} />
{:else if type === 'meilisearch'}
<Icons.MeiliSearch {isAbsolute} />
{:else if type === 'umami'}
<Icons.Umami {isAbsolute} />
{:else if type === 'hasura'}
<Icons.Hasura {isAbsolute} />
{:else if type === 'fider'}
<Icons.Fider {isAbsolute} />
{:else if type === 'appwrite'}
<Icons.Appwrite {isAbsolute} />
{:else if type === 'moodle'}
<Icons.Moodle {isAbsolute} />
{/if}

View File

@@ -1,17 +1,18 @@
//@ts-nocheck //@ts-nocheck
export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte'; export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte';
export { default as NocoDb } from './NocoDB.svelte'; export { default as NocoDb } from './NocoDB.svelte';
export { default as MinIo } from './MinIO.svelte'; export { default as MinIo } from './MinIO.svelte';
export { default as VsCodeServer } from './VSCodeServer.svelte'; export { default as VsCodeServer } from './VSCodeServer.svelte';
export { default as Wordpress } from './Wordpress.svelte'; export { default as Wordpress } from './Wordpress.svelte';
export { default as VaultWarden } from './VaultWarden.svelte'; export { default as VaultWarden } from './VaultWarden.svelte';
export { default as LanguageTool } from './LanguageTool.svelte'; export { default as LanguageTool } from './LanguageTool.svelte';
export { default as N8n } from './N8n.svelte'; export { default as N8n } from './N8n.svelte';
export { default as UptimeKuma } from './UptimeKuma.svelte'; export { default as UptimeKuma } from './UptimeKuma.svelte';
export { default as Ghost } from './Ghost.svelte'; export { default as Ghost } from './Ghost.svelte';
export { default as MeiliSearch } from './MeiliSearch.svelte'; export { default as MeiliSearch } from './MeiliSearch.svelte';
export { default as Umami } from './Umami.svelte'; export { default as Umami } from './Umami.svelte';
export { default as Hasura } from './Hasura.svelte'; export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte'; export { default as Fider } from './Fider.svelte';
export { default as Moodle } from './Moodle.svelte'; export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte';

View File

@@ -4,7 +4,7 @@
"wait_new_version_startup": "Waiting for the new version to start...", "wait_new_version_startup": "Waiting for the new version to start...",
"new_version": "New version reachable. Reloading...", "new_version": "New version reachable. Reloading...",
"switch_to_a_different_team": "Switch to a different team...", "switch_to_a_different_team": "Switch to a different team...",
"update_available": "Update available" "update_available": "Update Available"
}, },
"error": { "error": {
"you_can_find_your_way_back": "You can find your way back", "you_can_find_your_way_back": "You can find your way back",
@@ -144,8 +144,8 @@
}, },
"preview": { "preview": {
"need_during_buildtime": "Need during buildtime?", "need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-applications font-bold'>staging</span> environments.", "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-applications font-bold'>staging</span> environments.", "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.",
"redeploy": "Redeploy", "redeploy": "Redeploy",
"no_previews_available": "No previews available" "no_previews_available": "No previews available"
}, },
@@ -163,9 +163,9 @@
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
"stop_application": "Stop application", "stop_application": "Stop Application",
"permission_denied_stop_application": "You do not have permission to stop the application.", "permission_denied_stop_application": "You do not have permission to stop the application.",
"rebuild_application": "Rebuild application", "rebuild_application": "Rebuild Application",
"permission_denied_rebuild_application": "You do not have permission to rebuild application.", "permission_denied_rebuild_application": "You do not have permission to rebuild application.",
"build_and_start_application": "Deploy", "build_and_start_application": "Deploy",
"permission_denied_build_and_start_application": "You do not have permission to deploy application.", "permission_denied_build_and_start_application": "You do not have permission to deploy application.",
@@ -194,14 +194,14 @@
"application": "Application", "application": "Application",
"url_fqdn": "URL (FQDN)", "url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-applications font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-applications font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-applications'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",
"start_command": "Start Command", "start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-applications font-bold'>monorepos</span>.", "directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-applications font-bold'>dist</span>,<span class='text-applications font-bold'>_site</span> or <span class='text-applications font-bold'>public</span>.", "publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.",
"features": "Features", "features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment", "enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",

View File

@@ -65,7 +65,7 @@
"features": "Caractéristiques", "features": "Caractéristiques",
"git_repository": "Dépôt Git", "git_repository": "Dépôt Git",
"git_source": "Source Git", "git_source": "Source Git",
"https_explainer": "Si vous spécifiez <span class='text-applications font-bold'>https</span>, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-applications font-bold'>www</span>, l'application sera redirigée (302) à partir de non-www et vice versa \n.<br><br>Pour modifier le domaine, vous devez d'abord arrêter l'application.<br><br><span class='text-white font-bold'>Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.</span>", "https_explainer": "Si vous spécifiez <span class='text-green-500 font-bold'>https</span>, l'application sera accessible uniquement via https. \nUn certificat SSL sera généré pour vous.<br>Si vous spécifiez <span class='text-green-500 font-bold'>www</span>, l'application sera redirigée (302) à partir de non-www et vice versa \n.<br><br>Pour modifier le domaine, vous devez d'abord arrêter l'application.<br><br><span class='text-white font-bold'>Vous devez configurer, en avance, votre DNS pour pointer vers l'IP du serveur.</span>",
"install_command": "Commande d'installation", "install_command": "Commande d'installation",
"logs": "Journaux des applications", "logs": "Journaux des applications",
"no_applications_found": "Aucune application trouvée", "no_applications_found": "Aucune application trouvée",
@@ -78,11 +78,11 @@
"need_during_buildtime": "Besoin pendant la build ?", "need_during_buildtime": "Besoin pendant la build ?",
"no_previews_available": "Aucun aperçu disponible", "no_previews_available": "Aucun aperçu disponible",
"redeploy": "Redéployer", "redeploy": "Redéployer",
"setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n<br>Utile pour créer des environnements <span class='text-applications font-bold'>de mise en scène</span>.", "setup_secret_app_first": "Vous pouvez ajouter des secrets aux déploiements PR/MR. \nVeuillez d'abord ajouter des secrets à l'application. \n<br>Utile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>.",
"values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements <span class='text-applications font-bold'>de mise en scène</span>." "values_overwriting_app_secrets": "Ces valeurs remplacent les secrets d'application dans les déploiements PR/MR. \nUtile pour créer des environnements <span class='text-green-500 font-bold'>de mise en scène</span>."
}, },
"previews": "Aperçus", "previews": "Aperçus",
"publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n<br> Par exemple : <span class='text-applications font-bold'>dist</span>,<span class='text-applications font-bold'>_site</span> ou <span \nclass='text-applications font-bold'>public</span>.", "publish_directory_explainer": "Répertoire contenant tous les actifs à déployer. \n<br> Par exemple : <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> ou <span \nclass='text-green-500 font-bold'>public</span>.",
"rebuild_application": "Re-build l'application", "rebuild_application": "Re-build l'application",
"secret": "secrets", "secret": "secrets",
"secrets": { "secrets": {
@@ -91,7 +91,7 @@
"use_isbuildsecret": "Utiliser isBuildSecret" "use_isbuildsecret": "Utiliser isBuildSecret"
}, },
"settings_saved": "Paramètres sauvegardés.", "settings_saved": "Paramètres sauvegardés.",
"ssl_explainer": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-applications'>les deux entrées DNS</span> définies à l'avance.<br><br>Utile si vous prévoyez d'avoir des visiteurs sur les deux.", "ssl_explainer": "Il générera des certificats pour www et non-www. \n<br>Vous devez avoir <span class='font-bold text-green-500'>les deux entrées DNS</span> définies à l'avance.<br><br>Utile si vous prévoyez d'avoir des visiteurs sur les deux.",
"ssl_www_and_non_www": "Générer SSL pour www et non-www ?", "ssl_www_and_non_www": "Générer SSL pour www et non-www ?",
"start_command": "Démarrer la commande", "start_command": "Démarrer la commande",
"stop_application": "Arrêter l'application", "stop_application": "Arrêter l'application",

View File

@@ -1,3 +1,5 @@
import { dev } from '$app/env';
import cuid from 'cuid';
import { writable, readable, type Writable } from 'svelte/store'; import { writable, readable, type Writable } from 'svelte/store';
interface AppSession { interface AppSession {
@@ -21,7 +23,7 @@ interface AddToast {
type?: "info" | "success" | "error", type?: "info" | "success" | "error",
message: string, message: string,
timeout?: number | undefined timeout?: number | undefined
} }
export const loginEmail: Writable<string | undefined> = writable() export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
ipv4: null, ipv4: null,
@@ -69,42 +71,61 @@ export const features = readable({
}); });
export const location: Writable<null | string> = writable(null) export const location: Writable<null | string> = writable(null)
export const setLocation = (resource: any) => { export const setLocation = (resource: any, settings?: any) => {
if (resource.settings.isBot && resource.exposePort) {
disabledButton.set(false);
return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`)
}
if (GITPOD_WORKSPACE_URL && resource.exposePort) { if (GITPOD_WORKSPACE_URL && resource.exposePort) {
const { href } = new URL(GITPOD_WORKSPACE_URL); const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href const newURL = href
.replace('https://', `https://${resource.exposePort}-`) .replace('https://', `https://${resource.exposePort}-`)
.replace(/\/$/, ''); .replace(/\/$/, '');
return location.set(newURL) return location.set(newURL)
} else if (CODESANDBOX_HOST){ } else if (CODESANDBOX_HOST) {
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,resource.exposePort)}` const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}`
return location.set(newURL) return location.set(newURL)
}
if (resource.fqdn) {
return location.set(resource.fqdn)
} else {
location.set(null);
disabledButton.set(true);
} }
return location.set(resource.fqdn)
} }
export const toasts: any = writable([]) export const toasts: any = writable([])
export const dismissToast = (id: number) => { export const dismissToast = (id: string) => {
toasts.update((all: any) => all.filter((t: any) => t.id !== id)) toasts.update((all: any) => all.filter((t: any) => t.id !== id))
}
export const pauseToast = (id: string) => {
toasts.update((all: any) => {
const index = all.findIndex((t: any) => t.id === id);
if (index > -1) clearTimeout(all[index].timeoutInterval);
return all;
})
}
export const resumeToast = (id: string) => {
toasts.update((all: any) => {
const index = all.findIndex((t: any) => t.id === id);
if (index > -1) {
all[index].timeoutInterval = setTimeout(() => {
dismissToast(id)
}, all[index].timeout)
}
return all;
})
} }
export const addToast = (toast: AddToast) => { export const addToast = (toast: AddToast) => {
// Create a unique ID so we can easily find/remove it const id = cuid();
// if it is dismissible/has a timeout. const defaults = {
const id = Math.floor(Math.random() * 10000) id,
type: 'info',
// Setup some sensible defaults for a toast. timeout: 2000,
const defaults = { }
id, let t: any = { ...defaults, ...toast }
type: 'info', if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout)
timeout: 2000, toasts.update((all: any) => [t, ...all])
}
// Push the toast to the top of the list of toasts
const t = { ...defaults, ...toast }
toasts.update((all: any) => [t, ...all])
// If toast is dismissible, dismiss it after "timeout" amount of time.
if (t.timeout) setTimeout(() => dismissToast(id), t.timeout)
} }

View File

@@ -170,6 +170,16 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: 80 port: 80
}; };
} }
if (pack === 'heroku') {
return {
...metaData,
installCommand: null,
buildCommand: null,
startCommand: null,
publishDirectory: null,
port: 5000
};
}
return { return {
name: 'node', name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
@@ -187,112 +197,137 @@ export const buildPacks = [
name: 'node', name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'static', name: 'static',
fancyName: 'Static', fancyName: 'Static',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'php', name: 'php',
fancyName: 'PHP', fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700' color: 'bg-indigo-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'laravel', name: 'laravel',
fancyName: 'Laravel', fancyName: 'Laravel',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700' color: 'bg-indigo-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'docker', name: 'docker',
fancyName: 'Docker', fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700' color: 'bg-sky-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'svelte', name: 'svelte',
fancyName: 'Svelte', fancyName: 'Svelte',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700' color: 'bg-orange-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'vuejs', name: 'vuejs',
fancyName: 'VueJS', fancyName: 'VueJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'nuxtjs', name: 'nuxtjs',
fancyName: 'NuxtJS', fancyName: 'NuxtJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'gatsby', name: 'gatsby',
fancyName: 'Gatsby', fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'astro', name: 'astro',
fancyName: 'Astro', fancyName: 'Astro',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'eleventy', name: 'eleventy',
fancyName: 'Eleventy', fancyName: 'Eleventy',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700' color: 'bg-red-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'react', name: 'react',
fancyName: 'React', fancyName: 'React',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'preact', name: 'preact',
fancyName: 'Preact', fancyName: 'Preact',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'nextjs', name: 'nextjs',
fancyName: 'NextJS', fancyName: 'NextJS',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700' color: 'bg-blue-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'nestjs', name: 'nestjs',
fancyName: 'NestJS', fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700' color: 'bg-red-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'rust', name: 'rust',
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'python', name: 'python',
fancyName: 'Python', fancyName: 'Python',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700',
isCoolifyBuildPack: true,
}, },
{ {
name: 'deno', name: 'deno',
fancyName: 'Deno', fancyName: 'Deno',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700' color: 'bg-green-700',
} isCoolifyBuildPack: true,
},
{
name: 'heroku',
fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700',
isHerokuBuildPack: true,
}
]; ];
export const scanningTemplates = { export const scanningTemplates = {
'@sveltejs/kit': { '@sveltejs/kit': {

View File

@@ -39,7 +39,7 @@
async function createSecret(isNew: any) { async function createSecret(isNew: any) {
try { try {
if (!name || !value) return if (!name || !value) return;
await saveSecret({ await saveSecret({
isNew, isNew,
name, name,
@@ -53,12 +53,16 @@
name = ''; name = '';
value = ''; value = '';
isBuildSecret = false; isBuildSecret = false;
addToast({
message: 'Secret added.',
type: 'success'
});
} }
dispatch('refresh');
addToast({ addToast({
message: 'Secret removed.', message: 'Secret updated.',
type: 'success' type: 'success'
}); });
dispatch('refresh');
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return errorNotification(error); return errorNotification(error);
@@ -79,7 +83,7 @@
applicationId: id applicationId: id
}); });
addToast({ addToast({
message: 'Secret removed.', message: 'Secret updated.',
type: 'success' type: 'success'
}); });
} }

View File

@@ -16,7 +16,7 @@
export const load: Load = async ({ fetch, url, params }) => { export const load: Load = async ({ fetch, url, params }) => {
try { try {
const response = await get(`/applications/${params.id}`); const response = await get(`/applications/${params.id}`);
let { application, appId, settings, isQueueActive } = response; let { application, appId, settings } = response;
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
@@ -36,7 +36,8 @@
return { return {
props: { props: {
application application,
settings
}, },
stuff: { stuff: {
application, application,
@@ -52,7 +53,7 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
@@ -65,10 +66,10 @@
let loading = false; let loading = false;
let statusInterval: any; let statusInterval: any;
let isQueueActive= false; let isQueueActive = false;
$disabledButton = $disabledButton =
!$appSession.isAdmin || !$appSession.isAdmin ||
!application.fqdn || (!application.fqdn && !application.settings.isBot) ||
!application.gitSource || !application.gitSource ||
!application.repository || !application.repository ||
!application.destinationDocker || !application.destinationDocker ||
@@ -80,9 +81,9 @@
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy`, { ...application });
addToast({ addToast({
message: $t('application.deployment_queued'), message: $t('application.deployment_queued'),
type: 'success' type: 'success'
}); });
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else { } else {
@@ -126,18 +127,23 @@
$status.application.loading = false; $status.application.loading = false;
$status.application.initialLoading = false; $status.application.initialLoading = false;
} }
onDestroy(() => { onDestroy(() => {
$status.application.initialLoading = true; $status.application.initialLoading = true;
$location = null; $location = null;
clearInterval(statusInterval); clearInterval(statusInterval);
}); });
onMount(async () => { onMount(async () => {
setLocation(application); setLocation(application, settings);
$status.application.isRunning = false; $status.application.isRunning = false;
$status.application.isExited = false; $status.application.isExited = false;
$status.application.loading = false; $status.application.loading = false;
if (application.gitSourceId && application.destinationDockerId && application.fqdn) { if (
application.gitSourceId &&
application.destinationDockerId &&
(application.fqdn ||
application.settings.isBot)
) {
await getStatus(); await getStatus();
statusInterval = setInterval(async () => { statusInterval = setInterval(async () => {
await getStatus(); await getStatus();
@@ -258,7 +264,7 @@
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm flex items-center space-x-2"
data-tip={$appSession.isAdmin data-tip={$appSession.isAdmin
? isQueueActive ? isQueueActive
? 'Rebuild application' ? 'Rebuild Application'
: 'Autoupdate inprogress. Cannot rebuild application.' : 'Autoupdate inprogress. Cannot rebuild application.'
: 'You do not have permission to rebuild application.'} : 'You do not have permission to rebuild application.'}
> >
@@ -403,37 +409,39 @@
</svg> </svg>
</button></a </button></a
> >
<a {#if !application.settings.isBot}
href={!$disabledButton ? `/applications/${id}/previews` : null} <a
sveltekit:prefetch href={!$disabledButton ? `/applications/${id}/previews` : null}
class="hover:text-orange-500 rounded" sveltekit:prefetch
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`} class="hover:text-orange-500 rounded"
class:bg-coolgray-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`}
<button
disabled={$disabledButton}
class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
data-tip="Previews"
> >
<svg <button
xmlns="http://www.w3.org/2000/svg" disabled={$disabledButton}
class="w-6 h-6" class="icons bg-transparent tooltip tooltip-primary tooltip-bottom text-sm"
viewBox="0 0 24 24" data-tip="Previews"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <svg
<circle cx="7" cy="18" r="2" /> xmlns="http://www.w3.org/2000/svg"
<circle cx="7" cy="6" r="2" /> class="w-6 h-6"
<circle cx="17" cy="12" r="2" /> viewBox="0 0 24 24"
<line x1="7" y1="8" x2="7" y2="16" /> stroke-width="1.5"
<path d="M7 8a4 4 0 0 0 4 4h4" /> stroke="currentColor"
</svg></button fill="none"
></a stroke-linecap="round"
> stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
{/if}
<div class="border border-coolgray-500 h-8" /> <div class="border border-coolgray-500 h-8" />
<a <a
href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null} href={!$disabledButton && $status.application.isRunning ? `/applications/${id}/logs` : null}

View File

@@ -30,7 +30,6 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import { browser } from '$app/env';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
@@ -263,11 +262,27 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-7xl mx-auto flex flex-wrap justify-center">
{#each buildPacks as buildPack}
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Coolify Buildpacks</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-7xl mx-auto ">
<div class="title pb-2">Heroku</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter(bp => bp.isHerokuBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div> </div>
{/each} {/each}
</div> </div>
</div>
{/if} {/if}

View File

@@ -31,6 +31,7 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -55,6 +56,11 @@
return errorNotification(error); return errorNotification(error);
} }
} }
onMount(async () => {
if (destinations.length === 1) {
await handleSubmit(destinations[0].id);
}
});
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -65,7 +71,9 @@
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')}
</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@@ -5,7 +5,8 @@
if (stuff?.application?.id) { if (stuff?.application?.id) {
return { return {
props: { props: {
application: stuff.application application: stuff.application,
settings: stuff.settings
} }
}; };
} }
@@ -26,6 +27,7 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
@@ -60,6 +62,7 @@
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false; let isNonWWWDomainOK = false;
@@ -99,7 +102,7 @@
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
await handleSubmit(); await handleSubmit();
} }
domainEl.focus(); // !isBot && domainEl.focus();
await getUsage(); await getUsage();
usageInterval = setInterval(async () => { usageInterval = setInterval(async () => {
await getUsage(); await getUsage();
@@ -129,19 +132,25 @@
if (name === 'autodeploy') { if (name === 'autodeploy') {
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isBot') {
isBot = !isBot;
application.settings.isBot = isBot;
setLocation(application, settings);
}
try { try {
await post(`/applications/${id}/settings`, { await post(`/applications/${id}/settings`, {
previews, previews,
debug, debug,
dualCerts, dualCerts,
isBot,
autodeploy, autodeploy,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId
}); });
return addToast({ return addToast({
message: $t('application.settings_saved'), message: $t('application.settings_saved'),
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
if (name === 'debug') { if (name === 'debug') {
debug = !debug; debug = !debug;
@@ -155,29 +164,33 @@
if (name === 'autodeploy') { if (name === 'autodeploy') {
autodeploy = !autodeploy; autodeploy = !autodeploy;
} }
if (name === 'isBot') {
isBot = !isBot;
}
return errorNotification(error); return errorNotification(error);
} }
} }
async function handleSubmit() { async function handleSubmit() {
if (loading || !application.fqdn) return; if (loading || (!application.fqdn && !isBot)) return;
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
if (application.deploymentType) if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase(); application.deploymentType = application.deploymentType.toLowerCase();
await post(`/applications/${id}/check`, { !isBot &&
fqdn: application.fqdn, (await post(`/applications/${id}/check`, {
forceSave, fqdn: application.fqdn,
dualCerts, forceSave,
exposePort: application.exposePort dualCerts,
}); exposePort: application.exposePort
}));
await post(`/applications/${id}`, { ...application }); await post(`/applications/${id}`, { ...application });
setLocation(application); setLocation(application, settings);
$disabledButton = false; $disabledButton = false;
forceSave = false; forceSave = false;
addToast({ addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success', type: 'success'
}); });
} catch (error) { } catch (error) {
console.log(error); console.log(error);
@@ -222,9 +235,9 @@
try { try {
await get(`/applications/${id}/check?domain=${domain}`); await get(`/applications/${id}/check?domain=${domain}`);
addToast({ addToast({
message: 'DNS configuration is valid.', message: 'DNS configuration is valid.',
type: 'success' type: 'success'
}); });
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true); isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true; return true;
} catch (error) { } catch (error) {
@@ -289,29 +302,21 @@
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Application Usage</div> <div class="text-2xl font-bold">Application Usage</div>
<div class="mx-auto"> <div class="text-center">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <div class="stat w-64">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat-title">Used Memory / Memory Limit</div>
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt> <div class="stat-value text-xl">{usage?.MemUsage}</div>
<dd class="mt-1 text-xl font-semibold text-white"> </div>
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt> <div class="stat-title">Used CPU</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.CPUPerc}</div>
{usage?.CPUPerc} </div>
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Network IO</dt> <div class="stat-title">Network IO</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.NetIO}</div>
{usage?.NetIO} </div>
</dd>
</div>
</dl>
</div> </div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
@@ -324,11 +329,10 @@
class="btn btn-sm" class="btn btn-sm"
type="submit" type="submit"
class:bg-applications={!loading} class:bg-applications={!loading}
class:loading={loading} class:loading
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
class:hover:bg-orange-400={forceSave} class:hover:bg-orange-400={forceSave}
disabled={loading} disabled={loading}>{$t('forms.save')}</button
>{$t('forms.save')}</button
> >
{/if} {/if}
</div> </div>
@@ -341,53 +345,56 @@
<label for="gitSource" class="text-base font-bold text-stone-100" <label for="gitSource" class="text-base font-bold text-stone-100"
>{$t('application.git_source')}</label >{$t('application.git_source')}</label
> >
<a {#if isDisabled}
href={!isDisabled <input disabled={isDisabled} value={application.gitSource.name} />
? `/applications/${id}/configuration/source?from=/applications/${id}` {:else}
: ''} <a
class="no-underline" href={`/applications/${id}/configuration/source?from=/applications/${id}`}
><input class="no-underline"
value={application.gitSource.name} ><input
id="gitSource" value={application.gitSource.name}
disabled id="gitSource"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
{/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="repository" class="text-base font-bold text-stone-100" <label for="repository" class="text-base font-bold text-stone-100"
>{$t('application.git_repository')}</label >{$t('application.git_repository')}</label
> >
<a {#if isDisabled}
href={!isDisabled <input disabled={isDisabled} value="{application.repository}/{application.branch}" />
? `/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack` {:else}
: ''} <a
class="no-underline" href={`/applications/${id}/configuration/repository?from=/applications/${id}&to=/applications/${id}/configuration/buildpack`}
><input class="no-underline"
value="{application.repository}/{application.branch}" ><input
id="repository" value="{application.repository}/{application.branch}"
disabled id="repository"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500"
/></a /></a
> >
{/if}
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="buildPack" class="text-base font-bold text-stone-100" <label for="buildPack" class="text-base font-bold text-stone-100"
>{$t('application.build_pack')}</label >{$t('application.build_pack')}</label
> >
<a {#if isDisabled}
href={!isDisabled <input class="capitalize" disabled={isDisabled} value={application.buildPack} />
? `/applications/${id}/configuration/buildpack?from=/applications/${id}` {:else}
: ''} <a
class="no-underline " href={`/applications/${id}/configuration/buildpack?from=/applications/${id}`}
> class="no-underline "
<input >
value={application.buildPack} <input
id="buildPack" value={application.buildPack}
disabled id="buildPack"
class="cursor-pointer hover:bg-coolgray-500" class="cursor-pointer hover:bg-coolgray-500 capitalize"
/></a /></a
> >
{/if}
</div> </div>
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="destination" class="text-base font-bold text-stone-100" <label for="destination" class="text-base font-bold text-stone-100"
@@ -474,77 +481,88 @@
<div class="title">{$t('application.application')}</div> <div class="title">{$t('application.application')}</div>
</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-cols-2"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <Setting
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100" isCenter={false}
>{$t('application.url_fqdn')}</label bind:setting={isBot}
> on:click={() => changeSettings('isBot')}
{#if browser && window.location.hostname === 'demo.coolify.io'} title="Is your application a bot?"
<Explainer description="You can deploy applications without domains. <br>They will listen on <span class='text-green-500 font-bold'>IP:EXPOSEDPORT</span> instead.<br></Setting><br>Useful to host <span class='text-green-500 font-bold'>Twitch bots.</span>"
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>" />
</div>
{#if !isBot}
<div class="grid grid-cols-2">
<div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100"
>{$t('application.url_fqdn')}</label
>
{#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer
text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/>
{/if}
<Explainer text={$t('application.https_explainer')} />
</div>
<div>
<input
readonly={isDisabled}
disabled={isDisabled}
bind:this={domainEl}
name="fqdn"
id="fqdn"
required
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/> />
{/if} {#if forceSave}
<Explainer text={$t('application.https_explainer')} /> <div class="flex-col space-y-2 pt-4 text-center">
</div> {#if isNonWWWDomainOK}
<div>
<input
readonly={isDisabled}
disabled={isDisabled}
bind:this={domainEl}
name="fqdn"
id="fqdn"
required
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button <button
class="bg-green-600 hover:bg-green-500" class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)} >DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
> >
{:else} {:else}
<button <button
class="bg-red-600 hover:bg-red-500" class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)} >DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
> >
{/if} {/if}
{/if} {#if dualCerts}
</div> {#if isWWWDomainOK}
{/if} <button
class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() =>
isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{/if}
</div>
{/if}
</div>
</div> </div>
</div> <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={$status.application.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={() => !$status.application.isRunning && changeSettings('dualCerts')}
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} />
/> </div>
</div> {/if}
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI / ASGI</label> <label for="pythonModule" class="text-base font-bold text-stone-100">WSGI / ASGI</label>
@@ -594,7 +612,7 @@
</div> </div>
{/if} {/if}
{/if} {/if}
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack) && !isBot}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label> <label for="port" class="text-base font-bold text-stone-100">{$t('forms.port')}</label>
<input <input
@@ -615,6 +633,7 @@
name="exposePort" name="exposePort"
id="exposePort" id="exposePort"
bind:value={application.exposePort} bind:value={application.exposePort}
required={isBot}
placeholder="12345" placeholder="12345"
/> />
<Explainer <Explainer
@@ -707,7 +726,7 @@
/> />
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'laravel'} {#if application.buildPack !== 'laravel' && application.buildPack !== 'heroku'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<div class="flex-col"> <div class="flex-col">
<label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100" <label for="baseDirectory" class="pt-2 text-base font-bold text-stone-100"
@@ -760,15 +779,17 @@
description={$t('application.enable_auto_deploy_webhooks')} description={$t('application.enable_auto_deploy_webhooks')}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if !application.settings.isBot}
<Setting <div class="grid grid-cols-2 items-center">
isCenter={false} <Setting
bind:setting={previews} isCenter={false}
on:click={() => changeSettings('previews')} bind:setting={previews}
title={$t('application.enable_mr_pr_previews')} on:click={() => changeSettings('previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')} title={$t('application.enable_mr_pr_previews')}
/> description={$t('application.enable_preview_deploy_mr_pr_requests')}
</div> />
</div>
{/if}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
isCenter={false} isCenter={false}

View File

@@ -106,10 +106,10 @@
{#if currentStatus === 'queued'} {#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div> <div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else} {:else}
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-2 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500" class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom hover:text-green-500 hover:bg-coolgray-500"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingBuild} class:text-green-500={followingBuild}
> >
@@ -134,7 +134,7 @@
<button <button
on:click={cancelBuild} on:click={cancelBuild}
class:animation-spin={cancelInprogress} class:animation-spin={cancelInprogress}
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500 tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500 tooltip tooltip-primary tooltip-bottom"
data-tip="Cancel build" data-tip="Cancel build"
> >
{#if cancelInprogress} {#if cancelInprogress}

View File

@@ -147,7 +147,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >

View File

@@ -26,24 +26,8 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { getDomain } from '$lib/common'; import { getDomain } from '$lib/common';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import Deno from '$lib/components/svg/applications/Deno.svelte';
import Laravel from '$lib/components/svg/applications/Laravel.svelte';
ownApplications = applications.filter((application) => { ownApplications = applications.filter((application) => {
if (application.teams[0].id === $appSession.teamId) { if (application.teams[0].id === $appSession.teamId) {
return application; return application;
@@ -63,10 +47,7 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl">{$t('index.applications')}</div> <div class="mr-4 text-2xl">{$t('index.applications')}</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
on:click={newApplication}
class="btn btn-square btn-sm bg-applications"
>
<svg <svg
class="h-6 w-6" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -96,41 +77,7 @@
<a href="/applications/{application.id}" class="p-2 no-underline"> <a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600"> <div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack} {#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'} <ApplicationsIcons {application} />
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{:else if application.buildPack.toLowerCase() === 'laravel'}
<Laravel />
{/if}
{/if} {/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div> <div class="truncate text-center text-xl font-bold">{application.name}</div>
@@ -140,6 +87,9 @@
{#if application.fqdn} {#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div> <div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if} {/if}
{#if application.settings.isBot}
<div class="truncate text-center">BOT</div>
{/if}
{#if application.destinationDocker?.name} {#if application.destinationDocker?.name}
<div class="truncate text-center">{application.destinationDocker.name}</div> <div class="truncate text-center">{application.destinationDocker.name}</div>
{/if} {/if}
@@ -151,7 +101,7 @@
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Destination Missing Destination Missing
</div> </div>
{:else if !application.fqdn} {:else if !application.fqdn && !application.settings.isBot}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white"> <div class="truncate text-center font-bold text-red-500 group-hover:text-white">
URL Missing URL Missing
</div> </div>
@@ -167,41 +117,7 @@
<a href="/applications/{application.id}" class="p-2 no-underline"> <a href="/applications/{application.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600"> <div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack} {#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'} <ApplicationsIcons {application} />
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{:else if application.buildPack.toLowerCase() === 'deno'}
<Deno />
{:else if application.buildPack.toLowerCase() === 'laravel'}
<Laravel />
{/if}
{/if} {/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div> <div class="truncate text-center text-xl font-bold">{application.name}</div>

View File

@@ -58,7 +58,9 @@
} }
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (publicLoading || !$status.database.isRunning) return; if (name !== 'appendOnly') {
if (publicLoading || !$status.database.isRunning) return;
}
publicLoading = true; publicLoading = true;
let data = { let data = {
isPublic, isPublic,
@@ -92,9 +94,9 @@
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning }); await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
generateDbDetails(); generateDbDetails();
addToast({ addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'
}); });
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -111,7 +113,7 @@
<button <button
type="submit" type="submit"
class="btn btn-sm" class="btn btn-sm"
class:loading={loading} class:loading
class:bg-databases={!loading} class:bg-databases={!loading}
disabled={loading}>{$t('forms.save')}</button disabled={loading}>{$t('forms.save')}</button
> >
@@ -245,6 +247,7 @@
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={publicLoading}
bind:setting={appendOnly} bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')} on:click={() => changeSettings('appendOnly')}
title={$t('database.change_append_only_mode')} title={$t('database.change_append_only_mode')}

View File

@@ -30,6 +30,7 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -45,6 +46,11 @@
return errorNotification(error); return errorNotification(error);
} }
} }
onMount(async () => {
if (destinations.length === 1) {
await handleSubmit(destinations[0].id);
}
});
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -55,7 +61,9 @@
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')}
</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@@ -31,18 +31,11 @@
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
async function handleSubmit(type: any) { async function handleSubmit(type: any) {
try { try {
await post(`/databases/${id}/configuration/type`, { type }); await post(`/databases/${id}/configuration/type`, { type });
@@ -62,21 +55,8 @@
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}> <form on:submit|preventDefault={() => handleSubmit(type.name)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700"> <button type="submit" class="box-selection relative text-xl font-bold hover:bg-purple-700">
{#if type.name === 'clickhouse'} <DatabaseIcons type={type.name} isAbsolute={true} />
<Clickhouse isAbsolute /> {type.fancyName}
{:else if type.name === 'couchdb'}
<CouchDB isAbsolute />
{:else if type.name === 'mongodb'}
<MongoDB isAbsolute />
{:else if type.name === 'mariadb'}
<MariaDB isAbsolute />
{:else if type.name === 'mysql'}
<MySQL isAbsolute />
{:else if type.name === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if type.name === 'redis'}
<Redis isAbsolute />
{/if}{type.fancyName}
</button> </button>
</form> </form>
</div> </div>

View File

@@ -48,7 +48,7 @@
}); });
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="-mb-5 flex-col"> <div class="-mb-5 flex-col">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration Configuration
@@ -60,29 +60,21 @@
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Database Usage</div> <div class="text-2xl font-bold">Database Usage</div>
<div class="mx-auto"> <div class="text-center">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <div class="stat w-64">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat-title">Used Memory / Memory Limit</div>
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt> <div class="stat-value text-xl">{usage?.MemUsage}</div>
<dd class="mt-1 text-xl font-semibold text-white"> </div>
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt> <div class="stat-title">Used CPU</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.CPUPerc}</div>
{usage?.CPUPerc} </div>
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Network IO</dt> <div class="stat-title">Network IO</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.NetIO}</div>
{usage?.NetIO} </div>
</dd>
</div>
</dl>
</div> </div>
</div> </div>
<Databases bind:database {privatePort}/> <Databases bind:database {privatePort} />

View File

@@ -130,7 +130,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >

View File

@@ -19,17 +19,11 @@
<script lang="ts"> <script lang="ts">
export let databases: any = []; export let databases: any = [];
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
async function newDatabase() { async function newDatabase() {
const { id } = await post('/databases/new', {}); const { id } = await post('/databases/new', {});
@@ -50,24 +44,21 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div> <div class="mr-4 text-2xl tracking-tight">{$t('index.databases')}</div>
<button <button on:click={newDatabase} class="btn btn-square btn-sm bg-databases">
on:click={newDatabase} <svg
class="btn btn-square btn-sm bg-databases" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
> >
<svg </button>
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center">
@@ -82,21 +73,7 @@
{#each ownDatabases as database} {#each ownDatabases as database}
<a href="/databases/{database.id}" class="p-2 no-underline"> <a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600"> <div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'} <DatabaseIcons type={database.type} isAbsolute={true} />
<Clickhouse isAbsolute />
{:else if database.type === 'couchdb'}
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
<Redis isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{database.name} {database.name}
</div> </div>
@@ -121,21 +98,7 @@
{#each otherDatabases as database} {#each otherDatabases as database}
<a href="/databases/{database.id}" class="p-2 no-underline"> <a href="/databases/{database.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-purple-600"> <div class="box-selection group relative hover:bg-purple-600">
{#if database.type === 'clickhouse'} <DatabaseIcons type={database.type} isAbsolute={true} />
<Clickhouse isAbsolute />
{:else if database.type === 'couchdb'}
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
<Redis isAbsolute />
{/if}
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{database.name} {database.name}
</div> </div>

View File

@@ -20,91 +20,331 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { get } from '$lib/api'; export let applications: any;
export let databases: any;
export let services: any;
export let settings: any;
import { get, post } from '$lib/api';
import Usage from '$lib/components/Usage.svelte'; import Usage from '$lib/components/Usage.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, asyncSleep } from '$lib/common';
import { addToast, appSession } from '$lib/store';
export let applicationsCount: number = 0; import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte';
export let sourcesCount: number = 0; import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte';
export let destinationsCount: number = 0; import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
export let teamsCount: number = 0; import { dev } from '$app/env';
export let databasesCount: number = 0;
export let servicesCount: number = 0; let loading = {
cleanup: false
};
let numberOfGetStatus = 0;
function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function getStatus(resources: any) {
while (numberOfGetStatus > 1) {
await asyncSleep(getRndInteger(100, 200));
}
try {
numberOfGetStatus++;
const { id, buildPack, dualCerts } = resources;
let isRunning = false;
if (buildPack) {
const response = await get(`/applications/${id}/status`);
isRunning = response.isRunning;
} else if (typeof dualCerts !== 'undefined') {
const response = await get(`/services/${id}/status`);
isRunning = response.isRunning;
} else {
const response = await get(`/databases/${id}/status`);
isRunning = response.isRunning;
}
if (isRunning) {
return 'Running';
} else {
return 'Stopped';
}
} catch (error) {
return 'Error';
} finally {
numberOfGetStatus--;
}
}
async function manuallyCleanupStorage() {
try {
loading.cleanup = true;
await post('/internal/cleanup', {});
return addToast({
message: 'Cleanup done.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
} finally {
loading.cleanup = false;
}
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div> <div class="mr-4 text-2xl tracking-tight">{$t('index.dashboard')}</div>
<button on:click={manuallyCleanupStorage} class:loading={loading.cleanup} class="btn btn-sm"
>Cleanup Storage</button
>
</div> </div>
<div class="mt-10 pb-12 tracking-tight sm:pb-16"> <div class="mt-10 pb-12 tracking-tight sm:pb-16">
<div class="mx-auto max-w-4xl"> <div class="mx-auto px-10">
<Usage /> <div class="flex flex-col justify-center xl:flex-row">
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3"> {#if applications.length > 0}
<a <div>
href="/applications" <div class="title">Resources</div>
sveltekit:prefetch <div class="flex items-start justify-center p-8">
class="overflow-hidden rounded px-4 py-5 text-center text-green-500 no-underline transition-all duration-100 hover:bg-green-500 hover:text-white sm:p-6 sm:text-left" <table class="rounded-none text-base">
> <tbody>
<dt class="truncate text-sm font-medium text-white">{$t('index.applications')}</dt> {#each applications as application}
<dd class="mt-1 text-3xl font-semibold "> <tr>
{applicationsCount} <td class="space-x-2 items-center tracking-tight font-bold">
</dd> {#await getStatus(application)}
</a> <div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" />
<a {:then status}
href="/destinations" {#if status === 'Running'}
sveltekit:prefetch <div class="inline-flex w-2 h-2 bg-success rounded-full" />
class="overflow-hidden rounded px-4 py-5 text-center text-sky-500 no-underline transition-all duration-100 hover:bg-sky-500 hover:text-white sm:p-6 sm:text-left" {:else}
> <div class="inline-flex w-2 h-2 bg-error rounded-full" />
<dt class="truncate text-sm font-medium text-white">{$t('index.destinations')}</dt> {/if}
<dd class="mt-1 text-3xl font-semibold "> {/await}
{destinationsCount} <div class="inline-flex">{application.name}</div>
</dd> </td>
</a> <td class="px-10 inline-flex">
<ApplicationsIcons {application} isAbsolute={false} />
</td>
<td class="px-10">
<div
class="badge badge-outline text-xs border-applications rounded text-white"
>
Application
{#if application.settings.isBot}
| BOT
{/if}
</div></td
>
<td class="flex justify-end">
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons bg-transparent text-sm inline-flex"
><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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
{#if application.settings.isBot && application.exposePort}
<a
href={`http://${dev ? 'localhost' : settings.ipv4}:${
application.exposePort
}`}
target="_blank"
class="icons bg-transparent text-sm inline-flex"
><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="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href={`/applications/${application.id}`}
class="icons bg-transparent text-sm inline-flex"
>
<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" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg>
</a>
</td>
</tr>
{/each}
<a {#each services as service}
href="/sources" <tr>
sveltekit:prefetch <td class="space-x-2 items-center tracking-tight font-bold">
class="overflow-hidden rounded px-4 py-5 text-center text-orange-500 no-underline transition-all duration-100 hover:bg-orange-500 hover:text-white sm:p-6 sm:text-left" {#await getStatus(service)}
> <div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" />
<dt class="truncate text-sm font-medium text-white">{$t('index.git_sources')}</dt> {:then status}
<dd class="mt-1 text-3xl font-semibold"> {#if status === 'Running'}
{sourcesCount} <div class="inline-flex w-2 h-2 bg-success rounded-full" />
</dd> {:else}
</a> <div class="inline-flex w-2 h-2 bg-error rounded-full" />
</dl> {/if}
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3"> {/await}
<a <div class="inline-flex">{service.name}</div>
href="/databases" </td>
sveltekit:prefetch <td class="px-10 inline-flex">
class="overflow-hidden rounded px-4 py-5 text-center text-purple-500 no-underline transition-all duration-100 hover:bg-purple-500 hover:text-white sm:p-6 sm:text-left" <ServiceIcons type={service.type} isAbsolute={false} />
> </td>
<dt class="truncate text-sm font-medium text-white">{$t('index.databases')}</dt> <td class="px-10"
<dd class="mt-1 text-3xl font-semibold "> ><div class="badge badge-outline text-xs border-services rounded text-white">
{databasesCount} Service
</dd> </div>
</a> </td>
<a <td class="flex justify-end">
href="/services" {#if service.fqdn}
sveltekit:prefetch <a
class="overflow-hidden rounded px-4 py-5 text-center text-pink-500 no-underline transition-all duration-100 hover:bg-pink-500 hover:text-white sm:p-6 sm:text-left" href={service.fqdn}
> target="_blank"
<dt class="truncate text-sm font-medium text-white">{$t('index.services')}</dt> class="icons bg-transparent text-sm inline-flex"
<dd class="mt-1 text-3xl font-semibold "> ><svg
{servicesCount} xmlns="http://www.w3.org/2000/svg"
</dd> class="h-6 w-6"
</a> viewBox="0 0 24 24"
stroke-width="1.5"
<a stroke="currentColor"
href="/iam" fill="none"
sveltekit:prefetch stroke-linecap="round"
class="overflow-hidden rounded px-4 py-5 text-center text-cyan-500 no-underline transition-all duration-100 hover:bg-cyan-500 hover:text-white sm:p-6 sm:text-left" stroke-linejoin="round"
> >
<dt class="truncate text-sm font-medium text-white">{$t('index.teams')}</dt> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<dd class="mt-1 text-3xl font-semibold "> <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
{teamsCount} <line x1="10" y1="14" x2="20" y2="4" />
</dd> <polyline points="15 4 20 4 20 9" />
</a> </svg></a
</dl> >
{/if}
<a
href={`/services/${service.id}`}
class="icons bg-transparent text-sm inline-flex"
>
<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" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg>
</a>
</td>
</tr>
{/each}
{#each databases as database}
<tr>
<td class="space-x-2 items-center tracking-tight font-bold">
{#await getStatus(database)}
<div class="inline-flex w-2 h-2 bg-yellow-500 rounded-full" />
{:then status}
{#if status === 'Running'}
<div class="inline-flex w-2 h-2 bg-success rounded-full" />
{:else}
<div class="inline-flex w-2 h-2 bg-error rounded-full" />
{/if}
{/await}
<div class="inline-flex">{database.name}</div>
</td>
<td class="px-10 inline-flex">
<DatabaseIcons type={database.type} />
</td>
<td class="px-10">
<div class="badge badge-outline text-xs border-databases rounded text-white">
Database
</div>
</td>
<td class="flex justify-end">
<a
href={`/databases/${database.id}`}
class="icons bg-transparent text-sm inline-flex ml-11"
>
<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" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg>
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{/if}
{#if $appSession.teamId === '0'}
<Usage />
{/if}
</div>
</div> </div>
</div> </div>

View File

@@ -55,9 +55,12 @@
<a href="https://fider.io" target="_blank"> <a href="https://fider.io" target="_blank">
<Icons.Fider /> <Icons.Fider />
</a> </a>
{:else if service.type === 'appwrite'}
<a href="https://appwrite.io" target="_blank">
<Icons.Appwrite/>
</a>
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<a href="https://moodle.org" target="_blank"> <a href="https://moodle.org" target="_blank">
<Icons.Moodle /> <Icons.Moodle />
</a> </a>
{/if} {/if}

View File

@@ -1,36 +0,0 @@
<script lang="ts">
export let type: string;
import * as Icons from '$lib/components/svg/services';
</script>
{#if type === 'plausibleanalytics'}
<Icons.PlausibleAnalytics isAbsolute />
{:else if type === 'nocodb'}
<Icons.NocoDb isAbsolute />
{:else if type === 'minio'}
<Icons.MinIo isAbsolute />
{:else if type === 'vscodeserver'}
<Icons.VsCodeServer isAbsolute />
{:else if type === 'wordpress'}
<Icons.Wordpress isAbsolute />
{:else if type === 'vaultwarden'}
<Icons.VaultWarden isAbsolute />
{:else if type === 'languagetool'}
<Icons.LanguageTool isAbsolute />
{:else if type === 'n8n'}
<Icons.N8n isAbsolute />
{:else if type === 'uptimekuma'}
<Icons.UptimeKuma isAbsolute />
{:else if type === 'ghost'}
<Icons.Ghost isAbsolute />
{:else if type === 'meilisearch'}
<Icons.MeiliSearch isAbsolute />
{:else if type === 'umami'}
<Icons.Umami isAbsolute />
{:else if type === 'hasura'}
<Icons.Hasura isAbsolute />
{:else if type === 'fider'}
<Icons.Fider isAbsolute />
{:else if type === 'moodle'}
<Icons.Moodle isAbsolute />
{/if}

View File

@@ -0,0 +1,126 @@
<script lang="ts">
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { t } from '$lib/translations';
import Select from 'svelte-select';
export let service: any;
export let readOnly: any;
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">Appwrite</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="opensslKeyV1">Encryption Key</label>
<CopyPasswordField
name="opensslKeyV1"
id="opensslKeyV1"
isPasswordField
value={service.appwrite.opensslKeyV1}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="executorSecret">Executor Secret</label>
<CopyPasswordField
name="executorSecret"
id="executorSecret"
isPasswordField
value={service.appwrite.executorSecret}
readonly
disabled
/>
</div>
<!-- <div class="flex space-x-1 py-5 font-bold">
<div class="title">Redis</div>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="redisPassword">Password</label>
<CopyPasswordField
name="redisPassword"
id="redisPassword"
isPasswordField
value={service.appwrite.redisPassword}
readonly
disabled
/>
</div> -->
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<!-- <div class="grid grid-cols-2 items-center px-10">
<label for="mariadbHost">MariaDB Host</label>
<CopyPasswordField
name="mariadbHost"
id="mariadbHost"
value={service.appwrite.mariadbHost}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPort">MariaDB Port</label>
<CopyPasswordField
name="mariadbPort"
id="mariadbPort"
value={service.appwrite.mariadbPort}
readonly
disabled
/>
</div> -->
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbUser">{$t('forms.username')}</label>
<CopyPasswordField
name="mariadbUser"
id="mariadbUser"
value={service.appwrite.mariadbUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbPassword">{$t('forms.password')}</label>
<CopyPasswordField
id="mariadbPassword"
isPasswordField
readonly
disabled
name="mariadbPassword"
value={service.appwrite.mariadbPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUser">Root User</label>
<CopyPasswordField
name="mariadbRootUser"
id="mariadbRootUser"
value={service.appwrite.mariadbRootUser}
readonly
disabled
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbRootUserPassword">Root Password</label>
<CopyPasswordField
id="mariadbRootUserPassword"
isPasswordField
readonly
disabled
name="mariadbRootUserPassword"
value={service.appwrite.mariadbRootUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mariadbDatabase">{$t('index.database')}</label>
<CopyPasswordField
name="mariadbDatabase"
id="mariadbDatabase"
value={service.appwrite.mariadbDatabase}
readonly
disabled
/>
</div>

View File

@@ -26,6 +26,7 @@
import Umami from './_Umami.svelte'; import Umami from './_Umami.svelte';
import VsCodeServer from './_VSCodeServer.svelte'; import VsCodeServer from './_VSCodeServer.svelte';
import Wordpress from './_Wordpress.svelte'; import Wordpress from './_Wordpress.svelte';
import Appwrite from './_Appwrite.svelte';
import Moodle from './_Moodle.svelte'; import Moodle from './_Moodle.svelte';
const { id } = $page.params; const { id } = $page.params;
@@ -37,7 +38,7 @@
save: false, save: false,
verification: false, verification: false,
cleanup: false cleanup: false
} };
let dualCerts = service.dualCerts; let dualCerts = service.dualCerts;
let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, ''); let nonWWWDomain = service.fqdn && getDomain(service.fqdn).replace(/^www\./, '');
@@ -321,13 +322,13 @@
<div class="flex-col space-y-2 pt-4 text-center"> <div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK} {#if isNonWWWDomainOK}
<button <button
class="bg-green-600 hover:bg-green-500" class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)} on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button >DNS settings for {nonWWWDomain} is OK, click to recheck.</button
> >
{:else} {:else}
<button <button
class="bg-red-600 hover:bg-red-500" class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)} on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button >DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
> >
@@ -335,13 +336,13 @@
{#if dualCerts} {#if dualCerts}
{#if isWWWDomainOK} {#if isWWWDomainOK}
<button <button
class="bg-green-600 hover:bg-green-500" class="btn btn-sm bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)} on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button >DNS settings for www.{nonWWWDomain} is OK, click to recheck.</button
> >
{:else} {:else}
<button <button
class="bg-red-600 hover:bg-red-500" class="btn btn-sm bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)} on:click|preventDefault={() => isDNSValid(getDomain(`www.${nonWWWDomain}`), true)}
>DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button >DNS settings for www.{nonWWWDomain} is invalid, click to recheck.</button
> >
@@ -394,6 +395,8 @@
<Hasura bind:service /> <Hasura bind:service />
{:else if service.type === 'fider'} {:else if service.type === 'fider'}
<Fider bind:service {readOnly} /> <Fider bind:service {readOnly} />
{:else if service.type === 'appwrite'}
<Appwrite bind:service {readOnly} />
{:else if service.type === 'moodle'} {:else if service.type === 'moodle'}
<Moodle bind:service {readOnly} /> <Moodle bind:service {readOnly} />
{/if} {/if}

View File

@@ -22,7 +22,7 @@
function generateUrl(publicPort: any) { function generateUrl(publicPort: any) {
return browser return browser
? `sftp://${ ? `sftp://${
settings.fqdn ? getDomain(settings.fqdn) : window.location.hostname settings?.fqdn ? getDomain(settings.fqdn) : window.location.hostname
}:${publicPort}` }:${publicPort}`
: 'Loading...'; : 'Loading...';
} }

View File

@@ -110,7 +110,6 @@
loading = true; loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/start`, {}); await post(`/services/${service.id}/${service.type}/start`, {});
return window.location.reload();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {

View File

@@ -32,10 +32,16 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
onMount(async () => {
if (destinations.length === 1) {
await handleSubmit(destinations[0].id);
}
});
async function handleSubmit(destinationId: any) { async function handleSubmit(destinationId: any) {
try { try {
await post(`/services/${id}/configuration/destination`, { destinationId }); await post(`/services/${id}/configuration/destination`, { destinationId });
@@ -54,7 +60,9 @@
<div class="flex justify-center"> <div class="flex justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || destinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold">{$t('application.configuration.no_configurable_destination')}</div> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')}
</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg

View File

@@ -32,7 +32,7 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Services from '../_Services.svelte'; import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
@@ -56,7 +56,7 @@
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}> <form on:submit|preventDefault={() => handleSubmit(type.name)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600"> <button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
<Services type={type.name} /> <ServiceIcons type={type.name} />
{type.fancyName} {type.fancyName}
</button> </button>
</form> </form>

View File

@@ -32,13 +32,20 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, supportedServiceTypesAndVersions } from '$lib/common'; import { errorNotification, supportedServiceTypesAndVersions } from '$lib/common';
import { onMount } from 'svelte';
const { id } = $page.params; const { id } = $page.params;
const from = $page.url.searchParams.get('from'); const from = $page.url.searchParams.get('from');
let recommendedVersion = supportedServiceTypesAndVersions.find( let recommendedVersion = supportedServiceTypesAndVersions.find(
({ name }) => name === type ({ name }) => name === type
)?.recommendedVersion; )?.recommendedVersion;
onMount(async () => {
if (versions.length === 1) {
await handleSubmit(versions[0]);
}
});
async function handleSubmit(version: any) { async function handleSubmit(version: any) {
try { try {
await post(`/services/${id}/configuration/version`, { version }); await post(`/services/${id}/configuration/version`, { version });

View File

@@ -61,29 +61,21 @@
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4"> <div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Service Usage</div> <div class="text-2xl font-bold">Service Usage</div>
<div class="mx-auto"> <div class="text-center">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3"> <div class="stat w-64">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat-title">Used Memory / Memory Limit</div>
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt> <div class="stat-value text-xl">{usage?.MemUsage}</div>
<dd class="mt-1 text-xl font-semibold text-white"> </div>
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt> <div class="stat-title">Used CPU</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.CPUPerc}</div>
{usage?.CPUPerc} </div>
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"> <div class="stat w-64">
<dt class="truncate text-sm font-medium text-white">Network IO</dt> <div class="stat-title">Network IO</div>
<dd class="mt-1 text-xl font-semibold text-white "> <div class="stat-value text-xl">{usage?.NetIO}</div>
{usage?.NetIO} </div>
</dd>
</div>
</dl>
</div> </div>
</div> </div>
<Services bind:service bind:readOnly bind:settings /> <Services bind:service bind:readOnly bind:settings />

View File

@@ -127,7 +127,7 @@
<div class="flex justify-end sticky top-0 p-1 mx-1"> <div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
on:click={followBuild} on:click={followBuild}
class="bg-transparent tooltip tooltip-primary tooltip-bottom" class="bg-transparent btn btn-sm tooltip tooltip-primary tooltip-bottom"
data-tip="Follow logs" data-tip="Follow logs"
class:text-green-500={followingLogs} class:text-green-500={followingLogs}
> >

View File

@@ -24,9 +24,8 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession } from '$lib/store'; import { appSession } from '$lib/store';
import * as Icons from '$lib/components/svg/services';
import { getDomain } from '$lib/common'; import { getDomain } from '$lib/common';
import Services from './[id]/_Services.svelte'; import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
async function newService() { async function newService() {
const { id } = await post('/services/new', {}); const { id } = await post('/services/new', {});
@@ -46,24 +45,21 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div> <div class="mr-4 text-2xl tracking-tight">{$t('index.services')}</div>
<button <button on:click={newService} class="btn btn-square btn-sm bg-services">
on:click={newService} <svg
class="btn btn-square btn-sm bg-services" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
> >
<svg </button>
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg
>
</button>
</div> </div>
<div class="flex-col justify-center"> <div class="flex-col justify-center">
@@ -78,7 +74,7 @@
{#each ownServices as service} {#each ownServices as service}
<a href="/services/{service.id}" class=" p-2 no-underline"> <a href="/services/{service.id}" class=" p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600"> <div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} /> <ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}
</div> </div>
@@ -106,7 +102,7 @@
{#each otherServices as service} {#each otherServices as service}
<a href="/services/{service.id}" class="p-2 no-underline"> <a href="/services/{service.id}" class="p-2 no-underline">
<div class="box-selection group relative hover:bg-pink-600"> <div class="box-selection group relative hover:bg-pink-600">
<Services type={service.type} /> <ServiceIcons type={service.type} />
<div class="truncate text-center text-xl font-bold"> <div class="truncate text-center text-xl font-bold">
{service.name} {service.name}
</div> </div>

View File

@@ -31,7 +31,7 @@
let dualCerts = settings.dualCerts; let dualCerts = settings.dualCerts;
let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled;
let isDNSCheckEnabled = settings.isDNSCheckEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled;
let DNSServers = settings.DNSServers;
let minPort = settings.minPort; let minPort = settings.minPort;
let maxPort = settings.maxPort; let maxPort = settings.maxPort;
@@ -105,6 +105,10 @@
settings.minPort = minPort; settings.minPort = minPort;
settings.maxPort = maxPort; settings.maxPort = maxPort;
} }
if (DNSServers !== settings.DNSServers) {
await post(`/settings`, { DNSServers });
settings.DNSServers = DNSServers;
}
forceSave = false; forceSave = false;
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
@@ -275,6 +279,17 @@
on:click={() => changeSettings('isDNSCheckEnabled')} on:click={() => changeSettings('isDNSCheckEnabled')}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<div class="pt-2 text-base font-bold text-stone-100">
Custom DNS servers
</div>
<Explainer text="You can specify a custom DNS server to verify your domains all over Coolify.<br><br>By default, the OS defined DNS servers are used." />
</div>
<div class="mx-auto flex-row items-center justify-center space-y-2">
<input placeholder="1.1.1.1,8.8.8.8" bind:value={DNSServers} />
</div>
</div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
dataTooltip={$t('setting.must_remove_domain_before_changing')} dataTooltip={$t('setting.must_remove_domain_before_changing')}

View File

@@ -34,31 +34,6 @@
} }
} }
async function installRepositories(source: any) {
const { htmlUrl } = source;
let endpoint = 'apps'
if (htmlUrl !== 'https://github.com') {
endpoint = 'github-apps'
}
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 1000 / 2;
const newWindow = open(
`${htmlUrl}/${endpoint}/${source.githubApp.name}/installations/new`,
'GitHub',
'resizable=1, scrollbars=1, fullscreen=0, height=1000, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
window.location.reload();
}
}, 100);
}
async function newGithubApp() { async function newGithubApp() {
loading = true; loading = true;
try { try {
@@ -183,7 +158,7 @@
> >
<a <a
class="btn btn-sm" class="btn btn-sm"
href={`${source.htmlUrl}/apps/${source.githubApp.name}/installations/new`} href={`${source.htmlUrl}/${source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'}/${source.githubApp.name}/installations/new`}
>{$t('source.change_app_settings', { name: 'GitHub' })}</a >{$t('source.change_app_settings', { name: 'GitHub' })}</a
> >
{/if} {/if}
@@ -254,8 +229,8 @@
</form> </form>
{:else} {:else}
<div class="text-center"> <div class="text-center">
<a href={`${source.htmlUrl}/apps/${source.githubApp.name}/installations/new`}> <a href={`${source.htmlUrl}/${source.htmlUrl === 'https://github.com' ? 'apps' : 'github-apps'}/${source.githubApp.name}/installations/new`}>
<button class="box-selection bg-sources text-xl" <button class="box-selection bg-sources text-xl font-bold"
>Install Repositories</button >Install Repositories</button
></a ></a
> >

View File

@@ -46,7 +46,7 @@ textarea {
} }
#svelte .custom-select-wrapper .selectContainer { #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 border-coolgray-300 border-dashed 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 .listContainer { #svelte .listContainer {
@@ -193,3 +193,12 @@ a {
.sub-menu-active { .sub-menu-active {
@apply bg-coolgray-500 text-white; @apply bg-coolgray-500 text-white;
} }
.table tbody td,
.table tbody th,
.table thead th{
background-color: transparent;
}
.table *{
border: none;
}

View File

@@ -1,9 +1,10 @@
with import <nixpkgs> {}; with import <nixpkgs> {};
stdenv.mkDerivation { stdenv.mkDerivation {
name = "git"; name = "environment";
buildInputs = [ buildInputs = [
git git
git-lfs git-lfs
docker-compose
]; ];
} }

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": "3.2.0", "version": "3.5.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {