Merge pull request #858 from coollabsio/next

v3.12.12
This commit is contained in:
Andras Bacsai 2023-01-17 12:33:51 +01:00 committed by GitHub
commit becf37b676
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 563 additions and 247 deletions

View File

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

View File

@ -186,6 +186,7 @@ model ApplicationSettings {
isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])

View File

@ -53,12 +53,22 @@ export default async function (data) {
value['environment'] = [...environment, ...envs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (Object.keys(build).length > 0) {
build = Object.entries(build).map(([key, value]) => `${key}=${value}`);
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of buildArgs) {
const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg);
}
}
}
value['build'] = {
...build,
args: [...(build?.args || []), ...buildEnvs]
args: finalArgs
};
value['labels'] = labels;

View File

@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa';
export const version = '3.12.11';
export const version = '3.12.12';
export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN =
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216';
@ -714,8 +714,10 @@ export async function startTraefikProxy(id: string): Promise<void> {
--network coolify-infra \
-p "80:80" \
-p "443:443" \
${isDev ? '-p "8080:8080"' : ''} \
--name coolify-proxy \
-d ${defaultTraefikImage} \
${isDev ? '--api.insecure=true' : ''} \
--entrypoints.web.address=:80 \
--entrypoints.web.forwardedHeaders.insecure=true \
--entrypoints.websecure.address=:443 \

View File

@ -503,14 +503,24 @@ export async function saveApplicationSettings(
projectId,
isBot,
isDBBranching,
isCustomSSL
isCustomSSL,
isHttp2
} = request.body;
await prisma.application.update({
where: { id },
data: {
fqdn: isBot ? null : undefined,
settings: {
update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL }
update: {
debug,
previews,
dualCerts,
autodeploy,
isBot,
isDBBranching,
isCustomSSL,
isHttp2
}
}
},
include: { destinationDocker: true }

View File

@ -1,154 +1,170 @@
import type { OnlyId } from "../../../../types";
import type { OnlyId } from '../../../../types';
export interface SaveApplication extends OnlyId {
Body: {
name: string,
buildPack: string,
fqdn: string,
port: number,
exposePort: number,
installCommand: string,
buildCommand: string,
startCommand: string,
baseDirectory: string,
publishDirectory: string,
pythonWSGI: string,
pythonModule: string,
pythonVariable: string,
dockerFileLocation: string,
denoMainFile: string,
denoOptions: string,
baseImage: string,
gitCommitHash: string,
baseBuildImage: string,
deploymentType: string,
baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string,
simpleDockerfile: string,
dockerRegistryImageName: string
}
Body: {
name: string;
buildPack: string;
fqdn: string;
port: number;
exposePort: number;
installCommand: string;
buildCommand: string;
startCommand: string;
baseDirectory: string;
publishDirectory: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
dockerFileLocation: string;
denoMainFile: string;
denoOptions: string;
baseImage: string;
gitCommitHash: string;
baseBuildImage: string;
deploymentType: string;
baseDatabaseBranch: string;
dockerComposeFile: string;
dockerComposeFileLocation: string;
dockerComposeConfiguration: string;
simpleDockerfile: string;
dockerRegistryImageName: string;
};
}
export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean };
Querystring: { domain: string };
Body: {
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
branch: string;
projectId: number;
isBot: boolean;
isDBBranching: boolean;
isCustomSSL: boolean;
isHttp2: boolean;
};
}
export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; };
Body: { force: boolean }
Querystring: { domain: string };
Body: { force: boolean };
}
export interface CheckDomain extends OnlyId {
Querystring: { domain: string; };
Querystring: { domain: string };
}
export interface CheckDNS extends OnlyId {
Querystring: { domain: string; };
Body: {
exposePort: number,
fqdn: string,
forceSave: boolean,
dualCerts: boolean
}
Querystring: { domain: string };
Body: {
exposePort: number;
fqdn: string;
forceSave: boolean;
dualCerts: boolean;
};
}
export interface DeployApplication {
Querystring: { domain: string }
Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean }
Querystring: { domain: string };
Body: { pullmergeRequestId: string | null; branch: string; forceRebuild?: boolean };
}
export interface GetImages {
Body: { buildPack: string, deploymentType: string }
Body: { buildPack: string; deploymentType: string };
}
export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string }
Body: {
gitSourceId?: string | null;
forPublic?: boolean;
type?: string;
simpleDockerfile?: string;
};
}
export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string }
Querystring: { repository: string; branch: string };
}
export interface SaveDestination extends OnlyId {
Body: { destinationId: string }
Body: { destinationId: string };
}
export interface SaveSecret extends OnlyId {
Body: {
name: string,
value: string,
isBuildSecret: boolean,
previewSecret: boolean,
isNew: boolean
}
Body: {
name: string;
value: string;
isBuildSecret: boolean;
previewSecret: boolean;
isNew: boolean;
};
}
export interface DeleteSecret extends OnlyId {
Body: { name: string }
Body: { name: string };
}
export interface SaveStorage extends OnlyId {
Body: {
path: string,
newStorage: boolean,
storageId: string
}
Body: {
path: string;
newStorage: boolean;
storageId: string;
};
}
export interface DeleteStorage extends OnlyId {
Body: {
path: string,
}
Body: {
path: string;
};
}
export interface GetApplicationLogs {
Params: {
id: string,
containerId: string
}
Querystring: {
since: number,
}
Params: {
id: string;
containerId: string;
};
Querystring: {
since: number;
};
}
export interface GetBuilds extends OnlyId {
Querystring: {
buildId: string
skip: number,
}
Querystring: {
buildId: string;
skip: number;
};
}
export interface GetBuildIdLogs {
Params: {
id: string,
buildId: string
},
Querystring: {
sequence: number
}
Params: {
id: string;
buildId: string;
};
Querystring: {
sequence: number;
};
}
export interface SaveDeployKey extends OnlyId {
Body: {
deployKeyId: number
}
Body: {
deployKeyId: number;
};
}
export interface CancelDeployment {
Body: {
buildId: string,
applicationId: string
}
Body: {
buildId: string;
applicationId: string;
};
}
export interface DeployApplication extends OnlyId {
Body: {
pullmergeRequestId: string | null,
branch: string,
forceRebuild?: boolean
}
Body: {
pullmergeRequestId: string | null;
branch: string;
forceRebuild?: boolean;
};
}
export interface StopPreviewApplication extends OnlyId {
Body: {
pullmergeRequestId: string | null,
}
Body: {
pullmergeRequestId: string | null;
};
}
export interface RestartPreviewApplication {
Params: {
id: string,
pullmergeRequestId: string | null,
}
Params: {
id: string;
pullmergeRequestId: string | null;
};
}
export interface RestartApplication {
Params: {
id: string,
},
Body: {
imageId: string | null,
}
}
Params: {
id: string;
};
Body: {
imageId: string | null;
};
}

View File

@ -1,9 +1,31 @@
import { FastifyRequest } from "fastify";
import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common";
import { getTemplates } from "../../../lib/services";
import { OnlyId } from "../../../types";
import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common';
import { getTemplates } from '../../../lib/services';
import { OnlyId } from '../../../types';
function generateServices(serviceId, containerId, port) {
function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) {
return {
[serviceId]: {
loadbalancer: {
servers: [
{
url: `${isHttps ? 'https' : 'http'}://${containerId}:${port}`
}
]
}
},
[`${serviceId}-http2`]: {
loadbalancer: {
servers: [
{
url: `h2c://${containerId}:${port}`
}
]
}
}
};
}
return {
[serviceId]: {
loadbalancer: {
@ -14,43 +36,54 @@ function generateServices(serviceId, containerId, port) {
]
}
}
}
};
}
function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, isDualCerts, isCustomSSL) {
function generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts,
isCustomSSL,
isHttp2 = false
) {
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
let http: any = {
entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`,
rule,
service: `${serviceId}`,
priority: 2,
middlewares: []
}
};
let https: any = {
entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`,
rule,
service: `${serviceId}`,
priority: 2,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
}
};
let httpWWW: any = {
entrypoints: ['web'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`,
rule,
service: `${serviceId}`,
priority: 2,
middlewares: []
}
};
let httpsWWW: any = {
entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`,
rule,
service: `${serviceId}`,
priority: 2,
tls: {
certresolver: 'letsencrypt'
},
middlewares: []
}
};
// 2. http + non-www only
if (!isHttps && !isWWW) {
https.middlewares.push('redirect-to-http');
@ -58,19 +91,19 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpWWW.middlewares.push('redirect-to-non-www');
httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls
delete httpsWWW.tls
delete https.tls;
delete httpsWWW.tls;
}
// 3. http + www only
// 3. http + www only
if (!isHttps && isWWW) {
https.middlewares.push('redirect-to-http');
httpsWWW.middlewares.push('redirect-to-http');
http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www');
delete https.tls
delete httpsWWW.tls
delete https.tls;
delete httpsWWW.tls;
}
// 5. https + non-www only
if (isHttps && !isWWW) {
@ -86,17 +119,17 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpsWWW.tls = true;
} else {
https.tls = true;
delete httpsWWW.tls.certresolver
delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = {
main: domain
}
};
}
} else {
if (!isDualCerts) {
delete httpsWWW.tls.certresolver
delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = {
main: domain
}
};
}
}
}
@ -114,26 +147,59 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpsWWW.tls = true;
} else {
httpsWWW.tls = true;
delete https.tls.certresolver
delete https.tls.certresolver;
https.tls.domains = {
main: domain
}
};
}
} else {
if (!isDualCerts) {
delete https.tls.certresolver
delete https.tls.certresolver;
https.tls.domains = {
main: domain
}
};
}
}
}
if (isHttp2) {
let http2 = {
...http,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let http2WWW = {
...httpWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2 = {
...https,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
let https2WWW = {
...httpsWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
return {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-http2`]: { ...http2 },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-secure-http2`]: { ...https2 },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-www-http2`]: { ...http2WWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW },
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
};
}
return {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW },
}
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
};
}
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) {
const traefik = {
@ -174,26 +240,26 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const coolifySettings = await prisma.setting.findFirst();
if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) {
traefik.http.routers['catchall-http'] = {
entrypoints: ["web"],
rule: "HostRegexp(`{catchall:.*}`)",
service: "noop",
entrypoints: ['web'],
rule: 'HostRegexp(`{catchall:.*}`)',
service: 'noop',
priority: 1,
middlewares: ["redirect-regexp"]
}
middlewares: ['redirect-regexp']
};
traefik.http.routers['catchall-https'] = {
entrypoints: ["websecure"],
rule: "HostRegexp(`{catchall:.*}`)",
service: "noop",
entrypoints: ['websecure'],
rule: 'HostRegexp(`{catchall:.*}`)',
service: 'noop',
priority: 1,
middlewares: ["redirect-regexp"]
}
middlewares: ['redirect-regexp']
};
traefik.http.middlewares['redirect-regexp'] = {
redirectregex: {
regex: '(.*)',
replacement: coolifySettings.proxyDefaultRedirect,
permanent: false
}
}
};
traefik.http.services['noop'] = {
loadBalancer: {
servers: [
@ -202,25 +268,41 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}
]
}
}
};
}
const sslpath = '/etc/traefik/acme/custom';
let certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } })
let certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } }
}
}
});
if (remote) {
certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } })
certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: {
some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true }
}
}
}
});
}
let parsedCertificates = []
let parsedCertificates = [];
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
});
}
if (parsedCertificates.length > 0) {
traefik.tls.certificates = parsedCertificates
traefik.tls.certificates = parsedCertificates;
}
let applications = [];
@ -236,7 +318,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
serviceSetting: true
},
orderBy: { createdAt: 'desc' }
});
@ -251,23 +333,25 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
serviceSetting: true
},
orderBy: { createdAt: 'desc' },
orderBy: { createdAt: 'desc' }
});
}
if (applications.length > 0) {
const dockerIds = new Set()
const runningContainers = {}
const dockerIds = new Set();
const runningContainers = {};
applications.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) {
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
runningContainers[dockerId] = containersArray;
}
}
}
@ -289,38 +373,54 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (
!runningContainers[destinationDockerId] ||
runningContainers[destinationDockerId].length === 0 ||
runningContainers[destinationDockerId].filter((container) => container.startsWith(id)).length === 0
runningContainers[destinationDockerId].filter((container) => container.startsWith(id))
.length === 0
) {
continue
continue;
}
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) {
for (const service of services) {
const [key, value] = service
const [key, value] = service;
if (key && value) {
if (!value.fqdn || !value.port) {
continue;
}
const { fqdn, port } = value
const containerId = `${id}-${key}`
const { fqdn, port } = value;
const containerId = `${id}-${key}`;
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const pathPrefix = '/'
const pathPrefix = '/';
const isCustomSSL = false;
const dualCerts = false;
const serviceId = `${id}-${port || 'default'}`
const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, containerId, port) }
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, containerId, port)
};
}
}
}
continue;
}
const { previews, dualCerts, isCustomSSL } = settings;
const { previews, dualCerts, isCustomSSL, isHttp2 } = settings;
const { network, id: dockerId } = destinationDocker;
if (!fqdn) {
continue;
@ -329,12 +429,31 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const pathPrefix = '/'
const serviceId = `${id}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
const pathPrefix = '/';
const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL,
isHttp2
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps)
};
if (previews) {
const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` })
const { stdout } = await executeCommand({
dockerId,
command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
});
if (stdout) {
const containers = stdout
.trim()
@ -343,44 +462,57 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator}${domain}`;
const previewDomain = `${container.split('-')[1]}${
coolifySettings.previewSeparator
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/'
const serviceId = `${container}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) }
const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
previewDomain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2)
};
}
}
}
}
} catch (error) {
console.log(error)
console.log(error);
}
}
}
if (services.length > 0) {
const dockerIds = new Set()
const runningContainers = {}
const dockerIds = new Set();
const runningContainers = {};
services.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` })
const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) {
const containersArray = container.trim().split('\n');
if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray
runningContainers[dockerId] = containersArray;
}
}
}
for (const service of services) {
try {
let {
fqdn,
id,
type,
destinationDockerId,
dualCerts,
serviceSetting
} = service;
let { fqdn, id, type, destinationDockerId, dualCerts, serviceSetting } = service;
if (!fqdn) {
continue;
}
@ -392,7 +524,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
runningContainers[destinationDockerId].length === 0 ||
!runningContainers[destinationDockerId].includes(id)
) {
continue
continue;
}
const templates = await getTemplates();
let found = templates.find((a) => a.type === type);
@ -401,88 +533,143 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) {
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? [];
const isDomainConfiguration =
found?.services[oneService]?.proxy?.filter((p) => p.domain) ?? [];
if (isDomainConfiguration.length > 0) {
const { proxy } = found.services[oneService];
for (let configuration of proxy) {
if (configuration.domain) {
const setting = serviceSetting.find((a) => a.variableName === configuration.domain);
const setting = serviceSetting.find(
(a) => a.variableName === configuration.domain
);
if (setting) {
configuration.domain = configuration.domain.replace(configuration.domain, setting.value);
configuration.domain = configuration.domain.replace(
configuration.domain,
setting.value
);
}
}
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) {
configuration.port = foundPortVariable.value
configuration.port = foundPortVariable.value;
}
let port, pathPrefix, customDomain;
if (configuration) {
port = configuration?.port;
pathPrefix = configuration?.pathPrefix || '/';
customDomain = configuration?.domain
customDomain = configuration?.domain;
}
if (customDomain) {
fqdn = customDomain
fqdn = customDomain;
} else {
fqdn = service.fqdn
fqdn = service.fqdn;
}
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, oneService, port) }
const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, oneService, port)
};
}
} else {
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
for (let [index, port] of found.services[oneService].ports.entries()) {
if (port == 22) continue;
if (index === 0) {
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port')
const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) {
port = foundPortVariable.value
port = foundPortVariable.value;
}
}
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const pathPrefix = '/'
const isCustomSSL = false
const serviceId = `${oneService}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) }
const pathPrefix = '/';
const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port)
};
}
}
}
}
} catch (error) {
console.log(error)
console.log(error);
}
}
}
if (!remote) {
const { fqdn, dualCerts } = await prisma.setting.findFirst();
if (!fqdn) {
return
return;
}
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const id = isDev ? 'host.docker.internal' : 'coolify'
const container = isDev ? 'host.docker.internal' : 'coolify'
const port = 3000
const pathPrefix = '/'
const isCustomSSL = false
const serviceId = `${id}-${port || 'default'}`
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) }
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) }
const id = isDev ? 'host.docker.internal' : 'coolify';
const container = isDev ? 'host.docker.internal' : 'coolify';
const port = 3000;
const pathPrefix = '/';
const isCustomSSL = false;
const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = {
...traefik.http.routers,
...generateRouters(
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
dualCerts,
isCustomSSL
)
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port)
};
}
} catch (error) {
console.log(error)
console.log(error);
} finally {
if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null;
@ -496,9 +683,9 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
export async function otherProxyConfiguration(request: FastifyRequest<TraefikOtherConfiguration>) {
try {
const { id } = request.query
const { id } = request.query;
if (id) {
const { privatePort, publicPort, type, address = id } = request.query
const { privatePort, publicPort, type, address = id } = request.query;
let traefik = {};
if (publicPort && type && privatePort) {
if (type === 'tcp') {
@ -559,18 +746,18 @@ export async function otherProxyConfiguration(request: FastifyRequest<TraefikOth
}
}
} else {
throw { status: 500 }
throw { status: 500 };
}
}
} else {
throw { status: 500 }
throw { status: 500 };
}
return {
...traefik
};
}
throw { status: 500 }
throw { status: 500 };
} catch ({ status, message }) {
return errorHandler({ status, message })
return errorHandler({ status, message });
}
}

View File

@ -61,6 +61,7 @@
let isBot = application.settings?.isBot;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings?.isHttp2;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = [];
@ -177,6 +178,9 @@
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
if (name === 'isHttp2') {
isHttp2 = !isHttp2;
}
try {
await trpc.applications.saveSettings.mutate({
id: application.id,
@ -713,7 +717,7 @@
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-center pb-4">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip="Must be stopped to modify."
@ -737,6 +741,16 @@
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isHttp2"
isCenter={false}
bind:setting={isHttp2}
title="Enable HTTP/2 protocol?"
description="Enable HTTP/2 protocol. <br><br>HTTP/2 is a major revision of the HTTP network protocol used by the World Wide Web that allows faster web page loading by reducing the number of requests needed to load a web page.<br><br>Useful for gRPC and other HTTP/2 based services."
on:click={() => changeSettings('isHttp2')}
/>
</div>
{/if}
</div>
{#if isSimpleDockerfile}

View File

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

View File

@ -186,6 +186,7 @@ model ApplicationSettings {
isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id])

View File

@ -55,12 +55,22 @@ export default async function (data) {
value['environment'] = [...environment, ...envs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (Object.keys(build).length > 0) {
build = Object.entries(build).map(([key, value]) => `${key}=${value}`);
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of buildArgs) {
const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg);
}
}
}
value['build'] = {
...build,
args: [...(build?.args || []), ...buildEnvs]
args: finalArgs
};
value['labels'] = labels;

View File

@ -808,18 +808,19 @@ export const applicationsRouter = router({
isBot: z.boolean().optional(),
autodeploy: z.boolean().optional(),
isDBBranching: z.boolean().optional(),
isCustomSSL: z.boolean().optional()
isCustomSSL: z.boolean().optional(),
isHttp2: z.boolean().optional()
})
)
.mutation(async ({ ctx, input }) => {
const { id, debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } =
const { id, debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL, isHttp2 } =
input;
await prisma.application.update({
where: { id },
data: {
fqdn: isBot ? null : undefined,
settings: {
update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL }
update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL, isHttp2 }
}
},
include: { destinationDocker: true }

View File

@ -79,6 +79,7 @@
let isBot = application.settings?.isBot;
let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = [];
@ -195,6 +196,9 @@
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
if (name === 'isHttp2') {
isHttp2 = !isHttp2;
}
try {
await post(`/applications/${id}/settings`, {
previews,
@ -204,6 +208,7 @@
autodeploy,
isDBBranching,
isCustomSSL,
isHttp2,
branch: application.branch,
projectId: application.projectId
});
@ -734,7 +739,7 @@
{/if}
</div>
</div>
<div class="grid grid-cols-2 items-center pb-4">
<div class="grid grid-cols-2 items-center">
<Setting
id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')}
@ -746,6 +751,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')}
/>
</div>
{#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
@ -758,6 +764,16 @@
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isHttp2"
isCenter={false}
bind:setting={isHttp2}
title="Enable HTTP/2 protocol?"
description="Enable HTTP/2 protocol. <br><br>HTTP/2 is a major revision of the HTTP network protocol used by the World Wide Web that allows faster web page loading by reducing the number of requests needed to load a web page.<br><br>Useful for gRPC and other HTTP/2 based services."
on:click={() => changeSettings('isHttp2')}
/>
</div>
{/if}
</div>
{#if isSimpleDockerfile}

View File

@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.12.11",
"version": "3.12.12",
"license": "Apache-2.0",
"repository": "github:coollabsio/coolify",
"scripts": {