Compare commits

..

52 Commits

Author SHA1 Message Date
Andras Bacsai
67fc2fd3c0 fix: pr previews 2022-09-07 15:29:56 +00:00
Andras Bacsai
d3a1bbc3d0 Merge pull request #596 from coollabsio/next
v3.9.2
2022-09-07 10:16:54 +02:00
Andras Bacsai
0078574ee6 fix: add php 8.1/8.2 2022-09-07 10:02:59 +02:00
Andras Bacsai
7cfd313531 ui: fix loading start/stop db/services 2022-09-07 09:45:16 +02:00
Andras Bacsai
e7919e9a1b ui: fix initial loading icon bg 2022-09-07 09:39:29 +02:00
Andras Bacsai
98073202e9 fix: minio default env variables 2022-09-07 09:28:53 +02:00
Andras Bacsai
8dee345f85 enable autoupdate option for everyone 2022-09-07 09:23:03 +02:00
Andras Bacsai
9161882f33 debug: add debug log 2022-09-07 09:21:28 +02:00
Andras Bacsai
eef313665b fix: service volume generation 2022-09-07 09:18:44 +02:00
Andras Bacsai
53e70fbfcb dev: update devcontainer 2022-09-07 09:15:10 +02:00
Andras Bacsai
05a1721499 fix: revert last change with domain check 2022-09-07 09:15:03 +02:00
Andras Bacsai
2f772080b8 fix: add initial DNS servers 2022-09-07 09:06:30 +02:00
Andras Bacsai
a5548c080c fix: service state update 2022-09-07 08:58:51 +02:00
Andras Bacsai
7e0a1ecc80 ui: fix login/register page 2022-09-07 08:58:40 +02:00
Andras Bacsai
3f2dcccc07 fix: use ip instead of window location host 2022-09-07 08:58:27 +02:00
Andras Bacsai
adc5965b32 fix: use ip address instead of window location 2022-09-07 08:57:32 +02:00
Andras Bacsai
6088f2e573 chore: version++ 2022-09-06 15:46:22 +02:00
Andras Bacsai
fc705746c0 fix: gitlab webhook 2022-09-06 15:45:29 +02:00
Andras Bacsai
8182359fe4 Merge branch 'main' into next 2022-09-06 15:41:22 +02:00
Andras Bacsai
8b7406e168 revert 2022-09-06 15:25:18 +02:00
Andras Bacsai
9d6317f782 fix 2022-09-06 15:13:51 +02:00
Andras Bacsai
d8bdb73140 test tagging 2022-09-06 14:59:24 +02:00
Andras Bacsai
476db15431 test 2022-09-06 14:49:48 +02:00
Andras Bacsai
20ce356296 fix: move restart button to settings 2022-09-06 14:47:37 +02:00
Andras Bacsai
ea594dcbc6 fix: workdir 2022-09-06 14:32:50 +02:00
Andras Bacsai
021b9746a8 update production release 2022-09-06 14:29:54 +02:00
Andras Bacsai
c4615ae557 Dockerfile 2022-09-06 14:20:14 +02:00
Andras Bacsai
95a5089bdc fix: debug api logging + gh actions 2022-09-06 14:08:10 +02:00
Andras Bacsai
cef1fba281 finally?! 2022-09-06 13:23:53 +02:00
Andras Bacsai
5c7859a258 asd 2022-09-06 13:08:29 +02:00
Andras Bacsai
986cdae5b0 asd 2022-09-06 13:01:55 +02:00
Andras Bacsai
3b11e28d6c asd 2022-09-06 12:56:02 +02:00
Andras Bacsai
eba63e8e76 fix 2022-09-06 12:54:38 +02:00
Andras Bacsai
2fc65e3b42 grr 2022-09-06 12:04:58 +02:00
Andras Bacsai
7d504ab2bf fixxxx 2022-09-06 12:02:03 +02:00
Andras Bacsai
216c7efd42 fixxxxx 2022-09-06 11:55:16 +02:00
Andras Bacsai
8c4149db16 fix issues 2022-09-06 11:42:00 +02:00
Andras Bacsai
20ac8f69ea Update dockerfile 2022-09-06 11:38:36 +02:00
Andras Bacsai
1db3d7a6fb fixit 2022-09-06 11:31:54 +02:00
Andras Bacsai
b1c1138cf8 fixit now 2022-09-06 11:31:02 +02:00
Andras Bacsai
00b1a4f174 fix flow 2022-09-06 11:21:58 +02:00
Andras Bacsai
86cc665b58 fix 2022-09-06 11:05:40 +02:00
Andras Bacsai
e26dd578ef fixes 2022-09-06 11:02:33 +02:00
Andras Bacsai
f1f3217052 testing new gh actions 2022-09-06 10:59:50 +02:00
Andras Bacsai
8f14fd89ef show not allowed list 2022-09-06 10:43:59 +02:00
Andras Bacsai
26e4d52a61 github actions update 2022-09-06 10:30:37 +02:00
Andras Bacsai
319c647147 updates 2022-09-06 10:28:13 +02:00
Andras Bacsai
f4cd93bd36 test allowedlist 2022-09-06 10:14:39 +02:00
Andras Bacsai
5a80bb1d2a fix: dockerfile 2022-09-06 10:14:34 +02:00
Andras Bacsai
1126dcacf5 update 2022-09-06 10:08:31 +02:00
Andras Bacsai
bdf9a73d19 fix 2022-09-06 09:42:13 +02:00
Andras Bacsai
1f73b83a79 testing traefik stuff 2022-09-06 09:30:13 +02:00
19 changed files with 96 additions and 50 deletions

View File

@@ -20,7 +20,8 @@
"svelte.svelte-vscode", "svelte.svelte-vscode",
"ardenivanov.svelte-intellisense", "ardenivanov.svelte-intellisense",
"Prisma.prisma", "Prisma.prisma",
"bradlc.vscode-tailwindcss" "bradlc.vscode-tailwindcss",
"waderyan.gitblame"
], ],
// Use 'forwardPorts' to make a list of ports inside the container available locally. // Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [3000, 3001], "forwardPorts": [3000, 3001],

View File

@@ -17,7 +17,6 @@ const algorithm = 'aes-256-ctr';
async function main() { async function main() {
// Enable registration for the first user // Enable registration for the first user
// Set initial HAProxy password
const settingsFound = await prisma.setting.findFirst({}); const settingsFound = await prisma.setting.findFirst({});
if (!settingsFound) { if (!settingsFound) {
await prisma.setting.create({ await prisma.setting.create({
@@ -25,7 +24,8 @@ async function main() {
isRegistrationEnabled: true, isRegistrationEnabled: true,
proxyPassword: encrypt(generatePassword()), proxyPassword: encrypt(generatePassword()),
proxyUser: cuid(), proxyUser: cuid(),
arch: process.arch arch: process.arch,
DNSServers: '1.1.1.1,8.8.8.8'
} }
}); });
} else { } else {

View File

@@ -89,6 +89,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
} }
]; ];
const phpVersions = [ const phpVersions = [
{
value: 'webdevops/php-apache:8.2',
label: 'webdevops/php-apache:8.2'
},
{
value: 'webdevops/php-nginx:8.2',
label: 'webdevops/php-nginx:8.2'
},
{
value: 'webdevops/php-apache:8.1',
label: 'webdevops/php-apache:8.1'
},
{
value: 'webdevops/php-nginx:8.1',
label: 'webdevops/php-nginx:8.1'
},
{ {
value: 'webdevops/php-apache:8.0', value: 'webdevops/php-apache:8.0',
label: 'webdevops/php-apache:8.0' label: 'webdevops/php-apache:8.0'
@@ -145,6 +161,22 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
value: 'webdevops/php-nginx:5.6', value: 'webdevops/php-nginx:5.6',
label: 'webdevops/php-nginx:5.6' label: 'webdevops/php-nginx:5.6'
}, },
{
value: 'webdevops/php-apache:8.2-alpine',
label: 'webdevops/php-apache:8.2-alpine'
},
{
value: 'webdevops/php-nginx:8.2-alpine',
label: 'webdevops/php-nginx:8.2-alpine'
},
{
value: 'webdevops/php-apache:8.1-alpine',
label: 'webdevops/php-apache:8.1-alpine'
},
{
value: 'webdevops/php-nginx:8.1-alpine',
label: 'webdevops/php-nginx:8.1-alpine'
},
{ {
value: 'webdevops/php-apache:8.0-alpine', value: 'webdevops/php-apache:8.0-alpine',
label: 'webdevops/php-apache:8.0-alpine' label: 'webdevops/php-apache:8.0-alpine'
@@ -305,11 +337,11 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseImage = 'denoland/deno:latest'; payload.baseImage = 'denoland/deno:latest';
} }
if (buildPack === 'php') { if (buildPack === 'php') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions; payload.baseImages = phpVersions;
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.0-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }

View File

@@ -21,7 +21,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions'; import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common'; import { includeServices } from './services/common';
export const version = '3.9.1'; export const version = '3.9.3';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';

View File

@@ -198,7 +198,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
@@ -333,6 +333,8 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`, image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`], volumes: [`${id}-minio-data:/data`],
environmentVariables: { environmentVariables: {
MINIO_SERVER_URL: fqdn,
MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_ROOT_PASSWORD: rootUserPassword,
MINIO_BROWSER_REDIRECT_URL: fqdn MINIO_BROWSER_REDIRECT_URL: fqdn
@@ -852,7 +854,7 @@ async function startGhostService(request: FastifyRequest<ServiceStartStop>) {
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1086,7 +1088,7 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
FROM ${config.postgresql.image} FROM ${config.postgresql.image}
COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`;
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile);
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1114,6 +1116,7 @@ async function startUmamiService(request: FastifyRequest<ServiceStartStop>) {
}, },
volumes: volumeMounts volumes: volumeMounts
}; };
console.log(composeFile)
const composeFileDestination = `${workdir}/docker-compose.yaml`; const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination) await startServiceContainers(destinationDocker.id, composeFileDestination)
@@ -1167,7 +1170,7 @@ async function startHasuraService(request: FastifyRequest<ServiceStartStop>) {
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1272,7 +1275,7 @@ async function startFiderService(request: FastifyRequest<ServiceStartStop>) {
config.fider.environmentVariables[secret.name] = secret.value; config.fider.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -1880,7 +1883,7 @@ async function startMoodleService(request: FastifyRequest<ServiceStartStop>) {
config.moodle.environmentVariables[secret.name] = secret.value; config.moodle.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -2006,7 +2009,7 @@ async function startGlitchTipService(request: FastifyRequest<ServiceStartStop>)
config.glitchTip.environmentVariables[secret.name] = secret.value; config.glitchTip.environmentVariables[secret.name] = secret.value;
}); });
} }
const { volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {

View File

@@ -525,9 +525,7 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
} }
export async function checkDNS(request: FastifyRequest<CheckDNS>) { export async function checkDNS(request: FastifyRequest<CheckDNS>) {
try { try {
const { id } = request.params const { id } = request.params
let { exposePort, fqdn, forceSave, dualCerts } = request.body let { exposePort, fqdn, forceSave, dualCerts } = request.body
if (!fqdn) { if (!fqdn) {
return {} return {}

View File

@@ -173,16 +173,16 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
where: { id: application.id }, where: { id: application.id },
data: { updatedAt: new Date() } data: { updatedAt: new Date() }
}); });
if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') { // if (application.connectedDatabase && pullmergeRequestAction === 'opened' || pullmergeRequestAction === 'reopened') {
// Coolify hosted database // // Coolify hosted database
if (application.connectedDatabase.databaseId) { // if (application.connectedDatabase.databaseId) {
const databaseId = application.connectedDatabase.databaseId; // const databaseId = application.connectedDatabase.databaseId;
const database = await prisma.database.findUnique({ where: { id: databaseId } }); // const database = await prisma.database.findUnique({ where: { id: databaseId } });
if (database) { // if (database) {
await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId); // await createdBranchDatabase(database, application.connectedDatabase.hostedDatabaseDBName, pullmergeRequestId);
} // }
} // }
} // }
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,

View File

@@ -133,7 +133,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
pullmergeRequestId, pullmergeRequestId: pullmergeRequestId.toString(),
sourceBranch, sourceBranch,
applicationId: application.id, applicationId: application.id,
destinationDockerId: application.destinationDocker.id, destinationDockerId: application.destinationDocker.id,

View File

@@ -244,7 +244,7 @@
{/if} {/if}
{#if $status.application.initialLoading} {#if $status.application.initialLoading}
<button <button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -218,7 +218,8 @@
if (loading) return; if (loading) return;
loading = true; loading = true;
try { try {
nonWWWDomain = application.fqdn != null && getDomain(application.fqdn).replace(/^www\./, ''); nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, '');
console.log({debug: nonWWWDomain})
if (application.deploymentType) if (application.deploymentType)
application.deploymentType = application.deploymentType.toLowerCase(); application.deploymentType = application.deploymentType.toLowerCase();
!isBot && !isBot &&

View File

@@ -87,12 +87,15 @@
const sure = confirm($t('database.confirm_stop', { name: database.name })); const sure = confirm($t('database.confirm_stop', { name: database.name }));
if (sure) { if (sure) {
$status.database.initialLoading = true; $status.database.initialLoading = true;
$status.database.loading = true;
try { try {
await post(`/databases/${database.id}/stop`, {}); await post(`/databases/${database.id}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.database.initialLoading = false; $status.database.initialLoading = false;
$status.database.loading = false;
await getStatus();
} }
} }
} }
@@ -175,7 +178,7 @@
{/if} {/if}
{#if $status.database.initialLoading} {#if $status.database.initialLoading}
<button <button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out" class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -70,7 +70,7 @@
</div> </div>
</div> </div>
<div class="prose"> <div class="prose">
<h4>Coolify dashboard</h4> <h4>Coolify</h4>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -100,7 +100,7 @@
</div> </div>
</div> </div>
<div class="prose"> <div class="prose">
<h4>Coolify dashboard</h4> <h4>Coolify</h4>
</div> </div>
{/if} {/if}
</div> </div>

View File

@@ -12,7 +12,14 @@
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification, getDomain } from '$lib/common'; import { errorNotification, getDomain } from '$lib/common';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { appSession, disabledButton, status, location, setLocation, addToast } from '$lib/store'; import {
appSession,
status,
setLocation,
addToast,
checkIfDeploymentEnabledServices,
isDeploymentEnabled
} from '$lib/store';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@@ -78,8 +85,8 @@
}); });
await post(`/services/${id}`, { ...service }); await post(`/services/${id}`, { ...service });
setLocation(service); setLocation(service);
$disabledButton = false;
forceSave = false; forceSave = false;
$isDeploymentEnabled = checkIfDeploymentEnabledServices($appSession.isAdmin, service);
return addToast({ return addToast({
message: 'Configuration saved.', message: 'Configuration saved.',
type: 'success' type: 'success'

View File

@@ -12,7 +12,7 @@
export let readOnly: any; export let readOnly: any;
export let settings: any; export let settings: any;
const { id } = $page.params; const { id } = $page.params;
const { ipv4, ipv6 } = settings;
let ftpUrl = generateUrl(service.wordpress.ftpPublicPort); let ftpUrl = generateUrl(service.wordpress.ftpPublicPort);
let ftpUser = service.wordpress.ftpUser; let ftpUser = service.wordpress.ftpUser;
let ftpPassword = service.wordpress.ftpPassword; let ftpPassword = service.wordpress.ftpPassword;
@@ -22,7 +22,7 @@
function generateUrl(publicPort: any) { function generateUrl(publicPort: any) {
return browser return browser
? `sftp://${ ? `sftp://${
settings?.fqdn ? getDomain(settings.fqdn) : window.location.hostname settings?.fqdn ? getDomain(settings.fqdn) : ipv4 || ipv6
}:${publicPort}` }:${publicPort}`
: 'Loading...'; : 'Loading...';
} }

View File

@@ -97,12 +97,15 @@
const sure = confirm($t('database.confirm_stop', { name: service.name })); const sure = confirm($t('database.confirm_stop', { name: service.name }));
if (sure) { if (sure) {
$status.service.initialLoading = true; $status.service.initialLoading = true;
$status.service.loading = true;
try { try {
await post(`/services/${service.id}/${service.type}/stop`, {}); await post(`/services/${service.id}/${service.type}/stop`, {});
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$status.service.initialLoading = false; $status.service.initialLoading = false;
$status.service.loading = false;
await getStatus();
} }
} }
} }

View File

@@ -362,17 +362,15 @@
on:click={() => changeSettings('isAPIDebuggingEnabled')} on:click={() => changeSettings('isAPIDebuggingEnabled')}
/> />
</div> </div>
{#if browser && $features.beta} <div class="grid grid-cols-2 items-center">
<div class="grid grid-cols-2 items-center"> <Setting
<Setting id="isAutoUpdateEnabled"
id="isAutoUpdateEnabled" bind:setting={isAutoUpdateEnabled}
bind:setting={isAutoUpdateEnabled} title={$t('setting.auto_update_enabled')}
title={$t('setting.auto_update_enabled')} description={$t('setting.auto_update_enabled_explainer')}
description={$t('setting.auto_update_enabled_explainer')} on:click={() => changeSettings('isAutoUpdateEnabled')}
on:click={() => changeSettings('isAutoUpdateEnabled')} />
/> </div>
</div>
{/if}
</div> </div>
</form> </form>
</div> </div>

View File

@@ -46,8 +46,8 @@
customPort: source.customPort customPort: source.customPort
}); });
const { organization, htmlUrl } = source; const { organization, htmlUrl } = source;
const { fqdn } = settings; const { fqdn, ipv4, ipv6 } = settings;
const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${window.location.host}` || ''; const host = dev ? getAPIUrl() : fqdn ? fqdn : `http://${ipv4 || ipv6}` || '';
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
let url = 'settings/apps/new'; let url = 'settings/apps/new';

View File

@@ -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.9.1", "version": "3.9.3",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {