wip: docker compose

This commit is contained in:
Andras Bacsai
2022-10-05 15:34:52 +02:00
parent 3f1841a188
commit d8206c0e3e
16 changed files with 831 additions and 516 deletions

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "dockerComposeFile" TEXT;
ALTER TABLE "Application" ADD COLUMN "dockerComposeFileLocation" TEXT;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "dockerComposeConfiguration" TEXT;

View File

@@ -94,43 +94,46 @@ model TeamInvitation {
} }
model Application { model Application {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String
fqdn String? fqdn String?
repository String? repository String?
configHash String? configHash String?
branch String? branch String?
buildPack String? buildPack String?
projectId Int? projectId Int?
port Int? port Int?
exposePort Int? exposePort Int?
installCommand String? installCommand String?
buildCommand String? buildCommand String?
startCommand String? startCommand String?
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
deploymentType String? deploymentType String?
phpModules String? phpModules String?
pythonWSGI String? pythonWSGI String?
pythonModule String? pythonModule String?
pythonVariable String? pythonVariable String?
dockerFileLocation String? dockerFileLocation String?
denoMainFile String? denoMainFile String?
denoOptions String? denoOptions String?
createdAt DateTime @default(now()) dockerComposeFile String?
updatedAt DateTime @updatedAt dockerComposeFileLocation String?
destinationDockerId String? dockerComposeConfiguration String?
gitSourceId String? createdAt DateTime @default(now())
baseImage String? updatedAt DateTime @updatedAt
baseBuildImage String? destinationDockerId String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) gitSourceId String?
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) baseImage String?
persistentStorage ApplicationPersistentStorage[] baseBuildImage String?
settings ApplicationSettings? gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
secrets Secret[] destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
teams Team[] persistentStorage ApplicationPersistentStorage[]
connectedDatabase ApplicationConnectedDatabase? settings ApplicationSettings?
previewApplication PreviewApplication[] secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
} }
model PreviewApplication { model PreviewApplication {

View File

@@ -212,17 +212,37 @@ import * as buildpacks from '../lib/buildPacks';
// //
} }
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
if (forceRebuild) deployNeeded = true if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) { if (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId, buildId,
applicationId, applicationId,
domain, domain,
name, name,
type, type,
volumes,
labels,
pullmergeRequestId, pullmergeRequestId,
buildPack, buildPack,
repository, repository,
@@ -248,7 +268,7 @@ import * as buildpacks from '../lib/buildPacks';
denoOptions, denoOptions,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType deploymentType,
}); });
else { else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -257,112 +277,137 @@ import * as buildpacks from '../lib/buildPacks';
} else { } else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId }); await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
} }
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` }) if (buildPack === 'compose') {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` }) try {
} catch (error) { await executeDockerCmd({
// dockerId: destinationDockerId,
} command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
const envs = [ })
`PORT=${port}` await executeDockerCmd({
]; dockerId: destinationDockerId,
if (secrets.length > 0) { command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
secrets.forEach((secret) => { })
if (pullmergeRequestId) { } catch (error) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) //
if (isSecretFound.length > 0) { }
envs.push(`${secret.name}=${isSecretFound[0].value}`); try {
} else { await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
envs.push(`${secret.name}=${secret.value}`); await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
} else { await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!secret.isPRMRSecret) { await prisma.application.update({
envs.push(`${secret.name}=${secret.value}`); where: { id: applicationId },
} data: { configHash: currentHash }
});
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
} }
}); throw new Error(error);
} }
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({ } else {
applicationId, try {
fqdn, await executeDockerCmd({
name, dockerId: destinationDockerId,
type, command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
pullmergeRequestId, })
buildPack, await executeDockerCmd({
repository, dockerId: destinationDockerId,
branch, command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
projectId, })
port: exposePort ? `${exposePort}:${port}` : port, } catch (error) {
commit, //
installCommand, }
buildCommand, const envs = [
startCommand, `PORT=${port}`
baseDirectory, ];
publishDirectory if (secrets.length > 0) {
}); secrets.forEach((secret) => {
let envFound = false; if (pullmergeRequestId) {
try { const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
envFound = !!(await fs.stat(`${workdir}/.env`)); if (isSecretFound.length > 0) {
} catch (error) { envs.push(`${secret.name}=${isSecretFound[0].value}`);
// } else {
} envs.push(`${secret.name}=${secret.value}`);
try { }
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); } else {
const composeVolumes = volumes.map((volume) => { if (!secret.isPRMRSecret) {
return { envs.push(`${secret.name}=${secret.value}`);
[`${volume.split(':')[0]}`]: { }
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
} }
}); });
} }
throw new Error(error); await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} }
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} }
} }
catch (error) { catch (error) {

View File

@@ -634,6 +634,7 @@ export function makeLabelForStandaloneApplication({
return [ return [
'coolify.managed=true', 'coolify.managed=true',
`coolify.version=${version}`, `coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`, `coolify.type=standalone-application`,
`coolify.configuration=${base64Encode( `coolify.configuration=${base64Encode(
JSON.stringify({ JSON.stringify({

View File

@@ -1,34 +1,99 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { executeDockerCmd } from '../common'; import { defaultComposeConfiguration, executeDockerCmd } from '../common';
import { buildImage } from './common'; import { buildImage, saveBuildLog } from './common';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { getSecrets } from '../../routes/api/v1/applications/handlers';
export default async function (data) { export default async function (data) {
let { let {
applicationId, applicationId,
debug,
buildId,
dockerId, dockerId,
debug, network,
tag, volumes,
workdir, labels,
buildId, workdir,
baseDirectory, baseDirectory,
secrets, secrets,
pullmergeRequestId, pullmergeRequestId,
dockerFileLocation port
} = data } = data
const file = `${workdir}${baseDirectory}/docker-compose.yml`; const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`;
const dockerComposeRaw = await fs.readFile(`${file}`, 'utf8') const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`;
let dockerComposeRaw = null;
let isYml = false;
try {
dockerComposeRaw = await fs.readFile(`${fileYml}`, 'utf8')
isYml = true
} catch (error) { }
try {
dockerComposeRaw = await fs.readFile(`${fileYaml}`, 'utf8')
} catch (error) { }
if (!dockerComposeRaw) {
throw ('docker-compose.yml or docker-compose.yaml are not found!');
}
const dockerComposeYaml = yaml.load(dockerComposeRaw) const dockerComposeYaml = yaml.load(dockerComposeRaw)
if (!dockerComposeYaml.services) { if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.' throw 'No Services found in docker-compose file.'
} }
const envs = [
`PORT=${port}`
];
if (getSecrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
let networks = {}
for (let [key, value] of Object.entries(dockerComposeYaml.services)) { for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}` value['container_name'] = `${applicationId}-${key}`
console.log({key, value}); value['env_file'] = envFound ? [`${workdir}/.env`] : []
value['labels'] = labels
value['volumes'] = volumes
if (value['networks']?.length > 0) {
value['networks'].forEach((network) => {
networks[network] = {
name: network
}
})
}
value['networks'] = [...value['networks'] || '', network]
dockerComposeYaml.services[key] = { ...dockerComposeYaml.services[key], restart: defaultComposeConfiguration(network).restart, deploy: defaultComposeConfiguration(network).deploy }
} }
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
throw 'Halting' dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
// await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
// await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain --pull` }) await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })
// await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} up -d` }) await saveBuildLog({ line: 'Pulling images from Compose file.', buildId, applicationId });
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` })
await saveBuildLog({ line: 'Building images from Compose file.', buildId, applicationId });
} }

View File

@@ -289,13 +289,16 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
baseDatabaseBranch baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
} = request.body } = request.body
console.log({dockerComposeConfiguration})
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
exposePort = Number(exposePort); exposePort = Number(exposePort);
} }
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim(); if (denoOptions) denoOptions = denoOptions.trim();
@@ -324,6 +327,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration, ...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
} }
@@ -342,6 +348,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration ...defaultConfiguration
} }
}); });

View File

@@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
baseImage: string, baseImage: string,
baseBuildImage: string, baseBuildImage: string,
deploymentType: string, deploymentType: string,
baseDatabaseBranch: string baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string
} }
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {

View File

@@ -48,6 +48,7 @@
"daisyui": "2.24.2", "daisyui": "2.24.2",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0", "svelte-file-dropzone": "^1.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",

View File

@@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: 80 port: 80
}; };
} }
if (pack === 'docker') { if (pack === 'docker' || pack === 'compose') {
return { return {
...metaData, ...metaData,
installCommand: null, installCommand: null,
@@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: null port: null
}; };
} }
if (pack === 'svelte') { if (pack === 'svelte') {
return { return {
...metaData, ...metaData,
@@ -236,13 +237,13 @@ export const buildPacks = [
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'compose', name: 'compose',
type: 'base', type: 'base',
fancyName: 'Docker Compose', fancyName: 'Docker Compose',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700', color: 'bg-sky-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'svelte', name: 'svelte',
type: 'specific', type: 'specific',
@@ -357,14 +358,14 @@ export const buildPacks = [
color: 'bg-green-700', color: 'bg-green-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'heroku', name: 'heroku',
type: 'base', type: 'base',
fancyName: 'Heroku', fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700', hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700', color: 'bg-purple-700',
isHerokuBuildPack: true, isHerokuBuildPack: true,
} }
]; ];
export const scanningTemplates = { export const scanningTemplates = {
'@sveltejs/kit': { '@sveltejs/kit': {

View File

@@ -14,6 +14,8 @@
export let foundConfig: any; export let foundConfig: any;
export let scanning: any; export let scanning: any;
export let packageManager: any; export let packageManager: any;
export let dockerComposeFile: any = null;
export let dockerComposeFileLocation: string | null = null;
async function handleSubmit(name: string) { async function handleSubmit(name: string) {
try { try {
@@ -25,10 +27,12 @@
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
await post(`/applications/${id}`, {
if (foundConfig?.buildPack !== name) { ...tempBuildPack,
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name }); buildPack: name,
} dockerComposeFile,
dockerComposeFileLocation
});
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
return await goto(from || `/applications/${id}`); return await goto(from || `/applications/${id}`);
} catch (error) { } catch (error) {

View File

@@ -34,12 +34,15 @@
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates'; import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import BuildPack from './_BuildPack.svelte'; import BuildPack from './_BuildPack.svelte';
import yaml from 'js-yaml';
const { id } = $page.params; const { id } = $page.params;
let scanning = true; let scanning: boolean = true;
let foundConfig: any = null; let foundConfig: any = null;
let packageManager = 'npm'; let packageManager: string = 'npm';
let dockerComposeFile: any = null;
let dockerComposeFileLocation: string | null = null;
export let apiUrl: any; export let apiUrl: any;
export let projectId: any; export let projectId: any;
@@ -60,10 +63,14 @@
} }
} }
} }
async function scanRepository(): Promise<void> { async function scanRepository(isPublicRepository: boolean): Promise<void> {
try { try {
if (type === 'gitlab') { if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, { if (isPublicRepository) {
return;
}
const url = isPublicRepository ? `` : `/v4/projects/${projectId}/repository/tree`;
const files = await get(`${apiUrl}${url}`, {
Authorization: `Bearer ${$appSession.tokens.gitlab}` Authorization: `Bearer ${$appSession.tokens.gitlab}`
}); });
const packageJson = files.find( const packageJson = files.find(
@@ -82,6 +89,14 @@
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'blob' file.name === 'Dockerfile' && file.type === 'blob'
); );
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'blob'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'blob'
);
const cargoToml = files.find( const cargoToml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'blob' file.name === 'Cargo.toml' && file.type === 'blob'
@@ -105,11 +120,12 @@
const laravel = files.find( const laravel = files.find(
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob' (file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
); );
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('dockercompose', packageManager);
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager); foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) { } else if (packageJson && !laravel) {
const path = packageJson.path; const path = packageJson.path;
@@ -135,8 +151,13 @@
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);
} }
} else if (type === 'github') { } else if (type === 'github') {
const headers = isPublicRepository
? {}
: {
Authorization: `token ${$appSession.tokens.github}`
};
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, { const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${$appSession.tokens.github}`, ...headers,
Accept: 'application/vnd.github.v2.json' Accept: 'application/vnd.github.v2.json'
}); });
const packageJson = files.find( const packageJson = files.find(
@@ -155,6 +176,14 @@
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'file' file.name === 'Dockerfile' && file.type === 'file'
); );
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'file'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'file'
);
const cargoToml = files.find( const cargoToml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'file' file.name === 'Cargo.toml' && file.type === 'file'
@@ -182,7 +211,25 @@
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) { if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('compose', packageManager);
const data = await get(
`${apiUrl}/repos/${repository}/contents/${
dockerComposeFileYml ? 'docker-compose.yml' : 'docker-compose.yaml'
}?ref=${branch}`,
{
...headers,
Accept: 'application/vnd.github.v2.json'
}
);
if (data?.content) {
const content = atob(data.content);
dockerComposeFile = JSON.stringify(yaml.load(content) || null);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
}
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager); foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) { } else if (packageJson && !laravel) {
const data: any = await get(`${packageJson.git_url}`, { const data: any = await get(`${packageJson.git_url}`, {
@@ -237,7 +284,7 @@
if (error.message === 'Bad credentials') { if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`); const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token; $appSession.tokens.github = token;
return await scanRepository(); return await scanRepository(isPublicRepository);
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -246,11 +293,7 @@
} }
} }
onMount(async () => { onMount(async () => {
if (!isPublicRepository) { await scanRepository(isPublicRepository);
await scanRepository();
} else {
scanning = false;
}
}); });
</script> </script>
@@ -266,7 +309,12 @@
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
/>
</div> </div>
{/each} {/each}
</div> </div>
@@ -274,9 +322,16 @@
<div class="max-w-screen-2xl mx-auto px-10"> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Base</div> <div class="title pb-2">Coolify Base</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
{dockerComposeFile}
{dockerComposeFileLocation}
/>
</div> </div>
{/each} {/each}
</div> </div>
@@ -284,9 +339,14 @@
<div class="max-w-screen-2xl mx-auto px-10"> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Specific</div> <div class="title pb-2">Coolify Specific</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack} {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
<div class="p-2"> <div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig /> <BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
/>
</div> </div>
{/each} {/each}
</div> </div>

View File

@@ -29,7 +29,7 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import Select from 'svelte-select'; import Select from 'svelte-select';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import cuid from 'cuid'; import cuid from 'cuid';
@@ -45,10 +45,9 @@
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fade } from 'svelte/transition'; import yaml from 'js-yaml';
const { id } = $page.params; const { id } = $page.params;
@@ -58,6 +57,10 @@
let loading = false; let loading = false;
let fqdnEl: any = null; let fqdnEl: any = null;
let forceSave = false; let forceSave = false;
let isPublicRepository = application.settings.isPublicRepository;
let apiUrl = application.gitSource.apiUrl;
let branch = application.branch;
let repository = application.repository;
let debug = application.settings.debug; let debug = application.settings.debug;
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
@@ -66,6 +69,11 @@
let isBot = application.settings.isBot; let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = [];
let dockerComposeFileLocation = application.dockerComposeFileLocation;
let dockerComposeConfiguration = JSON.parse(application.dockerComposeConfiguration) || {};
let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null; let baseDatabaseBranch: any = application?.connectedDatabase?.hostedDatabaseDBName || null;
let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isHttps = application.fqdn && application.fqdn.startsWith('https://'); let isHttps = application.fqdn && application.fqdn.startsWith('https://');
@@ -86,6 +94,26 @@
label: 'Uvicorn' label: 'Uvicorn'
} }
]; ];
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
for (const service of tempdockerComposeServices) {
if (!dockerComposeConfiguration[service.name]) {
dockerComposeConfiguration[service.name] = {};
}
}
return tempdockerComposeServices;
}
if (dockerComposeFile?.services) {
dockerComposeServices = normalizeDockerServices(dockerComposeFile.services);
}
function containerClass() { function containerClass() {
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200'; return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
} }
@@ -214,7 +242,11 @@
dualCerts, dualCerts,
exposePort: application.exposePort exposePort: application.exposePort
})); }));
await post(`/applications/${id}`, { ...application, baseDatabaseBranch }); await post(`/applications/${id}`, {
...application,
baseDatabaseBranch,
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
});
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
@@ -281,6 +313,36 @@
return false; return false;
} }
} }
async function reloadCompose() {
try {
const headers = isPublicRepository
? {}
: {
Authorization: `token ${$appSession.tokens.github}`
};
const data = await get(
`${apiUrl}/repos/${repository}/contents/${dockerComposeFileLocation}?ref=${branch}`,
{
...headers,
Accept: 'application/vnd.github.v2.json'
}
);
if (data?.content) {
const content = atob(data.content);
let dockerComposeFileContent = JSON.stringify(yaml.load(content) || null);
let dockerComposeFileContentJSON = JSON.parse(dockerComposeFileContent);
dockerComposeServices = normalizeDockerServices(dockerComposeFileContentJSON?.services);
application.dockerComposeFile = dockerComposeFileContent;
await handleSubmit();
}
addToast({
message: 'Compose file reloaded.',
type: 'success'
});
} catch (error) {
errorNotification(error);
}
}
</script> </script>
<div class="w-full"> <div class="w-full">
@@ -372,18 +434,20 @@
/> />
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center"> {#if application.buildPack !== 'compose'}
<Setting <div class="grid grid-cols-2 items-center">
id="isBot" <Setting
isCenter={false} id="isBot"
bind:setting={isBot} isCenter={false}
on:click={() => changeSettings('isBot')} bind:setting={isBot}
title="Is your application a bot?" on:click={() => changeSettings('isBot')}
description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>" title="Is your application a bot?"
disabled={$status.application.isRunning} description="You can deploy applications without domains or make them to listen on the <span class='text-settings font-bold'>Exposed Port</span>.<br></Setting><br>Useful to host <span class='text-settings font-bold'>Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection.</span>"
/> disabled={$status.application.isRunning}
</div> />
{#if !isBot} </div>
{/if}
{#if !isBot && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="fqdn" <label for="fqdn"
>{$t('application.url_fqdn')} >{$t('application.url_fqdn')}
@@ -454,7 +518,7 @@
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps} {#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center pb-4">
<Setting <Setting
id="isCustomSSL" id="isCustomSSL"
@@ -468,349 +532,395 @@
{/if} {/if}
{/if} {/if}
</div> </div>
{#if application.buildPack !== 'compose'}
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div> <div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
<div class="grid grid-flow-row gap-2 px-4 pr-5"> Build & Deploy
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-flow-row gap-2 px-4 pr-5">
<label for="baseBuildImage" {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
>{$t('application.base_build_image')}
<Explainer
explanation={application.buildPack === 'laravel'
? 'For building frontend assets with webpack.'
: 'Image that will be used during the build process.'}
/>
</label>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseBuildImages"
showIndicator={!$status.application.isRunning}
items={application.baseBuildImages}
on:select={selectBaseBuildImage}
value={application.baseBuildImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker'}
<div class="grid grid-cols-2 items-center">
<label for="baseImage"
>{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseImages"
showIndicator={!$status.application.isRunning}
items={application.baseImages}
on:select={selectBaseImage}
value={application.baseImage}
isClearable={false}
/>
</div>
</div>
{/if}
{#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
<div class="grid grid-cols-2 items-center pb-8">
<label for="deploymentType"
>Deployment Type
<Explainer
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
/></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="deploymentTypes"
showIndicator={!$status.application.isRunning}
items={['static', 'node']}
on:select={selectDeploymentType}
value={application.deploymentType}
isClearable={false}
/>
</div>
</div>
{/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <label for="baseBuildImage"
id="isDBBranching" >{$t('application.base_build_image')}
isCenter={false} <Explainer
bind:setting={isDBBranching} explanation={application.buildPack === 'laravel'
on:click={() => changeSettings('isDBBranching')} ? 'For building frontend assets with webpack.'
title="Enable DB Branching" : 'Image that will be used during the build process.'}
description="Enable DB Branching" />
/> </label>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="baseBuildImages"
showIndicator={!$status.application.isRunning}
items={application.baseBuildImages}
on:select={selectBaseBuildImage}
value={application.baseBuildImage}
isClearable={false}
/>
</div>
</div> </div>
{#if isDBBranching} {/if}
<button {#if application.buildPack !== 'docker'}
on:click|stopPropagation|preventDefault={() => <div class="grid grid-cols-2 items-center">
goto(`/applications/${id}/configuration/database`)} <label for="baseImage"
class="btn btn-sm">Configure Connected Database</button >{$t('application.base_image')}
<Explainer explanation={'Image that will be used for the deployment.'} /></label
> >
{#if application.connectedDatabase} <div class="custom-select-wrapper">
<div class="grid grid-cols-2 items-center"> <Select
<label for="baseImage" {isDisabled}
>Base Database containerClasses={isDisabled && containerClass()}
<Explainer id="baseImages"
explanation={'The name of the database that will be used as base when branching.'} showIndicator={!$status.application.isRunning}
/></label items={application.baseImages}
> on:select={selectBaseImage}
<input value={application.baseImage}
name="baseDatabaseBranch" isClearable={false}
required />
id="baseDatabaseBranch" </div>
bind:value={baseDatabaseBranch} </div>
/> {/if}
</div> {#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')}
<div class="text-center bg-green-600 rounded"> <div class="grid grid-cols-2 items-center pb-8">
Connected to {application.connectedDatabase.databaseId} <label for="deploymentType"
</div> >Deployment Type
<Explainer
explanation={"Defines how to deploy your application. <br><br><span class='text-green-500 font-bold'>Static</span> is for static websites, <span class='text-green-500 font-bold'>node</span> is for server-side applications."}
/></label
>
<div class="custom-select-wrapper">
<Select
{isDisabled}
containerClasses={isDisabled && containerClass()}
id="deploymentTypes"
showIndicator={!$status.application.isRunning}
items={['static', 'node']}
on:select={selectDeploymentType}
value={application.deploymentType}
isClearable={false}
/>
</div>
</div>
{/if}
{#if $features.beta}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="isDBBranching"
isCenter={false}
bind:setting={isDBBranching}
on:click={() => changeSettings('isDBBranching')}
title="Enable DB Branching"
description="Enable DB Branching"
/>
</div>
{#if isDBBranching}
<button
on:click|stopPropagation|preventDefault={() =>
goto(`/applications/${id}/configuration/database`)}
class="btn btn-sm">Configure Connected Database</button
>
{#if application.connectedDatabase}
<div class="grid grid-cols-2 items-center">
<label for="baseImage"
>Base Database
<Explainer
explanation={'The name of the database that will be used as base when branching.'}
/></label
>
<input
name="baseDatabaseBranch"
required
id="baseDatabaseBranch"
bind:value={baseDatabaseBranch}
/>
</div>
<div class="text-center bg-green-600 rounded">
Connected to {application.connectedDatabase.databaseId}
</div>
{/if}
{/if} {/if}
{/if} {/if}
{/if} {/if}
{/if}
{#if application.buildPack === 'python'} {#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center">
<label for="pythonModule">WSGI / ASGI</label>
<div class="custom-select-wrapper">
<Select
id="wsgi"
items={wsgis}
on:select={selectWSGI}
value={application.pythonWSGI}
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="pythonModule">Module</label>
<input
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="pythonModule"
id="pythonModule"
required
class="w-full"
bind:value={application.pythonModule}
placeholder={application.pythonWSGI?.toLowerCase() !== 'none' ? 'main' : 'main.py'}
/>
</div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="pythonVariable">Variable</label> <label for="pythonModule">WSGI / ASGI</label>
<div class="custom-select-wrapper">
<Select
id="wsgi"
items={wsgis}
on:select={selectWSGI}
value={application.pythonWSGI}
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="pythonModule">Module</label>
<input <input
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="pythonModule"
id="pythonVariable" id="pythonModule"
required required
class="w-full" class="w-full"
bind:value={application.pythonVariable} bind:value={application.pythonModule}
placeholder="default: app" placeholder={application.pythonWSGI?.toLowerCase() !== 'none' ? 'main' : 'main.py'}
/> />
</div> </div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable">Variable</label>
<input
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
class="w-full"
bind:value={application.pythonVariable}
placeholder="default: app"
/>
</div>
{/if}
{#if application.pythonWSGI?.toLowerCase() === 'uvicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable">Variable</label>
<input
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
class="w-full"
bind:value={application.pythonVariable}
placeholder="default: app"
/>
</div>
{/if}
{/if} {/if}
{#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center pt-4">
<label for="pythonVariable">Variable</label> <label for="port"
>{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label
>
<input <input
class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="pythonVariable" name="port"
id="pythonVariable" id="port"
required bind:value={application.port}
class="w-full" placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
bind:value={application.pythonVariable}
placeholder="default: app"
/> />
</div> </div>
{/if} {/if}
{/if} <div class="grid grid-cols-2 items-center pb-4">
{#if !staticDeployments.includes(application.buildPack)} <label for="exposePort"
<div class="grid grid-cols-2 items-center pt-4"> >Exposed Port <Explainer
<label for="port" explanation={'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.'}
>{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label
>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="port"
id="port"
bind:value={application.port}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center pb-4">
<label for="exposePort"
>Exposed Port <Explainer
explanation={'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.'}
/></label
>
<input
class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning}
disabled={isDisabled}
name="exposePort"
id="exposePort"
bind:value={application.exposePort}
placeholder="12345"
/>
</div>
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<label for="installCommand">{$t('application.install_command')}</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="{$t('forms.default')}: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand">{$t('application.build_command')}</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="{$t('forms.default')}: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="startCommand">{$t('application.start_command')}</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="{$t('forms.default')}: yarn start"
/>
</div>
{/if}
{#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center">
<label for="denoMainFile">Main File</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="denoMainFile"
id="denoMainFile"
bind:value={application.denoMainFile}
placeholder="default: main.ts"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="denoOptions"
>Arguments <Explainer
explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
/></label /></label
> >
<input <input
class="w-full" class="w-full"
readonly={!$appSession.isAdmin && !$status.application.isRunning}
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} name="exposePort"
name="denoOptions" id="exposePort"
id="denoOptions" bind:value={application.exposePort}
bind:value={application.denoOptions} placeholder="12345"
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/> />
</div> </div>
{/if} {#if !notNodeDeployments.includes(application.buildPack)}
{#if application.buildPack !== 'laravel'} <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-2 items-center"> <label for="installCommand">{$t('application.install_command')}</label>
<div class="flex-col"> <input
<label for="baseDirectory" class="w-full"
>{$t('forms.base_directory')} disabled={isDisabled}
<Explainer readonly={!$appSession.isAdmin}
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."} name="installCommand"
id="installCommand"
bind:value={application.installCommand}
placeholder="{$t('forms.default')}: yarn install"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="buildCommand">{$t('application.build_command')}</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="buildCommand"
id="buildCommand"
bind:value={application.buildCommand}
placeholder="{$t('forms.default')}: yarn build"
/>
</div>
<div class="grid grid-cols-2 items-center pb-8">
<label for="startCommand">{$t('application.start_command')}</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="startCommand"
id="startCommand"
bind:value={application.startCommand}
placeholder="{$t('forms.default')}: yarn start"
/>
</div>
{/if}
{#if application.buildPack === 'deno'}
<div class="grid grid-cols-2 items-center">
<label for="denoMainFile">Main File</label>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="denoMainFile"
id="denoMainFile"
bind:value={application.denoMainFile}
placeholder="default: main.ts"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="denoOptions"
>Arguments <Explainer
explanation={"List of arguments to pass to <span class='text-settings font-bold'>deno run</span> command. Could include permissions, configurations files, etc."}
/></label /></label
> >
</div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="{$t('forms.default')}: /"
/>
</div>
{/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center pb-4">
<label for="dockerFileLocation" class=""
>Dockerfile Location <Explainer
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
/></label
>
<div class="form-control w-full">
<input <input
class="w-full input" class="w-full"
disabled={isDisabled} disabled={isDisabled}
readonly={!$appSession.isAdmin} readonly={!$appSession.isAdmin}
name="dockerFileLocation" name="denoOptions"
id="dockerFileLocation" id="denoOptions"
bind:value={application.dockerFileLocation} bind:value={application.denoOptions}
placeholder="default: /Dockerfile" placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/> />
{#if application.baseDirectory} </div>
<label class="label"> {/if}
<span class="label-text-alt text-xs" {#if application.buildPack !== 'laravel'}
>Path: {application.baseDirectory.replace( <div class="grid grid-cols-2 items-center">
/^\/$/, <div class="flex-col">
'' <label for="baseDirectory"
)}{application.dockerFileLocation}</span >{$t('forms.base_directory')}
> <Explainer
</label> explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
/></label
>
</div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="baseDirectory"
id="baseDirectory"
bind:value={application.baseDirectory}
placeholder="{$t('forms.default')}: /"
/>
</div>
{/if}
{#if application.buildPack === 'docker'}
<div class="grid grid-cols-2 items-center pb-4">
<label for="dockerFileLocation" class=""
>Dockerfile Location <Explainer
explanation={"Should be absolute path, like <span class='text-settings font-bold'>/data/Dockerfile</span> or <span class='text-settings font-bold'>/Dockerfile.</span>"}
/></label
>
<div class="form-control w-full">
<input
class="w-full input"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
/>
{#if application.baseDirectory}
<label class="label">
<span class="label-text-alt text-xs"
>Path: {application.baseDirectory.replace(
/^\/$/,
''
)}{application.dockerFileLocation}</span
>
</label>
{/if}
</div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory"
>{$t('forms.publish_directory')}
<Explainer
explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."}
/></label
>
</div>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="publishDirectory"
id="publishDirectory"
required={application.deploymentType === 'static'}
bind:value={application.publishDirectory}
placeholder=" {$t('forms.default')}: /"
/>
</div>
{/if}
</div>
{:else}
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">
Docker Compose
{#if $appSession.isAdmin}
<button
class="btn btn-sm btn-primary"
on:click|preventDefault={reloadCompose}
class:loading
disabled={loading}>Reload Docker Compose File</button
>
{/if}
</div>
<div class="grid grid-flow-row gap-2">
{#each dockerComposeServices as service}
<div class="grid items-center mb-6">
<div class="text-xl font-bold uppercase">{service.name}</div>
{#if service.data?.image}
<div class="text-xs">{service.data.image}</div>
{:else}
<div class="text-xs">No image, build required</div>
{/if} {/if}
</div> </div>
</div>
{/if}
{#if !notNodeDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="publishDirectory"
>{$t('forms.publish_directory')}
<Explainer
explanation={"Directory containing all the assets for deployment. <br> For example: <span class='text-settings font-bold'>dist</span>,<span class='text-settings font-bold'>_site</span> or <span class='text-settings font-bold'>public</span>."}
/></label
>
</div>
<input <div class="grid grid-cols-2 items-center">
class="w-full" <label for="fqdn"
disabled={isDisabled} >{$t('application.url_fqdn')}
readonly={!$appSession.isAdmin} <Explainer
name="publishDirectory" explanation={"If you specify <span class='text-settings font-bold'>https</span>, the application will be accessible only over https.<br>SSL certificate will be generated automatically.<br><br>If you specify <span class='text-settings font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-settings font-bold'>You must set your DNS to point to the server IP in advance.</span>"}
id="publishDirectory" />
required={application.deploymentType === 'static'} </label>
bind:value={application.publishDirectory} <div>
placeholder=" {$t('forms.default')}: /" <input
/> class="w-full"
</div> name="fqdn"
{/if} id="fqdn"
</div> bind:value={dockerComposeConfiguration[service.name].fqdn}
pattern="^https?://([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{'{'}2,{'}'}$"
placeholder="eg: https://coollabs.io"
/>
</div>
</div>
{/each}
</div>
{/if}
</div> </div>
</form> </form>
</div> </div>

View File

@@ -22,6 +22,10 @@ services:
published: 3001 published: 3001
protocol: tcp protocol: tcp
mode: host mode: host
- target: 5555
published: 5555
protocol: tcp
mode: host
volumes: volumes:
- ./:/app - ./:/app
- '/var/run/docker.sock:/var/run/docker.sock' - '/var/run/docker.sock:/var/run/docker.sock'

View File

@@ -8,14 +8,16 @@
"oc": "opencollective-setup", "oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate", "translate": "pnpm run --filter i18n-converter translate",
"db:studio": "pnpm run --filter api db:studio", "db:studio": "pnpm run --filter api db:studio",
"db:studio:container": "docker exec coolify pnpm run --filter api db:studio",
"db:push": "pnpm run --filter api db:push", "db:push": "pnpm run --filter api db:push",
"db:seed": "pnpm run --filter api db:seed", "db:seed": "pnpm run --filter api db:seed",
"db:migrate": "pnpm run --filter api db:migrate", "db:migrate": "pnpm run --filter api db:migrate",
"db:migrate:container": "docker exec coolify pnpm run --filter api db:migrate",
"format": "run-p -l -n format:*", "format": "run-p -l -n format:*",
"format:api": "NODE_ENV=development pnpm run --filter api format", "format:api": "NODE_ENV=development pnpm run --filter api format",
"lint": "run-p -l -n lint:*", "lint": "run-p -l -n lint:*",
"lint:api": "NODE_ENV=development pnpm run --filter api lint", "lint:api": "NODE_ENV=development pnpm run --filter api lint",
"dev:container": "docker-compose -f docker-compose-dev.yaml up", "dev:container": "docker-compose -f docker-compose-dev.yaml up || docker compose -f docker-compose-dev.yaml up",
"dev": "run-p -l -n dev:api dev:ui", "dev": "run-p -l -n dev:api dev:ui",
"dev:api": "NODE_ENV=development pnpm run --filter api dev", "dev:api": "NODE_ENV=development pnpm run --filter api dev",
"dev:ui": "NODE_ENV=development pnpm run --filter ui dev", "dev:ui": "NODE_ENV=development pnpm run --filter ui dev",

2
pnpm-lock.yaml generated
View File

@@ -159,6 +159,7 @@ importers:
flowbite: 1.5.2 flowbite: 1.5.2
flowbite-svelte: 0.26.2 flowbite-svelte: 0.26.2
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0 p-limit: 4.0.0
postcss: 8.4.16 postcss: 8.4.16
prettier: 2.7.1 prettier: 2.7.1
@@ -181,6 +182,7 @@ importers:
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
dayjs: 1.11.5 dayjs: 1.11.5
js-cookie: 3.0.1 js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0 p-limit: 4.0.0
svelte-file-dropzone: 1.0.0 svelte-file-dropzone: 1.0.0
svelte-select: 4.4.7 svelte-select: 4.4.7