Compare commits

...

42 Commits
v3.12.33 ... v3

Author SHA1 Message Date
Andras Bacsai
b80a519b80 updates 2023-09-05 11:31:55 +02:00
Andras Bacsai
2fa7ffc931 updates 2023-09-05 11:25:16 +02:00
Andras Bacsai
4abec14a21 updates 2023-09-05 11:24:42 +02:00
Andras Bacsai
18d0623011 force volume prune 2023-08-16 15:27:10 +02:00
Andras Bacsai
aa634c78d1 fix seed js 2023-08-16 15:26:36 +02:00
Andras Bacsai
a2d4373104 cleanup volumes as well 2023-08-16 15:26:33 +02:00
Andras Bacsai
702e16d643 rename rollback to upgrade 2023-08-14 09:23:33 +02:00
Andras Bacsai
3b25c8f96b fix: docker compose env file 2023-08-12 00:10:14 +02:00
Andras Bacsai
1c8c567791 fix: env variables in compose deplyoments 2023-07-27 12:40:58 +02:00
Andras Bacsai
807a3c9d66 fix: n8n double mount
version++
2023-07-26 10:38:36 +02:00
Andras Bacsai
2abd7bd7bb copy to persisten storage 2023-07-25 12:38:36 +02:00
Andras Bacsai
343957ab8b update backups 2023-07-25 12:33:05 +02:00
Andras Bacsai
49261308f7 update webhook 2023-07-25 12:00:31 +02:00
Andras Bacsai
d037409237 updates 2023-07-25 11:40:27 +02:00
Andras Bacsai
338cbf62a1 fix: encrypt decrypt 2023-07-25 10:48:31 +02:00
Andras Bacsai
4c51bffc7b save db backup on seed 2023-07-20 21:41:47 +02:00
Andras Bacsai
fd98ba8812 auto update every hour 2023-07-20 16:37:24 +02:00
Andras Bacsai
930251e9c8 autoupdate fixed 2023-07-20 16:19:54 +02:00
Andras Bacsai
7cd441266a remove console log 2023-07-20 14:52:32 +02:00
Andras Bacsai
990fb8ec15 fix 2023-07-20 14:52:16 +02:00
Andras Bacsai
3fe982b2f4 Merge pull request #1052 from f-kawamura/bugfix-http-git-source
[Bug] Added support for HTTP source URLs in Git source
2023-07-20 13:48:34 +02:00
Andras Bacsai
9dd874e959 Merge pull request #1147 from martijnmichel/v3
Update serviceFields.ts
2023-07-20 13:46:52 +02:00
Andras Bacsai
b91368223b update plausible docs 2023-07-20 13:39:29 +02:00
Andras Bacsai
139670372b updates for templates 2023-07-20 13:29:03 +02:00
Andras Bacsai
1c0769ad75 update tags + only download on service view 2023-07-20 13:12:54 +02:00
Andras Bacsai
e6cbcf98cb fix: cleanup plausible 2023-07-20 13:06:19 +02:00
martijnmichel
64b0481055 Update serviceFields.ts 2023-07-20 08:19:51 +02:00
Andras Bacsai
ce15161926 Merge pull request #1144 from coollabsio/feature/implement-basic-auth-handling
fix: traefik config + ui + api
2023-07-18 15:37:57 +02:00
Andras Bacsai
4003d4d894 Merge pull request #1071 from pascal-klesse/feature/implement-basic-auth-handling
feat: Implement basic auth for applications
2023-07-18 15:37:35 +02:00
Andras Bacsai
6e011025a7 fix: traefik config + ui + api 2023-07-18 15:34:05 +02:00
Andras Bacsai
6c0544adb2 Merge branch 'v2' into feature/implement-basic-auth-handling 2023-07-18 14:48:02 +02:00
Andras Bacsai
8e4f7c9065 remove console.log 2023-07-18 14:44:46 +02:00
Andras Bacsai
e71f890b54 Merge pull request #1084 from Geczy/main
add trycatch
2023-07-18 14:44:32 +02:00
Andras Bacsai
4dc35dea97 Merge pull request #1119 from jenishngl/patch-1
Fixing the help link - source.svelte line 263
2023-07-18 14:40:33 +02:00
Andras Bacsai
b63dfb4bcd feat: backup databases 2023-07-18 14:36:54 +02:00
Andras Bacsai
b2ffd9183b fix: set connection string on publicity change 2023-07-18 13:01:10 +02:00
Jenish J
1fbcfcaf74 Merge branch 'v3' into patch-1 2023-07-17 20:15:27 +05:30
Jenish J
79c98657b1 Update source.svelte line 263 - Correcting the help link
Updated to the correct help link, as the old link was not pointing to the correct section in the docs.coollabs.io page
2023-06-28 20:45:03 +05:30
Geczy
d1be7e44af add trycatch 2023-05-25 13:39:57 -04:00
Pascal Klesse
eefc2a3d0e remove TODO 2023-05-15 09:33:03 +02:00
Pascal Klesse
d14ca724e9 feat: Implement basic auth for applications 2023-05-15 09:27:49 +02:00
f-kawamura
7b05aaffc3 fix: Added support for HTTP source URLs in Git source. Currently only support HTTPS 2023-04-24 16:12:47 +09:00
29 changed files with 5597 additions and 4974 deletions

View File

@@ -4,9 +4,8 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
on: on:
push: push:
branches-ignore: branches:
- "main" - "v3"
- "v4"
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify" IMAGE_NAME: "coollabsio/coolify"

11
.vscode/settings.json vendored
View File

@@ -18,5 +18,14 @@
"ts", "ts",
"json" "json"
], ],
"i18n-ally.extract.autoDetect": true "i18n-ally.extract.autoDetect": true,
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true
},
"hide-files.files": []
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -135,6 +135,8 @@ model Application {
dockerRegistryId String? dockerRegistryId String?
dockerRegistryImageName String? dockerRegistryImageName String?
simpleDockerfile String? simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[] persistentStorage ApplicationPersistentStorage[]
secrets Secret[] secrets Secret[]
@@ -187,6 +189,7 @@ model ApplicationSettings {
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false) isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false) isHttp2 Boolean @default(false)
basicAuth Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])

View File

@@ -95,8 +95,19 @@ async function main() {
} }
async function reEncryptSecrets() { async function reEncryptSecrets() {
const { execaCommand } = await import('execa'); const { execaCommand } = await import('execa');
const image = await execaCommand("docker inspect coolify --format '{{ .Config.Image }}'", {
shell: true
});
const version = image.stdout.split(':')[1] ?? null;
const date = new Date().getTime(); const date = new Date().getTime();
await execaCommand('env | grep COOLIFY > .env', { shell: true });
let backupfile = `/app/db/prod.db_${date}`;
if (version) {
backupfile = `/app/db/prod.db_${version}_${date}`;
}
await execaCommand('env | grep "^COOLIFY" | sort > .env', {
shell: true
});
const secretOld = process.env['COOLIFY_SECRET_KEY']; const secretOld = process.env['COOLIFY_SECRET_KEY'];
let secretNew = process.env['COOLIFY_SECRET_KEY_BETTER']; let secretNew = process.env['COOLIFY_SECRET_KEY_BETTER'];
if (!secretNew) { if (!secretNew) {
@@ -108,6 +119,8 @@ async function reEncryptSecrets() {
secretNew = newKey; secretNew = newKey;
} }
if (secretOld !== secretNew) { if (secretOld !== secretNew) {
console.log(`Backup database to ${backupfile}.`);
await execaCommand(`cp /app/db/prod.db ${backupfile}`, { shell: true });
console.log( console.log(
'Secrets (COOLIFY_SECRET_KEY & COOLIFY_SECRET_KEY_BETTER) are different, so re-encrypting everything...' 'Secrets (COOLIFY_SECRET_KEY & COOLIFY_SECRET_KEY_BETTER) are different, so re-encrypting everything...'
); );
@@ -120,171 +133,209 @@ async function reEncryptSecrets() {
await execaCommand(`echo "COOLIFY_SECRET_KEY_OLD_${date}=${secretOld}" >> .env`, { await execaCommand(`echo "COOLIFY_SECRET_KEY_OLD_${date}=${secretOld}" >> .env`, {
shell: true shell: true
}); });
console.log(`Backup database to /app/db/prod.db_${date}.`);
await execaCommand(`cp /app/db/prod.db /app/db/prod.db_${date}`, { shell: true });
const transactions = []; const transactions = [];
const secrets = await prisma.secret.findMany(); const secrets = await prisma.secret.findMany();
if (secrets.length > 0) { if (secrets.length > 0) {
for (const secret of secrets) { for (const secret of secrets) {
const value = decrypt(secret.value, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(secret.value, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.secret.update({ transactions.push(
where: { id: secret.id }, prisma.secret.update({
data: { value: newValue } where: { id: secret.id },
}) data: { value: newValue }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const serviceSecrets = await prisma.serviceSecret.findMany(); const serviceSecrets = await prisma.serviceSecret.findMany();
if (serviceSecrets.length > 0) { if (serviceSecrets.length > 0) {
for (const secret of serviceSecrets) { for (const secret of serviceSecrets) {
const value = decrypt(secret.value, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(secret.value, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.serviceSecret.update({ transactions.push(
where: { id: secret.id }, prisma.serviceSecret.update({
data: { value: newValue } where: { id: secret.id },
}) data: { value: newValue }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const gitlabApps = await prisma.gitlabApp.findMany(); const gitlabApps = await prisma.gitlabApp.findMany();
if (gitlabApps.length > 0) { if (gitlabApps.length > 0) {
for (const gitlabApp of gitlabApps) { for (const gitlabApp of gitlabApps) {
const value = decrypt(gitlabApp.privateSshKey, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(gitlabApp.privateSshKey, secretOld);
const appSecret = decrypt(gitlabApp.appSecret, secretOld); const newValue = encrypt(value, secretNew);
const newAppSecret = encrypt(appSecret, secretNew); const appSecret = decrypt(gitlabApp.appSecret, secretOld);
transactions.push( const newAppSecret = encrypt(appSecret, secretNew);
prisma.gitlabApp.update({ transactions.push(
where: { id: gitlabApp.id }, prisma.gitlabApp.update({
data: { privateSshKey: newValue, appSecret: newAppSecret } where: { id: gitlabApp.id },
}) data: { privateSshKey: newValue, appSecret: newAppSecret }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const githubApps = await prisma.githubApp.findMany(); const githubApps = await prisma.githubApp.findMany();
if (githubApps.length > 0) { if (githubApps.length > 0) {
for (const githubApp of githubApps) { for (const githubApp of githubApps) {
const clientSecret = decrypt(githubApp.clientSecret, secretOld); try {
const newClientSecret = encrypt(clientSecret, secretNew); const clientSecret = decrypt(githubApp.clientSecret, secretOld);
const webhookSecret = decrypt(githubApp.webhookSecret, secretOld); const newClientSecret = encrypt(clientSecret, secretNew);
const newWebhookSecret = encrypt(webhookSecret, secretNew); const webhookSecret = decrypt(githubApp.webhookSecret, secretOld);
const privateKey = decrypt(githubApp.privateKey, secretOld); const newWebhookSecret = encrypt(webhookSecret, secretNew);
const newPrivateKey = encrypt(privateKey, secretNew); const privateKey = decrypt(githubApp.privateKey, secretOld);
const newPrivateKey = encrypt(privateKey, secretNew);
transactions.push( transactions.push(
prisma.githubApp.update({ prisma.githubApp.update({
where: { id: githubApp.id }, where: { id: githubApp.id },
data: { data: {
clientSecret: newClientSecret, clientSecret: newClientSecret,
webhookSecret: newWebhookSecret, webhookSecret: newWebhookSecret,
privateKey: newPrivateKey privateKey: newPrivateKey
} }
}) })
); );
} catch (e) {
console.log(e);
}
} }
} }
const databases = await prisma.database.findMany(); const databases = await prisma.database.findMany();
if (databases.length > 0) { if (databases.length > 0) {
for (const database of databases) { for (const database of databases) {
const dbUserPassword = decrypt(database.dbUserPassword, secretOld); try {
const newDbUserPassword = encrypt(dbUserPassword, secretNew); const dbUserPassword = decrypt(database.dbUserPassword, secretOld);
const rootUserPassword = decrypt(database.rootUserPassword, secretOld); const newDbUserPassword = encrypt(dbUserPassword, secretNew);
const newRootUserPassword = encrypt(rootUserPassword, secretNew); const rootUserPassword = decrypt(database.rootUserPassword, secretOld);
transactions.push( const newRootUserPassword = encrypt(rootUserPassword, secretNew);
prisma.database.update({ transactions.push(
where: { id: database.id }, prisma.database.update({
data: { where: { id: database.id },
dbUserPassword: newDbUserPassword, data: {
rootUserPassword: newRootUserPassword dbUserPassword: newDbUserPassword,
} rootUserPassword: newRootUserPassword
}) }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const databaseSecrets = await prisma.databaseSecret.findMany(); const databaseSecrets = await prisma.databaseSecret.findMany();
if (databaseSecrets.length > 0) { if (databaseSecrets.length > 0) {
for (const databaseSecret of databaseSecrets) { for (const databaseSecret of databaseSecrets) {
const value = decrypt(databaseSecret.value, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(databaseSecret.value, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.databaseSecret.update({ transactions.push(
where: { id: databaseSecret.id }, prisma.databaseSecret.update({
data: { value: newValue } where: { id: databaseSecret.id },
}) data: { value: newValue }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const wordpresses = await prisma.wordpress.findMany(); const wordpresses = await prisma.wordpress.findMany();
if (wordpresses.length > 0) { if (wordpresses.length > 0) {
for (const wordpress of wordpresses) { for (const wordpress of wordpresses) {
const value = decrypt(wordpress.ftpHostKey, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(wordpress.ftpHostKey, secretOld);
const ftpHostKeyPrivate = decrypt(wordpress.ftpHostKeyPrivate, secretOld); const newValue = encrypt(value, secretNew);
const newFtpHostKeyPrivate = encrypt(ftpHostKeyPrivate, secretNew); const ftpHostKeyPrivate = decrypt(wordpress.ftpHostKeyPrivate, secretOld);
let newFtpPassword = undefined; const newFtpHostKeyPrivate = encrypt(ftpHostKeyPrivate, secretNew);
if (wordpress.ftpPassword != null) { let newFtpPassword = undefined;
const ftpPassword = decrypt(wordpress.ftpPassword, secretOld); if (wordpress.ftpPassword != null) {
newFtpPassword = encrypt(ftpPassword, secretNew); const ftpPassword = decrypt(wordpress.ftpPassword, secretOld);
} newFtpPassword = encrypt(ftpPassword, secretNew);
}
transactions.push( transactions.push(
prisma.wordpress.update({ prisma.wordpress.update({
where: { id: wordpress.id }, where: { id: wordpress.id },
data: { data: {
ftpHostKey: newValue, ftpHostKey: newValue,
ftpHostKeyPrivate: newFtpHostKeyPrivate, ftpHostKeyPrivate: newFtpHostKeyPrivate,
ftpPassword: newFtpPassword ftpPassword: newFtpPassword
} }
}) })
); );
} catch (e) {
console.log(e);
}
} }
} }
const sshKeys = await prisma.sshKey.findMany(); const sshKeys = await prisma.sshKey.findMany();
if (sshKeys.length > 0) { if (sshKeys.length > 0) {
for (const key of sshKeys) { for (const key of sshKeys) {
const value = decrypt(key.privateKey, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(key.privateKey, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.sshKey.update({ transactions.push(
where: { id: key.id }, prisma.sshKey.update({
data: { where: { id: key.id },
privateKey: newValue data: {
} privateKey: newValue
}) }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const dockerRegistries = await prisma.dockerRegistry.findMany(); const dockerRegistries = await prisma.dockerRegistry.findMany();
if (dockerRegistries.length > 0) { if (dockerRegistries.length > 0) {
for (const registry of dockerRegistries) { for (const registry of dockerRegistries) {
const value = decrypt(registry.password, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(registry.password, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.dockerRegistry.update({ transactions.push(
where: { id: registry.id }, prisma.dockerRegistry.update({
data: { where: { id: registry.id },
password: newValue data: {
} password: newValue
}) }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
const certificates = await prisma.certificate.findMany(); const certificates = await prisma.certificate.findMany();
if (certificates.length > 0) { if (certificates.length > 0) {
for (const certificate of certificates) { for (const certificate of certificates) {
const value = decrypt(certificate.key, secretOld); try {
const newValue = encrypt(value, secretNew); const value = decrypt(certificate.key, secretOld);
transactions.push( const newValue = encrypt(value, secretNew);
prisma.certificate.update({ transactions.push(
where: { id: certificate.id }, prisma.certificate.update({
data: { where: { id: certificate.id },
key: newValue data: {
} key: newValue
}) }
); })
);
} catch (e) {
console.log(e);
}
} }
} }
await prisma.$transaction(transactions); await prisma.$transaction(transactions);
@@ -306,18 +357,16 @@ const encrypt = (text, secret) => {
}; };
const decrypt = (hashString, secret) => { const decrypt = (hashString, secret) => {
if (hashString && secret) { if (hashString && secret) {
try { const hash = JSON.parse(hashString);
const hash = JSON.parse(hashString); const decipher = crypto.createDecipheriv(algorithm, secret, Buffer.from(hash.iv, 'hex'));
const decipher = crypto.createDecipheriv(algorithm, secret, Buffer.from(hash.iv, 'hex')); const decrpyted = Buffer.concat([
const decrpyted = Buffer.concat([ decipher.update(Buffer.from(hash.content, 'hex')),
decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()
decipher.final() ]);
]); if (/<2F>/.test(decrpyted.toString())) {
return decrpyted.toString(); throw new Error('Invalid secret. Skipping...');
} catch (error) {
console.log({ decryptionError: error.message });
return hashString;
} }
return decrpyted.toString();
} }
}; };

View File

@@ -1,14 +1,19 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import cookie from '@fastify/cookie';
import cors from '@fastify/cors';
import env from '@fastify/env';
import multipart from '@fastify/multipart';
import serve from '@fastify/static';
import Fastify from 'fastify';
import socketIO from 'fastify-socket.io'; import socketIO from 'fastify-socket.io';
import path, { join } from 'path';
import socketIOServer from './realtime'; import socketIOServer from './realtime';
import Graceful from '@ladjs/graceful';
import { compareVersions } from 'compare-versions';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import { import {
cleanupDockerStorage, cleanupDockerStorage,
createRemoteEngineConfiguration, createRemoteEngineConfiguration,
@@ -22,14 +27,9 @@ import {
startTraefikTCPProxy, startTraefikTCPProxy,
version version
} from './lib/common'; } from './lib/common';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful';
import yaml from 'js-yaml';
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker'; import { checkContainer } from './lib/docker';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib'; import { scheduler } from './lib/scheduler';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
@@ -167,7 +167,7 @@ const host = '0.0.0.0';
// autoUpdater // autoUpdater
setInterval(async () => { setInterval(async () => {
await autoUpdater(); await autoUpdater();
}, 60000 * 15); }, 60000 * 60);
// cleanupStorage // cleanupStorage
setInterval(async () => { setInterval(async () => {
@@ -209,7 +209,9 @@ const host = '0.0.0.0';
getTagsTemplates(), getTagsTemplates(),
getArch(), getArch(),
getIPAddress(), getIPAddress(),
configureRemoteDockers() configureRemoteDockers(),
refreshTemplates(),
refreshTags()
// cleanupStuckedContainers() // cleanupStuckedContainers()
]); ]);
} catch (error) { } catch (error) {
@@ -402,16 +404,21 @@ async function autoUpdater() {
if (!isDev) { if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) { if (isAutoUpdateEnabled) {
await executeCommand({ let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
command: `docker pull ghcr.io/coollabsio/coolify:${latestVersion}` try {
}); await executeCommand({ command: `docker pull ${image}` });
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); } catch (error) {
image = `coollabsio/coolify:${latestVersion}`;
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({ await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
}); });
await executeCommand({ await executeCommand({
shell: true, shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ghcr.io/coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
}); });
} }
} else { } else {
@@ -548,7 +555,11 @@ async function copySSLCertificates() {
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }); try {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` });
} catch (e) {
console.log(e);
}
} }
} }

View File

@@ -19,7 +19,9 @@ export default async function (data) {
dockerComposeConfiguration, dockerComposeConfiguration,
dockerComposeFileLocation dockerComposeFileLocation
} = data; } = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`; const baseDir = `${workdir}${baseDirectory}`;
const envFile = `${baseDir}/.env`;
const fileYaml = `${baseDir}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8'); const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw); const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) { if (!dockerComposeYaml.services) {
@@ -31,7 +33,7 @@ export default async function (data) {
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)]; buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
} }
await fs.writeFile(envFile, envs.join('\n'));
const composeVolumes = []; const composeVolumes = [];
if (volumes.length > 0) { if (volumes.length > 0) {
for (const volume of volumes) { for (const volume of volumes) {
@@ -50,32 +52,38 @@ export default async function (data) {
if (value['env_file']) { if (value['env_file']) {
delete value['env_file']; delete value['env_file'];
} }
value['env_file'] = [envFile];
let environment = typeof value['environment'] === 'undefined' ? [] : value['environment']; // let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
console.log({ key, environment }); // let finalEnvs = [...envs];
if (Object.keys(environment).length > 0) { // if (Object.keys(environment).length > 0) {
environment = Object.entries(environment).map(([key, value]) => `${key}=${value}`); // for (const arg of Object.keys(environment)) {
} // const [key, _] = arg.split('=');
value['environment'] = [...environment, ...envs]; // if (finalEnvs.filter((env) => env.startsWith(key)).length === 0) {
// finalEnvs.push(arg);
// }
// }
// }
// value['environment'] = [...finalEnvs];
let build = typeof value['build'] === 'undefined' ? [] : value['build']; let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') { if (typeof build === 'string') {
build = { context: build }; build = { context: build };
} }
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args']; const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalArgs = [...buildEnvs]; let finalBuildArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) { if (Object.keys(buildArgs).length > 0) {
for (const arg of Object.keys(buildArgs)) { for (const arg of Object.keys(buildArgs)) {
const [key, _] = arg.split('='); const [key, _] = arg.split('=');
if (finalArgs.filter((env) => env.startsWith(key)).length === 0) { if (finalBuildArgs.filter((env) => env.startsWith(key)).length === 0) {
finalArgs.push(arg); finalBuildArgs.push(arg);
} }
} }
} }
if (build.length > 0 || buildArgs.length > 0) { if (build.length > 0 || buildArgs.length > 0) {
value['build'] = { value['build'] = {
...build, ...build,
args: finalArgs args: finalBuildArgs
}; };
} }
@@ -122,7 +130,6 @@ export default async function (data) {
.replace(/^\./, `~`) .replace(/^\./, `~`)
.replace(/^\.\./, '~') .replace(/^\.\./, '~')
.replace(/^\$PWD/, '~'); .replace(/^\$PWD/, '~');
console.log({ source });
} else { } else {
if (!target) { if (!target) {
target = source; target = source;

View File

@@ -1,6 +1,5 @@
import { exec } from 'node:child_process';
import util from 'util';
import fs from 'fs/promises'; import fs from 'fs/promises';
import fsNormal from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import forge from 'node-forge'; import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
@@ -17,8 +16,9 @@ import { day } from './dayjs';
import { saveBuildLog } from './buildPacks/common'; import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
import { FastifyReply } from 'fastify';
export const version = '3.12.33'; export const version = '3.12.39';
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;
@@ -1762,7 +1762,7 @@ export function convertTolOldVolumeNames(type) {
} }
} }
export async function cleanupDockerStorage(dockerId) { export async function cleanupDockerStorage(dockerId, volumes = false) {
// Cleanup images that are not used by any container // Cleanup images that are not used by any container
try { try {
await executeCommand({ dockerId, command: `docker image prune -af` }); await executeCommand({ dockerId, command: `docker image prune -af` });
@@ -1780,6 +1780,11 @@ export async function cleanupDockerStorage(dockerId) {
try { try {
await executeCommand({ dockerId, command: `docker builder prune -af` }); await executeCommand({ dockerId, command: `docker builder prune -af` });
} catch (error) { } } catch (error) { }
if (volumes) {
try {
await executeCommand({ dockerId, command: `docker volume prune -af` });
} catch (error) { }
}
} }
export function persistentVolumes(id, persistentStorage, config) { export function persistentVolumes(id, persistentStorage, config) {
@@ -1942,3 +1947,49 @@ export function generateSecrets(
} }
return envs; return envs;
} }
export async function backupDatabaseNow(database, reply) {
const backupFolder = '/tmp'
const fileName = `${database.id}-${new Date().getTime()}.gz`
const backupFileName = `${backupFolder}/${fileName}`
const backupStorageFilename = `/app/backups/${fileName}`
let command = null
switch (database?.type) {
case 'postgresql':
command = `docker exec ${database.id} sh -c "PGPASSWORD=${database.rootUserPassword} pg_dumpall -U postgres | gzip > ${backupFileName}"`
break;
case 'mongodb':
command = `docker exec ${database.id} sh -c "mongodump --archive=${backupFileName} --gzip --username=${database.rootUser} --password=${database.rootUserPassword}"`
break;
case 'mysql':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'mariadb':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'couchdb':
command = `docker exec ${database.id} sh -c "tar -czvf ${backupFileName} /bitnami/couchdb/data"`
break;
default:
return;
}
await executeCommand({
dockerId: database.destinationDockerId,
command,
});
const copyCommand = `docker cp ${database.id}:${backupFileName} ${backupFileName}`
await executeCommand({
dockerId: database.destinationDockerId,
command: copyCommand
});
await executeCommand({
dockerId: database.destinationDockerId,
command: `docker cp ${database.id}:${backupFileName} /app/backups/`
});
const stream = fsNormal.createReadStream(backupFileName);
reply.header('Content-Type', 'application/octet-stream');
reply.header('Content-Disposition', `attachment; filename=${fileName}`);
reply.header('Content-Length', fsNormal.statSync(backupFileName).size);
reply.header('Content-Transfer-Encoding', 'binary');
return reply.send(stream)
}

View File

@@ -624,7 +624,7 @@ export const glitchTip = [{
isEncrypted: false isEncrypted: false
}, },
{ {
name: 'emailSmtpUseSsl', name: 'emailSmtpUseTls',
isEditable: true, isEditable: true,
isLowerCase: false, isLowerCase: false,
isNumber: false, isNumber: false,

View File

@@ -398,7 +398,9 @@ export async function saveApplication(
dockerComposeFileLocation, dockerComposeFileLocation,
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
dockerRegistryImageName dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
} = request.body; } = request.body;
if (port) port = Number(port); if (port) port = Number(port);
if (exposePort) { if (exposePort) {
@@ -453,6 +455,8 @@ export async function saveApplication(
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
dockerRegistryImageName, dockerRegistryImageName,
basicAuthPw,
basicAuthUser,
...defaultConfiguration, ...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
} }
@@ -476,6 +480,8 @@ export async function saveApplication(
dockerComposeFileLocation, dockerComposeFileLocation,
dockerComposeConfiguration, dockerComposeConfiguration,
simpleDockerfile, simpleDockerfile,
basicAuthPw,
basicAuthUser,
dockerRegistryImageName, dockerRegistryImageName,
...defaultConfiguration ...defaultConfiguration
} }
@@ -499,12 +505,11 @@ export async function saveApplicationSettings(
previews, previews,
dualCerts, dualCerts,
autodeploy, autodeploy,
branch,
projectId,
isBot, isBot,
isDBBranching, isDBBranching,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
basicAuth,
} = request.body; } = request.body;
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
@@ -519,7 +524,8 @@ export async function saveApplicationSettings(
isBot, isBot,
isDBBranching, isDBBranching,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
basicAuth,
} }
} }
}, },

View File

@@ -28,6 +28,8 @@ export interface SaveApplication extends OnlyId {
dockerComposeConfiguration: string; dockerComposeConfiguration: string;
simpleDockerfile: string; simpleDockerfile: string;
dockerRegistryImageName: string; dockerRegistryImageName: string;
basicAuthPw: string;
basicAuthUser: string;
}; };
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
@@ -43,6 +45,7 @@ export interface SaveApplicationSettings extends OnlyId {
isDBBranching: boolean; isDBBranching: boolean;
isCustomSSL: boolean; isCustomSSL: boolean;
isHttp2: boolean; isHttp2: boolean;
basicAuth: boolean;
}; };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {

View File

@@ -5,6 +5,7 @@ import yaml from 'js-yaml';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { import {
ComposeFile, ComposeFile,
backupDatabaseNow,
createDirectories, createDirectories,
decrypt, decrypt,
defaultComposeConfiguration, defaultComposeConfiguration,
@@ -351,6 +352,21 @@ export async function startDatabase(request: FastifyRequest<OnlyId>) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
} }
export async function backupDatabase(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const database = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, settings: true }
});
if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword);
if (database.rootUserPassword) database.rootUserPassword = decrypt(database.rootUserPassword);
return await backupDatabaseNow(database, reply);
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function stopDatabase(request: FastifyRequest<OnlyId>) { export async function stopDatabase(request: FastifyRequest<OnlyId>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; import { backupDatabase, cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
@@ -39,6 +39,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request)); fastify.post<OnlyId>('/:id/start', async (request) => await startDatabase(request));
fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request)); fastify.post<OnlyId>('/:id/stop', async (request) => await stopDatabase(request));
fastify.post<OnlyId>('/:id/backup', async (request, reply) => await backupDatabase(request, reply));
}; };
export default root; export default root;

View File

@@ -19,8 +19,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import type { Login, Update } from '.'; import type { Login, Update } from '.';
import type { GetCurrentUser } from './types'; import type { GetCurrentUser } from './types';
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string, saltRounds = 15): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
@@ -58,7 +57,7 @@ export async function cleanupManually(request: FastifyRequest) {
const destination = await prisma.destinationDocker.findUnique({ const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId } where: { id: serverId }
}); });
await cleanupDockerStorage(destination.id); await cleanupDockerStorage(destination.id, true);
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -77,7 +76,7 @@ export async function refreshTags() {
tags = JSON.parse(tags).concat(JSON.parse(testTags)); tags = JSON.parse(tags).concat(JSON.parse(testTags));
} }
} }
} catch (error) {} } catch (error) { }
await fs.writeFile('./tags.json', tags); await fs.writeFile('./tags.json', tags);
} else { } else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text(); const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text();
@@ -102,7 +101,7 @@ export async function refreshTemplates() {
if (await fs.stat('./testTemplate.yaml')) { if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8')); templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
} }
} catch (error) {} } catch (error) { }
const response = await fs.readFile('./devTemplates.yaml', 'utf8'); const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response))); await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else { } else {
@@ -163,13 +162,13 @@ export async function update(request: FastifyRequest<Update>) {
await executeCommand({ command: `docker pull ${image}` }); await executeCommand({ command: `docker pull ${image}` });
} }
await executeCommand({ shell: true, command: `ls .env || env | grep COOLIFY > .env` }); await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({ await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
}); });
await executeCommand({ await executeCommand({
shell: true, shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
}); });
return {}; return {};
} else { } else {

View File

@@ -50,6 +50,7 @@ import type {
SetWordpressSettings SetWordpressSettings
} from './types'; } from './types';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import { refreshTags, refreshTemplates } from '../handlers';
export async function listServices(request: FastifyRequest) { export async function listServices(request: FastifyRequest) {
try { try {
@@ -476,7 +477,7 @@ export async function saveServiceType(
const [volumeName, path] = volume.split(':'); const [volumeName, path] = volume.split(':');
if (!volumeName.startsWith('/')) { if (!volumeName.startsWith('/')) {
const found = await prisma.servicePersistentStorage.findFirst({ const found = await prisma.servicePersistentStorage.findFirst({
where: { volumeName, serviceId: id } where: { volumeName, serviceId: id, path }
}); });
if (!found) { if (!found) {
await prisma.servicePersistentStorage.create({ await prisma.servicePersistentStorage.create({
@@ -985,11 +986,22 @@ export async function cleanupPlausibleLogs(request: FastifyRequest<OnlyId>, repl
const teamId = request.user.teamId; const teamId = request.user.teamId;
const { destinationDockerId, destinationDocker } = await getServiceFromDB({ id, teamId }); const { destinationDockerId, destinationDocker } = await getServiceFromDB({ id, teamId });
if (destinationDockerId) { if (destinationDockerId) {
await executeCommand({ const logTables = await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`, command: `docker exec ${id}-clickhouse clickhouse-client -q "SELECT name FROM system.tables;"`,
shell: true shell: false
}); });
if (logTables.stdout !== '') {
const tables = logTables.stdout.split('\n').filter((t) => t.includes('_log'));
for (const table of tables) {
console.log(`Truncating table ${table}`)
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id}-clickhouse clickhouse-client -q "TRUNCATE TABLE system.${table};"`,
shell: false
});
}
}
return await reply.code(201).send(); return await reply.code(201).send();
} }
throw { status: 500, message: 'Could cleanup logs.' }; throw { status: 500, message: 'Could cleanup logs.' };
@@ -1105,17 +1117,14 @@ export async function activateWordpressFtp(
shell: true shell: true
}); });
} }
} catch (error) {} } catch (error) { }
const volumes = [ const volumes = [
`${id}-wordpress-data:/home/${ftpUser}/wordpress`, `${id}-wordpress-data:/home/${ftpUser}/wordpress`,
`${ `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`, }/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
`${ `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`, }/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
`${ `${isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
}/${id}.sh:/etc/sftp.d/chmod.sh` }/${id}.sh:/etc/sftp.d/chmod.sh`
]; ];
@@ -1185,6 +1194,6 @@ export async function activateWordpressFtp(
await executeCommand({ await executeCommand({
command: `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh` command: `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
}); });
} catch (error) {} } catch (error) { }
} }
} }

View File

@@ -1,8 +1,9 @@
import { FastifyRequest } from 'fastify'; import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from '../../../lib/common'; import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common';
import { getTemplates } from '../../../lib/services'; import { getTemplates } from '../../../lib/services';
import { OnlyId } from '../../../types'; import { OnlyId } from '../../../types';
import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers'; import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers';
import { hashPassword } from '../../api/v1/handlers';
function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) { function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) { if (isHttp2) {
@@ -39,7 +40,7 @@ function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps
} }
}; };
} }
function generateRouters( async function generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
@@ -48,20 +49,22 @@ function generateRouters(
isWWW, isWWW,
isDualCerts, isDualCerts,
isCustomSSL, isCustomSSL,
isHttp2 = false isHttp2 = false,
) { httpBasicAuth = null,
let rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`; }) {
let ruleWWW = `Host(\`www.${nakedDomain}\`)${ const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : '' const ruleWWW = `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`; }`;
let http: any = {
const http: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
}; };
let https: any = { const https: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule, rule,
service: `${serviceId}`, service: `${serviceId}`,
@@ -71,14 +74,14 @@ function generateRouters(
}, },
middlewares: [] middlewares: []
}; };
let httpWWW: any = { const httpWWW: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: ruleWWW, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
}; };
let httpsWWW: any = { const httpsWWW: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: ruleWWW, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
@@ -97,6 +100,10 @@ function generateRouters(
httpsWWW.middlewares.push('redirect-to-non-www'); httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls; delete https.tls;
delete httpsWWW.tls; delete httpsWWW.tls;
if (httpBasicAuth) {
http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 3. http + www only // 3. http + www only
@@ -108,6 +115,10 @@ function generateRouters(
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
delete https.tls; delete https.tls;
delete httpsWWW.tls; delete httpsWWW.tls;
if (httpBasicAuth) {
httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 5. https + non-www only // 5. https + non-www only
if (isHttps && !isWWW) { if (isHttps && !isWWW) {
@@ -136,6 +147,10 @@ function generateRouters(
}; };
} }
} }
if (httpBasicAuth) {
https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 6. https + www only // 6. https + www only
if (isHttps && isWWW) { if (isHttps && isWWW) {
@@ -145,6 +160,11 @@ function generateRouters(
http.middlewares.push('redirect-to-www'); http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
} }
if (httpBasicAuth) {
httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
if (isCustomSSL) { if (isCustomSSL) {
if (isDualCerts) { if (isDualCerts) {
https.tls = true; https.tls = true;
@@ -166,23 +186,23 @@ function generateRouters(
} }
} }
if (isHttp2) { if (isHttp2) {
let http2 = { const http2 = {
...http, ...http,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let http2WWW = { const http2WWW = {
...httpWWW, ...httpWWW,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let https2 = { const https2 = {
...https, ...https,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
}; };
let https2WWW = { const https2WWW = {
...httpsWWW, ...httpsWWW,
service: `${serviceId}-http2`, service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)` rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
@@ -198,14 +218,17 @@ function generateRouters(
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW } [`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
}; };
} }
return {
const result = {
[`${serviceId}-${pathPrefix}`]: { ...http }, [`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https }, [`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW }, [`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW } [`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
}; };
return result;
} }
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) { export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote = false) {
const traefik = { const traefik = {
tls: { tls: {
certificates: [] certificates: []
@@ -298,7 +321,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
}); });
} }
let parsedCertificates = []; const parsedCertificates = [];
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`, certFile: `${sslpath}/${certificate.id}-cert.pem`,
@@ -369,7 +392,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
dockerComposeConfiguration, dockerComposeConfiguration,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings settings,
basicAuthUser,
basicAuthPw,
settings: { basicAuth: isBasicAuthEnabled }
} = application; } = application;
if (!destinationDockerId) { if (!destinationDockerId) {
continue; continue;
@@ -382,6 +408,14 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
) { ) {
continue; continue;
} }
let httpBasicAuth = null;
if (basicAuthUser && basicAuthPw && isBasicAuthEnabled) {
httpBasicAuth = {
basicAuth: {
users: [basicAuthUser + ':' + await hashPassword(basicAuthPw, 1)]
}
};
}
if (buildPack === 'compose') { if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration)); const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) { if (services.length > 0) {
@@ -404,27 +438,33 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, containerId, port) ...generateServices(serviceId, containerId, port)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
continue; continue;
} }
const { previews, dualCerts, isCustomSSL, isHttp2 } = settings; const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings;
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
if (!fqdn) { if (!fqdn) {
continue; continue;
@@ -437,22 +477,28 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`; const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL, isCustomSSL,
isHttp2 isHttp2,
) httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps) ...generateServices(serviceId, id, port, isHttp2, isHttps)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
if (previews) { if (previews) {
const { stdout } = await executeCommand({ const { stdout } = await executeCommand({
dockerId, dockerId,
@@ -466,29 +512,35 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, '')); .map((c) => c.replace(/"/g, ''));
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${ const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator
coolifySettings.previewSeparator }${domain}`;
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, ''); const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/'; const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}`; const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
previewDomain, domain: previewDomain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) isHttp2: false,
httpBasicAuth
})
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2) ...generateServices(serviceId, container, port, isHttp2)
}; };
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
@@ -542,7 +594,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if (isDomainAndProxyConfiguration.length > 0) { if (isDomainAndProxyConfiguration.length > 0) {
const template: any = await parseAndFindServiceTemplates(service, null, true); const template: any = await parseAndFindServiceTemplates(service, null, true);
const { proxy } = template.services[oneService] || found.services[oneService]; const { proxy } = template.services[oneService] || found.services[oneService];
for (let configuration of proxy) { for (const configuration of proxy) {
if (configuration.hostPort) { if (configuration.hostPort) {
continue; continue;
} }
@@ -582,16 +634,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`; const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL,
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
@@ -619,16 +671,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${oneService}-${port || 'default'}`; const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,
@@ -660,16 +712,16 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const serviceId = `${id}-${port || 'default'}`; const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { traefik.http.routers = {
...traefik.http.routers, ...traefik.http.routers,
...generateRouters( ...await generateRouters({
serviceId, serviceId,
domain, domain,
nakedDomain, nakedDomain,
pathPrefix, pathPrefix,
isHttps, isHttps,
isWWW, isWWW,
dualCerts, isDualCerts: dualCerts,
isCustomSSL isCustomSSL
) })
}; };
traefik.http.services = { traefik.http.services = {
...traefik.http.services, ...traefik.http.services,

View File

@@ -4,9 +4,9 @@ import { proxyConfiguration, otherProxyConfiguration } from './handlers';
import { OtherProxyConfiguration } from './types'; import { OtherProxyConfiguration } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/main.json', async (request, reply) => proxyConfiguration(request, false)); fastify.get<OnlyId>('/main.json', async (request) => proxyConfiguration(request, false));
fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true)); fastify.get<OnlyId>('/remote/:id', async (request) => proxyConfiguration(request, true));
fastify.get<OtherProxyConfiguration>('/other.json', async (request, reply) => fastify.get<OtherProxyConfiguration>('/other.json', async (request) =>
otherProxyConfiguration(request) otherProxyConfiguration(request)
); );
}; };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { dashify } from './common';
export function getAPIUrl() { export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
@@ -72,17 +73,19 @@ async function send({
...headers ...headers
}; };
} }
if (token && !path.startsWith('https://')) {
if (token && !path.startsWith('https://') && !path.startsWith('http://')) {
opts.headers = { opts.headers = {
...opts.headers, ...opts.headers,
Authorization: `Bearer ${token}` Authorization: `Bearer ${token}`
}; };
} }
if (!path.startsWith('https://')) {
if (!path.startsWith('https://') && !path.startsWith('http://')) {
path = `/api/v1${path}`; path = `/api/v1${path}`;
} }
if (dev && !path.startsWith('https://')) { if (dev && !path.startsWith('https://') && !path.startsWith('http://')) {
path = `${getAPIUrl()}${path}`; path = `${getAPIUrl()}${path}`;
} }
if (method === 'POST' && data && !opts.body) { if (method === 'POST' && data && !opts.body) {
@@ -100,6 +103,14 @@ async function send({
responseData = await response.json(); responseData = await response.json();
} else if (contentType?.indexOf('text/plain') !== -1) { } else if (contentType?.indexOf('text/plain') !== -1) {
responseData = await response.text(); responseData = await response.text();
} else if (contentType?.indexOf('application/octet-stream') !== -1) {
responseData = await response.blob();
const fileName = dashify(data.id + '-' + data.name)
const fileLink = document.createElement('a');
fileLink.href = URL.createObjectURL(new Blob([responseData]))
fileLink.download = fileName + '.gz';
fileLink.click();
fileLink.remove();
} else { } else {
return {}; return {};
} }

View File

@@ -196,6 +196,9 @@
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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-white '>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>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-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"basic_auth": "Basic Auth",
"basic_auth_user": "User",
"basic_auth_pw": "Password",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",

View File

@@ -260,7 +260,7 @@
<PublicRepository /> <PublicRepository />
<div class="flex flex-row items-center pt-10"> <div class="flex flex-row items-center pt-10">
<div class="title py-4 pr-4">Simple Dockerfile <Beta /></div> <div class="title py-4 pr-4">Simple Dockerfile <Beta /></div>
<DocLink url="https://docs.coollabs.io/coolify-v3/applications/#dockerfile" /> <DocLink url="https://docs.coollabs.io/coolify-v3/applications/#simple-dockerfile" />
</div> </div>
<div class="mx-auto max-w-screen-2xl"> <div class="mx-auto max-w-screen-2xl">
<form class="flex flex-col" on:submit|preventDefault={handleDockerImage}> <form class="flex flex-col" on:submit|preventDefault={handleDockerImage}>

View File

@@ -29,27 +29,28 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import yaml from 'js-yaml'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, getAPIUrl, post } from '$lib/api'; import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { import {
addToast, addToast,
appSession, appSession,
checkIfDeploymentEnabledApplications, checkIfDeploymentEnabledApplications,
setLocation, features,
status,
isDeploymentEnabled, isDeploymentEnabled,
features setLocation,
status
} from '$lib/store'; } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import cuid from 'cuid';
import Setting from '$lib/components/Setting.svelte'; import yaml from 'js-yaml';
import Explainer from '$lib/components/Explainer.svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import Select from 'svelte-select';
import Beta from '$lib/components/Beta.svelte';
import { saveForm } from './utils'; import { saveForm } from './utils';
const { id } = $page.params; const { id } = $page.params;
@@ -77,6 +78,7 @@
let isCustomSSL = application.settings?.isCustomSSL; let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy; let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot; let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching; let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl; let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2; let isHttp2 = application.settings.isHttp2;
@@ -186,6 +188,9 @@
if (name === 'isCustomSSL') { if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL; isCustomSSL = !isCustomSSL;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return; if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot; isBot = !isBot;
@@ -210,7 +215,8 @@
isCustomSSL, isCustomSSL,
isHttp2, isHttp2,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId,
basicAuth
}); });
return addToast({ return addToast({
message: $t('application.settings_saved'), message: $t('application.settings_saved'),
@@ -232,6 +238,9 @@
if (name === 'isBot') { if (name === 'isBot') {
isBot = !isBot; isBot = !isBot;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
@@ -272,6 +281,7 @@
} }
} }
} }
console.log(application);
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration); await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications(application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
@@ -498,7 +508,7 @@
<div class="title font-bold pb-3">General</div> <div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
type="submit" type="submit"
class:loading={loading.save} class:loading={loading.save}
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
@@ -751,7 +761,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')} on:click={() => !isDisabled && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps && application.buildPack !== 'compose'} {#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center pb-4">
<Setting <Setting
@@ -774,6 +784,46 @@
on:click={() => changeSettings('isHttp2')} on:click={() => changeSettings('isHttp2')}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<Setting
id="basicAuth"
isCenter={false}
bind:setting={basicAuth}
title={$t('application.basic_auth')}
description="Activate basic authentication for your application. <br>Useful if you want to protect your application with a password. <br><br>Use the <span class='font-bold text-settings'>username</span> and <span class='font-bold text-settings'>password</span> fields to set the credentials."
on:click={() => changeSettings('basicAuth')}
/>
</div>
{#if basicAuth}
<div class="grid grid-cols-2 items-center">
<label for="basicAuthUser">{$t('application.basic_auth_user')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
name="basicAuthUser"
id="basicAuthUser"
class:border={!application.settings?.basicAuth && !application.basicAuthUser}
class:border-red-500={!application.settings?.basicAuth &&
!application.basicAuthUser}
bind:value={application.basicAuthUser}
placeholder="eg: admin"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="basicAuthPw">{$t('application.basic_auth_pw')}</label>
<CopyPasswordField
bind:this={fqdnEl}
isPasswordField={true}
required={!application.settings?.basicAuth}
name="basicAuthPw"
id="basicAuthPw"
bind:value={application.basicAuthPw}
placeholder="**********"
/>
</div>
{/if}
{/if} {/if}
</div> </div>
{#if isSimpleDockerfile} {#if isSimpleDockerfile}
@@ -782,7 +832,7 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-4 pr-5"> <div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="simpleDockerfile">Dockerfile</label> <label for="simpleDockerfile">Dockerfile</label>
<div class="flex gap-2"> <div class="flex gap-2">
<textarea <textarea

View File

@@ -13,17 +13,19 @@
import Redis from './_Redis.svelte'; import Redis from './_Redis.svelte';
import CouchDb from './_CouchDb.svelte'; import CouchDb from './_CouchDb.svelte';
import EdgeDB from './_EdgeDB.svelte'; import EdgeDB from './_EdgeDB.svelte';
import { post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast, appSession, status } from '$lib/store'; import { addToast, appSession, status } from '$lib/store';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params; const { id } = $page.params;
let loading = { let loading = {
main: false, main: false,
public: false public: false,
backup: false
}; };
let publicUrl = ''; let publicUrl = '';
let appendOnly = database.settings.appendOnly; let appendOnly = database.settings.appendOnly;
@@ -109,6 +111,7 @@
if ($status.database.isPublic) { if ($status.database.isPublic) {
database.publicPort = publicPort; database.publicPort = publicPort;
} }
generateUrl();
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -130,6 +133,22 @@
loading.main = false; loading.main = false;
} }
} }
async function backupDatabase() {
try {
loading.backup = true;
addToast({
message:
'Backup will be downloaded soon and saved to /var/lib/docker/volumes/coolify-local-backup/_data/ on the host system.',
type: 'success',
timeout: 15000
});
return await post(`/databases/${id}/backup`, { id, name: database.name });
} catch (error) {
return errorNotification(error);
} finally {
loading.backup = false;
}
}
</script> </script>
<div class="mx-auto max-w-6xl p-4"> <div class="mx-auto max-w-6xl p-4">
@@ -144,6 +163,19 @@
class:bg-databases={!loading.main} class:bg-databases={!loading.main}
disabled={loading.main}>{$t('forms.save')}</button disabled={loading.main}>{$t('forms.save')}</button
> >
{#if database.type !== 'redis' && database.type !== 'edgedb'}
{#if $status.database.isRunning}
<button
class="btn btn-sm"
on:click={backupDatabase}
class:loading={loading.backup}
class:bg-databases={!loading.backup}
disabled={loading.backup}>Backup Database</button
>
{:else}
<button disabled class="btn btn-sm">Backup Database (start the database)</button>
{/if}
{/if}
{/if} {/if}
</div> </div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2"> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">

View File

@@ -56,23 +56,23 @@
async function rollback() { async function rollback() {
if (rollbackVersion) { if (rollbackVersion) {
const sure = confirm(`Are you sure you want rollback Coolify to ${rollbackVersion}?`); const sure = confirm(`Are you sure you want upgrade Coolify to ${rollbackVersion}?`);
if (sure) { if (sure) {
try { try {
loading.rollback = true; loading.rollback = true;
console.log('loading.rollback', loading.rollback); console.log('loading.rollback', loading.rollback);
if (dev) { if (dev) {
console.log('rolling back to', rollbackVersion); console.log('Upgrading to ', rollbackVersion);
await asyncSleep(4000); await asyncSleep(4000);
return window.location.reload(); return window.location.reload();
} else { } else {
addToast({ addToast({
message: 'Rollback started...', message: 'Upgrade started...',
type: 'success' type: 'success'
}); });
await post(`/update`, { type: 'update', latestVersion: rollbackVersion }); await post(`/update`, { type: 'update', latestVersion: rollbackVersion });
addToast({ addToast({
message: 'Rollback completed.<br><br>Waiting for the new version to start...', message: 'Upgrade completed.<br><br>Waiting for the new version to start...',
type: 'success' type: 'success'
}); });
@@ -381,12 +381,12 @@
/> />
</div> </div>
<div class="grid grid-cols-4 items-center"> <div class="grid grid-cols-4 items-center pb-12">
<div class="col-span-2"> <div class="col-span-2">
Rollback Coolify to a specific version Upgrade Coolify to a specific version
<Explainer <Explainer
position="dropdown-bottom" position="dropdown-bottom"
explanation="You can rollback to a specific version of Coolify. This will not affect your current running resources.<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>" explanation="You can upgrade to a specific version of Coolify. This will not affect your current running resources, but could cause issues if you downgrade to an older version where the database layout was different..<br><br><a href='https://github.com/coollabsio/coolify/releases' target='_blank'>See available versions</a>"
/> />
</div> </div>
<input <input
@@ -401,7 +401,7 @@
class:loading={loading.rollback} class:loading={loading.rollback}
class="btn btn-primary ml-2" class="btn btn-primary ml-2"
disabled={!rollbackVersion || loading.rollback} disabled={!rollbackVersion || loading.rollback}
on:click|preventDefault|stopPropagation={rollback}>Rollback</button on:click|preventDefault|stopPropagation={rollback}>Upgrade</button
> >
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">

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

1279
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff