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

View File

@ -212,17 +212,37 @@ import * as buildpacks from '../lib/buildPacks';
//
}
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 (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId,
applicationId,
domain,
name,
type,
volumes,
labels,
pullmergeRequestId,
buildPack,
repository,
@ -248,7 +268,7 @@ import * as buildpacks from '../lib/buildPacks';
denoOptions,
baseImage,
baseBuildImage,
deploymentType
deploymentType,
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@ -257,112 +277,137 @@ import * as buildpacks from '../lib/buildPacks';
} else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
}
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
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}`);
}
if (buildPack === 'compose') {
try {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) {
//
}
try {
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
await prisma.application.update({
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'
}
});
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
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}`] } : {}),
// 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);
}
} else {
try {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
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}`);
}
}
});
}
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) {

View File

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

View File

@ -1,34 +1,99 @@
import { promises as fs } from 'fs';
import { executeDockerCmd } from '../common';
import { buildImage } from './common';
import { defaultComposeConfiguration, executeDockerCmd } from '../common';
import { buildImage, saveBuildLog } from './common';
import yaml from 'js-yaml';
import { getSecrets } from '../../routes/api/v1/applications/handlers';
export default async function (data) {
let {
applicationId,
let {
applicationId,
debug,
buildId,
dockerId,
debug,
tag,
workdir,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
} = data
const file = `${workdir}${baseDirectory}/docker-compose.yml`;
const dockerComposeRaw = await fs.readFile(`${file}`, 'utf8')
network,
volumes,
labels,
workdir,
baseDirectory,
secrets,
pullmergeRequestId,
port
} = data
const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`;
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)
if (!dockerComposeYaml.services) {
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)) {
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 }
}
throw 'Halting'
// 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} build --progress plain --pull` })
// await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} up -d` })
dockerComposeYaml['volumes'] = Object.assign({ ...dockerComposeYaml['volumes'] }, ...composeVolumes)
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } })
await fs.writeFile(`${workdir}/docker-compose.${isYml ? 'yml' : 'yaml'}`, yaml.dump(dockerComposeYaml));
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` })
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,
baseBuildImage,
deploymentType,
baseDatabaseBranch
baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
} = request.body
console.log({dockerComposeConfiguration})
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
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 (denoOptions) denoOptions = denoOptions.trim();
@ -324,6 +327,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
@ -342,6 +348,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration
}
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@
export let application: any;
export let settings: any;
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, post } from '$lib/api';
import cuid from 'cuid';
@ -45,10 +45,9 @@
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import { goto } from '$app/navigation';
import { fade } from 'svelte/transition';
import yaml from 'js-yaml';
const { id } = $page.params;
@ -58,6 +57,10 @@
let loading = false;
let fqdnEl: any = null;
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 previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
@ -66,6 +69,11 @@
let isBot = application.settings.isBot;
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 nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
let isHttps = application.fqdn && application.fqdn.startsWith('https://');
@ -86,6 +94,26 @@
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() {
return 'text-white bg-transparent font-thin px-0 w-full border border-dashed border-coolgray-200';
}
@ -214,7 +242,11 @@
dualCerts,
exposePort: application.exposePort
}));
await post(`/applications/${id}`, { ...application, baseDatabaseBranch });
await post(`/applications/${id}`, {
...application,
baseDatabaseBranch,
dockerComposeConfiguration: JSON.stringify(dockerComposeConfiguration)
});
setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
@ -281,6 +313,36 @@
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>
<div class="w-full">
@ -372,18 +434,20 @@
/>
</div>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="isBot"
isCenter={false}
bind:setting={isBot}
on:click={() => changeSettings('isBot')}
title="Is your application a bot?"
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}
{#if application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center">
<Setting
id="isBot"
isCenter={false}
bind:setting={isBot}
on:click={() => changeSettings('isBot')}
title="Is your application a bot?"
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}
{#if !isBot && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
@ -454,7 +518,7 @@
on:click={() => !$status.application.isRunning && changeSettings('dualCerts')}
/>
</div>
{#if isHttps}
{#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isCustomSSL"
@ -468,349 +532,395 @@
{/if}
{/if}
</div>
<div class="title font-bold pb-3 pt-10 border-b border-coolgray-500 mb-6">Build & Deploy</div>
<div class="grid grid-flow-row gap-2 px-4 pr-5">
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<div class="grid grid-cols-2 items-center">
<label for="baseBuildImage"
>{$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}
{#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="grid grid-flow-row gap-2 px-4 pr-5">
{#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'}
<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"
/>
<label for="baseBuildImage"
>{$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 isDBBranching}
<button
on:click|stopPropagation|preventDefault={() =>
goto(`/applications/${id}/configuration/database`)}
class="btn btn-sm">Configure Connected Database</button
{/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
>
{#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>
<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">
<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 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'}
{#if application.buildPack === 'python'}
<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
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="pythonVariable"
id="pythonVariable"
name="pythonModule"
id="pythonModule"
required
class="w-full"
bind:value={application.pythonVariable}
placeholder="default: app"
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">
<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 application.pythonWSGI?.toLowerCase() === 'uvicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable">Variable</label>
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center pt-4">
<label for="port"
>{$t('forms.port')}
<Explainer explanation={'The port your application listens on.'} /></label
>
<input
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
class="w-full"
bind:value={application.pythonVariable}
placeholder="default: app"
name="port"
id="port"
bind:value={application.port}
placeholder="{$t('forms.default')}: 'python' ? '8000' : '3000'"
/>
</div>
{/if}
{/if}
{#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center pt-4">
<label for="port"
>{$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."}
<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}
readonly={!$appSession.isAdmin}
name="denoOptions"
id="denoOptions"
bind:value={application.denoOptions}
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
name="exposePort"
id="exposePort"
bind:value={application.exposePort}
placeholder="12345"
/>
</div>
{/if}
{#if application.buildPack !== 'laravel'}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory"
>{$t('forms.base_directory')}
<Explainer
explanation={"Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings font-bold'>monorepos</span>."}
{#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
>
</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"
class="w-full"
disabled={isDisabled}
readonly={!$appSession.isAdmin}
name="dockerFileLocation"
id="dockerFileLocation"
bind:value={application.dockerFileLocation}
placeholder="default: /Dockerfile"
name="denoOptions"
id="denoOptions"
bind:value={application.denoOptions}
placeholder="eg: --allow-net --allow-hrtime --config path/to/file.json"
/>
{#if application.baseDirectory}
<label class="label">
<span class="label-text-alt text-xs"
>Path: {application.baseDirectory.replace(
/^\/$/,
''
)}{application.dockerFileLocation}</span
>
</label>
</div>
{/if}
{#if application.buildPack !== 'laravel'}
<div class="grid grid-cols-2 items-center">
<div class="flex-col">
<label for="baseDirectory"
>{$t('forms.base_directory')}
<Explainer
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}
</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>
<div class="grid grid-cols-2 items-center">
<label for="fqdn"
>{$t('application.url_fqdn')}
<Explainer
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>"}
/>
</label>
<div>
<input
class="w-full"
name="fqdn"
id="fqdn"
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>
</form>
</div>

View File

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

View File

@ -8,14 +8,16 @@
"oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate",
"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:seed": "pnpm run --filter api db:seed",
"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:api": "NODE_ENV=development pnpm run --filter api format",
"lint": "run-p -l -n 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:api": "NODE_ENV=development pnpm run --filter api 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-svelte: 0.26.2
js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0
postcss: 8.4.16
prettier: 2.7.1
@ -181,6 +182,7 @@ importers:
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
dayjs: 1.11.5
js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0
svelte-file-dropzone: 1.0.0
svelte-select: 4.4.7