@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					-- AlterTable
 | 
				
			||||||
 | 
					ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;
 | 
				
			||||||
@@ -195,6 +195,7 @@ model ApplicationSettings {
 | 
				
			|||||||
model ApplicationPersistentStorage {
 | 
					model ApplicationPersistentStorage {
 | 
				
			||||||
  id            String      @id @default(cuid())
 | 
					  id            String      @id @default(cuid())
 | 
				
			||||||
  applicationId String
 | 
					  applicationId String
 | 
				
			||||||
 | 
					  hostPath      String?
 | 
				
			||||||
  path          String
 | 
					  path          String
 | 
				
			||||||
  oldPath       Boolean     @default(false)
 | 
					  oldPath       Boolean     @default(false)
 | 
				
			||||||
  createdAt     DateTime    @default(now())
 | 
					  createdAt     DateTime    @default(now())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -110,6 +110,9 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
													.replace(/\//gi, '-')
 | 
																		.replace(/\//gi, '-')
 | 
				
			||||||
													.replace('-app', '')}:${storage.path}`;
 | 
																		.replace('-app', '')}:${storage.path}`;
 | 
				
			||||||
											}
 | 
																}
 | 
				
			||||||
 | 
																if (storage.hostPath) {
 | 
				
			||||||
 | 
																	return `${storage.hostPath}:${storage.path}`
 | 
				
			||||||
 | 
																}
 | 
				
			||||||
											return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
 | 
																return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
 | 
				
			||||||
										}) || [];
 | 
															}) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -160,7 +163,11 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
											port: exposePort ? `${exposePort}:${port}` : port
 | 
																port: exposePort ? `${exposePort}:${port}` : port
 | 
				
			||||||
										});
 | 
															});
 | 
				
			||||||
										try {
 | 
															try {
 | 
				
			||||||
											const composeVolumes = volumes.map((volume) => {
 | 
																const composeVolumes = volumes.filter(v => {
 | 
				
			||||||
 | 
																	if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
 | 
				
			||||||
 | 
																		return v;
 | 
				
			||||||
 | 
																	}
 | 
				
			||||||
 | 
																}).map((volume) => {
 | 
				
			||||||
												return {
 | 
																	return {
 | 
				
			||||||
													[`${volume.split(':')[0]}`]: {
 | 
																		[`${volume.split(':')[0]}`]: {
 | 
				
			||||||
														name: volume.split(':')[0]
 | 
																			name: volume.split(':')[0]
 | 
				
			||||||
@@ -381,6 +388,9 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
												.replace(/\//gi, '-')
 | 
																	.replace(/\//gi, '-')
 | 
				
			||||||
												.replace('-app', '')}:${storage.path}`;
 | 
																	.replace('-app', '')}:${storage.path}`;
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
 | 
															if (storage.hostPath) {
 | 
				
			||||||
 | 
																return `${storage.hostPath}:${storage.path}`
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
										return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
 | 
															return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
 | 
				
			||||||
									}) || [];
 | 
														}) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -691,7 +701,11 @@ import * as buildpacks from '../lib/buildPacks';
 | 
				
			|||||||
											await saveDockerRegistryCredentials({ url, username, password, workdir });
 | 
																await saveDockerRegistryCredentials({ url, username, password, workdir });
 | 
				
			||||||
										}
 | 
															}
 | 
				
			||||||
										try {
 | 
															try {
 | 
				
			||||||
											const composeVolumes = volumes.map((volume) => {
 | 
																const composeVolumes = volumes.filter(v => {
 | 
				
			||||||
 | 
																	if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
 | 
				
			||||||
 | 
																		return v;
 | 
				
			||||||
 | 
																	}
 | 
				
			||||||
 | 
																}).map((volume) => {
 | 
				
			||||||
												return {
 | 
																	return {
 | 
				
			||||||
													[`${volume.split(':')[0]}`]: {
 | 
																		[`${volume.split(':')[0]}`]: {
 | 
				
			||||||
														name: volume.split(':')[0]
 | 
																			name: volume.split(':')[0]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,12 +36,13 @@ export default async function (data) {
 | 
				
			|||||||
	if (volumes.length > 0) {
 | 
						if (volumes.length > 0) {
 | 
				
			||||||
		for (const volume of volumes) {
 | 
							for (const volume of volumes) {
 | 
				
			||||||
			let [v, path] = volume.split(':');
 | 
								let [v, path] = volume.split(':');
 | 
				
			||||||
 | 
								if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
 | 
				
			||||||
				composeVolumes[v] = {
 | 
									composeVolumes[v] = {
 | 
				
			||||||
					name: v
 | 
										name: v
 | 
				
			||||||
				};
 | 
									};
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	let networks = {};
 | 
						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}`;
 | 
				
			||||||
@@ -77,7 +78,17 @@ export default async function (data) {
 | 
				
			|||||||
		// TODO: If we support separated volume for each service, we need to add it here
 | 
							// TODO: If we support separated volume for each service, we need to add it here
 | 
				
			||||||
		if (value['volumes']?.length > 0) {
 | 
							if (value['volumes']?.length > 0) {
 | 
				
			||||||
			value['volumes'] = value['volumes'].map((volume) => {
 | 
								value['volumes'] = value['volumes'].map((volume) => {
 | 
				
			||||||
 | 
									if (typeof volume === 'string') {
 | 
				
			||||||
					let [v, path, permission] = volume.split(':');
 | 
										let [v, path, permission] = volume.split(':');
 | 
				
			||||||
 | 
										if (
 | 
				
			||||||
 | 
											v.startsWith('.') ||
 | 
				
			||||||
 | 
											v.startsWith('..') ||
 | 
				
			||||||
 | 
											v.startsWith('/') ||
 | 
				
			||||||
 | 
											v.startsWith('~') ||
 | 
				
			||||||
 | 
											v.startsWith('$PWD')
 | 
				
			||||||
 | 
										) {
 | 
				
			||||||
 | 
											v = v.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
						if (!path) {
 | 
											if (!path) {
 | 
				
			||||||
							path = v;
 | 
												path = v;
 | 
				
			||||||
							v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
												v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
@@ -87,7 +98,34 @@ export default async function (data) {
 | 
				
			|||||||
						composeVolumes[v] = {
 | 
											composeVolumes[v] = {
 | 
				
			||||||
							name: v
 | 
												name: v
 | 
				
			||||||
						};
 | 
											};
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
					return `${v}:${path}${permission ? ':' + permission : ''}`;
 | 
										return `${v}:${path}${permission ? ':' + permission : ''}`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if (typeof volume === 'object') {
 | 
				
			||||||
 | 
										let { source, target, mode } = volume;
 | 
				
			||||||
 | 
										if (
 | 
				
			||||||
 | 
											source.startsWith('.') ||
 | 
				
			||||||
 | 
											source.startsWith('..') ||
 | 
				
			||||||
 | 
											source.startsWith('/') ||
 | 
				
			||||||
 | 
											source.startsWith('~') ||
 | 
				
			||||||
 | 
											source.startsWith('$PWD')
 | 
				
			||||||
 | 
										) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
 | 
				
			||||||
 | 
											console.log({source})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											if (!target) {
 | 
				
			||||||
 | 
												target = source;
 | 
				
			||||||
 | 
												source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										return `${source}:${target}${mode ? ':' + mode : ''}`;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (volumes.length > 0) {
 | 
							if (volumes.length > 0) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import { promises as dns } from 'dns';
 | 
				
			|||||||
import * as Sentry from '@sentry/node';
 | 
					import * as Sentry from '@sentry/node';
 | 
				
			||||||
import { PrismaClient } from '@prisma/client';
 | 
					import { PrismaClient } from '@prisma/client';
 | 
				
			||||||
import os from 'os';
 | 
					import os from 'os';
 | 
				
			||||||
import sshConfig from 'ssh-config';
 | 
					import * as SSHConfig from 'ssh-config/src/ssh-config';
 | 
				
			||||||
import jsonwebtoken from 'jsonwebtoken';
 | 
					import jsonwebtoken from 'jsonwebtoken';
 | 
				
			||||||
import { checkContainer, removeContainer } from './docker';
 | 
					import { checkContainer, removeContainer } from './docker';
 | 
				
			||||||
import { day } from './dayjs';
 | 
					import { day } from './dayjs';
 | 
				
			||||||
@@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
 | 
				
			|||||||
import { scheduler } from './scheduler';
 | 
					import { scheduler } from './scheduler';
 | 
				
			||||||
import type { ExecaChildProcess } from 'execa';
 | 
					import type { ExecaChildProcess } from 'execa';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const version = '3.12.25';
 | 
					export const version = '3.12.26';
 | 
				
			||||||
export const isDev = process.env.NODE_ENV === 'development';
 | 
					export const isDev = process.env.NODE_ENV === 'development';
 | 
				
			||||||
export const proxyPort = process.env.COOLIFY_PROXY_PORT;
 | 
					export const proxyPort = process.env.COOLIFY_PROXY_PORT;
 | 
				
			||||||
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
 | 
					export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
 | 
				
			||||||
@@ -498,33 +498,56 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
 | 
				
			|||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Update the ssh config file with a host
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @param id Destination ID
 | 
				
			||||||
 | 
					 * @returns
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
export async function createRemoteEngineConfiguration(id: string) {
 | 
					export async function createRemoteEngineConfiguration(id: string) {
 | 
				
			||||||
	const homedir = os.homedir();
 | 
					 | 
				
			||||||
	const sshKeyFile = `/tmp/id_rsa-${id}`;
 | 
						const sshKeyFile = `/tmp/id_rsa-${id}`;
 | 
				
			||||||
	const localPort = await getFreeSSHLocalPort(id);
 | 
						const localPort = await getFreeSSHLocalPort(id);
 | 
				
			||||||
	const {
 | 
						const {
 | 
				
			||||||
		sshKey: { privateKey },
 | 
							sshKey: { privateKey },
 | 
				
			||||||
		network,
 | 
					 | 
				
			||||||
		remoteIpAddress,
 | 
							remoteIpAddress,
 | 
				
			||||||
		remotePort,
 | 
							remotePort,
 | 
				
			||||||
		remoteUser
 | 
							remoteUser
 | 
				
			||||||
	} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
 | 
						} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Write new keyfile
 | 
				
			||||||
	await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
 | 
						await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
 | 
				
			||||||
	const config = sshConfig.parse('');
 | 
					
 | 
				
			||||||
	const Host = `${remoteIpAddress}-remote`;
 | 
						const Host = `${remoteIpAddress}-remote`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Removes previous ssh-keys
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await executeCommand({ command: `ssh-keygen -R ${Host}` });
 | 
							await executeCommand({ command: `ssh-keygen -R ${Host}` });
 | 
				
			||||||
		await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
 | 
							await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
 | 
				
			||||||
		await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
 | 
							await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
 | 
				
			||||||
	} catch (error) { }
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							//
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const homedir = os.homedir();
 | 
				
			||||||
 | 
						let currentConfigFileContent = '';
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							// Read the current config file
 | 
				
			||||||
 | 
							currentConfigFileContent = (await fs.readFile(`${homedir}/.ssh/config`)).toString();
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							// File doesn't exist, so we do nothing, a new one is going to be created
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Parse the config file
 | 
				
			||||||
 | 
						const config = SSHConfig.parse(currentConfigFileContent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Remove current config for the given host
 | 
				
			||||||
	const found = config.find({ Host });
 | 
						const found = config.find({ Host });
 | 
				
			||||||
	const foundIp = config.find({ Host: remoteIpAddress });
 | 
						const foundIp = config.find({ Host: remoteIpAddress });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (found) config.remove({ Host });
 | 
						if (found) config.remove({ Host });
 | 
				
			||||||
	if (foundIp) config.remove({ Host: remoteIpAddress });
 | 
						if (foundIp) config.remove({ Host: remoteIpAddress });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create the new config
 | 
				
			||||||
	config.append({
 | 
						config.append({
 | 
				
			||||||
		Host,
 | 
							Host,
 | 
				
			||||||
		Hostname: remoteIpAddress,
 | 
							Hostname: remoteIpAddress,
 | 
				
			||||||
@@ -537,13 +560,17 @@ export async function createRemoteEngineConfiguration(id: string) {
 | 
				
			|||||||
		ControlPersist: '10m'
 | 
							ControlPersist: '10m'
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Check if .ssh folder exists, and if not create one
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		await fs.stat(`${homedir}/.ssh/`);
 | 
							await fs.stat(`${homedir}/.ssh/`);
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error) {
 | 
				
			||||||
		await fs.mkdir(`${homedir}/.ssh/`);
 | 
							await fs.mkdir(`${homedir}/.ssh/`);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
 | 
					
 | 
				
			||||||
 | 
						// Write the config
 | 
				
			||||||
 | 
						return await fs.writeFile(`${homedir}/.ssh/config`, SSHConfig.stringify(config));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function executeCommand({
 | 
					export async function executeCommand({
 | 
				
			||||||
	command,
 | 
						command,
 | 
				
			||||||
	dockerId = null,
 | 
						dockerId = null,
 | 
				
			||||||
@@ -1633,6 +1660,9 @@ export function errorHandler({
 | 
				
			|||||||
	type?: string | null;
 | 
						type?: string | null;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
	if (message.message) message = message.message;
 | 
						if (message.message) message = message.message;
 | 
				
			||||||
 | 
						if (message.includes('Unique constraint failed')) {
 | 
				
			||||||
 | 
							message = 'This data is unique and already exists. Please try again with a different value.';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if (type === 'normal') {
 | 
						if (type === 'normal') {
 | 
				
			||||||
		Sentry.captureException(message);
 | 
							Sentry.captureException(message);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1340,16 +1340,16 @@ export async function getStorages(request: FastifyRequest<OnlyId>) {
 | 
				
			|||||||
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
 | 
					export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		const { id } = request.params;
 | 
							const { id } = request.params;
 | 
				
			||||||
		const { path, newStorage, storageId } = request.body;
 | 
							const { hostPath, path, newStorage, storageId } = request.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (newStorage) {
 | 
							if (newStorage) {
 | 
				
			||||||
			await prisma.applicationPersistentStorage.create({
 | 
								await prisma.applicationPersistentStorage.create({
 | 
				
			||||||
				data: { path, application: { connect: { id } } }
 | 
									data: { hostPath, path, application: { connect: { id } } }
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			await prisma.applicationPersistentStorage.update({
 | 
								await prisma.applicationPersistentStorage.update({
 | 
				
			||||||
				where: { id: storageId },
 | 
									where: { id: storageId },
 | 
				
			||||||
				data: { path }
 | 
									data: { hostPath, path }
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return reply.code(201).send();
 | 
							return reply.code(201).send();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,6 +96,7 @@ export interface DeleteSecret extends OnlyId {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
export interface SaveStorage extends OnlyId {
 | 
					export interface SaveStorage extends OnlyId {
 | 
				
			||||||
	Body: {
 | 
						Body: {
 | 
				
			||||||
 | 
							hostPath?: string;
 | 
				
			||||||
		path: string;
 | 
							path: string;
 | 
				
			||||||
		newStorage: boolean;
 | 
							newStorage: boolean;
 | 
				
			||||||
		storageId: string;
 | 
							storageId: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@
 | 
				
			|||||||
	import { errorNotification } from '$lib/common';
 | 
						import { errorNotification } from '$lib/common';
 | 
				
			||||||
	import { addToast } from '$lib/store';
 | 
						import { addToast } from '$lib/store';
 | 
				
			||||||
	import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
 | 
						import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
 | 
				
			||||||
 | 
						import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
 | 
				
			||||||
	const { id } = $page.params;
 | 
						const { id } = $page.params;
 | 
				
			||||||
	let isHttps = browser && window.location.protocol === 'https:';
 | 
						let isHttps = browser && window.location.protocol === 'https:';
 | 
				
			||||||
	export let value: string;
 | 
						export let value: string;
 | 
				
			||||||
@@ -33,11 +34,13 @@
 | 
				
			|||||||
			storage.path.replace(/\/\//g, '/');
 | 
								storage.path.replace(/\/\//g, '/');
 | 
				
			||||||
			await post(`/applications/${id}/storages`, {
 | 
								await post(`/applications/${id}/storages`, {
 | 
				
			||||||
				path: storage.path,
 | 
									path: storage.path,
 | 
				
			||||||
 | 
									hostPath: storage.hostPath,
 | 
				
			||||||
				storageId: storage.id,
 | 
									storageId: storage.id,
 | 
				
			||||||
				newStorage
 | 
									newStorage
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
			dispatch('refresh');
 | 
								dispatch('refresh');
 | 
				
			||||||
			if (isNew) {
 | 
								if (isNew) {
 | 
				
			||||||
 | 
									storage.hostPath = null;
 | 
				
			||||||
				storage.path = null;
 | 
									storage.path = null;
 | 
				
			||||||
				storage.id = null;
 | 
									storage.id = null;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -80,27 +83,42 @@
 | 
				
			|||||||
		<div class="flex gap-4 pb-2" class:pt-8={isNew}>
 | 
							<div class="flex gap-4 pb-2" class:pt-8={isNew}>
 | 
				
			||||||
			{#if storage.applicationId}
 | 
								{#if storage.applicationId}
 | 
				
			||||||
				{#if storage.oldPath}
 | 
									{#if storage.oldPath}
 | 
				
			||||||
		
 | 
					 | 
				
			||||||
					<CopyVolumeField
 | 
										<CopyVolumeField
 | 
				
			||||||
						value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
 | 
											value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
 | 
				
			||||||
					/>
 | 
										/>
 | 
				
			||||||
				{:else}
 | 
									{:else if !storage.hostPath}
 | 
				
			||||||
				
 | 
					 | 
				
			||||||
					<CopyVolumeField
 | 
										<CopyVolumeField
 | 
				
			||||||
						value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
 | 
											value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
 | 
				
			||||||
					/>
 | 
										/>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
			{/if}
 | 
								{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								{#if isNew}
 | 
				
			||||||
 | 
									<div class="w-full">
 | 
				
			||||||
 | 
										<input
 | 
				
			||||||
 | 
											disabled={!isNew}
 | 
				
			||||||
 | 
											readonly={!isNew}
 | 
				
			||||||
 | 
											bind:value={storage.hostPath}
 | 
				
			||||||
 | 
											placeholder="Host path, example: ~/.directory"
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<SimpleExplainer
 | 
				
			||||||
 | 
											text="You can mount <span class='text-yellow-400 font-bold'>host paths</span> from the operating system.<br>Leave it empty to define a volume based volume."
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								{:else if storage.hostPath}
 | 
				
			||||||
 | 
									<input disabled readonly value={storage.hostPath} />
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
			<input
 | 
								<input
 | 
				
			||||||
				disabled={!isNew}
 | 
									disabled={!isNew}
 | 
				
			||||||
				readonly={!isNew}
 | 
									readonly={!isNew}
 | 
				
			||||||
				class="w-full"
 | 
									class="w-full"
 | 
				
			||||||
				bind:value={storage.path}
 | 
									bind:value={storage.path}
 | 
				
			||||||
				required
 | 
									required
 | 
				
			||||||
				placeholder="eg: /data"
 | 
									placeholder="Mount point inside the container, example: /data"
 | 
				
			||||||
			/>
 | 
								/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<div class="flex items-center justify-center">
 | 
								<div class="flex items-start justify-center">
 | 
				
			||||||
				{#if isNew}
 | 
									{#if isNew}
 | 
				
			||||||
					<div class="w-full lg:w-64">
 | 
										<div class="w-full lg:w-64">
 | 
				
			||||||
						<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
 | 
											<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -427,29 +427,6 @@
 | 
				
			|||||||
				</svg> Stop
 | 
									</svg> Stop
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
		{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
 | 
							{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
 | 
				
			||||||
			{#if $status.application.overallStatus === 'degraded'}
 | 
					 | 
				
			||||||
				<button
 | 
					 | 
				
			||||||
					on:click={stopApplication}
 | 
					 | 
				
			||||||
					type="submit"
 | 
					 | 
				
			||||||
					disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
 | 
					 | 
				
			||||||
					class="btn btn-sm gap-2"
 | 
					 | 
				
			||||||
				>
 | 
					 | 
				
			||||||
					<svg
 | 
					 | 
				
			||||||
						xmlns="http://www.w3.org/2000/svg"
 | 
					 | 
				
			||||||
						class="w-6 h-6 text-error"
 | 
					 | 
				
			||||||
						viewBox="0 0 24 24"
 | 
					 | 
				
			||||||
						stroke-width="1.5"
 | 
					 | 
				
			||||||
						stroke="currentColor"
 | 
					 | 
				
			||||||
						fill="none"
 | 
					 | 
				
			||||||
						stroke-linecap="round"
 | 
					 | 
				
			||||||
						stroke-linejoin="round"
 | 
					 | 
				
			||||||
					>
 | 
					 | 
				
			||||||
						<path stroke="none" d="M0 0h24v24H0z" fill="none" />
 | 
					 | 
				
			||||||
						<rect x="6" y="5" width="4" height="14" rx="1" />
 | 
					 | 
				
			||||||
						<rect x="14" y="5" width="4" height="14" rx="1" />
 | 
					 | 
				
			||||||
					</svg> Stop
 | 
					 | 
				
			||||||
				</button>
 | 
					 | 
				
			||||||
			{/if}
 | 
					 | 
				
			||||||
			<button
 | 
								<button
 | 
				
			||||||
				class="btn btn-sm gap-2"
 | 
									class="btn btn-sm gap-2"
 | 
				
			||||||
				disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
 | 
									disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
 | 
				
			||||||
@@ -493,6 +470,29 @@
 | 
				
			|||||||
						: 'Redeploy Stack'
 | 
											: 'Redeploy Stack'
 | 
				
			||||||
					: 'Deploy'}
 | 
										: 'Deploy'}
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
 | 
								{#if $status.application.overallStatus === 'degraded'}
 | 
				
			||||||
 | 
									<button
 | 
				
			||||||
 | 
										on:click={stopApplication}
 | 
				
			||||||
 | 
										type="submit"
 | 
				
			||||||
 | 
										disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
 | 
				
			||||||
 | 
										class="btn btn-sm gap-2"
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<svg
 | 
				
			||||||
 | 
											xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
											class="w-6 h-6 text-error"
 | 
				
			||||||
 | 
											viewBox="0 0 24 24"
 | 
				
			||||||
 | 
											stroke-width="1.5"
 | 
				
			||||||
 | 
											stroke="currentColor"
 | 
				
			||||||
 | 
											fill="none"
 | 
				
			||||||
 | 
											stroke-linecap="round"
 | 
				
			||||||
 | 
											stroke-linejoin="round"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											<path stroke="none" d="M0 0h24v24H0z" fill="none" />
 | 
				
			||||||
 | 
											<rect x="6" y="5" width="4" height="14" rx="1" />
 | 
				
			||||||
 | 
											<rect x="14" y="5" width="4" height="14" rx="1" />
 | 
				
			||||||
 | 
										</svg> Stop
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
		{/if}
 | 
							{/if}
 | 
				
			||||||
		{#if $location && $status.application.overallStatus === 'healthy'}
 | 
							{#if $location && $status.application.overallStatus === 'healthy'}
 | 
				
			||||||
			<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
 | 
								<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,18 +35,47 @@
 | 
				
			|||||||
		for (const [_, service] of Object.entries(composeJson.services)) {
 | 
							for (const [_, service] of Object.entries(composeJson.services)) {
 | 
				
			||||||
			if (service?.volumes) {
 | 
								if (service?.volumes) {
 | 
				
			||||||
				for (const [_, volumeName] of Object.entries(service.volumes)) {
 | 
									for (const [_, volumeName] of Object.entries(service.volumes)) {
 | 
				
			||||||
 | 
										if (typeof volumeName === 'string') {
 | 
				
			||||||
						let [volume, target] = volumeName.split(':');
 | 
											let [volume, target] = volumeName.split(':');
 | 
				
			||||||
					if (volume === '.') {
 | 
											if (
 | 
				
			||||||
						volume = target;
 | 
												volume.startsWith('.') ||
 | 
				
			||||||
					}
 | 
												volume.startsWith('..') ||
 | 
				
			||||||
 | 
												volume.startsWith('/') ||
 | 
				
			||||||
 | 
												volume.startsWith('~') ||
 | 
				
			||||||
 | 
												volume.startsWith('$PWD')
 | 
				
			||||||
 | 
											) {
 | 
				
			||||||
 | 
												volume = volume.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
							if (!target) {
 | 
												if (!target) {
 | 
				
			||||||
								target = volume;
 | 
													target = volume;
 | 
				
			||||||
								volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
													volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
							} else {
 | 
												} else {
 | 
				
			||||||
								volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
													volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
						predefinedVolumes.push({ id: volume, path: target, predefined: true });
 | 
											predefinedVolumes.push({ id: volume, path: target, predefined: true });
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
										if (typeof volumeName === 'object') {
 | 
				
			||||||
 | 
											let { source, target } = volumeName;
 | 
				
			||||||
 | 
											if (
 | 
				
			||||||
 | 
												source.startsWith('.') ||
 | 
				
			||||||
 | 
												source.startsWith('..') ||
 | 
				
			||||||
 | 
												source.startsWith('/') ||
 | 
				
			||||||
 | 
												source.startsWith('~') ||
 | 
				
			||||||
 | 
												source.startsWith('$PWD')
 | 
				
			||||||
 | 
											) {
 | 
				
			||||||
 | 
												source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												if (!target) {
 | 
				
			||||||
 | 
													target = source;
 | 
				
			||||||
 | 
													source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											predefinedVolumes.push({ id: source, path: target, predefined: true });
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "coolify",
 | 
					  "name": "coolify",
 | 
				
			||||||
  "description": "An open-source & self-hostable Heroku / Netlify alternative.",
 | 
					  "description": "An open-source & self-hostable Heroku / Netlify alternative.",
 | 
				
			||||||
  "version": "3.12.25",
 | 
					  "version": "3.12.26",
 | 
				
			||||||
  "license": "Apache-2.0",
 | 
					  "license": "Apache-2.0",
 | 
				
			||||||
  "repository": "github:coollabsio/coolify",
 | 
					  "repository": "github:coollabsio/coolify",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user