Merge remote-tracking branch 'upstream/next' into next

This commit is contained in:
Arpit Vasani 2022-05-28 12:59:30 +00:00 committed by GitHub
commit 68b220d06e
143 changed files with 4704 additions and 1038 deletions

View File

@ -0,0 +1,47 @@
name: 🐞 Bug report
description: Create a bug report to help us improve coolify
title: "[Bug]: "
labels: [Bug]
assignees:
- andrasbacsai
- vasani-arpit
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! Please fill the form in English
- type: checkboxes
attributes:
label: Is there an existing issue for this?
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Description
description: A concise description of what you're experiencing and what you expect.
placeholder: |
When I do <X>, <Y> happens and I see the error message attached below:
```...```
What I expect is <Z>
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Add steps to reproduce this behaviour, include console / network logs & videos
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: "The version of your coolify Instance"
placeholder: "2.5.2"
validations:
required: true

View File

@ -0,0 +1,31 @@
name: 🛠️ Feature request
description: Suggest an idea to improve coolify
title: '[Feature]: '
labels: [Enhancement]
assignees:
- andrasbacsai
- vasani-arpit
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to request a feature for coolify! Please also add your request here to get feedback from the community: https://feedback.coolify.io/!
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue related to this feature request already exists.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Summary
description: One paragraph description of the feature.
validations:
required: true
- type: textarea
attributes:
label: Why should this be worked on?
description: A concise description of the problems or use cases for this feature request.
validations:
required: true

20
.github/ISSUE_TEMPLATE/--task.yaml vendored Normal file
View File

@ -0,0 +1,20 @@
name: 📝 Task
description: Create a task for the team to work on
title: "[Task]: "
labels: [Task]
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue related to this already exists.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: SubTasks
placeholder: |
- Sub Task 1
- Sub Task 2
validations:
required: false

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: 🤔 Questions and Help
url: https://discord.com/invite/6rDM4fkymF
about: Reach out to us on discord or our github discussions page.

39
.github/workflows/github-actions.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: release-coolify
on:
release:
types: published
jobs:
make-it-coolifyed:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: coollabsio/coolify:latest,coollabsio/coolify:${{steps.package-version.outputs.current-version}}
cache-from: type=registry,ref=coollabsio/coolify:buildcache
cache-to: type=registry,ref=coollabsio/coolify:buildcache,mode=max

View File

@ -8,6 +8,10 @@ https://demo.coolify.io/
(If it is unresponsive, that means someone overloaded the server. 🙃) (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 ## How to install
Installation is automated with the following command: Installation is automated with the following command:
@ -52,18 +56,21 @@ These are the predefined build packs, but with the Docker build pack, you can ho
- NuxtJS - NuxtJS
- NextJS - NextJS
- React/Preact - React/Preact
- NextJS
- Gatsby - Gatsby
- Svelte - Svelte
- PHP - PHP
- Laravel
- Rust - Rust
- Docker - Docker
- Python
- Deno
### Databases ### Databases
One-click database is ready to be used internally or shared over the internet: One-click database is ready to be used internally or shared over the internet:
- MongoDB - MongoDB
- MariaDB
- MySQL - MySQL
- PostgreSQL - PostgreSQL
- CouchDB - CouchDB
@ -73,9 +80,9 @@ One-click database is ready to be used internally or shared over the internet:
You can host cool open-source services as well: You can host cool open-source services as well:
- [WordPress](https://wordpress.org) - [WordPress](https://docs.coollabs.io/coolify/services/wordpress)
- [Ghost](https://ghost.org) - [Ghost](https://ghost.org)
- [Plausible Analytics](https://plausible.io) - [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics)
- [NocoDB](https://nocodb.com) - [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server) - [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io) - [MinIO](https://min.io)
@ -85,6 +92,8 @@ You can host cool open-source services as well:
- [Uptime Kuma](https://github.com/louislam/uptime-kuma) - [Uptime Kuma](https://github.com/louislam/uptime-kuma)
- [MeiliSearch](https://github.com/meilisearch/meilisearch) - [MeiliSearch](https://github.com/meilisearch/meilisearch)
- [Umami](https://github.com/mikecao/umami) - [Umami](https://github.com/mikecao/umami)
- [Fider](https://fider.io)
- [Hasura](https://hasura.io)
## Migration from v1 ## Migration from v1

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.6.1", "version": "2.9.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
@ -30,60 +30,63 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.73", "@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.316", "@sveltejs/adapter-static": "1.0.0-next.31",
"@types/js-cookie": "3.0.1", "@sveltejs/kit": "1.0.0-next.334",
"@types/js-cookie": "3.0.2",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/node": "17.0.25", "@types/node": "17.0.34",
"@types/node-forge": "1.0.1", "@types/node-forge": "1.0.2",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.1", "@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.4", "autoprefixer": "10.4.7",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "3.4.1", "eslint-plugin-svelte3": "3.4.1",
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.4.0", "lint-staged": "12.4.1",
"postcss": "8.4.12", "postcss": "8.4.13",
"prettier": "2.6.2", "prettier": "2.6.2",
"prettier-plugin-svelte": "2.7.0", "prettier-plugin-svelte": "2.7.0",
"prettier-plugin-tailwindcss": "0.1.10", "prettier-plugin-tailwindcss": "0.1.11",
"prisma": "3.11.1", "prisma": "3.11.1",
"svelte": "3.47.0", "svelte": "3.48.0",
"svelte-check": "2.7.0", "svelte-check": "2.7.1",
"svelte-preprocess": "4.10.6", "svelte-preprocess": "4.10.6",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",
"sveltekit-i18n": "2.1.2", "sveltekit-i18n": "2.2.1",
"tailwindcss": "3.0.24", "tailwindcss": "3.0.24",
"ts-node": "10.7.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.4.0",
"typescript": "4.6.3" "typescript": "4.6.4"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1", "@prisma/client": "3.11.1",
"@sentry/node": "6.19.6", "@sentry/node": "6.19.7",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bullmq": "1.80.4", "bullmq": "1.82.2",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.5.0", "cookie": "0.5.0",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.1", "dayjs": "1.11.2",
"dockerode": "3.3.1", "dockerode": "3.3.1",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2", "get-port": "6.1.2",
"got": "12.0.3", "got": "12.0.4",
"is-ip": "4.0.0",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "4.2.0", "mustache": "4.2.0",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.6",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.3", "svelte-kit-cookie-session": "2.1.4",
"tailwindcss-scrollbar": "0.1.0", "tailwindcss-scrollbar": "0.1.0",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },

434
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "exposePort" INTEGER;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Service" ADD COLUMN "exposePort" INTEGER;

View File

@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_PlausibleAnalytics" (
"id" TEXT NOT NULL PRIMARY KEY,
"email" TEXT,
"username" TEXT,
"password" TEXT NOT NULL,
"postgresqlUser" TEXT NOT NULL,
"postgresqlPassword" TEXT NOT NULL,
"postgresqlDatabase" TEXT NOT NULL,
"postgresqlPublicPort" INTEGER,
"secretKeyBase" TEXT,
"scriptName" TEXT NOT NULL DEFAULT 'plausible.js',
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "PlausibleAnalytics_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_PlausibleAnalytics" ("createdAt", "email", "id", "password", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "serviceId", "updatedAt", "username") SELECT "createdAt", "email", "id", "password", "postgresqlDatabase", "postgresqlPassword", "postgresqlPublicPort", "postgresqlUser", "secretKeyBase", "serviceId", "updatedAt", "username" FROM "PlausibleAnalytics";
DROP TABLE "PlausibleAnalytics";
ALTER TABLE "new_PlausibleAnalytics" RENAME TO "PlausibleAnalytics";
CREATE UNIQUE INDEX "PlausibleAnalytics_serviceId_key" ON "PlausibleAnalytics"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -0,0 +1,32 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"ownMysql" BOOLEAN NOT NULL DEFAULT false,
"mysqlHost" TEXT,
"mysqlPort" INTEGER,
"mysqlUser" TEXT NOT NULL,
"mysqlPassword" TEXT NOT NULL,
"mysqlRootUser" TEXT NOT NULL,
"mysqlRootUserPassword" TEXT NOT NULL,
"mysqlDatabase" TEXT,
"mysqlPublicPort" INTEGER,
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
"ftpUser" TEXT,
"ftpPassword" TEXT,
"ftpPublicPort" INTEGER,
"ftpHostKey" TEXT,
"ftpHostKeyPrivate" TEXT,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
DROP TABLE "Wordpress";
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

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

View File

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

View File

@ -20,6 +20,7 @@ model Setting {
proxyHash String? proxyHash String?
isAutoUpdateEnabled Boolean @default(false) isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true) isDNSCheckEnabled Boolean @default(true)
isTraefikUsed Boolean @default(true)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
@ -84,6 +85,7 @@ model Application {
buildPack String? buildPack String?
projectId Int? projectId Int?
port Int? port Int?
exposePort Int?
installCommand String? installCommand String?
buildCommand String? buildCommand String?
startCommand String? startCommand String?
@ -289,6 +291,7 @@ model Service {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
exposePort Int?
dualCerts Boolean @default(false) dualCerts Boolean @default(false)
type String? type String?
version String? version String?
@ -320,6 +323,7 @@ model PlausibleAnalytics {
postgresqlDatabase String postgresqlDatabase String
postgresqlPublicPort Int? postgresqlPublicPort Int?
secretKeyBase String? secretKeyBase String?
scriptName String @default("plausible.js")
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -331,6 +335,7 @@ model Minio {
rootUser String rootUser String
rootUserPassword String rootUserPassword String
publicPort Int? publicPort Int?
apiFqdn String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@ -350,6 +355,9 @@ model Wordpress {
id String @id @default(cuid()) id String @id @default(cuid())
extraConfig String? extraConfig String?
tablePrefix String? tablePrefix String?
ownMysql Boolean @default(false)
mysqlHost String?
mysqlPort Int?
mysqlUser String mysqlUser String
mysqlPassword String mysqlPassword String
mysqlRootUser String mysqlRootUser String

View File

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

View File

@ -114,5 +114,5 @@ export const getSession: GetSession = function ({ locals }) {
}; };
export async function handleError({ error, event }) { export async function handleError({ error, event }) {
if (!dev) sentry.captureException(error, event); // if (!dev) sentry.captureException(error, event);
} }

View File

@ -8,14 +8,16 @@ import { staticDeployments } from '$lib/components/common';
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [ const nodeBased = [
'react', 'react',
'preact',
'vuejs', 'vuejs',
'svelte', 'svelte',
'gatsby', 'gatsby',
'php',
'astro', 'astro',
'eleventy', 'eleventy',
'node', 'node',
'nestjs' 'nestjs',
'nuxtjs',
'nextjs'
]; ];
export function makeLabelForStandaloneApplication({ export function makeLabelForStandaloneApplication({
@ -403,7 +405,72 @@ export function setDefaultBaseImage(buildPack) {
label: 'webdevops/php-nginx:7.1-alpine' label: 'webdevops/php-nginx:7.1-alpine'
} }
]; ];
const pythonVersions = [
{
value: 'python:3.10-alpine',
label: 'python:3.10-alpine'
},
{
value: 'python:3.10-buster',
label: 'python:3.10-buster'
},
{
value: 'python:3.10-bullseye',
label: 'python:3.10-bullseye'
},
{
value: 'python:3.10-slim-bullseye',
label: 'python:3.10-slim-bullseye'
},
{
value: 'python:3.9-alpine',
label: 'python:3.9-alpine'
},
{
value: 'python:3.9-buster',
label: 'python:3.9-buster'
},
{
value: 'python:3.9-bullseye',
label: 'python:3.9-bullseye'
},
{
value: 'python:3.9-slim-bullseye',
label: 'python:3.9-slim-bullseye'
},
{
value: 'python:3.8-alpine',
label: 'python:3.8-alpine'
},
{
value: 'python:3.8-buster',
label: 'python:3.8-buster'
},
{
value: 'python:3.8-bullseye',
label: 'python:3.8-bullseye'
},
{
value: 'python:3.8-slim-bullseye',
label: 'python:3.8-slim-bullseye'
},
{
value: 'python:3.7-alpine',
label: 'python:3.7-alpine'
},
{
value: 'python:3.7-buster',
label: 'python:3.7-buster'
},
{
value: 'python:3.7-bullseye',
label: 'python:3.7-bullseye'
},
{
value: 'python:3.7-slim-bullseye',
label: 'python:3.7-slim-bullseye'
}
];
let payload = { let payload = {
baseImage: null, baseImage: null,
baseBuildImage: null, baseBuildImage: null,
@ -423,7 +490,8 @@ export function setDefaultBaseImage(buildPack) {
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
if (buildPack === 'python') { if (buildPack === 'python') {
payload.baseImage = 'python:3-alpine'; payload.baseImage = 'python:3.10-alpine';
payload.baseImages = pythonVersions;
} }
if (buildPack === 'rust') { if (buildPack === 'rust') {
payload.baseImage = 'rust:latest'; payload.baseImage = 'rust:latest';

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, imageforBuild): Promise<void> => { const createDockerfile = async (data, imageforBuild): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
if (baseImage.includes('nginx')) { if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -2,7 +2,7 @@ import { buildCacheImageForLaravel, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId } = data; const { workdir, applicationId, tag, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -24,7 +24,7 @@ const createDockerfile = async (data, image): Promise<void> => {
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
); );
Dockerfile.push(`COPY --chown=application:application . ./`); Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -2,7 +2,7 @@ import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => { const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory, buildId } = data; const { workdir, baseDirectory, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
let composerFound = false; let composerFound = false;
try { try {
@ -22,7 +22,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
} }
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) { if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -12,7 +12,8 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets, secrets,
pullmergeRequestId, pullmergeRequestId,
baseImage, baseImage,
buildId buildId,
port
} = data; } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
@ -42,7 +43,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) { if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) { if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) { if (baseImage.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@ -4,6 +4,8 @@ import { dev } from '$app/env';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
import type { Config } from 'unique-names-generator'; import type { Config } from 'unique-names-generator';
import { promises as dns } from 'dns';
import { isIP } from 'is-ip';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { buildLogQueue } from './queues'; import { buildLogQueue } from './queues';
@ -14,24 +16,25 @@ import Cookie from 'cookie';
import os from 'os'; import os from 'os';
import type { RequestEvent } from '@sveltejs/kit/types/internal'; import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq'; import type { Job } from 'bullmq';
import { t } from './translations';
try { try {
if (!dev) { if (!dev) {
Sentry.init({ // Sentry.init({
dsn: process.env['COOLIFY_SENTRY_DSN'], // dsn: process.env['COOLIFY_SENTRY_DSN'],
tracesSampleRate: 0, // tracesSampleRate: 0,
environment: 'production', // environment: 'production',
debug: true, // debug: true,
release: currentVersion, // release: currentVersion,
initialScope: { // initialScope: {
tags: { // tags: {
appId: process.env['COOLIFY_APP_ID'], // appId: process.env['COOLIFY_APP_ID'],
'os.arch': getOsArch(), // 'os.arch': getOsArch(),
'os.platform': os.platform(), // 'os.platform': os.platform(),
'os.release': os.release() // 'os.release': os.release()
} // }
} // }
}); // });
} }
} catch (err) { } catch (err) {
console.log('Could not initialize Sentry, no worries.'); console.log('Could not initialize Sentry, no worries.');
@ -93,12 +96,16 @@ export const getUserDetails = async (
const userId = event?.locals?.session?.data?.userId || null; const userId = event?.locals?.session?.data?.userId || null;
let permission = 'read'; let permission = 'read';
if (teamId && userId) { if (teamId && userId) {
try {
const data = await db.prisma.permission.findFirst({ const data = await db.prisma.permission.findFirst({
where: { teamId, userId }, where: { teamId, userId },
select: { permission: true }, select: { permission: true },
rejectOnNotFound: true rejectOnNotFound: true
}); });
if (data.permission) permission = data.permission; if (data.permission) permission = data.permission;
} catch (error) {
console.log(error);
}
} }
const payload = { const payload = {
@ -179,3 +186,97 @@ export function getDomain(domain: string): string {
export function getOsArch() { export function getOsArch() {
return os.arch(); return os.arch();
} }
export async function isDNSValid(event: any, domain: string): Promise<any> {
let resolves = [];
try {
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
try {
let ipDomainFound = false;
dns.setServers(['1.1.1.1', '8.8.8.8']);
const dnsResolve = await dns.resolve4(domain);
if (dnsResolve.length > 0) {
for (const ip of dnsResolve) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
}
if (!ipDomainFound) throw false;
} catch (error) {
throw {
message: t.get('application.domain_not_valid')
};
}
}
export async function checkDomainsIsValidInDNS({ event, fqdn, dualCerts }): Promise<any> {
const domain = getDomain(fqdn);
const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
dns.setServers(['1.1.1.1', '8.8.8.8']);
let resolves = [];
try {
if (isIP(event.url.hostname)) {
resolves = [event.url.hostname];
} else {
resolves = await dns.resolve4(event.url.hostname);
}
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
if (dualCerts) {
try {
const ipDomain = await dns.resolve4(domain);
const ipDomainDualCert = await dns.resolve4(domainDualCert);
let ipDomainFound = false;
let ipDomainDualCertFound = false;
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
for (const ip of ipDomainDualCert) {
if (resolves.includes(ip)) {
ipDomainDualCertFound = true;
}
}
if (ipDomainFound && ipDomainDualCertFound) return { status: 200 };
throw false;
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
} else {
try {
const ipDomain = await dns.resolve4(domain);
let ipDomainFound = false;
for (const ip of ipDomain) {
if (resolves.includes(ip)) {
ipDomainFound = true;
}
}
if (ipDomainFound) return { status: 200 };
throw false;
} catch (error) {
throw {
message: t.get('application.dns_not_set_error', { domain })
};
}
}
}

View File

@ -3,6 +3,7 @@
import Clickhouse from './svg/databases/Clickhouse.svelte'; import Clickhouse from './svg/databases/Clickhouse.svelte';
import CouchDb from './svg/databases/CouchDB.svelte'; import CouchDb from './svg/databases/CouchDB.svelte';
import MongoDb from './svg/databases/MongoDB.svelte'; import MongoDb from './svg/databases/MongoDB.svelte';
import MariaDb from './svg/databases/MariaDB.svelte';
import MySql from './svg/databases/MySQL.svelte'; import MySql from './svg/databases/MySQL.svelte';
import PostgreSql from './svg/databases/PostgreSQL.svelte'; import PostgreSql from './svg/databases/PostgreSQL.svelte';
import Redis from './svg/databases/Redis.svelte'; import Redis from './svg/databases/Redis.svelte';
@ -17,6 +18,8 @@
<MongoDb /> <MongoDb />
{:else if database.type === 'mysql'} {:else if database.type === 'mysql'}
<MySql /> <MySql />
{:else if database.type === 'mariadb'}
<MariaDb />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
<PostgreSql /> <PostgreSql />
{:else if database.type === 'redis'} {:else if database.type === 'redis'}

View File

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

View File

@ -52,6 +52,12 @@ export const supportedDatabaseTypesAndVersions = [
versions: ['5.0', '4.4', '4.2'] versions: ['5.0', '4.4', '4.2']
}, },
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] }, { 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', name: 'postgresql',
fancyName: 'PostgreSQL', fancyName: 'PostgreSQL',
@ -213,5 +219,25 @@ export const supportedServiceTypesAndVersions = [
ports: { ports: {
main: 3000 main: 3000
} }
// },
// {
// name: 'appwrite',
// fancyName: 'AppWrite',
// baseImage: 'appwrite/appwrite',
// images: ['appwrite/influxdb', 'appwrite/telegraf', 'mariadb:10.7', 'redis:6.0-alpine3.12'],
// versions: ['latest', '0.13.0'],
// recommendedVersion: '0.13.0',
// ports: {
// main: 3000
// }
// }
} }
]; ];
export const getServiceMainPort = (service: string) => {
const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service);
if (serviceType) {
return serviceType.ports.main;
}
return null;
};

View File

@ -0,0 +1,24 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 309.88 252.72"
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
>
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path
class="cls-1"
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
transform="translate(-7.45 -9.1)"
/>
</svg>

View File

@ -3,31 +3,88 @@
</script> </script>
<svg <svg
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-10 w-10' : 'mx-auto w-8 h-8'} viewBox="0 0 128 128"
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'}
id="Layer_1" >
data-name="Layer 1" <path
viewBox="0 0 216.56 448.5" fill-rule="evenodd"
><defs clip-rule="evenodd"
><style> fill="#439934"
.cls-1 { d="M88.038 42.812c1.605 4.643 2.761 9.383 3.141 14.296.472 6.095.256 12.147-1.029 18.142-.035.165-.109.32-.164.48-.403.001-.814-.049-1.208.012-3.329.523-6.655 1.065-9.981 1.604-3.438.557-6.881 1.092-10.313 1.687-1.216.21-2.721-.041-3.212 1.641-.014.046-.154.054-.235.08l.166-10.051-.169-24.252 1.602-.275c2.62-.429 5.24-.864 7.862-1.281 3.129-.497 6.261-.98 9.392-1.465 1.381-.215 2.764-.412 4.148-.618z"
fill: #10aa50;
}
.cls-2 {
fill: #b8c4c2;
}
.cls-3 {
fill: #12924f;
}
</style></defs
><path
class="cls-1"
d="M202.8,179.68c-23-101.47-71-128.49-83.18-147.59C113,21.7,106.25,5.91,106.25,5.91c-.66,9-1.83,14.7-9.51,21.54C81.36,41.16,16,94.42,10.51,209.72c-5.12,107.5,79,173.8,90.18,180.65,8.54,4.2,19,.08,24-3.77,40.54-27.84,96-102.07,78.06-206.92"
/><path /><path
class="cls-2" fill-rule="evenodd"
d="M109.73,333.11c-2.11,26.62-3.63,42.11-9,57.29,0,0,3.54,25.33,6,52.17l8.77,0a488.62,488.62,0,0,1,9.57-56.2C113.71,380.8,110.16,356.46,109.73,333.11Z" clip-rule="evenodd"
fill="#45A538"
d="M61.729 110.054c-1.69-1.453-3.439-2.842-5.059-4.37-8.717-8.222-15.093-17.899-18.233-29.566-.865-3.211-1.442-6.474-1.627-9.792-.13-2.322-.318-4.665-.154-6.975.437-6.144 1.325-12.229 3.127-18.147l.099-.138c.175.233.427.439.516.702 1.759 5.18 3.505 10.364 5.242 15.551 5.458 16.3 10.909 32.604 16.376 48.9.107.318.384.579.583.866l-.87 2.969z"
/><path /><path
class="cls-3" fill-rule="evenodd"
d="M125.06,386.39h0c-11.48-5.3-14.8-30.13-15.31-53.28A1090.8,1090.8,0,0,0,112.2,218.4c-.6-20.07.3-185.92-4.94-210.2,2.12,4.75,7.24,15.91,12.36,23.88,12.23,19.11,60.19,46.13,83.17,147.61C220.7,284.27,165.57,358.37,125.06,386.39Z" clip-rule="evenodd"
fill="#46A037"
d="M88.038 42.812c-1.384.206-2.768.403-4.149.616-3.131.485-6.263.968-9.392 1.465-2.622.417-5.242.852-7.862 1.281l-1.602.275-.012-1.045c-.053-.859-.144-1.717-.154-2.576-.069-5.478-.112-10.956-.18-16.434-.042-3.429-.105-6.857-.175-10.285-.043-2.13-.089-4.261-.185-6.388-.052-1.143-.236-2.28-.311-3.423-.042-.657.016-1.319.029-1.979.817 1.583 1.616 3.178 2.456 4.749 1.327 2.484 3.441 4.314 5.344 6.311 7.523 7.892 12.864 17.068 16.193 27.433z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#409433"
d="M65.036 80.753c.081-.026.222-.034.235-.08.491-1.682 1.996-1.431 3.212-1.641 3.432-.594 6.875-1.13 10.313-1.687 3.326-.539 6.652-1.081 9.981-1.604.394-.062.805-.011 1.208-.012-.622 2.22-1.112 4.488-1.901 6.647-.896 2.449-1.98 4.839-3.131 7.182a49.142 49.142 0 01-6.353 9.763c-1.919 2.308-4.058 4.441-6.202 6.548-1.185 1.165-2.582 2.114-3.882 3.161l-.337-.23-1.214-1.038-1.256-2.753a41.402 41.402 0 01-1.394-9.838l.023-.561.171-2.426c.057-.828.133-1.655.168-2.485.129-2.982.241-5.964.359-8.946z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4FAA41"
d="M65.036 80.753c-.118 2.982-.23 5.964-.357 8.947-.035.83-.111 1.657-.168 2.485l-.765.289c-1.699-5.002-3.399-9.951-5.062-14.913-2.75-8.209-5.467-16.431-8.213-24.642a4498.887 4498.887 0 00-6.7-19.867c-.105-.31-.407-.552-.617-.826l4.896-9.002c.168.292.39.565.496.879a6167.476 6167.476 0 016.768 20.118c2.916 8.73 5.814 17.467 8.728 26.198.116.349.308.671.491 1.062l.67-.78-.167 10.052z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4AA73C"
d="M43.155 32.227c.21.274.511.516.617.826a4498.887 4498.887 0 016.7 19.867c2.746 8.211 5.463 16.433 8.213 24.642 1.662 4.961 3.362 9.911 5.062 14.913l.765-.289-.171 2.426-.155.559c-.266 2.656-.49 5.318-.814 7.968-.163 1.328-.509 2.632-.772 3.947-.198-.287-.476-.548-.583-.866-5.467-16.297-10.918-32.6-16.376-48.9a3888.972 3888.972 0 00-5.242-15.551c-.089-.263-.34-.469-.516-.702l3.272-8.84z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#57AE47"
d="M65.202 70.702l-.67.78c-.183-.391-.375-.714-.491-1.062-2.913-8.731-5.812-17.468-8.728-26.198a6167.476 6167.476 0 00-6.768-20.118c-.105-.314-.327-.588-.496-.879l6.055-7.965c.191.255.463.482.562.769 1.681 4.921 3.347 9.848 5.003 14.778 1.547 4.604 3.071 9.215 4.636 13.813.105.308.47.526.714.786l.012 1.045c.058 8.082.115 16.167.171 24.251z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#60B24F"
d="M65.021 45.404c-.244-.26-.609-.478-.714-.786-1.565-4.598-3.089-9.209-4.636-13.813-1.656-4.93-3.322-9.856-5.003-14.778-.099-.287-.371-.514-.562-.769 1.969-1.928 3.877-3.925 5.925-5.764 1.821-1.634 3.285-3.386 3.352-5.968.003-.107.059-.214.145-.514l.519 1.306c-.013.661-.072 1.322-.029 1.979.075 1.143.259 2.28.311 3.423.096 2.127.142 4.258.185 6.388.069 3.428.132 6.856.175 10.285.067 5.478.111 10.956.18 16.434.008.861.098 1.718.152 2.577z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#A9AA88"
d="M62.598 107.085c.263-1.315.609-2.62.772-3.947.325-2.649.548-5.312.814-7.968l.066-.01.066.011a41.402 41.402 0 001.394 9.838c-.176.232-.425.439-.518.701-.727 2.05-1.412 4.116-2.143 6.166-.1.28-.378.498-.574.744l-.747-2.566.87-2.969z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#B6B598"
d="M62.476 112.621c.196-.246.475-.464.574-.744.731-2.05 1.417-4.115 2.143-6.166.093-.262.341-.469.518-.701l1.255 2.754c-.248.352-.59.669-.728 1.061l-2.404 7.059c-.099.283-.437.483-.663.722l-.695-3.985z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#C2C1A7"
d="M63.171 116.605c.227-.238.564-.439.663-.722l2.404-7.059c.137-.391.48-.709.728-1.061l1.215 1.037c-.587.58-.913 1.25-.717 2.097l-.369 1.208c-.168.207-.411.387-.494.624-.839 2.403-1.64 4.819-2.485 7.222-.107.305-.404.544-.614.812-.109-1.387-.22-2.771-.331-4.158z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#CECDB7"
d="M63.503 120.763c.209-.269.506-.508.614-.812.845-2.402 1.646-4.818 2.485-7.222.083-.236.325-.417.494-.624l-.509 5.545c-.136.157-.333.294-.398.477-.575 1.614-1.117 3.24-1.694 4.854-.119.333-.347.627-.525.938-.158-.207-.441-.407-.454-.623-.051-.841-.016-1.688-.013-2.533z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#DBDAC7"
d="M63.969 123.919c.178-.312.406-.606.525-.938.578-1.613 1.119-3.239 1.694-4.854.065-.183.263-.319.398-.477l.012 3.64-1.218 3.124-1.411-.495z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#EBE9DC"
d="M65.38 124.415l1.218-3.124.251 3.696-1.469-.572z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#CECDB7"
d="M67.464 110.898c-.196-.847.129-1.518.717-2.097l.337.23-1.054 1.867z"
/><path
fill-rule="evenodd"
clip-rule="evenodd"
fill="#4FAA41"
d="M64.316 95.172l-.066-.011-.066.01.155-.559-.023.56z"
/> />
</svg> </svg>

View File

@ -4,6 +4,6 @@
<img <img
alt="plausible logo" alt="plausible logo"
class={isAbsolute ? 'w-10 absolute top-0 left-0 -m-5' : 'w-6 mx-auto'} class={isAbsolute ? 'w-9 absolute top-0 left-0 -m-4' : 'w-6 mx-auto'}
src="/plausible.png" src="/plausible.png"
/> />

View File

@ -128,9 +128,7 @@ export function findBuildPack(pack, packageManager = 'npm') {
if (pack === 'astro') { if (pack === 'astro') {
return { return {
...metaData, ...metaData,
installCommand: `yarn install`, ...defaultBuildAndDeploy(packageManager),
buildCommand: `yarn build`,
startCommand: null,
publishDirectory: `dist`, publishDirectory: `dist`,
port: 80 port: 80
}; };
@ -138,9 +136,7 @@ export function findBuildPack(pack, packageManager = 'npm') {
if (pack === 'eleventy') { if (pack === 'eleventy') {
return { return {
...metaData, ...metaData,
installCommand: `yarn install`, ...defaultBuildAndDeploy(packageManager),
buildCommand: `yarn build`,
startCommand: null,
publishDirectory: `_site`, publishDirectory: `_site`,
port: 80 port: 80
}; };

View File

@ -138,7 +138,18 @@ export async function getApplicationWebhook({
return s; return s;
}); });
} }
return { ...application }; const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
application.buildPack
);
// Set default build images
if (!application.baseImage) {
application.baseImage = baseImage;
}
if (!application.baseBuildImage) {
application.baseBuildImage = baseBuildImage;
}
return { ...application, baseBuildImages, baseImages };
} catch (e) { } catch (e) {
throw { status: 404, body: { message: e.message } }; throw { status: 404, body: { message: e.message } };
} }
@ -267,6 +278,7 @@ export async function configureApplication({
name, name,
fqdn, fqdn,
port, port,
exposePort,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
@ -286,6 +298,7 @@ export async function configureApplication({
name: string; name: string;
fqdn: string; fqdn: string;
port: number; port: number;
exposePort: number;
installCommand: string; installCommand: string;
buildCommand: string; buildCommand: string;
startCommand: string; startCommand: string;
@ -307,6 +320,7 @@ export async function configureApplication({
buildPack, buildPack,
fqdn, fqdn,
port, port,
exposePort,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,

View File

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

View File

@ -28,7 +28,7 @@ if (!dev) {
} }
export const prisma = new PrismaClient({ export const prisma = new PrismaClient({
errorFormat: 'pretty', errorFormat: 'minimal',
rejectOnNotFound: false rejectOnNotFound: false
}); });
@ -58,7 +58,7 @@ export function ErrorHandler(e: {
truncatedError.message = 'git clone failed'; truncatedError.message = 'git clone failed';
} }
if (!e.message?.includes('Coolify Proxy is not running')) { if (!e.message?.includes('Coolify Proxy is not running')) {
sentry.captureException(truncatedError); // sentry.captureException(truncatedError);
} }
const payload = { const payload = {
status: truncatedError.status || 500, status: truncatedError.status || 500,
@ -149,6 +149,19 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
MONGODB_ROOT_PASSWORD: string; MONGODB_ROOT_PASSWORD: string;
}; };
} }
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MARIADB_ROOT_USER: string;
MARIADB_ROOT_PASSWORD: string;
MARIADB_USER: string;
MARIADB_PASSWORD: string;
MARIADB_DATABASE: string;
};
}
| { | {
volume: string; volume: string;
image: string; image: string;
@ -207,6 +220,20 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
volume: `${id}-${type}-data:/bitnami/mysql/data`, volume: `${id}-${type}-data:/bitnami/mysql/data`,
ulimits: {} ulimits: {}
}; };
} else if (type === 'mariadb') {
return {
privatePort: 3306,
environmentVariables: {
MARIADB_ROOT_USER: rootUser,
MARIADB_ROOT_PASSWORD: rootUserPassword,
MARIADB_USER: dbUser,
MARIADB_PASSWORD: dbUserPassword,
MARIADB_DATABASE: defaultDatabase
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mariadb`,
ulimits: {}
};
} else if (type === 'mongodb') { } else if (type === 'mongodb') {
return { return {
privatePort: 27017, privatePort: 27017,
@ -278,6 +305,12 @@ export async function getFreePort() {
select: { mysqlPublicPort: true } select: { mysqlPublicPort: true }
}) })
).map((a) => a.mysqlPublicPort); ).map((a) => a.mysqlPublicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed]; const minioUsed = await (
await prisma.minio.findMany({
where: { publicPort: { not: null } },
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
} }

View File

@ -184,6 +184,10 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
); );
} else if (type === 'mariadb') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
);
} else if (type === 'postgresql') { } else if (type === 'postgresql') {
if (isRoot) { if (isRoot) {
await asyncExecShell( await asyncExecShell(

View File

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

View File

@ -327,35 +327,62 @@ export async function updatePlausibleAnalyticsService({
id, id,
fqdn, fqdn,
email, email,
exposePort,
username, username,
name name,
scriptName
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
exposePort?: number;
name: string; name: string;
email: string; email: string;
username: string; username: string;
scriptName: string;
}): Promise<void> { }): Promise<void> {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({
await prisma.service.update({ where: { id }, data: { name, fqdn } }); where: { serviceId: id },
data: { email, username, scriptName }
});
await prisma.service.update({ where: { id }, data: { name, fqdn, exposePort } });
} }
export async function updateService({ export async function updateService({
id, id,
fqdn, fqdn,
exposePort,
name name
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
exposePort?: number;
name: string; name: string;
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } });
}
export async function updateMinioService({
id,
fqdn,
apiFqdn,
exposePort,
name
}: {
id: string;
fqdn: string;
apiFqdn: string;
exposePort?: number;
name: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, exposePort, minio: { update: { apiFqdn } } }
});
} }
export async function updateFiderService({ export async function updateFiderService({
id, id,
fqdn, fqdn,
name, name,
exposePort,
emailNoreply, emailNoreply,
emailMailgunApiKey, emailMailgunApiKey,
emailMailgunDomain, emailMailgunDomain,
@ -368,6 +395,7 @@ export async function updateFiderService({
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
exposePort?: number;
name: string; name: string;
emailNoreply: string; emailNoreply: string;
emailMailgunApiKey: string; emailMailgunApiKey: string;
@ -384,6 +412,7 @@ export async function updateFiderService({
data: { data: {
fqdn, fqdn,
name, name,
exposePort,
fider: { fider: {
update: { update: {
emailNoreply, emailNoreply,
@ -405,22 +434,49 @@ export async function updateWordpress({
id, id,
fqdn, fqdn,
name, name,
exposePort,
ownMysql,
mysqlDatabase, mysqlDatabase,
extraConfig extraConfig,
mysqlHost,
mysqlPort,
mysqlUser,
mysqlPassword
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
name: string; name: string;
exposePort?: number;
ownMysql: boolean;
mysqlDatabase: string; mysqlDatabase: string;
extraConfig: string; extraConfig: string;
mysqlHost?: string;
mysqlPort?: number;
mysqlUser?: string;
mysqlPassword?: string;
}): Promise<Service> { }): Promise<Service> {
mysqlPassword = encrypt(mysqlPassword);
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } data: {
fqdn,
name,
exposePort,
wordpress: {
update: {
mysqlDatabase,
extraConfig,
mysqlHost,
mysqlUser,
mysqlPassword,
mysqlPort
}
}
}
}); });
} }
export async function updateMinioService({ export async function updateMinioServicePort({
id, id,
publicPort publicPort
}: { }: {
@ -434,16 +490,18 @@ export async function updateGhostService({
id, id,
fqdn, fqdn,
name, name,
exposePort,
mariadbDatabase mariadbDatabase
}: { }: {
id: string; id: string;
fqdn: string; fqdn: string;
name: string; name: string;
exposePort?: number;
mariadbDatabase: string; mariadbDatabase: string;
}): Promise<Service> { }): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } } data: { fqdn, name, exposePort, ghost: { update: { mariadbDatabase } } }
}); });
} }

View File

@ -55,6 +55,9 @@ frontend http
http-request redirect location {{{redirectValue}}} code ${ http-request redirect location {{{redirectValue}}} code ${
dev ? 302 : 301 dev ? 302 : 301
} if { req.hdr(host) -i {{redirectTo}} } } if { req.hdr(host) -i {{redirectTo}} }
{{#scriptName}}
http-request set-path /js/plausible.js if { hdr(host) -i {{domain}} } { path_beg -i /js/{{scriptName}} }
{{/scriptName}}
{{/services}} {{/services}}
{{#coolify}} {{#coolify}}
@ -218,7 +221,15 @@ export async function configureHAProxy(): Promise<void> {
const services = await listServicesWithIncludes(); const services = await listServicesWithIncludes();
for (const service of services) { for (const service of services) {
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; const {
fqdn,
id,
type,
destinationDocker,
destinationDockerId,
updatedAt,
plausibleAnalytics
} = service;
if (destinationDockerId) { if (destinationDockerId) {
const { engine } = destinationDocker; const { engine } = destinationDocker;
const found = supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
@ -232,6 +243,12 @@ export async function configureHAProxy(): Promise<void> {
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) { if (isRunning) {
// Plausible Analytics custom script
let scriptName = false;
if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') {
scriptName = plausibleAnalytics.scriptName;
}
data.services.push({ data.services.push({
id, id,
port, port,
@ -241,7 +258,8 @@ export async function configureHAProxy(): Promise<void> {
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain, redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime() updatedAt: updatedAt.getTime(),
scriptName
}); });
} }
} }

View File

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

View File

@ -290,3 +290,30 @@ export async function generateSSLCerts(): Promise<void> {
} }
} }
} }
export async function renewSSLCerts(): Promise<void> {
if (!dev) {
const host = 'unix:///var/run/docker.sock';
await asyncExecShell(`docker pull alpine:latest`);
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
const { stdout: certificates } = await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /etc/letsencrypt/live/ | grep -v README"`
);
for (const certificate of certificates.trim().split('\n')) {
try {
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm --name certbot-renewal -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --cert-name ${certificate} --logs-dir /etc/letsencrypt/logs renew --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080`
);
await asyncExecShell(
`DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${certificate}/ && cat /etc/letsencrypt/live/${certificate}/fullchain.pem /etc/letsencrypt/live/${certificate}/privkey.pem > /app/ssl/${certificate}.pem"`
);
} catch (error) {
console.log(error);
}
}
await reloadHaproxy('unix:///var/run/docker.sock');
}
}

View File

@ -178,13 +178,15 @@
"delete_application": "Delete application", "delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application", "permission_denied_delete_application": "You do not have permission to delete this application",
"domain_already_in_use": "Domain {{domain}} is already used.", "domain_already_in_use": "Domain {{domain}} is already used.",
"dns_not_set_error": "DNS not set or propogated for {{domain}}.<br><br>Please check your DNS settings.", "dns_not_set_error": "DNS not set correctly or propogated for {{domain}}.<br><br>Please check your DNS settings.",
"domain_required": "Domain is required.",
"settings_saved": "Settings saved.", "settings_saved": "Settings saved.",
"dns_not_set_partial_error": "DNS not set", "dns_not_set_partial_error": "DNS not set",
"domain_not_valid": "Could not resolve domain or it's not pointing to the server IP address.<br><br>Please check your DNS configuration and try again.",
"git_source": "Git Source", "git_source": "Git Source",
"git_repository": "Git Repository", "git_repository": "Git Repository",
"build_pack": "Build Pack", "build_pack": "Build Pack",
"base_image": "Deplyoment Image", "base_image": "Deployment Image",
"base_image_explainer": "Image that will be used for the deployment.", "base_image_explainer": "Image that will be used for the deployment.",
"base_build_image": "Build Image", "base_build_image": "Build Image",
"base_build_image_explainer": "Image that will be used during the build process.", "base_build_image_explainer": "Image that will be used during the build process.",
@ -204,6 +206,7 @@
"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.",
"enable_mr_pr_previews": "Enable MR/PR Previews", "enable_mr_pr_previews": "Enable MR/PR Previews",
"expose_a_port": "Expose a port",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs", "debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.", "enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-red-500 font-bold'>Sensitive information</span> could be visible and saved in logs.",
@ -311,7 +314,7 @@
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.", "credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?", "auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-500'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted.", "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-yellow-500'>both DNS entries</span> set in advance.",
"is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled": "DNS check enabled?",
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel." "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
}, },

View File

@ -61,6 +61,7 @@
"enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.<br><span class='text-red-500 font-bold'>Les informations sensibles</span> peuvent être visibles et enregistrées dans les journaux.", "enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.<br><span class='text-red-500 font-bold'>Les informations sensibles</span> peuvent être visibles et enregistrées dans les journaux.",
"enable_mr_pr_previews": "Activer les aperçus MR/PR", "enable_mr_pr_previews": "Activer les aperçus MR/PR",
"enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.", "enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.",
"expose_a_port": "Exposer un port",
"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",

View File

@ -48,6 +48,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
pythonModule, pythonModule,
pythonVariable, pythonVariable,
denoOptions, denoOptions,
exposePort,
baseImage, baseImage,
baseBuildImage baseBuildImage
} = job.data; } = job.data;
@ -152,6 +153,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
JSON.stringify({ JSON.stringify({
buildPack, buildPack,
port, port,
exposePort,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
@ -207,7 +209,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
tag, tag,
workdir, workdir,
docker, docker,
port, port: exposePort ? `${exposePort}:${port}` : port,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
@ -263,7 +265,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
repository, repository,
branch, branch,
projectId, projectId,
port, port: exposePort ? `${exposePort}:${port}` : port,
commit, commit,
installCommand, installCommand,
buildCommand, buildCommand,
@ -298,6 +300,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
labels, labels,
depends_on: [], depends_on: [],
restart: 'always', restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: { // logging: {
// driver: 'fluentd', // driver: 'fluentd',
// }, // },
@ -325,7 +328,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error); // sentry.captureException(error);
throw new Error(error); throw new Error(error);
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });

View File

@ -41,7 +41,6 @@ export default async function (): Promise<void> {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }
console.log(`Is LowDiskSpace detected? ${lowDiskSpace}`);
if (lowDiskSpace) { if (lowDiskSpace) {
// Cleanup old coolify images // Cleanup old coolify images
try { try {

View File

@ -116,8 +116,8 @@ const cron = async (): Promise<void> => {
await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } }); await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); if (!dev) await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); if (!dev) await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } });
}; };
cron().catch((error) => { cron().catch((error) => {
console.log('cron failed to start'); console.log('cron failed to start');

View File

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

View File

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

View File

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

View File

@ -1,9 +1,14 @@
import { asyncExecShell } from '$lib/common'; import { renewSSLCerts } from '$lib/letsencrypt';
import { reloadHaproxy } from '$lib/haproxy'; import { prisma } from '$lib/database';
export default async function (): Promise<void> { export default async function (): Promise<void> {
await asyncExecShell( try {
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew` const settings = await prisma.setting.findFirst();
); if (!settings.isTraefikUsed) {
await reloadHaproxy('unix:///var/run/docker.sock'); return await renewSSLCerts();
}
} catch (error) {
console.log(error);
throw error;
}
} }

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

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

View File

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

View File

@ -12,6 +12,7 @@ export type BuilderJob = {
buildPack: BuildPackName; buildPack: BuildPackName;
projectId: number; projectId: number;
port: number; port: number;
exposePort?: number;
installCommand: string; installCommand: string;
buildCommand?: string; buildCommand?: string;
startCommand?: string; startCommand?: string;

View File

@ -18,6 +18,7 @@ export type ComposeFileService = {
restart: ComposeFileRestartOption; restart: ComposeFileRestartOption;
depends_on?: string[]; depends_on?: string[];
command?: string; command?: string;
ports?: string[];
build?: { build?: {
context: string; context: string;
dockerfile: string; dockerfile: string;

39
src/routes/_Trend.svelte Normal file
View File

@ -0,0 +1,39 @@
<script lang="ts">
export let trend;
</script>
{#if trend === 'up'}
<span class="-mt-1 inline-flex px-2 text-green-500">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
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" />
<line x1="17" y1="7" x2="7" y2="17" />
<polyline points="8 7 17 7 17 16" />
</svg></span
>
{:else if trend === 'down'}
<span class="text-red-500 px-2 inline-flex -mt-1">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8"
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" />
<line x1="7" y1="7" x2="17" y2="17" />
<polyline points="17 8 17 17 8 17" />
</svg>
</span>
{/if}

View File

@ -34,23 +34,30 @@
</script> </script>
<script> <script>
export let settings;
import '../tailwind.css'; import '../tailwind.css';
import { SvelteToast, toast } from '@zerodevx/svelte-toast'; import { SvelteToast, toast } from '@zerodevx/svelte-toast';
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { fade } from 'svelte/transition';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { asyncSleep } from '$lib/components/common'; import { asyncSleep } from '$lib/components/common';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { dev } from '$app/env'; import { dev } from '$app/env';
import { features } from '$lib/store'; import { features, isTraefikUsed } from '$lib/store';
let isUpdateAvailable = false; import { navigating } from '$app/stores';
import PageLoader from '$lib/components/PageLoader.svelte';
$isTraefikUsed = settings?.isTraefikUsed || false;
let isUpdateAvailable = false;
let updateStatus = { let updateStatus = {
found: false, found: false,
loading: false, loading: false,
success: null success: null
}; };
let latestVersion = 'latest'; let latestVersion = 'latest';
onMount(async () => { onMount(async () => {
if ($session.userId) { if ($session.userId) {
const overrideVersion = $features.latestVersion; const overrideVersion = $features.latestVersion;
@ -78,6 +85,7 @@
} }
} }
}); });
async function logout() { async function logout() {
try { try {
await del(`/logout.json`, {}); await del(`/logout.json`, {});
@ -128,16 +136,23 @@
<title>Coolify</title> <title>Coolify</title>
{#if !$session.whiteLabeled} {#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
{:else if $session.whiteLabelDetails.icon}
<link rel="icon" href={$session.whiteLabelDetails.icon} />
{/if} {/if}
</svelte:head> </svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $navigating}
<div out:fade={{ delay: 100 }}>
<PageLoader />
</div>
{/if}
{#if $session.userId} {#if $session.userId}
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
{#if !$session.whiteLabeled} {#if !$session.whiteLabeled}
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> <div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
{/if} {/if}
<div class="flex flex-col space-y-4 py-2" class:mt-2={$session.whiteLabeled}> <div class="flex flex-col space-y-2 py-2" class:mt-2={$session.whiteLabeled}>
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
@ -222,7 +237,6 @@
<polyline points="10 15 13 18 10 21" /> <polyline points="10 15 13 18 10 21" />
</svg> </svg>
</a> </a>
<div class="border-t border-stone-700" />
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/destinations" href="/destinations"
@ -284,7 +298,6 @@
<path d="M4 12v6a8 3 0 0 0 16 0v-6" /> <path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg> </svg>
</a> </a>
<div class="border-t border-stone-700" />
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/services" href="/services"
@ -423,7 +436,7 @@
{/if} {/if}
{/if} {/if}
</div> </div>
<div class="flex flex-col space-y-4 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/iam" href="/iam"

View File

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

View File

@ -3,35 +3,37 @@ import { buildQueue } from '$lib/queues';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import * as db from '$lib/database'; import * as db from '$lib/database';
export const post: RequestHandler = async (event) => { async function cleanupDB(buildId: string) {
const { buildId, applicationId } = await event.request.json(); const data = await db.prisma.build.findUnique({ where: { id: buildId } });
if (!buildId) { if (data?.status === 'queued' || data?.status === 'running') {
return { await db.prisma.build.update({ where: { id: buildId }, data: { status: 'failed' } });
status: 500,
body: {
message: 'Build ID not found.'
} }
}; }
}
try { async function stopBuild(buildId, applicationId) {
let count = 0; let count = 0;
await new Promise<void>(async (resolve, reject) => { await new Promise<void>(async (resolve, reject) => {
const job = await buildQueue.getJob(buildId); const job = await buildQueue.getJob(buildId);
if (!job) {
await cleanupDB(buildId);
return resolve();
}
const { const {
destinationDocker: { engine } destinationDocker: { engine }
} = job.data; } = job?.data;
const host = getEngine(engine); const host = getEngine(engine);
let interval = setInterval(async () => { let interval = setInterval(async () => {
const { status } = await db.prisma.build.findUnique({ where: { id: buildId } }); try {
if (status === 'failed') { const data = await db.prisma.build.findUnique({ where: { id: buildId } });
if (data?.status === 'failed') {
clearInterval(interval); clearInterval(interval);
return resolve(); return resolve();
} }
if (count > 1200) { if (count > 100) {
clearInterval(interval); clearInterval(interval);
reject(new Error('Could not cancel build.')); return reject(new Error('Build canceled'));
} }
try {
const { stdout: buildContainers } = await asyncExecShell( const { stdout: buildContainers } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` `DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
); );
@ -54,10 +56,20 @@ export const post: RequestHandler = async (event) => {
count++; count++;
} catch (error) {} } catch (error) {}
}, 100); }, 100);
resolve();
}); });
}
export const post: RequestHandler = async (event) => {
const { buildId, applicationId } = await event.request.json();
if (!buildId) {
return {
status: 500,
body: {
message: 'Build ID not found.'
}
};
}
try {
await stopBuild(buildId, applicationId);
return { return {
status: 200, status: 200,
body: { body: {

View File

@ -1,21 +1,44 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { getDomain, getUserDetails } from '$lib/common'; import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import getPort from 'get-port';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export const get: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const domain = event.url.searchParams.get('domain');
if (!domain) {
return {
status: 500,
body: {
message: t.get('application.domain_required')
}
};
}
try {
await isDNSValid(event, domain);
return {
status: 200
};
} catch (error) {
return ErrorHandler(error);
}
};
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
let { fqdn, forceSave } = await event.request.json(); let { exposePort, fqdn, forceSave, dualCerts } = await event.request.json();
fqdn = fqdn.toLowerCase(); fqdn = fqdn.toLowerCase();
try { try {
const domain = getDomain(fqdn); const { isDNSCheckEnabled } = await db.prisma.setting.findFirst({});
const found = await db.isDomainConfigured({ id, fqdn }); const found = await db.isDomainConfigured({ id, fqdn });
if (found) { if (found) {
throw { throw {
@ -24,25 +47,22 @@ export const post: RequestHandler = async (event) => {
}) })
}; };
} }
if (!dev && !forceSave) {
let ip = [];
let localIp = [];
dns.setServers(['1.1.1.1', '8.8.8.8']);
try { if (exposePort) {
localIp = await dns.resolve4(event.url.hostname); exposePort = Number(exposePort);
} catch (error) {}
try {
ip = await dns.resolve4(domain);
} catch (error) {}
if (localIp?.length > 0) { if (exposePort < 1024 || exposePort > 65535) {
if (ip?.length === 0 || !ip.includes(localIp[0])) { throw { message: `Exposed Port needs to be between 1024 and 65535.` };
throw { }
message: t.get('application.dns_not_set_error', { domain: domain })
}; const publicPort = await getPort({ port: exposePort });
if (publicPort !== exposePort) {
throw { message: `Port ${exposePort} is already in use.` };
} }
} }
if (isDNSCheckEnabled && !dev && !forceSave) {
return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts });
} }
return { return {

View File

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

View File

@ -22,6 +22,7 @@ export const post: RequestHandler = async (event) => {
JSON.stringify({ JSON.stringify({
buildPack: applicationFound.buildPack, buildPack: applicationFound.buildPack,
port: applicationFound.port, port: applicationFound.port,
exposePort: applicationFound.exposePort,
installCommand: applicationFound.installCommand, installCommand: applicationFound.installCommand,
buildCommand: applicationFound.buildCommand, buildCommand: applicationFound.buildCommand,
startCommand: applicationFound.startCommand startCommand: applicationFound.startCommand

View File

@ -1,10 +1,8 @@
import { getUserDetails } from '$lib/common'; import { asyncExecShell, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { checkContainer, isContainerExited } from '$lib/haproxy'; import { checkContainer, getContainerUsage, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
import { setDefaultConfiguration } from '$lib/buildPacks/common'; import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@ -14,21 +12,14 @@ export const get: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
const appId = process.env['COOLIFY_APP_ID']; const appId = process.env['COOLIFY_APP_ID'];
let isRunning = false;
let isExited = false;
let githubToken = event.locals.cookies?.githubToken || null; let githubToken = event.locals.cookies?.githubToken || null;
let gitlabToken = event.locals.cookies?.gitlabToken || null; let gitlabToken = event.locals.cookies?.gitlabToken || null;
try { try {
const application = await db.getApplication({ id, teamId }); const application = await db.getApplication({ id, teamId });
if (application.destinationDockerId) {
isRunning = await checkContainer(application.destinationDocker.engine, id);
isExited = await isContainerExited(application.destinationDocker.engine, id);
}
return { return {
status: 200, status: 200,
body: { body: {
isRunning,
isExited,
application, application,
appId, appId,
githubToken, githubToken,
@ -52,6 +43,7 @@ export const post: RequestHandler = async (event) => {
buildPack, buildPack,
fqdn, fqdn,
port, port,
exposePort,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,
@ -67,6 +59,9 @@ export const post: RequestHandler = async (event) => {
baseBuildImage baseBuildImage
} = await event.request.json(); } = await event.request.json();
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
if (denoOptions) denoOptions = denoOptions.trim(); if (denoOptions) denoOptions = denoOptions.trim();
try { try {
@ -87,6 +82,7 @@ export const post: RequestHandler = async (event) => {
name, name,
fqdn, fqdn,
port, port,
exposePort,
installCommand, installCommand,
buildCommand, buildCommand,
startCommand, startCommand,

View File

@ -4,8 +4,7 @@
if (stuff?.application?.id) { if (stuff?.application?.id) {
return { return {
props: { props: {
application: stuff.application, application: stuff.application
isRunning: stuff.isRunning
} }
}; };
} }
@ -36,33 +35,46 @@
baseImages: Array<{ value: string; label: string }>; baseImages: Array<{ value: string; label: string }>;
baseBuildImages: Array<{ value: string; label: string }>; baseBuildImages: Array<{ value: string; label: string }>;
}; };
export let isRunning;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { notNodeDeployments, staticDeployments } from '$lib/components/common'; import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { post } from '$lib/api'; import { get, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
import { browser } from '$app/env'; import { browser } from '$app/env';
import { disabledButton } from '$lib/store'; import { disabledButton, status } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
const { id } = $page.params; const { id } = $page.params;
let domainEl: HTMLInputElement; let domainEl: HTMLInputElement;
let loading = false; let loading = false;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval;
let forceSave = false; let forceSave = false;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
$: isDisabled = !$session.isAdmin || $status.application.isRunning;
let wsgis = [ let wsgis = [
{ {
value: 'None', value: 'None',
@ -74,15 +86,29 @@
} }
]; ];
function containerClass() { function containerClass() {
if (!$session.isAdmin || isRunning) {
return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0'; return 'text-white border border-dashed border-coolgray-300 bg-transparent font-thin px-0';
} }
async function getUsage() {
if (usageLoading) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage.json`);
usage = data.usage;
usageLoading = false;
} }
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
await post(`/applications/${id}.json`, { ...application });
} }
onMount(() => {
domainEl.focus(); domainEl.focus();
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
}); });
async function changeSettings(name) { async function changeSettings(name) {
@ -125,15 +151,34 @@
} }
} }
async function handleSubmit() { async function handleSubmit() {
if (loading) return;
loading = true; loading = true;
try { try {
await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
await post(`/applications/${id}/check.json`, {
fqdn: application.fqdn,
forceSave,
dualCerts,
exposePort: application.exposePort
});
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
$disabledButton = false; $disabledButton = false;
forceSave = false;
return toast.push('Configurations saved.'); return toast.push('Configurations saved.');
} catch ({ error }) { } catch ({ error }) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) { if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true; forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(application.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@ -151,6 +196,19 @@
application.baseBuildImage = event.detail.value; application.baseBuildImage = event.detail.value;
await handleSubmit(); await handleSubmit();
} }
async function isDNSValid(domain, isWWW) {
try {
await get(`/applications/${id}/check.json?domain=${domain}`);
toast.push('DNS configuration is valid.');
isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true);
return true;
} catch ({ error }) {
errorNotification(error);
isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false);
return false;
}
}
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
@ -226,6 +284,33 @@
</a> </a>
</div> </div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-2xl font-bold">Application Usage</div>
<div class="mx-auto">
<dl class="relative mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class=" text-sm font-medium text-white">Used Memory / Memory Limit</dt>
<dd class="mt-1 text-xl font-semibold text-white">
{usage?.MemUsage}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Used CPU</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.CPUPerc}
</dd>
</div>
<div class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left">
<dt class="truncate text-sm font-medium text-white">Network IO</dt>
<dd class="mt-1 text-xl font-semibold text-white ">
{usage?.NetIO}
</dd>
</div>
</dl>
</div>
</div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">
<!-- svelte-ignore missing-declaration --> <!-- svelte-ignore missing-declaration -->
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
@ -319,20 +404,21 @@
value={application.destinationDocker.name} value={application.destinationDocker.name}
id="destination" id="destination"
disabled disabled
class="bg-transparent " class="bg-transparent"
/> />
</div> </div>
</div> </div>
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="baseImage" class="text-base font-bold text-stone-100" <label for="baseImage" class="text-base font-bold text-stone-100"
>{$t('application.base_image')}</label >{$t('application.base_image')}</label
> >
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
isDisabled={!$session.isAdmin || isRunning} {isDisabled}
containerClasses={containerClass()} containerClasses={isDisabled && containerClass()}
id="baseImages" id="baseImages"
showIndicator={!isRunning} showIndicator={!$status.application.isRunning}
items={application.baseImages} items={application.baseImages}
on:select={selectBaseImage} on:select={selectBaseImage}
value={application.baseImage} value={application.baseImage}
@ -341,6 +427,7 @@
</div> </div>
<Explainer text={$t('application.base_image_explainer')} /> <Explainer text={$t('application.base_image_explainer')} />
</div> </div>
{/if}
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<label for="baseBuildImage" class="text-base font-bold text-stone-100" <label for="baseBuildImage" class="text-base font-bold text-stone-100"
@ -349,10 +436,10 @@
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
isDisabled={!$session.isAdmin || isRunning} {isDisabled}
containerClasses={containerClass()} containerClasses={isDisabled && containerClass()}
id="baseBuildImages" id="baseBuildImages"
showIndicator={!isRunning} showIndicator={!$status.application.isRunning}
items={application.baseBuildImages} items={application.baseBuildImages}
on:select={selectBaseBuildImage} on:select={selectBaseBuildImage}
value={application.baseBuildImage} value={application.baseBuildImage}
@ -383,27 +470,62 @@
{/if} {/if}
<Explainer text={$t('application.https_explainer')} /> <Explainer text={$t('application.https_explainer')} />
</div> </div>
<div>
<input <input
readonly={!$session.isAdmin || isRunning} readonly={isDisabled}
disabled={!$session.isAdmin || isRunning} disabled={isDisabled}
bind:this={domainEl} bind:this={domainEl}
name="fqdn" name="fqdn"
id="fqdn" id="fqdn"
bind:value={application.fqdn} bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$" pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io" placeholder="eg: https://coollabs.io"
required
/> />
{#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
class="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="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 class="grid grid-cols-2 items-center pb-8"> <div class="grid grid-cols-2 items-center pb-8">
<Setting <Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
disabled={isRunning} disabled={$status.application.isRunning}
isCenter={false} isCenter={false}
bind:setting={dualCerts} bind:setting={dualCerts}
title={$t('application.ssl_www_and_non_www')} title={$t('application.ssl_www_and_non_www')}
description={$t('application.ssl_explainer')} description={$t('application.ssl_explainer')}
on:click={() => !isRunning && changeSettings('dualCerts')} on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
@ -451,9 +573,24 @@
/> />
</div> </div>
{/if} {/if}
{#if application.buildPack !== 'docker'}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="exposePort" class="text-base font-bold text-stone-100">Exposed Port</label>
<input
readonly={!$session.isAdmin && !$status.application.isRunning}
disabled={isDisabled}
name="exposePort"
id="exposePort"
bind:value={application.exposePort}
placeholder="12345"
/>
<Explainer
text={'You can expose your application to a port on the host system.<br><br>Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'}
/>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center pt-4">
<label for="installCommand" class="text-base font-bold text-stone-100" <label for="installCommand" class="text-base font-bold text-stone-100"
>{$t('application.install_command')}</label >{$t('application.install_command')}</label
> >
@ -491,7 +628,7 @@
</div> </div>
{/if} {/if}
{#if application.buildPack === 'docker'} {#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pt-4">
<label for="dockerFileLocation" class="text-base font-bold text-stone-100" <label for="dockerFileLocation" class="text-base font-bold text-stone-100"
>Dockerfile Location</label >Dockerfile Location</label
> >

View File

@ -1,4 +1,8 @@
<div class="lds-ripple absolute left-0"> <script>
export let position = 'absolute';
</script>
<div class="lds-ripple {position} left-0">
<div /> <div />
<div /> <div />
</div> </div>

View File

@ -129,9 +129,13 @@
{#if currentStatus === 'running'} {#if currentStatus === 'running'}
<button <button
on:click={cancelBuild} on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="bg-transparent hover:text-red-500 hover:bg-coolgray-500" class="bg-transparent hover:text-red-500 hover:bg-coolgray-500"
data-tooltip="Cancel build" data-tooltip="Cancel build"
> >
{#if cancelInprogress}
Cancelling...
{:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@ -146,6 +150,7 @@
<circle cx="12" cy="12" r="9" /> <circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" /> <path d="M10 10l4 4m0 -4l-4 4" />
</svg> </svg>
{/if}
</button> </button>
{/if} {/if}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex-col justify-center">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@ -2,11 +2,32 @@ import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import os from 'node:os';
import osu from 'node-os-utils';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { userId, teamId, status, body } = await getUserDetails(event); const { userId, teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const usage = event.url.searchParams.get('usage');
if (usage) {
try {
return {
status: 200,
body: {
uptime: os.uptime(),
memory: await osu.mem.info(),
cpu: {
load: os.loadavg(),
usage: await osu.cpu.usage(),
count: os.cpus().length
},
disk: await osu.drive.info()
}
};
} catch (error) {
return ErrorHandler(error);
}
} else {
try { try {
const applicationsCount = await db.prisma.application.count({ const applicationsCount = await db.prisma.application.count({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
@ -28,6 +49,7 @@ export const get: RequestHandler = async (event) => {
where: { userId }, where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } } include: { team: { include: { _count: { select: { users: true } } } } }
}); });
const settings = await db.prisma.setting.findFirst();
return { return {
body: { body: {
teams, teams,
@ -36,12 +58,14 @@ export const get: RequestHandler = async (event) => {
destinationsCount, destinationsCount,
teamsCount, teamsCount,
databasesCount, databasesCount,
servicesCount servicesCount,
settings
} }
}; };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);
} }
}
}; };
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {

View File

@ -11,6 +11,7 @@
import MySql from './_MySQL.svelte'; import MySql from './_MySQL.svelte';
import MongoDb from './_MongoDB.svelte'; import MongoDb from './_MongoDB.svelte';
import MariaDb from './_MariaDB.svelte';
import PostgreSql from './_PostgreSQL.svelte'; import PostgreSql from './_PostgreSQL.svelte';
import Redis from './_Redis.svelte'; import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte'; import CouchDb from './_CouchDb.svelte';
@ -190,6 +191,8 @@
<PostgreSql bind:database {isRunning} /> <PostgreSql bind:database {isRunning} />
{:else if database.type === 'mongodb'} {:else if database.type === 'mongodb'}
<MongoDb bind:database {isRunning} /> <MongoDb bind:database {isRunning} />
{:else if database.type === 'mariadb'}
<MariaDb bind:database {isRunning} />
{:else if database.type === 'redis'} {:else if database.type === 'redis'}
<Redis bind:database {isRunning} /> <Redis bind:database {isRunning} />
{:else if database.type === 'couchdb'} {:else if database.type === 'couchdb'}

View File

@ -0,0 +1,79 @@
<script>
export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations';
</script>
<div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div>
</div>
<div class="space-y-2 px-10">
<div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100"
>{$t('database.default_database')}</label
>
<CopyPasswordField
required
readonly={database.defaultDatabase}
disabled={database.defaultDatabase}
placeholder="{$t('forms.eg')}: mydb"
id="defaultDatabase"
name="defaultDatabase"
bind:value={database.defaultDatabase}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label>
<CopyPasswordField
readonly
disabled
placeholder={$t('forms.generated_automatically_after_start')}
id="dbUser"
name="dbUser"
value={database.dbUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="dbUserPassword"
name="dbUserPassword"
bind:value={database.dbUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label>
<CopyPasswordField
readonly
disabled
placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100"
>{$t('forms.roots_password')}</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
<Explainer text="Could be changed while the database is running." />
</div>
</div>

View File

@ -37,6 +37,7 @@
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.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 MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
@ -68,6 +69,8 @@
<CouchDB isAbsolute /> <CouchDB isAbsolute />
{:else if type.name === 'mongodb'} {:else if type.name === 'mongodb'}
<MongoDB isAbsolute /> <MongoDB isAbsolute />
{:else if type.name === 'mariadb'}
<MariaDB isAbsolute />
{:else if type.name === 'mysql'} {:else if type.name === 'mysql'}
<MySQL isAbsolute /> <MySQL isAbsolute />
{:else if type.name === 'postgresql'} {:else if type.name === 'postgresql'}

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.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 MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte';
@ -46,7 +47,7 @@
</div> </div>
</div> </div>
<div class="flex flex-col flex-wrap justify-center"> <div class="flex-col justify-center">
{#if !databases || ownDatabases.length === 0} {#if !databases || ownDatabases.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div> <div class="text-center text-xl font-bold">{$t('database.no_databases_found')}</div>
@ -66,6 +67,8 @@
<MongoDB isAbsolute /> <MongoDB isAbsolute />
{:else if database.type === 'mysql'} {:else if database.type === 'mysql'}
<MySQL isAbsolute /> <MySQL isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute /> <PostgreSQL isAbsolute />
{:else if database.type === 'redis'} {:else if database.type === 'redis'}
@ -98,6 +101,8 @@
<CouchDB isAbsolute /> <CouchDB isAbsolute />
{:else if database.type === 'mongodb'} {:else if database.type === 'mongodb'}
<MongoDB isAbsolute /> <MongoDB isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'mysql'} {:else if database.type === 'mysql'}
<MySQL isAbsolute /> <MySQL isAbsolute />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}

View File

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

View File

@ -39,10 +39,13 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
</script> </script>
<div class="flex space-x-1 p-6 text-2xl font-bold"> <div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold">
<div class="tracking-tight">{$t('application.destination')}</div> <div class="-mb-5 flex-col">
<span class="arrow-right-applications px-1">></span> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
<span class="pr-2">{destination.name}</span> Configuration
</div>
<span class="text-xs">{destination.name}</span>
</div>
</div> </div>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-4xl px-6">

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@
</a> </a>
{/if} {/if}
</div> </div>
<div class="flex justify-center"> <div class="flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div> <div class="text-center text-xl font-bold">{$t('destination.no_destination_found')}</div>

View File

@ -21,6 +21,10 @@
<script lang="ts"> <script lang="ts">
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { get } from '$lib/api';
import { onDestroy, onMount } from 'svelte';
import Trend from './_Trend.svelte';
import { session } from '$app/stores';
export let applicationsCount: number; export let applicationsCount: number;
export let sourcesCount: number; export let sourcesCount: number;
@ -28,89 +32,260 @@
export let teamsCount: number; export let teamsCount: number;
export let databasesCount: number; export let databasesCount: number;
export let servicesCount: number; export let servicesCount: number;
let loading = {
usage: false
};
let usageInterval = null;
let memoryWarning = false;
let cpuWarning = false;
let diskWarning = false;
let trends = {
memory: 'stable',
cpu: 'stable',
disk: 'stable'
};
let usage = {
cpu: {
load: [0, 0, 0],
count: 0,
usage: 0
},
memory: {
totalMemMb: 0,
freeMemMb: 0,
usedMemMb: 0,
freeMemPercentage: 0
},
disk: {
freePercentage: 0,
totalGb: 0,
usedGb: 0
}
};
async function getStatus() {
if (loading.usage) return;
try {
loading.usage = true;
const data = await get(`/dashboard.json?usage=true`);
if (data.memory.freeMemPercentage === usage.memory.freeMemPercentage) {
trends.memory = 'stable';
} else {
if (data.memory.freeMemPercentage > usage.memory.freeMemPercentage) {
trends.memory = 'up';
} else {
trends.memory = 'down';
}
}
if (data.cpu.usage === usage.cpu.usage) {
trends.cpu = 'stable';
} else {
if (data.cpu.usage > usage.cpu.usage) {
trends.cpu = 'up';
} else {
trends.cpu = 'down';
}
}
if (data.disk.freePercentage === usage.disk.freePercentage) {
trends.disk = 'stable';
} else {
if (data.disk.freePercentage > usage.disk.freePercentage) {
trends.disk = 'up';
} else {
trends.disk = 'down';
}
}
usage = data;
if (usage.memory.freeMemPercentage < 15) {
memoryWarning = true;
} else {
memoryWarning = false;
}
if (usage.cpu.usage > 90) {
cpuWarning = true;
} else {
cpuWarning = false;
}
if (usage.disk.freePercentage < 10) {
diskWarning = true;
} else {
diskWarning = false;
}
} catch (error) {
} finally {
loading.usage = false;
}
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
if ($session.teamId === '0') {
await getStatus();
usageInterval = setInterval(async () => {
await getStatus();
}, 1000);
}
});
</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>
</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="relative">
<div class="absolute inset-0 h-1/2" />
<div class="relative mx-auto px-4 sm:px-6 lg:px-8">
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl">
<dl class="gap-5 gap-y-16 sm:grid sm:grid-cols-3"> {#if $session.teamId === '0'}
<div class="px-6 text-2xl font-bold">Server Usage</div>
<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 Memory</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{(usage?.memory.totalMemMb).toFixed(0)}<span class="text-sm">MB</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 Memory</dt>
<dd class="mt-1 text-3xl font-semibold text-white ">
{(usage?.memory.usedMemMb).toFixed(0)}<span class="text-sm">MB</span>
</dd>
</div>
<div
class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"
class:bg-red-500={memoryWarning}
>
<dt class="truncate text-sm font-medium text-white">Free Memory</dt>
<dd class="mt-1 text-3xl font-semibold text-white">
{usage?.memory.freeMemPercentage}<span class="text-sm">%</span>
{#if !memoryWarning}
<Trend trend={trends.memory} />
{/if}
</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 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={cpuWarning}
>
<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 !cpuWarning}
<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>
</div>
<div
class="overflow-hidden rounded px-4 py-5 text-center sm:p-6 sm:text-left"
class:bg-red-500={diskWarning}
>
<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 !diskWarning}
<Trend trend={trends.disk} />
{/if}
</dd>
</div>
</dl>
<div class="px-6 pt-20 text-2xl font-bold">Resources</div>
{/if}
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3">
<a <a
href="/applications" href="/applications"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-green-500 no-underline transition duration-150 hover:bg-green-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.applications')}</dt>
{$t('index.applications')} <dd class="mt-1 text-3xl font-semibold ">
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{applicationsCount} {applicationsCount}
</dd> </dd>
</a> </a>
<a <a
href="/destinations" href="/destinations"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-sky-500 no-underline transition duration-150 hover:bg-sky-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.destinations')}</dt>
{$t('index.destinations')} <dd class="mt-1 text-3xl font-semibold ">
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{destinationsCount} {destinationsCount}
</dd> </dd>
</a> </a>
<a <a
href="/sources" href="/sources"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-orange-500 no-underline transition duration-150 hover:bg-orange-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.git_sources')}</dt>
{$t('index.git_sources')} <dd class="mt-1 text-3xl font-semibold">
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{sourcesCount} {sourcesCount}
</dd> </dd>
</a> </a>
</dl>
<dl class="mt-5 grid grid-cols-1 gap-5 px-2 sm:grid-cols-3">
<a <a
href="/databases" href="/databases"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-purple-500 no-underline transition duration-150 hover:bg-purple-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.databases')}</dt>
{$t('index.databases')} <dd class="mt-1 text-3xl font-semibold ">
</dt> {databasesCount}
<dd class="order-1 text-5xl font-extrabold ">{databasesCount}</dd> </dd>
</a> </a>
<a <a
href="/services" href="/services"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-pink-500 no-underline transition duration-150 hover:bg-pink-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.services')}</dt>
{$t('index.services')} <dd class="mt-1 text-3xl font-semibold ">
</dt> {servicesCount}
<dd class="order-1 text-5xl font-extrabold ">{servicesCount}</dd> </dd>
</a> </a>
<a <a
href="/iam" href="/iam"
sveltekit:prefetch sveltekit:prefetch
class="flex cursor-pointer flex-col rounded p-6 text-center text-cyan-500 no-underline transition duration-150 hover:bg-cyan-500 hover:text-white" 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"
> >
<dt class="order-2 mt-2 text-sm font-bold uppercase leading-6 text-white"> <dt class="truncate text-sm font-medium text-white">{$t('index.teams')}</dt>
{$t('index.teams')} <dd class="mt-1 text-3xl font-semibold ">
</dt>
<dd class="order-1 text-5xl font-extrabold ">
{teamsCount} {teamsCount}
</dd> </dd>
</a> </a>
</dl> </dl>
</div> </div>
</div>
</div>
</div> </div>

View File

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

View File

@ -1,13 +1,32 @@
<script lang="ts"> <script lang="ts">
import { session } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
export let service; export let service;
export let readOnly; export let readOnly;
export let isRunning;
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">Plausible Analytics</div> <div class="title">Plausible Analytics</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<label for="scriptName">Script Name</label>
<input
name="scriptName"
id="scriptName"
readonly={!$session.isAdmin && !isRunning}
disabled={!$session.isAdmin || isRunning}
placeholder="plausible.js"
bind:value={service.plausibleAnalytics.scriptName}
required
/>
<Explainer
text="Useful if you would like to rename the collector script to prevent it blocked by AdBlockers."
/>
</div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="email">{$t('forms.email')}</label> <label for="email">{$t('forms.email')}</label>
<input <input
@ -77,16 +96,3 @@
disabled disabled
/> />
</div> </div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="postgresqlPublicPort">Public Port</label>
<div class="col-span-2 ">
<CopyPasswordField
placeholder="{ $t('forms.generated_automatically_after_start') }"
readonly
disabled
id="postgresqlPublicPort"
name="postgresqlPublicPort"
value={service.plausibleAnalytics.postgresqlPublicPort}
/>
</div>
</div> -->

View File

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

View File

@ -18,6 +18,7 @@
let ftpUser = service.wordpress.ftpUser; let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword; let ftpPassword = service.wordpress.ftpPassword;
let ftpLoading = false; let ftpLoading = false;
let ownMysql = service.wordpress.ownMysql;
function generateUrl(publicPort) { function generateUrl(publicPort) {
return browser return browser
@ -40,7 +41,7 @@
publicPort, publicPort,
ftpUser: user, ftpUser: user,
ftpPassword: password ftpPassword: password
} = await post(`/services/${id}/wordpress/settings.json`, { } = await post(`/services/${id}/wordpress/ftp.json`, {
ftpEnabled ftpEnabled
}); });
ftpUrl = generateUrl(publicPort); ftpUrl = generateUrl(publicPort);
@ -52,6 +53,18 @@
} finally { } finally {
ftpLoading = false; ftpLoading = false;
} }
} else {
try {
if (name === 'ownMysql') {
ownMysql = !ownMysql;
}
await post(`/services/${id}/wordpress/settings.json`, {
ownMysql
});
service.wordpress.ownMysql = ownMysql;
} catch ({ error }) {
return errorNotification(error);
}
} }
} }
</script> </script>
@ -106,52 +119,96 @@ define('SUBDOMAIN_INSTALL', false);`
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MySQL</div> <div class="title">MySQL</div>
</div> </div>
<div class="grid grid-cols-2 items-center px-10">
<Setting
dataTooltip={$t('forms.must_be_stopped_to_modify')}
bind:setting={service.wordpress.ownMysql}
disabled={isRunning}
on:click={() => !isRunning && changeSettings('ownMysql')}
title="Use your own MySQL server"
description="Enables the use of your own MySQL server. If you don't have one, you can use the one provided by Coolify."
/>
</div>
{#if service.wordpress.ownMysql}
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlHost">Host</label>
<input
name="mysqlHost"
id="mysqlHost"
required
readonly={isRunning}
disabled={isRunning}
bind:value={service.wordpress.mysqlHost}
placeholder="{$t('forms.eg')}: db.coolify.io"
/>
</div>
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPort">Port</label>
<input
name="mysqlPort"
id="mysqlPort"
required
readonly={isRunning}
disabled={isRunning}
bind:value={service.wordpress.mysqlPort}
placeholder="{$t('forms.eg')}: 3306"
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlDatabase">{$t('index.database')}</label> <label for="mysqlDatabase">{$t('index.database')}</label>
<input <input
name="mysqlDatabase" name="mysqlDatabase"
id="mysqlDatabase" id="mysqlDatabase"
required required
readonly={readOnly} readonly={readOnly && !service.wordpress.ownMysql}
disabled={readOnly} disabled={readOnly && !service.wordpress.ownMysql}
bind:value={service.wordpress.mysqlDatabase} bind:value={service.wordpress.mysqlDatabase}
placeholder="{$t('forms.eg')}: wordpress_db" placeholder="{$t('forms.eg')}: wordpress_db"
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> {#if !service.wordpress.ownMysql}
<div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUser">{$t('forms.root_user')}</label> <label for="mysqlRootUser">{$t('forms.root_user')}</label>
<input <input
name="mysqlRootUser" name="mysqlRootUser"
id="mysqlRootUser" id="mysqlRootUser"
placeholder="MySQL {$t('forms.root_user')}" placeholder="MySQL {$t('forms.root_user')}"
value={service.wordpress.mysqlRootUser} value={service.wordpress.mysqlRootUser}
disabled readonly={isRunning || !service.wordpress.ownMysql}
readonly disabled={isRunning || !service.wordpress.ownMysql}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label> <label for="mysqlRootUserPassword">{$t('forms.roots_password')}</label>
<CopyPasswordField <CopyPasswordField
id="mysqlRootUserPassword" id="mysqlRootUserPassword"
isPasswordField isPasswordField
readonly readonly={isRunning || !service.wordpress.ownMysql}
disabled disabled={isRunning || !service.wordpress.ownMysql}
name="mysqlRootUserPassword" name="mysqlRootUserPassword"
value={service.wordpress.mysqlRootUserPassword} value={service.wordpress.mysqlRootUserPassword}
/> />
</div> </div>
{/if}
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlUser">{$t('forms.user')}</label> <label for="mysqlUser">{$t('forms.user')}</label>
<input name="mysqlUser" id="mysqlUser" value={service.wordpress.mysqlUser} disabled readonly /> <input
name="mysqlUser"
id="mysqlUser"
bind:value={service.wordpress.mysqlUser}
readonly={isRunning || !service.wordpress.ownMysql}
disabled={isRunning || !service.wordpress.ownMysql}
/>
</div> </div>
<div class="grid grid-cols-2 items-center px-10"> <div class="grid grid-cols-2 items-center px-10">
<label for="mysqlPassword">{$t('forms.password')}</label> <label for="mysqlPassword">{$t('forms.password')}</label>
<CopyPasswordField <CopyPasswordField
id="mysqlPassword" id="mysqlPassword"
isPasswordField isPasswordField
readonly readonly={isRunning || !service.wordpress.ownMysql}
disabled disabled={isRunning || !service.wordpress.ownMysql}
name="mysqlPassword" name="mysqlPassword"
value={service.wordpress.mysqlPassword} bind:value={service.wordpress.mysqlPassword}
/> />
</div> </div>

View File

@ -0,0 +1,21 @@
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
export const post: RequestHandler = async (event) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn, exposePort } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
try {
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);
}
};

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client'; import type { Service, DestinationDocker, Prisma } from '@prisma/client';
import { getServiceMainPort } from '$lib/components/common';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -30,6 +31,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
serviceSecret, serviceSecret,
exposePort,
fider: { fider: {
postgresqlUser, postgresqlUser,
postgresqlPassword, postgresqlPassword,
@ -48,6 +50,7 @@ export const post: RequestHandler = async (event) => {
} = service; } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('fider');
const { workdir } = await createDirectories({ repository: type, buildId: id }); const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type); const image = getServiceImage(type);
@ -97,6 +100,7 @@ export const post: RequestHandler = async (event) => {
volumes: [], volumes: [],
restart: 'always', restart: 'always',
labels: makeLabelForServices('fider'), labels: makeLabelForServices('fider'),
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
deploy: { deploy: {
restart_policy: { restart_policy: {
condition: 'on-failure', condition: 'on-failure',

View File

@ -11,11 +11,12 @@ export const post: RequestHandler = async (event) => {
let { let {
name, name,
fqdn, fqdn,
exposePort,
ghost: { mariadbDatabase } ghost: { mariadbDatabase }
} = await event.request.json(); } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase(); if (fqdn) fqdn = fqdn.toLowerCase();
try { try {
await db.updateGhostService({ id, fqdn, name, mariadbDatabase }); await db.updateGhostService({ id, fqdn, name, exposePort, mariadbDatabase });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {
return ErrorHandler(error); return ErrorHandler(error);

View File

@ -12,6 +12,7 @@ import type { RequestHandler } from '@sveltejs/kit';
import { ErrorHandler, getServiceImage } from '$lib/database'; import { ErrorHandler, getServiceImage } from '$lib/database';
import { makeLabelForServices } from '$lib/buildPacks/common'; import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile'; import type { ComposeFile } from '$lib/types/composeFile';
import { getServiceMainPort } from '$lib/components/common';
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@ -19,6 +20,8 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params; const { id } = event.params;
const port = getServiceMainPort('ghost');
try { try {
const service = await db.getService({ id, teamId }); const service = await db.getService({ id, teamId });
const { const {
@ -27,6 +30,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId, destinationDockerId,
destinationDocker, destinationDocker,
serviceSecret, serviceSecret,
exposePort,
fqdn, fqdn,
ghost: { ghost: {
defaultEmail, defaultEmail,
@ -89,6 +93,7 @@ export const post: RequestHandler = async (event) => {
volumes: [config.ghost.volume], volumes: [config.ghost.volume],
environment: config.ghost.environmentVariables, environment: config.ghost.environmentVariables,
restart: 'always', restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('ghost'), labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`], depends_on: [`${id}-mariadb`],
deploy: { deploy: {

Some files were not shown because too many files have changed in this diff Show More