Merge pull request #412 from coollabsio/next

v2.7.0
This commit is contained in:
Andras Bacsai 2022-05-06 14:55:45 +02:00 committed by GitHub
commit 22f1a3c908
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 946 additions and 233 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.

View File

@ -55,7 +55,6 @@ These are the predefined build packs, but with the Docker build pack, you can ho
- NuxtJS
- NextJS
- React/Preact
- NextJS
- Gatsby
- Svelte
- PHP
@ -68,6 +67,7 @@ These are the predefined build packs, but with the Docker build pack, you can ho
One-click database is ready to be used internally or shared over the internet:
- MongoDB
- MariaDB
- MySQL
- PostgreSQL
- CouchDB

View File

@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.6.3",
"version": "2.7.0",
"license": "AGPL-3.0",
"scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0",
@ -77,6 +77,7 @@
"generate-password": "1.7.0",
"get-port": "6.1.2",
"got": "12.0.3",
"is-ip": "^4.0.0",
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1",

64
pnpm-lock.yaml generated
View File

@ -1,4 +1,4 @@
lockfileVersion: 5.3
lockfileVersion: 5.4
specifiers:
'@iarna/toml': 2.2.5
@ -31,6 +31,7 @@ specifiers:
get-port: 6.1.2
got: 12.0.3
husky: 7.0.4
is-ip: ^4.0.0
js-cookie: 3.0.1
js-yaml: 4.1.0
jsonwebtoken: 8.5.1
@ -71,6 +72,7 @@ dependencies:
generate-password: 1.7.0
get-port: 6.1.2
got: 12.0.3
is-ip: 4.0.0
js-cookie: 3.0.1
js-yaml: 4.1.0
jsonwebtoken: 8.5.1
@ -88,29 +90,29 @@ devDependencies:
'@types/js-yaml': 4.0.5
'@types/node': 17.0.25
'@types/node-forge': 1.0.1
'@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3
'@typescript-eslint/eslint-plugin': 4.31.1_r3ph5xlwsrsg4ewthrjemd3cfq
'@typescript-eslint/parser': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu
'@zerodevx/svelte-toast': 0.7.1
autoprefixer: 10.4.4_postcss@8.4.12
cross-env: 7.0.3
cross-var: 1.1.0
eslint: 7.32.0
eslint-config-prettier: 8.5.0_eslint@7.32.0
eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0
eslint-plugin-svelte3: 3.4.1_4oxeyilw5mxcaksmcxtpjddhfe
husky: 7.0.4
lint-staged: 12.4.0
postcss: 8.4.12
prettier: 2.6.2
prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.47.0
prettier-plugin-svelte: 2.7.0_sqtt6dzjlskmywoml5ykunxlce
prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2
prisma: 3.11.1
svelte: 3.47.0
svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
svelte-check: 2.7.0_cp6olp7pwsfaq5mjijwt65d6uy
svelte-preprocess: 4.10.6_igaqrb5onrwvsmrrc32h4m72ha
svelte-select: 4.4.7
sveltekit-i18n: 2.1.2_svelte@3.47.0
tailwindcss: 3.0.24_ts-node@10.7.0
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
ts-node: 10.7.0_3z6inmgn4ud4moqealnfxgbl2m
tslib: 2.3.1
typescript: 4.6.3
@ -571,7 +573,7 @@ packages:
'@types/node': 17.0.25
dev: true
/@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c:
/@typescript-eslint/eslint-plugin/4.31.1_r3ph5xlwsrsg4ewthrjemd3cfq:
resolution:
{
integrity: sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA==
@ -585,8 +587,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/experimental-utils': 4.31.1_eslint@7.32.0+typescript@4.6.3
'@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3
'@typescript-eslint/experimental-utils': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu
'@typescript-eslint/parser': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu
'@typescript-eslint/scope-manager': 4.31.1
debug: 4.3.3
eslint: 7.32.0
@ -599,7 +601,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/experimental-utils/4.31.1_eslint@7.32.0+typescript@4.6.3:
/@typescript-eslint/experimental-utils/4.31.1_hrkuebk64jiu2ut2d2sm4oylnu:
resolution:
{
integrity: sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q==
@ -620,7 +622,7 @@ packages:
- typescript
dev: true
/@typescript-eslint/parser/4.31.1_eslint@7.32.0+typescript@4.6.3:
/@typescript-eslint/parser/4.31.1_hrkuebk64jiu2ut2d2sm4oylnu:
resolution:
{
integrity: sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ==
@ -2613,7 +2615,7 @@ packages:
eslint: 7.32.0
dev: true
/eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.47.0:
/eslint-plugin-svelte3/3.4.1_4oxeyilw5mxcaksmcxtpjddhfe:
resolution:
{
integrity: sha512-7p59WG8qV8L6wLdl4d/c3mdjkgVglQCdv5XOTk/iNPBKXuuV+Q0eFP5Wa6iJd/G2M1qR3BkLPEzaANOqKAZczw==
@ -3283,6 +3285,14 @@ packages:
- supports-color
dev: false
/ip-regex/5.0.0:
resolution:
{
integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dev: false
/is-binary-path/2.1.0:
resolution:
{
@ -3341,6 +3351,16 @@ packages:
is-extglob: 2.1.1
dev: true
/is-ip/4.0.0:
resolution:
{
integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
ip-regex: 5.0.0
dev: false
/is-number/7.0.0:
resolution:
{
@ -4103,7 +4123,7 @@ packages:
postcss: 8.4.12
dev: true
/postcss-load-config/3.1.4_postcss@8.4.12+ts-node@10.7.0:
/postcss-load-config/3.1.4_ysmyu6g5dtd6yanj6zrab4uqoy:
resolution:
{
integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==
@ -4120,7 +4140,7 @@ packages:
dependencies:
lilconfig: 2.0.5
postcss: 8.4.12
ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3
ts-node: 10.7.0_3z6inmgn4ud4moqealnfxgbl2m
yaml: 1.10.2
dev: true
@ -4175,7 +4195,7 @@ packages:
engines: { node: '>= 0.8.0' }
dev: true
/prettier-plugin-svelte/2.7.0_prettier@2.6.2+svelte@3.47.0:
/prettier-plugin-svelte/2.7.0_sqtt6dzjlskmywoml5ykunxlce:
resolution:
{
integrity: sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA==
@ -4858,7 +4878,7 @@ packages:
engines: { node: '>= 0.4' }
dev: true
/svelte-check/2.7.0_postcss@8.4.12+svelte@3.47.0:
/svelte-check/2.7.0_cp6olp7pwsfaq5mjijwt65d6uy:
resolution:
{
integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A==
@ -4874,7 +4894,7 @@ packages:
sade: 1.7.4
source-map: 0.7.3
svelte: 3.47.0
svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38
svelte-preprocess: 4.10.6_igaqrb5onrwvsmrrc32h4m72ha
typescript: 4.6.3
transitivePeerDependencies:
- '@babel/core'
@ -4907,7 +4927,7 @@ packages:
}
dev: false
/svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38:
/svelte-preprocess/4.10.6_igaqrb5onrwvsmrrc32h4m72ha:
resolution:
{
integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ==
@ -5039,7 +5059,7 @@ packages:
picocolors: 1.0.0
postcss: 8.4.12
postcss-js: 4.0.0_postcss@8.4.12
postcss-load-config: 3.1.4_postcss@8.4.12+ts-node@10.7.0
postcss-load-config: 3.1.4_ysmyu6g5dtd6yanj6zrab4uqoy
postcss-nested: 5.0.6_postcss@8.4.12
postcss-selector-parser: 6.0.10
postcss-value-parser: 4.2.0
@ -5113,7 +5133,7 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
/ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3:
/ts-node/10.7.0_3z6inmgn4ud4moqealnfxgbl2m:
resolution:
{
integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==

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

@ -84,6 +84,7 @@ model Application {
buildPack String?
projectId Int?
port Int?
exposePort Int?
installCommand String?
buildCommand String?
startCommand String?
@ -289,6 +290,7 @@ model Service {
id String @id @default(cuid())
name String
fqdn String?
exposePort Int?
dualCerts Boolean @default(false)
type String?
version String?

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
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> = [];
Dockerfile.push(`FROM ${imageforBuild}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
if (baseImage.includes('nginx')) {
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'));
};

View File

@ -2,7 +2,7 @@ import { buildCacheImageForLaravel, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId } = data;
const { workdir, applicationId, tag, buildId, port } = data;
const Dockerfile: Array<string> = [];
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`
);
Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push(`EXPOSE 80`);
Dockerfile.push(`EXPOSE ${port}`);
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';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory, buildId } = data;
const { workdir, baseDirectory, buildId, port } = data;
const Dockerfile: Array<string> = [];
let composerFound = false;
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(`EXPOSE 80`);
Dockerfile.push(`EXPOSE ${port}`);
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';
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> = [];
Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) {
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'));
};

View File

@ -12,7 +12,8 @@ const createDockerfile = async (data, image): Promise<void> => {
secrets,
pullmergeRequestId,
baseImage,
buildId
buildId,
port
} = data;
const Dockerfile: Array<string> = [];
@ -42,7 +43,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) {
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'));
};

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
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> = [];
Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) {
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'));
};

View File

@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
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> = [];
Dockerfile.push(`FROM ${image}`);
@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage.includes('nginx')) {
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'));
};

View File

@ -4,6 +4,8 @@ import { dev } from '$app/env';
import * as Sentry from '@sentry/node';
import { uniqueNamesGenerator, adjectives, colors, animals } 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 { buildLogQueue } from './queues';
@ -14,6 +16,7 @@ import Cookie from 'cookie';
import os from 'os';
import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq';
import { t } from './translations';
try {
if (!dev) {
@ -179,3 +182,97 @@ export function getDomain(domain: string): string {
export function getOsArch() {
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 CouchDb from './svg/databases/CouchDB.svelte';
import MongoDb from './svg/databases/MongoDB.svelte';
import MariaDb from './svg/databases/MariaDB.svelte';
import MySql from './svg/databases/MySQL.svelte';
import PostgreSql from './svg/databases/PostgreSQL.svelte';
import Redis from './svg/databases/Redis.svelte';
@ -17,6 +18,8 @@
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'mariadb'}
<MariaDb />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}

View File

@ -52,6 +52,12 @@ export const supportedDatabaseTypesAndVersions = [
versions: ['5.0', '4.4', '4.2']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{
name: 'mariadb',
fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb',
versions: ['10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
},
{
name: 'postgresql',
fancyName: 'PostgreSQL',
@ -215,3 +221,11 @@ export const supportedServiceTypesAndVersions = [
}
}
];
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,13 @@
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-10 w-10' : 'mx-auto w-8 h-8'}
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'absolute top-0 left-0 -m-10 h-20 w-20' : 'mx-auto w-8 h-8'}
id="Layer_1"
data-name="Layer 1"
viewBox="0 0 216.56 448.5"
><defs
><style>
.cls-1 {
fill: #10aa50;
}
.cls-2 {
fill: #b8c4c2;
}
.cls-3 {
fill: #12924f;
}
</style></defs
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 361.67651 499.33603"
><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
class="cls-2"
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"
/><path
class="cls-3"
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"
/>
</svg>
d="M203.77731,148.85754c-10.8147-12.762-20.13269-25.8139-22.02478-28.49224a.426.426,0,0,0-.70032.00006c-1.89172,2.6784-11.20758,15.73022-22.02191,28.49218-92.69141,118.085,14.62982,197.75507,14.62982,197.75507l.87.60461c.8136,12.32624,2.83508,30.041,2.83508,30.041H185.442s2.01282-17.63849,2.83-29.96106l.87549-.68451S296.46774,266.94257,203.77731,148.85754ZM181.404,344.88123h-.001s-4.811-4.10383-6.10962-6.16l-.01172-.22131,5.81946-128.56055a.30281.30281,0,0,1,.605,0l5.81946,128.56036-.01135.22065C186.21652,340.77625,181.404,344.88123,181.404,344.88123Z"
fill="#00684a"
/></svg
>

View File

@ -278,6 +278,7 @@ export async function configureApplication({
name,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
@ -297,6 +298,7 @@ export async function configureApplication({
name: string;
fqdn: string;
port: number;
exposePort: number;
installCommand: string;
buildCommand: string;
startCommand: string;
@ -318,6 +320,7 @@ export async function configureApplication({
buildPack,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,

View File

@ -127,60 +127,73 @@ export function getServiceImages(type: string): string[] {
export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }):
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MONGODB_ROOT_USER: string;
MONGODB_ROOT_PASSWORD: string;
};
}
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MONGODB_ROOT_USER: string;
MONGODB_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: 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;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
const {
id,
dbUser,
@ -207,6 +220,20 @@ export function generateDatabaseConfiguration(database: Database & { settings: D
volume: `${id}-${type}-data:/bitnami/mysql/data`,
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') {
return {
privatePort: 27017,

View File

@ -184,6 +184,10 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) {
await asyncExecShell(
`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') {
if (isRoot) {
await asyncExecShell(

View File

@ -327,35 +327,40 @@ export async function updatePlausibleAnalyticsService({
id,
fqdn,
email,
exposePort,
username,
name
}: {
id: string;
fqdn: string;
exposePort?: number;
name: string;
email: string;
username: string;
}): Promise<void> {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } });
await prisma.service.update({ where: { id }, data: { name, fqdn, exposePort } });
}
export async function updateService({
id,
fqdn,
exposePort,
name
}: {
id: string;
fqdn: string;
exposePort?: number;
name: string;
}): 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 updateFiderService({
id,
fqdn,
name,
exposePort,
emailNoreply,
emailMailgunApiKey,
emailMailgunDomain,
@ -368,6 +373,7 @@ export async function updateFiderService({
}: {
id: string;
fqdn: string;
exposePort?: number;
name: string;
emailNoreply: string;
emailMailgunApiKey: string;
@ -384,6 +390,7 @@ export async function updateFiderService({
data: {
fqdn,
name,
exposePort,
fider: {
update: {
emailNoreply,
@ -405,18 +412,20 @@ export async function updateWordpress({
id,
fqdn,
name,
exposePort,
mysqlDatabase,
extraConfig
}: {
id: string;
fqdn: string;
name: string;
exposePort?: number;
mysqlDatabase: string;
extraConfig: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } }
data: { fqdn, name, exposePort, wordpress: { update: { mysqlDatabase, extraConfig } } }
});
}
@ -434,16 +443,18 @@ export async function updateGhostService({
id,
fqdn,
name,
exposePort,
mariadbDatabase
}: {
id: string;
fqdn: string;
name: string;
exposePort?: number;
mariadbDatabase: string;
}): Promise<Service> {
return await prisma.service.update({
where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
data: { fqdn, name, exposePort, ghost: { update: { mariadbDatabase } } }
});
}

View File

@ -292,26 +292,28 @@ export async function generateSSLCerts(): Promise<void> {
}
export async function renewSSLCerts(): Promise<void> {
const host = 'unix:///var/run/docker.sock';
await asyncExecShell(`docker pull alpine:latest`);
const certbotImage =
process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest';
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"`
);
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);
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');
}
await reloadHaproxy('unix:///var/run/docker.sock');
}

View File

@ -178,9 +178,11 @@
"delete_application": "Delete application",
"permission_denied_delete_application": "You do not have permission to delete this application",
"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.",
"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_repository": "Git Repository",
"build_pack": "Build Pack",
@ -204,6 +206,7 @@
"enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
"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.",
"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.",
@ -311,7 +314,7 @@
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"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_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_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.",
"expose_a_port": "Exposer un port",
"features": "Caractéristiques",
"git_repository": "Dépôt Git",
"git_source": "Source Git",

View File

@ -48,6 +48,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
pythonModule,
pythonVariable,
denoOptions,
exposePort,
baseImage,
baseBuildImage
} = job.data;
@ -152,6 +153,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
JSON.stringify({
buildPack,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
@ -207,7 +209,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
tag,
workdir,
docker,
port,
port: exposePort ? `${exposePort}:${port}` : port,
installCommand,
buildCommand,
startCommand,
@ -263,7 +265,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
repository,
branch,
projectId,
port,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
@ -298,6 +300,7 @@ export default async function (job: Job<BuilderJob, void, string>): Promise<void
labels,
depends_on: [],
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },

View File

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

View File

@ -117,7 +117,7 @@ const cron = async (): Promise<void> => {
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
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) => {
console.log('cron failed to start');

View File

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

View File

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

View File

@ -17,21 +17,25 @@ export const post: RequestHandler = async (event) => {
let count = 0;
await new Promise<void>(async (resolve, reject) => {
const job = await buildQueue.getJob(buildId);
if (!job) {
return resolve();
}
const {
destinationDocker: { engine }
} = job.data;
} = job?.data;
const host = getEngine(engine);
let interval = setInterval(async () => {
const { status } = await db.prisma.build.findUnique({ where: { id: buildId } });
if (status === 'failed') {
clearInterval(interval);
return resolve();
}
if (count > 1200) {
clearInterval(interval);
reject(new Error('Could not cancel build.'));
}
try {
const data = await db.prisma.build.findUnique({ where: { id: buildId } });
if (data?.status === 'failed') {
clearInterval(interval);
return resolve();
}
if (count > 60) {
clearInterval(interval);
reject(new Error('Could not cancel build.'));
}
const { stdout: buildContainers } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
);
@ -53,11 +57,14 @@ export const post: RequestHandler = async (event) => {
}
count++;
} catch (error) {}
}, 100);
}, 1000);
resolve();
});
const data = await db.prisma.build.findUnique({ where: { id: buildId } });
if (data?.status === 'queued' || data?.status === 'running') {
await db.prisma.build.update({ where: { id: buildId }, data: { status: 'failed' } });
}
return {
status: 200,
body: {

View File

@ -1,21 +1,44 @@
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 { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit';
import { promises as dns } from 'dns';
import getPort from 'get-port';
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) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { fqdn, forceSave } = await event.request.json();
let { exposePort, fqdn, forceSave, dualCerts } = await event.request.json();
fqdn = fqdn.toLowerCase();
try {
const domain = getDomain(fqdn);
const { isDNSCheckEnabled } = await db.prisma.setting.findFirst({});
const found = await db.isDomainConfigured({ id, fqdn });
if (found) {
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 {
localIp = await dns.resolve4(event.url.hostname);
} catch (error) {}
try {
ip = await dns.resolve4(domain);
} catch (error) {}
if (exposePort) {
exposePort = Number(exposePort);
if (localIp?.length > 0) {
if (ip?.length === 0 || !ip.includes(localIp[0])) {
throw {
message: t.get('application.dns_not_set_error', { domain: domain })
};
}
if (exposePort < 1024 || exposePort > 65535) {
throw { message: `Expose 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.` };
}
}
if (isDNSCheckEnabled && !dev && !forceSave) {
return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts });
}
return {

View File

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

View File

@ -3,8 +3,6 @@ import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { checkContainer, isContainerExited } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api';
import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => {
@ -52,6 +50,7 @@ export const post: RequestHandler = async (event) => {
buildPack,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,
@ -67,6 +66,9 @@ export const post: RequestHandler = async (event) => {
baseBuildImage
} = await event.request.json();
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
if (denoOptions) denoOptions = denoOptions.trim();
try {
@ -87,6 +89,7 @@ export const post: RequestHandler = async (event) => {
name,
fqdn,
port,
exposePort,
installCommand,
buildCommand,
startCommand,

View File

@ -45,9 +45,9 @@
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
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 { post } from '$lib/api';
import { get, post } from '$lib/api';
import cuid from 'cuid';
import { browser } from '$app/env';
import { disabledButton } from '$lib/store';
@ -63,6 +63,10 @@
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let wsgis = [
{
value: 'None',
@ -127,13 +131,31 @@
async function handleSubmit() {
loading = true;
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 });
$disabledButton = false;
forceSave = false;
return toast.push('Configurations saved.');
} catch ({ error }) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
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);
} finally {
@ -151,6 +173,19 @@
application.baseBuildImage = event.detail.value;
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>
<div class="flex items-center space-x-2 p-5 px-6 font-bold">
@ -383,17 +418,52 @@
{/if}
<Explainer text={$t('application.https_explainer')} />
</div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
required
/>
<div>
<input
readonly={!$session.isAdmin || isRunning}
disabled={!$session.isAdmin || isRunning}
bind:this={domainEl}
name="fqdn"
id="fqdn"
bind:value={application.fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
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 class="grid grid-cols-2 items-center pb-8">
<Setting
@ -451,9 +521,23 @@
/>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
{#if application.buildPack !== 'docker'}
<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}
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"
>{$t('application.install_command')}</label
>
@ -491,7 +575,7 @@
</div>
{/if}
{#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"
>Dockerfile Location</label
>

View File

@ -11,6 +11,7 @@
import MySql from './_MySQL.svelte';
import MongoDb from './_MongoDB.svelte';
import MariaDb from './_MariaDB.svelte';
import PostgreSql from './_PostgreSQL.svelte';
import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte';
@ -190,6 +191,8 @@
<PostgreSql bind:database {isRunning} />
{:else if database.type === 'mongodb'}
<MongoDb bind:database {isRunning} />
{:else if database.type === 'mariadb'}
<MariaDb bind:database {isRunning} />
{:else if database.type === 'redis'}
<Redis bind:database {isRunning} />
{: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 CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
@ -68,6 +69,8 @@
<CouchDB isAbsolute />
{:else if type.name === 'mongodb'}
<MongoDB isAbsolute />
{:else if type.name === 'mariadb'}
<MariaDB isAbsolute />
{:else if type.name === 'mysql'}
<MySQL isAbsolute />
{:else if type.name === 'postgresql'}

View File

@ -3,6 +3,7 @@
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte';
import CouchDB from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDB from '$lib/components/svg/databases/MongoDB.svelte';
import MariaDB from '$lib/components/svg/databases/MariaDB.svelte';
import MySQL from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
@ -66,6 +67,8 @@
<MongoDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'postgresql'}
<PostgreSQL isAbsolute />
{:else if database.type === 'redis'}
@ -98,6 +101,8 @@
<CouchDB isAbsolute />
{:else if database.type === 'mongodb'}
<MongoDB isAbsolute />
{:else if database.type === 'mariadb'}
<MariaDB isAbsolute />
{:else if database.type === 'mysql'}
<MySQL isAbsolute />
{:else if database.type === 'postgresql'}

View File

@ -27,6 +27,7 @@
let loading = false;
let loadingVerification = false;
let dualCerts = service.dualCerts;
let showExposePort = service.exposePort !== null;
async function handleSubmit() {
loading = true;
@ -160,6 +161,32 @@
on:click={() => !isRunning && changeSettings('dualCerts')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={showExposePort}
on:click={() => {
showExposePort = !showExposePort;
service.exposePort = undefined;
}}
title={$t('application.expose_a_port')}
description="Expose a port to the host system"
/>
</div>
{#if showExposePort}
<div class="grid grid-cols-2 items-center">
<label for="exposePort" class="text-base font-bold text-stone-100">Expose Port</label>
<input
readonly={!$session.isAdmin}
name="exposePort"
id="exposePort"
bind:value={service.exposePort}
placeholder="12345"
/>
</div>
{/if}
{#if service.type === 'plausibleanalytics'}
<PlausibleAnalytics bind:service {readOnly} />
{:else if service.type === 'minio'}

View File

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

View File

@ -12,6 +12,7 @@ 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);
@ -19,6 +20,8 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
const port = getServiceMainPort('ghost');
try {
const service = await db.getService({ id, teamId });
const {
@ -27,6 +30,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId,
destinationDocker,
serviceSecret,
exposePort,
fqdn,
ghost: {
defaultEmail,
@ -89,6 +93,7 @@ export const post: RequestHandler = async (event) => {
volumes: [config.ghost.volume],
environment: config.ghost.environmentVariables,
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('ghost'),
depends_on: [`${id}-mariadb`],
deploy: {

View File

@ -9,13 +9,15 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
console.log(error);
return ErrorHandler(error);
}
};

View File

@ -6,6 +6,7 @@ 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);
@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => {
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('languagetool');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
environment: config.environmentVariables,
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: [config.volume],
labels: makeLabelForServices('languagetool'),
deploy: {

View File

@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -18,9 +19,11 @@ export const post: RequestHandler = async (event) => {
const {
meiliSearch: { masterKey }
} = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('meilisearch');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -47,6 +50,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
environment: config.environmentVariables,
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
volumes: [config.volume],
labels: makeLabelForServices('meilisearch'),
deploy: {

View File

@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -7,6 +7,7 @@ import { startHttpProxy } from '$lib/haproxy';
import { ErrorHandler, getFreePort, 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);
@ -22,12 +23,14 @@ export const post: RequestHandler = async (event) => {
fqdn,
destinationDockerId,
destinationDocker,
exposePort,
minio: { rootUser, rootUserPassword },
serviceSecret
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('minio');
const publicPort = await getFreePort();
@ -62,6 +65,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
volumes: [config.volume],
restart: 'always',
...(exposePort && { ports: [`${port}:${port}`] }),
labels: makeLabelForServices('minio'),
deploy: {
restart_policy: {

View File

@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => {
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
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);

View File

@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => {
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('nocodb');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -40,6 +43,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
environment: config.environmentVariables,
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('nocodb'),
deploy: {
restart_policy: {

View File

@ -11,14 +11,16 @@ export const post: RequestHandler = async (event) => {
let {
name,
fqdn,
exposePort,
plausibleAnalytics: { email, username }
} = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
if (email) email = email.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
try {
await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username });
await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -22,6 +23,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId,
destinationDocker,
serviceSecret,
exposePort,
plausibleAnalytics: {
id: plausibleDbId,
username,
@ -78,6 +80,7 @@ export const post: RequestHandler = async (event) => {
}
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('plausibleanalytics');
const { workdir } = await createDirectories({ repository: type, buildId: id });
@ -132,6 +135,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
networks: [network],
environment: config.plausibleAnalytics.environmentVariables,
restart: 'always',
...(exposePort && { ports: [`${port}:${exposePort}`] }),
depends_on: [`${id}-postgresql`, `${id}-clickhouse`],
labels: makeLabelForServices('plausibleAnalytics'),
deploy: {

View File

@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -8,6 +8,7 @@ import { makeLabelForServices } from '$lib/buildPacks/common';
import type { ComposeFile } from '$lib/types/composeFile';
import type { Service, DestinationDocker, Prisma } from '@prisma/client';
import bcrypt from 'bcryptjs';
import { getServiceMainPort } from '$lib/components/common';
export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event);
@ -24,6 +25,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId,
destinationDocker,
serviceSecret,
exposePort,
umami: {
umamiAdminPassword,
postgresqlUser,
@ -34,6 +36,7 @@ export const post: RequestHandler = async (event) => {
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('umami');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -156,6 +159,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
volumes: [],
restart: 'always',
...(exposePort ? { ports: [`${port}:${port}`] } : {}),
labels: makeLabelForServices('umami'),
deploy: {
restart_policy: {

View File

@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => {
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('uptimekuma');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => {
volumes: [config.volume],
environment: config.environmentVariables,
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('uptimekuma'),
deploy: {
restart_policy: {

View File

@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => {
if (status === 401) return { status, body };
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit';
import { getServiceImage, ErrorHandler } 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);
@ -15,10 +16,12 @@ export const post: RequestHandler = async (event) => {
try {
const service = await db.getService({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } =
service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vaultwarden');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -43,6 +46,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
volumes: [config.volume],
restart: 'always',
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('vaultWarden'),
deploy: {
restart_policy: {

View File

@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => {
const { id } = event.params;
let { name, fqdn } = await event.request.json();
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 });
await db.updateService({ id, fqdn, name, exposePort });
return { status: 201 };
} catch (error) {
return ErrorHandler(error);

View File

@ -6,6 +6,7 @@ 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);
@ -22,11 +23,13 @@ export const post: RequestHandler = async (event) => {
destinationDocker,
serviceSecret,
persistentStorage,
exposePort,
vscodeserver: { password }
} = service;
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const port = getServiceMainPort('vscodeserver');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
@ -75,6 +78,7 @@ export const post: RequestHandler = async (event) => {
networks: [network],
volumes: [config.volume, ...volumes],
restart: 'always',
...(exposePort ? { ports: [`${port}:${exposePort}`] } : {}),
labels: makeLabelForServices('vscodeServer'),
deploy: {
restart_policy: {

View File

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

View File

@ -8,7 +8,6 @@ import type { ComposeFile } from '$lib/types/composeFile';
import type { RequestHandler } from '@sveltejs/kit';
import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port';
import yaml from 'js-yaml';
export const post: RequestHandler = async (event) => {

View File

@ -6,6 +6,7 @@ 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);
@ -22,6 +23,7 @@ export const post: RequestHandler = async (event) => {
destinationDockerId,
serviceSecret,
destinationDocker,
exposePort,
wordpress: {
mysqlDatabase,
mysqlUser,
@ -35,6 +37,7 @@ export const post: RequestHandler = async (event) => {
const network = destinationDockerId && destinationDocker.network;
const host = getEngine(destinationDocker.engine);
const image = getServiceImage(type);
const port = getServiceMainPort('wordpress');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const config = {
@ -76,6 +79,7 @@ export const post: RequestHandler = async (event) => {
volumes: [config.wordpress.volume],
networks: [network],
restart: 'always',
...(exposePort ? { ports: [`${port}:${port}`] } : {}),
depends_on: [`${id}-mysql`],
labels: makeLabelForServices('wordpress'),
deploy: {

View File

@ -1,25 +1,54 @@
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import { dev } from '$app/env';
import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common';
import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
import { t } from '$lib/translations';
import type { RequestHandler } from '@sveltejs/kit';
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) => {
const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body };
const { id } = event.params;
let { fqdn } = await event.request.json();
let { fqdn, forceSave, dualCerts, isDNSCheckEnabled } = await event.request.json();
if (fqdn) fqdn = fqdn.toLowerCase();
try {
const found = await db.isDomainConfigured({ id, fqdn });
console.log(found);
if (found) {
throw {
message: t.get('application.domain_already_in_use', {
domain: getDomain(fqdn).replace('www.', '')
})
};
}
if (isDNSCheckEnabled && !forceSave) {
return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts });
}
return {
status: found ? 500 : 200,
body: {
error:
found && t.get('application.domain_already_in_use', { domain: fqdn.replace('www.', '') })
}
status: 200
};
} catch (error) {
return ErrorHandler(error);

View File

@ -1,3 +1,4 @@
import { dev } from '$app/env';
import { getUserDetails } from '$lib/common';
import * as db from '$lib/database';
import { listSettings, ErrorHandler } from '$lib/database';
@ -71,7 +72,8 @@ export const post: RequestHandler = async (event) => {
minPort,
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled
isDNSCheckEnabled,
forceSave
} = await event.request.json();
try {
const { id } = await db.listSettings();

View File

@ -31,7 +31,7 @@
import Setting from '$lib/components/Setting.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { errorNotification } from '$lib/form';
import { del, post } from '$lib/api';
import { del, get, post } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { browser } from '$app/env';
import { getDomain } from '$lib/components/common';
@ -47,7 +47,11 @@
let minPort = settings.minPort;
let maxPort = settings.maxPort;
let forceSave = false;
let fqdn = settings.fqdn;
let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
let isNonWWWDomainOK = false;
let isWWWDomainOK = false;
let isFqdnSet = !!settings.fqdn;
let loading = {
save: false,
@ -69,6 +73,7 @@
}
async function changeSettings(name) {
try {
resetView();
if (name === 'isRegistrationEnabled') {
isRegistrationEnabled = !isRegistrationEnabled;
}
@ -95,8 +100,10 @@
async function handleSubmit() {
try {
loading.save = true;
nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, '');
if (fqdn !== settings.fqdn) {
await post(`/settings/check.json`, { fqdn });
await post(`/settings/check.json`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled });
await post(`/settings.json`, { fqdn });
return window.location.reload();
}
@ -105,7 +112,22 @@
settings.minPort = minPort;
settings.maxPort = maxPort;
}
forceSave = false;
} catch ({ error }) {
if (error?.startsWith($t('application.dns_not_set_partial_error'))) {
forceSave = true;
if (dualCerts) {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
const isWWW = getDomain(settings.fqdn).includes('www.');
if (isWWW) {
isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true);
} else {
isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false);
}
}
}
return errorNotification(error);
} finally {
loading.save = false;
@ -119,6 +141,21 @@
return errorNotification(error);
}
}
async function isDNSValid(domain, isWWW) {
try {
await get(`/settings/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;
}
}
function resetView() {
forceSave = false;
}
</script>
<div class="flex space-x-1 p-6 font-bold">
@ -131,11 +168,18 @@
<div class="title font-bold">{$t('index.global_settings')}</div>
<button
type="submit"
class:bg-green-600={!loading.save}
class:bg-orange-600={forceSave}
class:hover:bg-green-500={!loading.save}
class:hover:bg-orange-400={forceSave}
disabled={loading.save}
class:bg-yellow-500={!loading.save}
class:hover:bg-yellow-400={!loading.save}
class="mx-2 ">{loading.save ? $t('forms.saving') : $t('forms.save')}</button
>{loading.save
? $t('forms.saving')
: forceSave
? $t('forms.confirm_continue')
: $t('forms.save')}</button
>
{#if isFqdnSet}
<button
on:click|preventDefault={removeFqdn}
@ -160,11 +204,47 @@
bind:value={fqdn}
readonly={!$session.isAdmin || isFqdnSet}
disabled={!$session.isAdmin || isFqdnSet}
on:input={resetView}
name="fqdn"
id="fqdn"
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="{$t('forms.eg')}: https://coolify.io"
/>
{#if forceSave}
<div class="flex-col space-y-2 pt-4 text-center">
{#if isNonWWWDomainOK}
<button
class="bg-green-600 hover:bg-green-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is OK, click to recheck.</button
>
{:else}
<button
class="bg-red-600 hover:bg-red-500"
on:click|preventDefault={() => isDNSValid(getDomain(nonWWWDomain), false)}
>DNS settings for {nonWWWDomain} is invalid, click to recheck.</button
>
{/if}
{#if dualCerts}
{#if isWWWDomainOK}
<button
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 class="grid grid-cols-2 items-start py-6">

View File

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

View File

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