Merge pull request #656 from coollabsio/next

v3.10.15
This commit is contained in:
Andras Bacsai 2022-10-12 14:51:56 +02:00 committed by GitHub
commit fb955e15f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2321 additions and 1242 deletions

View File

@ -5,7 +5,7 @@ on:
types: [released]
jobs:
arm64-build:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
@ -31,7 +31,7 @@ jobs:
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
amd64-build:
amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -57,9 +57,35 @@ jobs:
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
aarch64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/aarch64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
merge-manifest:
runs-on: ubuntu-latest
needs: [amd64-build, arm64-build]
needs: [amd64, arm64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
@ -77,7 +103,7 @@ jobs:
id: package-version
- name: Create & publish manifest
run: |
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
- uses: sarisia/actions-status-discord@v1
if: always()

View File

@ -6,7 +6,7 @@ on:
- next
jobs:
arm64-making-something-cool:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
@ -34,7 +34,7 @@ jobs:
tags: coollabsio/coolify:next-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
amd64-making-something-cool:
amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -59,12 +59,12 @@ jobs:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
tags: coollabsio/coolify:next-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
merge-manifest-to-be-cool:
merge-manifest:
runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool]
needs: [arm64, amd64]
steps:
- name: Checkout
uses: actions/checkout@v3

View File

@ -5,19 +5,27 @@ # Contribution
You can ask for guidance anytime on our Discord server in the #contribution channel.
## Setup your development environment
### Container based development flow (recommended and the easiest)
All you need is to intall [Docker Engine 20.11+](https://docs.docker.com/engine/install/) on your local machine and run `pnpm dev:container`. It will build the base image for Coolify and start the development server inside Docker. All required ports (3000, 3001) will be exposed to your host.
### Github codespaces
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
### Gitpod
1. Use [container based development flow](#container-based-development-flow-easiest)
2. Or setup your workspace manually:
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
Create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
> Some packages, just `pack` are not installed in this way. You cannot test all the features. Please use the [container based development flow](#container-based-development-flow-easiest).
### Local Machine
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
Install all the prerequisites manually to your host system. If you would not like to install anything, I suggest to use the [container based development flow](#container-based-development-flow-easiest).
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.

View File

@ -1,5 +1,4 @@
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
FROM node:18-slim as build
WORKDIR /app
@ -17,20 +16,26 @@ WORKDIR /app
ENV NODE_ENV production
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/others/fluentbit/ ./fluentbit

31
Dockerfile-dev Normal file
View File

@ -0,0 +1,31 @@
FROM node:18-slim
ENV NODE_ENV development
ARG TARGETPLATFORM
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0
WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
EXPOSE 3000
ENV CHECKPOINT_DISABLE=1

View File

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

View File

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

View File

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

View File

@ -6,11 +6,14 @@ import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, getDomain, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful'
import axios from 'axios';
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker';
declare module 'fastify' {
interface FastifyInstance {
config: {
@ -72,7 +75,6 @@ const host = '0.0.0.0';
}
};
const options = {
schema,
dotenv: true
@ -131,29 +133,26 @@ const host = '0.0.0.0';
if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication');
}
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000)
// autoUpdater
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
await autoUpdater()
}, 60000 * 15)
// cleanupStorage
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
await cleanupStorage()
}, 60000 * 10)
// checkProxies and checkFluentBit
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
await checkProxies();
await checkFluentBit();
}, 10000)
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
await copySSLCertificates();
}, 2000)
await Promise.all([
@ -165,9 +164,6 @@ const host = '0.0.0.0';
console.error(error);
process.exit(1);
}
})();
@ -227,3 +223,237 @@ async function configureRemoteDockers() {
console.log(error)
}
}
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db 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"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
try {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true });
if (!found) {
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
} catch (error) {
console.log(error)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}

View File

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

View File

@ -1,296 +0,0 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db 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"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function cleanupPrismaEngines() {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) { }
}
}
async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set()
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return
if (destination.engine) enginesDone.add(destination.engine)
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}
(async () => {
let status = {
cleanupStorage: false,
autoUpdater: false,
copySSLCertificates: false,
}
if (parentPort) {
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
process.exit(1);
}
if (message === 'action:cleanupStorage') {
if (!status.autoUpdater) {
status.cleanupStorage = true
await cleanupStorage();
status.cleanupStorage = false
}
return;
}
if (message === 'action:cleanupPrismaEngines') {
await cleanupPrismaEngines();
return;
}
if (message === 'action:checkProxies') {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true
await autoUpdater();
status.autoUpdater = false
}
return;
}
}
});
} else process.exit(0);
})();

View File

@ -468,9 +468,9 @@ export const saveBuildLog = async ({
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
if (isDev) {
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
console.debug(`[${applicationId}] ${addTimestamp}`);
}
try {
@ -580,7 +580,8 @@ export async function buildImage({
dockerId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile'
dockerFileLocation = '/Dockerfile',
commit
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@ -596,7 +597,9 @@ export async function buildImage({
}
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
const { status } = await prisma.build.findUnique({ where: { id: buildId } })
if (status === 'canceled') {
throw new Error('Deployment canceled.')
@ -634,6 +637,7 @@ export function makeLabelForStandaloneApplication({
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`,
`coolify.configuration=${base64Encode(
JSON.stringify({
@ -758,4 +762,4 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
}

View File

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

View File

@ -49,7 +49,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions ? denoOptions.split(' ') : ''} ${denoMainFile}`);
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};

View File

@ -16,6 +16,7 @@ import python from './python';
import deno from './deno';
import laravel from './laravel';
import heroku from './heroku';
import compose from './compose'
export {
node,
@ -35,5 +36,6 @@ export {
python,
deno,
laravel,
heroku
heroku,
compose
};

View File

@ -20,7 +20,7 @@ import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common';
export const version = '3.10.14';
export const version = '3.10.15';
export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr';
@ -264,7 +264,9 @@ export async function isDomainConfigured({
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
],
id: { not: id },
destinationDocker: {
@ -598,7 +600,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
command = command.replace(/docker compose/gi, 'docker-compose');
}
}
if (command.startsWith(`docker build --progress plain`) || command.startsWith(`pack build`)) {
if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
}
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })

View File

@ -87,6 +87,9 @@ export async function removeContainer({
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
if (JSON.parse(stdout).Status === 'exited') {
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
throw error;
}

View File

@ -73,6 +73,4 @@ export default async function ({
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
}

View File

@ -11,15 +11,14 @@ const options: any = {
defaultExtension: 'js',
logger: new Cabin(),
// logger: false,
workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication' && message?.deploying) {
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
scheduler.workers.get('deployApplication').postMessage('cancel')
}
}
},
// workerMessageHandler: async ({ name, message }) => {
// if (name === 'deployApplication' && message?.deploying) {
// if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
// scheduler.workers.get('deployApplication').postMessage('cancel')
// }
// }
// },
jobs: [
{ name: 'infrastructure' },
{ name: 'deployApplication' },
],
};

View File

@ -1410,6 +1410,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
depends_on: [
`${id}-mariadb`,
`${id}-redis`,
`${id}-influxdb`,
],
environment: [
"_APP_ENV=production",
@ -1772,54 +1773,77 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
],
...defaultComposeConfiguration(network),
},
[`${id}-usage-timeseries`]: {
image: `${image}:${version}`,
container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage --type=timeseries",
depends_on: [
`${id}-mariadb`,
`${id}-influxdb`,
],
environment: [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets
],
...defaultComposeConfiguration(network),
},
[`${id}-usage-database`]: {
image: `${image}:${version}`,
container_name: `${id}-usage-database`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage --type=database",
depends_on: [
`${id}-mariadb`,
`${id}-influxdb`,
],
environment: [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets
],
...defaultComposeConfiguration(network),
},
[`${id}-influxdb`]: {
image: "appwrite/influxdb:1.5.0",
container_name: `${id}-influxdb`,
volumes: [
`${id}-influxdb:/var/lib/influxdb:rw`
],
...defaultComposeConfiguration(network),
},
[`${id}-telegraf`]: {
image: "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`,
environment: [
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`OPEN_RUNTIMES_NETWORK=${network}`,
],
...defaultComposeConfiguration(network),
}
};
dockerCompose[id].depends_on.push(`${id}-influxdb`);
dockerCompose[`${id}-usage`] = {
image: `${image}:${version}`,
container_name: `${id}-usage`,
labels: makeLabelForServices('appwrite'),
entrypoint: "usage",
depends_on: [
`${id}-mariadb`,
`${id}-influxdb`,
],
environment: [
"_APP_ENV=production",
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_DB_HOST=${mariadbHost}`,
`_APP_DB_PORT=${mariadbPort}`,
`_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`,
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets
],
...defaultComposeConfiguration(network),
}
dockerCompose[`${id}-influxdb`] = {
image: "appwrite/influxdb:1.5.0",
container_name: `${id}-influxdb`,
volumes: [
`${id}-influxdb:/var/lib/influxdb:rw`
],
...defaultComposeConfiguration(network),
}
dockerCompose[`${id}-telegraf`] = {
image: "appwrite/telegraf:1.4.0",
container_name: `${id}-telegraf`,
environment: [
`_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086",
`OPEN_RUNTIMES_NETWORK=${network}`,
],
...defaultComposeConfiguration(network),
}
const composeFile: any = {
version: '3.8',
services: dockerCompose,
@ -1868,7 +1892,9 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
}
}
async function startServiceContainers(dockerId, composeFileDestination) {
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
try {
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
} catch (error) { }
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` })

View File

@ -29,7 +29,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'stable',
ports: {
main: 8000
}
},
labels: ['analytics', 'plausible', 'plausible-analytics', 'gdpr', 'no-cookie']
},
{
name: 'nocodb',
@ -39,7 +40,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
labels: ['nocodb', 'airtable', 'database']
},
{
name: 'minio',
@ -49,7 +51,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 9001
}
},
labels: ['minio', 's3', 'storage']
},
{
name: 'vscodeserver',
@ -59,7 +62,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
labels: ['vscodeserver', 'vscode', 'code-server', 'ide']
},
{
name: 'wordpress',
@ -70,7 +74,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 80
}
},
labels: ['wordpress', 'blog', 'cms']
},
{
name: 'vaultwarden',
@ -80,7 +85,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 80
}
},
labels: ['vaultwarden', 'password-manager', 'passwords']
},
{
name: 'languagetool',
@ -90,7 +96,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8010
}
},
labels: ['languagetool', 'grammar', 'spell-checker']
},
{
name: 'n8n',
@ -100,7 +107,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 5678
}
},
labels: ['n8n', 'workflow', 'automation', 'ifttt', 'zapier', 'nodered']
},
{
name: 'uptimekuma',
@ -110,7 +118,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 3001
}
},
labels: ['uptimekuma', 'uptime', 'monitoring']
},
{
name: 'ghost',
@ -121,7 +130,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 2368
}
},
labels: ['ghost', 'blog', 'cms']
},
{
name: 'meilisearch',
@ -132,7 +142,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 7700
}
},
labels: ['meilisearch', 'search', 'search-engine']
},
{
name: 'umami',
@ -143,7 +154,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
},
labels: ['umami', 'analytics', 'gdpr', 'no-cookie']
},
{
name: 'hasura',
@ -154,7 +166,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'v2.10.0',
ports: {
main: 8080
}
},
labels: ['hasura', 'graphql', 'database']
},
{
name: 'fider',
@ -165,7 +178,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'stable',
ports: {
main: 3000
}
},
labels: ['fider', 'feedback', 'suggestions']
},
{
name: 'appwrite',
@ -176,7 +190,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: '1.0',
ports: {
main: 80
}
},
labels: ['appwrite', 'database', 'storage', 'api', 'serverless']
},
// {
// name: 'moodle',
@ -198,7 +213,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
labels: ['glitchtip', 'error-reporting', 'error', 'sentry', 'bugsnag']
},
{
name: 'searxng',
@ -209,7 +225,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
labels: ['searxng', 'search', 'search-engine']
},
{
name: 'weblate',
@ -220,7 +237,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
labels: ['weblate', 'translation', 'localization']
},
// {
// name: 'taiga',
@ -242,7 +260,8 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 3000
}
},
labels: ['grafana', 'monitoring', 'metrics', 'dashboard']
},
{
name: 'trilium',
@ -253,6 +272,7 @@ export const supportedServiceTypesAndVersions = [
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
labels: ['trilium', 'notes', 'note-taking', 'wiki']
},
];

View File

@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
let isRestarting = false;
let payload = []
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const containerObj = JSON.parse(container);
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload.push({
name: containerObj.Names,
status: {
isRunning,
isExited,
isRestarting
}
})
}
}
} else {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
payload.push({
name: id,
status: {
isRunning,
isExited,
isRestarting
}
})
}
}
}
return {
isRunning,
isRestarting,
isExited,
};
return payload
} catch ({ status, message }) {
return errorHandler({ status, message })
}
@ -289,13 +330,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
baseDatabaseBranch
baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
} = request.body
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim();
@ -324,6 +367,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
@ -342,6 +388,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration
}
});
@ -506,6 +555,21 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
const containerObj = JSON.parse(container);
await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id });
}
}
return
}
const { found } = await checkContainer({ dockerId, container: id });
if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id });
@ -613,6 +677,24 @@ export async function getUsage(request) {
return errorHandler({ status, message })
}
}
export async function getUsageByContainer(request) {
try {
const { id, containerId } = request.params
const teamId = request.user?.teamId;
let usage = {};
const application: any = await getApplicationFromDB(id, teamId);
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, containerId)]);
}
return {
usage
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
try {
const { id } = request.params
@ -1159,7 +1241,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try {
const { id } = request.params;
const { id, containerId } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
@ -1170,10 +1252,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
});
if (destinationDockerId) {
try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
@ -1181,7 +1261,10 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
return { logs: sortedLogs }
// }
} catch (error) {
const { statusCode } = error;
const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true }
}
if (statusCode === 404) {
return {
logs: []

View File

@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, getUsageByContainer, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@ -45,11 +45,13 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request));
fastify.post<RestartPreviewApplication>('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply));
fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
// fastify.get<GetApplicationLogs>('/:id/logs', async (request) => await getApplicationLogs(request));
fastify.get<GetApplicationLogs>('/:id/logs/:containerId', async (request) => await getApplicationLogs(request));
fastify.get<GetBuilds>('/:id/logs/build', async (request) => await getBuilds(request));
fastify.get<GetBuildIdLogs>('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request));
fastify.get('/:id/usage', async (request) => await getUsage(request))
fastify.get('/:id/usage/:containerId', async (request) => await getUsageByContainer(request))
fastify.post<DeployApplication>('/:id/deploy', async (request) => await deployApplication(request))
fastify.post<CancelDeployment>('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply));

View File

@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
baseImage: string,
baseBuildImage: string,
deploymentType: string,
baseDatabaseBranch: string
baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string
}
}
export interface SaveApplicationSettings extends OnlyId {
@ -84,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
path: string,
}
}
export interface GetApplicationLogs extends OnlyId {
export interface GetApplicationLogs {
Params: {
id: string,
containerId: string
}
Querystring: {
since: number,
}

View File

@ -146,7 +146,7 @@ export async function showDashboard(request: FastifyRequest) {
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
foundUnconfiguredApplication = true
}
}

View File

@ -234,6 +234,8 @@ export async function traefikConfiguration(request, reply) {
fqdn,
id,
port,
buildPack,
dockerComposeConfiguration,
destinationDocker,
destinationDockerId,
settings: { previews, dualCerts, isCustomSSL }
@ -241,6 +243,33 @@ export async function traefikConfiguration(request, reply) {
if (destinationDockerId) {
const { network, id: dockerId } = destinationDocker;
const isRunning = true;
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
for (const service of services) {
const [key, value] = service
const { port: customPort, fqdn } = value
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
data.applications.push({
id: `${id}-${key}`,
container: `${id}-${key}`,
port: customPort ? customPort : port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
});
}
}
continue;
}
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
@ -604,13 +633,41 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
fqdn,
id,
port,
buildPack,
dockerComposeConfiguration,
destinationDocker,
destinationDockerId,
settings: { previews, dualCerts }
settings: { previews, dualCerts, isCustomSSL }
} = application;
if (destinationDockerId) {
const { id: dockerId, network } = destinationDocker;
const isRunning = true;
if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration))
for (const service of services) {
const [key, value] = service
const { port: customPort, fqdn } = value
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
data.applications.push({
id: `${id}-${key}`,
container: `${id}-${key}`,
port: customPort ? customPort : port || 3000,
domain,
nakedDomain,
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
});
}
}
continue;
}
if (fqdn) {
const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, '');
@ -626,7 +683,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
isRunning,
isHttps,
isWWW,
isDualCerts: dualCerts
isDualCerts: dualCerts,
isCustomSSL
});
}
if (previews) {
@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>
nakedDomain,
isHttps,
isWWW,
isDualCerts: dualCerts
isDualCerts: dualCerts,
isCustomSSL
});
}
}

View File

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

View File

@ -110,7 +110,7 @@ async function send({
if (
response.status === 401 &&
!path.startsWith('https://api.github') &&
!path.includes('/v4/user')
!path.includes('/v4/')
) {
Cookies.remove('token');
}

View File

@ -12,6 +12,7 @@
</script>
<div class={`dropdown dropdown-end ${position}`}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</label>

View File

@ -40,4 +40,6 @@
<Icons.Laravel {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'heroku'}
<Icons.Heroku {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'compose'}
<Icons.Compose {isAbsolute} />
{/if}

View File

@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="docker compose logo"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-8' : 'w-8 h-8 mx-auto'}
src="/docker-compose.png"
/>

View File

@ -17,3 +17,4 @@ export { default as Eleventy } from './Eleventy.svelte';
export { default as Deno } from './Deno.svelte';
export { default as Laravel } from './Laravel.svelte';
export { default as Heroku } from './Heroku.svelte';
export { default as Compose } from './Compose.svelte';

View File

@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
return (
isAdmin &&
(application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) &&
application.gitSource &&
application.repository &&
@ -74,9 +75,8 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any)
}
export const status: Writable<any> = writable({
application: {
isRunning: false,
isExited: false,
isRestarting: false,
statuses: [],
overallStatus: 'stopped',
loading: false,
initialLoading: true
},

View File

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

View File

@ -18,7 +18,7 @@
<div class="dropdown dropdown-bottom">
<slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-52">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
@ -37,7 +37,7 @@
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
<li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
<button on:click={newApplication} class="no-underline hover:bg-applications tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -58,7 +58,7 @@
>
</li>
<li>
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
<button on:click={newService} class="no-underline hover:bg-services tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -75,7 +75,7 @@
>
</li>
<li>
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
<button on:click={newDatabase} class="no-underline hover:bg-databases tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -94,7 +94,7 @@
>
</li>
<li>
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
<a href="/sources/new" class="no-underline hover:bg-sources tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -116,7 +116,7 @@
>
</li>
<li>
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
<a href="/destinations/new" class="no-underline hover:bg-destinations tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@ -160,12 +160,12 @@
<span>Logs</span>
</li>
<li
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus === 'stopped'}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
href={$status.application.overallStatus !== 'stopped' ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
@ -218,10 +218,10 @@
</li>
<li
class="rounded"
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
<a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"

View File

@ -59,7 +59,6 @@
import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import {
appSession,
status,
@ -140,13 +139,11 @@
async function stopApplication() {
try {
$status.application.initialLoading = true;
// $status.application.loading = true;
await post(`/applications/${id}/stop`, {});
} catch (error) {
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
// $status.application.loading = false;
await getStatus();
}
}
@ -154,18 +151,48 @@
if ($status.application.loading) return;
$status.application.loading = true;
const data = await get(`/applications/${id}/status`);
$status.application.isRunning = data.isRunning;
$status.application.isExited = data.isExited;
$status.application.isRestarting = data.isRestarting;
$status.application.statuses = data;
let numberOfApplications = 0;
if (application.dockerComposeConfiguration) {
numberOfApplications =
application.buildPack === 'compose'
? Object.entries(JSON.parse(application.dockerComposeConfiguration)).length
: 1;
} else {
numberOfApplications = 1;
}
if ($status.application.statuses.length === 0) {
$status.application.overallStatus = 'stopped';
} else {
if ($status.application.statuses.length !== numberOfApplications) {
$status.application.overallStatus = 'degraded';
} else {
for (const oneStatus of $status.application.statuses) {
if (oneStatus.status.isExited || oneStatus.status.isRestarting) {
$status.application.overallStatus = 'degraded';
break;
}
if (oneStatus.status.isRunning) {
$status.application.overallStatus = 'healthy';
}
if (
!oneStatus.status.isExited &&
!oneStatus.status.isRestarting &&
!oneStatus.status.isRunning
) {
$status.application.overallStatus = 'stopped';
}
}
}
}
$status.application.loading = false;
$status.application.initialLoading = false;
}
onDestroy(() => {
$status.application.initialLoading = true;
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false;
$location = null;
$isDeploymentEnabled = false;
@ -173,15 +200,8 @@
});
onMount(async () => {
setLocation(application, settings);
$status.application.isRunning = false;
$status.application.isExited = false;
$status.application.isRestarting = false;
$status.application.loading = false;
if (
application.gitSourceId &&
application.destinationDockerId &&
(application.fqdn || application.settings.isBot)
) {
if ($isDeploymentEnabled) {
await getStatus();
statusInterval = setInterval(async () => {
await getStatus();
@ -207,11 +227,16 @@
<div class="flex justify-center items-center space-x-2">
<div>Configurations</div>
<div
class="badge rounded uppercase"
class:text-green-500={$status.application.isRunning}
class:text-red-500={!$status.application.isRunning}
class="badge badge-lg rounded uppercase"
class:text-green-500={$status.application.overallStatus === 'healthy'}
class:text-yellow-400={$status.application.overallStatus === 'degraded'}
class:text-red-500={$status.application.overallStatus === 'stopped'}
>
{$status.application.isRunning ? 'Running' : 'Stopped'}
{$status.application.overallStatus === 'healthy'
? 'Running'
: $status.application.overallStatus === 'degraded'
? 'Degraded'
: 'Stopped'}
</div>
</div>
{/if}
@ -245,16 +270,15 @@
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.application.isExited || $status.application.isRestarting}
{#if $status.application.overallStatus === 'degraded' && application.buildPack !== 'compose'}
<a
id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm text-error"
class="btn btn-sm text-sm gap-2"
sveltekit:prefetch
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
class="w-6 h-6 text-red-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
@ -269,14 +293,14 @@
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
Application Error
</a>
<Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
{/if}
{#if $status.application.initialLoading}
<button class="icons animate-spin bg-transparent duration-500 ease-in-out">
<button class="btn btn-ghost btn-sm gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
class="h-6 w-6 animate-spin duration-500 ease-in-out"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@ -292,18 +316,18 @@
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
Loading...
</button>
{:else if $status.application.isRunning}
{:else if $status.application.overallStatus === 'healthy'}
<button
id="stop"
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-error"
class="btn btn-sm btn-error gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
class="w-6 h-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
@ -314,38 +338,34 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</svg> Stop
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
<button
id="restart"
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
{#if application.buildPack !== 'compose'}
<button
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg> Restart
</button>
{/if}
<button
id="forceredeploy"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent "
class="btn btn-sm gap-2"
on:click={() => handleDeploySubmit(true)}
>
<svg
@ -364,33 +384,80 @@
transform="rotate(-45 12 12)"
/>
</svg>
Force Redeploy
</button>
<Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm btn-error gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 "
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{/if}
<button
class="icons flex items-center font-bold"
class="btn btn-sm gap-2"
class:btn-primary={$status.application.overallStatus !== 'degraded'}
disabled={!$isDeploymentEnabled}
on:click={() => handleDeploySubmit(false)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2 text-green-500"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
Deploy
{#if $status.application.overallStatus !== 'degraded'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" />
</svg>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
transform="rotate(-45 12 12)"
/>
</svg>
{/if}
{$status.application.overallStatus === 'degraded'
? $status.application.statuses.length === 1
? 'Force Redeploy'
: 'Redeploy Stack'
: 'Deploy'}
</button>
{/if}
{#if $location && $status.application.isRunning}
<a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
{#if $location && $status.application.overallStatus === 'healthy'}
<a href={$location} target="_blank" class="btn btn-sm gap-2 text-sm bg-primary"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@ -405,9 +472,8 @@
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
</svg>Open</a
>
<Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
{/if}
</div>
</div>

View File

@ -14,6 +14,9 @@
export let foundConfig: any;
export let scanning: any;
export let packageManager: any;
export let dockerComposeFile: string | null = null;
export let dockerComposeFileLocation: string | null = null;
export let dockerComposeConfiguration: any = null;
async function handleSubmit(name: string) {
try {
@ -25,10 +28,20 @@
delete tempBuildPack.fancyName;
delete tempBuildPack.color;
delete tempBuildPack.hoverColor;
if (foundConfig?.buildPack !== name) {
await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name });
let composeConfiguration: any = {}
if (!dockerComposeConfiguration && dockerComposeFile) {
for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) {
composeConfiguration[name] = {};
}
}
await post(`/applications/${id}`, {
...tempBuildPack,
buildPack: name,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration: JSON.stringify(composeConfiguration) || JSON.stringify({})
});
await post(`/applications/${id}/configuration/buildpack`, { buildPack: name });
return await goto(from || `/applications/${id}`);
} catch (error) {

View File

@ -95,7 +95,7 @@
if (newWindow?.closed) {
clearInterval(timer);
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
localStorage.removeItem('gitLabToken');
// localStorage.removeItem('gitLabToken');
resolve();
}
}, 100);

View File

@ -165,7 +165,7 @@
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
bind:value={publicRepositoryLink}
/>
<button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
<button class="btn bg-orange-600" type="submit">
Load Repository
</button>
</div>

View File

@ -12,6 +12,7 @@
const response = await get(`/applications/${params.id}/configuration/buildpack`);
return {
props: {
application,
...response
}
};
@ -25,22 +26,6 @@
</script>
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { get } from '$lib/api';
import { appSession } from '$lib/store';
import { t } from '$lib/translations';
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common';
import BuildPack from './_BuildPack.svelte';
const { id } = $page.params;
let scanning = true;
let foundConfig: any = null;
let packageManager = 'npm';
export let apiUrl: any;
export let projectId: any;
export let repository: any;
@ -49,6 +34,28 @@
export let application: any;
export let isPublicRepository: boolean;
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { get, getAPIUrl } from '$lib/api';
import { appSession } from '$lib/store';
import { t } from '$lib/translations';
import { buildPacks, findBuildPack, scanningTemplates } from '$lib/templates';
import { errorNotification } from '$lib/common';
import BuildPack from './_BuildPack.svelte';
import yaml from 'js-yaml';
const { id } = $page.params;
let htmlUrl = application.gitSource.htmlUrl;
let scanning: boolean = true;
let foundConfig: any = null;
let packageManager: string = 'npm';
let dockerComposeFile: string | null = application.dockerComposeFile || null;
let dockerComposeFileLocation: string | null = application.dockerComposeFileLocation || null;
let dockerComposeConfiguration: any = application.dockerComposeConfiguration || null;
function checkPackageJSONContents({ key, json }: { key: any; json: any }) {
return json?.dependencies?.hasOwnProperty(key) || json?.devDependencies?.hasOwnProperty(key);
}
@ -60,11 +67,45 @@
}
}
}
async function scanRepository(): Promise<void> {
async function getGitlabToken() {
return await new Promise<void>((resolve, reject) => {
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${
application.gitSource.gitlabApp.appId
}&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
$page.params.id
}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
$appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
localStorage.removeItem('gitLabToken');
resolve();
}
}, 100);
});
}
async function scanRepository(isPublicRepository: boolean): Promise<void> {
try {
if (type === 'gitlab') {
const files = await get(`${apiUrl}/v4/projects/${projectId}/repository/tree`, {
Authorization: `Bearer ${$appSession.tokens.gitlab}`
const headers = isPublicRepository
? {}
: {
Authorization: `Bearer ${$appSession.tokens.gitlab}`
};
const url = isPublicRepository ? `/projects/${projectId}/repository/tree` : `/v4/projects/${projectId}/repository/tree`;
const files = await get(`${apiUrl}${url}`, {
...headers
});
const packageJson = files.find(
(file: { name: string; type: string }) =>
@ -82,6 +123,14 @@
(file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'blob'
);
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'blob'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'blob'
);
const cargoToml = files.find(
(file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'blob'
@ -105,11 +154,24 @@
const laravel = files.find(
(file: { name: string; type: string }) => file.name === 'artisan' && file.type === 'blob'
);
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) {
if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('compose', packageManager);
const id = dockerComposeFileYml.id || dockerComposeFileYaml.id;
const data = await get(`${apiUrl}/v4/projects/${projectId}/repository/blobs/${id}`, {
...headers
});
if (data?.content) {
const content = atob(data.content);
const dockerComposeJson = yaml.load(content) || null;
dockerComposeFile = JSON.stringify(dockerComposeJson);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
}
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) {
const path = packageJson.path;
@ -135,8 +197,13 @@
foundConfig = findBuildPack('node', packageManager);
}
} else if (type === 'github') {
const headers = isPublicRepository
? {}
: {
Authorization: `token ${$appSession.tokens.github}`
};
const files = await get(`${apiUrl}/repos/${repository}/contents?ref=${branch}`, {
Authorization: `Bearer ${$appSession.tokens.github}`,
...headers,
Accept: 'application/vnd.github.v2.json'
});
const packageJson = files.find(
@ -155,6 +222,14 @@
(file: { name: string; type: string }) =>
file.name === 'Dockerfile' && file.type === 'file'
);
const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yml' && file.type === 'file'
);
const dockerComposeFileYaml = files.find(
(file: { name: string; type: string }) =>
file.name === 'docker-compose.yaml' && file.type === 'file'
);
const cargoToml = files.find(
(file: { name: string; type: string }) =>
file.name === 'Cargo.toml' && file.type === 'file'
@ -182,11 +257,30 @@
if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm';
if (dockerfile) {
if (dockerComposeFileYml || dockerComposeFileYaml) {
foundConfig = findBuildPack('compose', packageManager);
const data = await get(
`${apiUrl}/repos/${repository}/contents/${
dockerComposeFileYml ? 'docker-compose.yml' : 'docker-compose.yaml'
}?ref=${branch}`,
{
...headers,
Accept: 'application/vnd.github.v2.json'
}
);
if (data?.content) {
const content = atob(data.content);
const dockerComposeJson = yaml.load(content) || null;
dockerComposeFile = JSON.stringify(dockerComposeJson);
dockerComposeFileLocation = dockerComposeFileYml
? 'docker-compose.yml'
: 'docker-compose.yaml';
}
} else if (dockerfile) {
foundConfig = findBuildPack('docker', packageManager);
} else if (packageJson && !laravel) {
const data: any = await get(`${packageJson.git_url}`, {
Authorization: `Bearer ${$appSession.tokens.github}`,
...headers,
Accept: 'application/vnd.github.v2.raw'
});
const json = JSON.parse(data) || {};
@ -214,30 +308,39 @@
error.message === '401 Unauthorized'
) {
if (application.gitSource.gitlabAppId) {
let htmlUrl = application.gitSource.htmlUrl;
const left = screen.width / 2 - 1020 / 2;
const top = screen.height / 2 - 618 / 2;
const newWindow = open(
`${htmlUrl}/oauth/authorize?client_id=${application.gitSource.gitlabApp.appId}&redirect_uri=${window.location.origin}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${$page.params.id}`,
'GitLab',
'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
top +
', left=' +
left +
', toolbar=0, menubar=0, status=0'
);
const timer = setInterval(() => {
if (newWindow?.closed) {
clearInterval(timer);
window.location.reload();
}
}, 100);
if (!$appSession.tokens.gitlab) {
await getGitlabToken();
}
scanRepository(isPublicRepository);
// let htmlUrl = application.gitSource.htmlUrl;
// const left = screen.width / 2 - 1020 / 2;
// const top = screen.height / 2 - 618 / 2;
// const newWindow = open(
// `${htmlUrl}/oauth/authorize?client_id=${
// application.gitSource.gitlabApp.appId
// }&redirect_uri=${getAPIUrl()}/webhooks/gitlab&response_type=code&scope=api+email+read_repository&state=${
// $page.params.id
// }`,
// 'GitLab',
// 'resizable=1, scrollbars=1, fullscreen=0, height=618, width=1020,top=' +
// top +
// ', left=' +
// left +
// ', toolbar=0, menubar=0, status=0'
// );
// const timer = setInterval(() => {
// if (newWindow?.closed) {
// clearInterval(timer);
// $appSession.tokens.gitlab = localStorage.getItem('gitLabToken');
// // localStorage.removeItem('gitLabToken' );
// }
// }, 100);
}
}
if (error.message === 'Bad credentials') {
} else if (error.message === 'Bad credentials') {
const { token } = await get(`/applications/${id}/configuration/githubToken`);
$appSession.tokens.github = token;
return await scanRepository();
return await scanRepository(isPublicRepository);
}
return errorNotification(error);
} finally {
@ -246,11 +349,7 @@
}
}
onMount(async () => {
if (!isPublicRepository) {
await scanRepository();
} else {
scanning = false;
}
await scanRepository(isPublicRepository);
});
</script>
@ -274,9 +373,17 @@
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Base</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack}
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
<BuildPack
{packageManager}
{buildPack}
{scanning}
bind:foundConfig
{dockerComposeFile}
{dockerComposeFileLocation}
{dockerComposeConfiguration}
/>
</div>
{/each}
</div>
@ -284,7 +391,7 @@
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Specific</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack}
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>

File diff suppressed because it is too large Load Diff

View File

@ -3,11 +3,9 @@
import { get } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { onMount, onDestroy } from 'svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { status } from '$lib/store';
import { goto } from '$app/navigation';
let application: any = {};
let logsLoading = false;
let loadLogsInterval: any = null;
@ -17,47 +15,52 @@
let followingLogs: any;
let logsEl: any;
let position = 0;
if (
!$status.application.isExited &&
!$status.application.isRestarting &&
!$status.application.isRunning
) {
goto(`/applications/${$page.params.id}/`, { replaceState: true });
}
let services: any = [];
let selectedService: any = null;
let noContainer = false;
const { id } = $page.params;
onMount(async () => {
const response = await get(`/applications/${id}`);
application = response.application;
loadAllLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
if (response.application.dockerComposeFile) {
services = normalizeDockerServices(
JSON.parse(response.application.dockerComposeFile).services
);
} else {
services = [
{
name: ''
}
];
await selectService('');
}
});
onDestroy(() => {
clearInterval(loadLogsInterval);
clearInterval(followingInterval);
});
async function loadAllLogs() {
try {
logsLoading = true;
const data: any = await get(`/applications/${id}/logs`);
if (data?.logs) {
lastLog = data.logs[data.logs.length - 1];
logs = data.logs;
}
} catch (error) {
return errorNotification(error);
} finally {
logsLoading = false;
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function loadLogs() {
if (logsLoading) return;
try {
const newLogs: any = await get(
`/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}`
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}`
);
if (newLogs.noContainer) {
noContainer = true;
} else {
noContainer = false;
}
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs);
lastLog = newLogs.logs[newLogs.logs.length - 1];
@ -89,6 +92,22 @@
clearInterval(followingInterval);
}
}
async function selectService(service: any, init: boolean = false) {
if (services.length === 1 && init) return;
if (loadLogsInterval) clearInterval(loadLogsInterval);
if (followingInterval) clearInterval(followingInterval);
logs = [];
lastLog = null;
followingLogs = false;
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
loadLogs();
loadLogsInterval = setInterval(() => {
loadLogs();
}, 1000);
}
</script>
<div class="mx-auto w-full">
@ -96,50 +115,69 @@
<div class="title font-bold pb-3">Application Logs</div>
</div>
</div>
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
<div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0}
{#if noContainer}
<div class="text-xl font-bold tracking-tighter">Container not found / exited.</div>
{/if}
{:else}
<div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
{#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div>
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{#each logs as log}
<p>{log + '\n'}</p>
{/each}
</div>
</div>
{/if}
</div>
{/if}

View File

@ -1,11 +1,14 @@
<script lang="ts">
export let application: any;
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import { get } from '$lib/api';
import { status } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte';
const { id } = $page.params;
let services: any = [];
let selectedService: any = null;
let usageLoading = false;
let usage = {
MemUsage: 0,
@ -16,20 +19,55 @@
async function getUsage() {
if (usageLoading) return;
if (!$status.application.isRunning) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage`);
const data = await get(`/applications/${id}/usage/${selectedService}`);
usage = data.usage;
usageLoading = false;
}
function normalizeDockerServices(services: any[]) {
const tempdockerComposeServices = [];
for (const [name, data] of Object.entries(services)) {
tempdockerComposeServices.push({
name,
data
});
}
return tempdockerComposeServices;
}
async function selectService(service: any, init: boolean = false) {
if (services.length === 1 && init) return;
if (usageInterval) clearInterval(usageInterval);
usageLoading = false;
usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`;
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
const response = await get(`/applications/${id}`);
application = response.application;
if (response.application.dockerComposeFile) {
services = normalizeDockerServices(
JSON.parse(response.application.dockerComposeFile).services
);
} else {
services = [
{
name: ''
}
];
await selectService('');
}
});
</script>
@ -38,21 +76,44 @@
<div class="title font-bold pb-3">Monitoring</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="flex gap-2 lg:gap-8 pb-4">
{#each services as service}
<button
on:click={() => selectService(service, true)}
class:bg-primary={selectedService ===
`${application.id}${service.name ? `-${service.name}` : ''}`}
class:bg-coolgray-200={selectedService !==
`${application.id}${service.name ? `-${service.name}` : ''}`}
class="w-full rounded p-5 hover:bg-primary font-bold"
>
{application.id}{service.name ? `-${service.name}` : ''}</button
>
{/each}
</div>
{#if selectedService}
<div class="mx-auto max-w-4xl px-6 py-4 bg-coolgray-100 border border-coolgray-200 relative">
{#if usageLoading}
<button
id="streaming"
class="btn btn-sm bg-transparent border-none loading absolute top-0 left-0 text-xs"
/>
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>
</div>
{/if}

View File

@ -21,9 +21,11 @@
const { id } = $page.params;
let loading = false;
let publicLoading = false;
let loading = {
main: false,
public: false
};
let publicUrl = '';
let appendOnly = database.settings.appendOnly;
let databaseDefault: any;
@ -47,23 +49,46 @@
databaseDbUser = '';
}
}
function generateUrl(): string {
return `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${
$status.database.isPublic
? database.destinationDocker.remoteEngine
? database.destinationDocker.remoteIpAddress
: $appSession.ipv4
: database.id
}:${$status.database.isPublic ? database.publicPort : privatePort}/${databaseDefault}`;
function generateUrl() {
const ipAddress = () => {
if ($status.database.isPublic) {
if (database.destinationDocker.remoteEngine) {
return database.destinationDocker.remoteIpAddress;
}
if ($appSession.ipv6) {
return $appSession.ipv6;
}
if ($appSession.ipv4) {
return $appSession.ipv4;
}
return '<Cannot determine public IP address>';
} else {
return database.id;
}
};
const user = () => {
if (databaseDbUser) {
return databaseDbUser + ':';
}
return '';
};
const port = () => {
if ($status.database.isPublic) {
return database.publicPort;
} else {
return privatePort;
}
};
publicUrl = `${
database.type
}://${user()}${databaseDbUserPassword}@${ipAddress()}:${port()}/${databaseDefault}`;
}
async function changeSettings(name: any) {
if (name !== 'appendOnly') {
if (publicLoading || !$status.database.isRunning) return;
if (loading.public || !$status.database.isRunning) return;
}
publicLoading = true;
loading.public = true;
let data = {
isPublic: $status.database.isPublic,
appendOnly
@ -87,12 +112,12 @@
} catch (error) {
return errorNotification(error);
} finally {
publicLoading = false;
loading.public = false;
}
}
async function handleSubmit() {
try {
loading = true;
loading.main = true;
await post(`/databases/${id}`, { ...database, isRunning: $status.database.isRunning });
generateDbDetails();
addToast({
@ -102,7 +127,7 @@
} catch (error) {
return errorNotification(error);
} finally {
loading = false;
loading.main = false;
}
}
</script>
@ -115,9 +140,9 @@
<button
type="submit"
class="btn btn-sm"
class:loading
class:bg-databases={!loading}
disabled={loading}>{$t('forms.save')}</button
class:loading={loading.main}
class:bg-databases={!loading.main}
disabled={loading.main}>{$t('forms.save')}</button
>
{/if}
</div>
@ -175,7 +200,7 @@
readonly
disabled
name="publicPort"
value={publicLoading
value={loading.public
? 'Loading...'
: $status.database.isPublic
? database.publicPort
@ -198,8 +223,8 @@
<EdgeDB {database} />
{/if}
<div class="flex flex-col space-y-2 mt-5">
<div>
<label class="px-2" for="url"
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<label for="url"
>{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer
@ -207,18 +232,21 @@
/>
{/if}</label
>
<button class="btn btn-sm" on:click|preventDefault={generateUrl}
>Show Connection String</button
>
</div>
<div class="lg:px-10 px-2">
<CopyPasswordField
textarea={true}
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
id="url"
name="url"
readonly
disabled
value={publicLoading || loading ? 'Loading...' : generateUrl()}
/>
{#if publicUrl}
<CopyPasswordField
placeholder="Click on the button to generate URL"
id="url"
name="url"
readonly
disabled
value={loading.public ? 'Loading...' : publicUrl}
/>
{/if}
</div>
</div>
</form>
@ -228,7 +256,7 @@
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<Setting
id="isPublic"
loading={publicLoading}
loading={loading.public}
bind:setting={$status.database.isPublic}
on:click={() => changeSettings('isPublic')}
title={$t('database.set_public')}
@ -238,7 +266,7 @@
{#if database.type === 'redis'}
<Setting
id="appendOnly"
loading={publicLoading}
loading={loading.public}
bind:setting={appendOnly}
on:click={() => changeSettings('appendOnly')}
title={$t('database.change_append_only_mode')}

View File

@ -146,9 +146,29 @@
try {
numberOfGetStatus++;
let isRunning = false;
let isDegraded = false;
if (buildPack) {
const response = await get(`/applications/${id}/status`);
isRunning = response.isRunning;
if (response.length === 0) {
isRunning = false;
} else if (response.length === 1) {
isRunning = response[0].status.isRunning;
} else {
let overallStatus = false;
for (const oneStatus of response) {
if (oneStatus.status.isRunning) {
overallStatus = true;
} else {
isDegraded = true;
break;
}
}
if (overallStatus) {
isRunning = true;
} else {
isRunning = false;
}
}
} else if (typeof dualCerts !== 'undefined') {
const response = await get(`/services/${id}/status`);
isRunning = response.isRunning;
@ -156,9 +176,13 @@
const response = await get(`/databases/${id}/status`);
isRunning = response.isRunning;
}
if (isRunning) {
status[id] = 'running';
return 'running';
} else if (isDegraded) {
status[id] = 'degraded';
return 'degraded';
} else {
status[id] = 'stopped';
return 'stopped';
@ -213,6 +237,7 @@
(application.id && application.id.toLowerCase().includes($search.toLowerCase())) ||
(application.name && application.name.toLowerCase().includes($search.toLowerCase())) ||
(application.fqdn && application.fqdn.toLowerCase().includes($search.toLowerCase())) ||
(application.dockerComposeConfiguration && application.dockerComposeConfiguration.toLowerCase().includes($search.toLowerCase())) ||
(application.repository &&
application.repository.toLowerCase().includes($search.toLowerCase())) ||
(application.buildpack &&
@ -594,6 +619,11 @@
<span class="indicator-item badge bg-yellow-300 badge-sm" />
{:else if status[application.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else if status[application.id] === 'degraded'}
<span
class="indicator-item indicator-middle indicator-center badge bg-warning text-black font-bold badge-xl"
>Degraded</span
>
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if}
@ -613,7 +643,7 @@
<div class="h-10 text-xs">
{#if application?.fqdn}
<h2>{application?.fqdn.replace('https://', '').replace('http://', '')}</h2>
{:else if !application.settings?.isBot && !application?.fqdn}
{:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}
<h2 class="text-red-500">Not configured</h2>
{/if}
{#if application.destinationDocker?.name}

View File

@ -27,10 +27,12 @@
<script lang="ts">
export let types: any;
let search = '';
let filteredTypes = types;
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { get, post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte';
@ -45,10 +47,50 @@
return errorNotification(error);
}
}
function doSearch() {
filteredTypes = types.filter(
(type: any) =>
type.name.toLowerCase().includes(search.toLowerCase()) ||
type.labels.some((label: string) => label.toLowerCase().includes(search.toLowerCase()))
);
}
function cleanupSearch() {
search = '';
filteredTypes = types;
}
</script>
<div class="flex flex-wrap justify-center">
{#each types as type}
<div class="container lg:mx-auto lg:p-0 px-8 pt-5">
<div class="input-group flex w-full">
<div class="btn btn-square cursor-default no-animation hover:bg-error" on:click={cleanupSearch}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentcolor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</div>
<input
id="search"
class="input w-full"
type="text"
placeholder="Search for services"
bind:value={search}
on:input={() => doSearch()}
/>
</div>
</div>
<div class="container lg:mx-auto lg:pt-20 lg:p-0 px-8 pt-20">
<div class="flex flex-wrap justify-center gap-8">
{#each filteredTypes as type}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(type.name)}>
<button type="submit" class="box-selection relative text-xl font-bold hover:bg-pink-600">
@ -59,3 +101,4 @@
</div>
{/each}
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

View File

@ -10,7 +10,7 @@ module.exports = {
"base-100": "#323232",
"base-200": "#242424",
"base-300": "#181818",
"primary": "#6d28d9",
"primary": "#6B16ED",
"primary-content": "#fff",
"secondary": "#343232",
"accent": "#343232",

View File

@ -1,6 +1,38 @@
version: '3.8'
services:
coolify:
build:
context: .
dockerfile: Dockerfile-dev
command: bash -c 'pnpm install && pnpm db:push && pnpm db:seed && pnpm dev'
environment:
- COOLIFY_CONTAINER_DEV=true
- COOLIFY_APP_ID=random-local-id
- COOLIFY_SECRET_KEY=12341234123412341234123412341234
- COOLIFY_DATABASE_URL=file:../db/dev.db
- GITPOD_WORKSPACE_URL=${GITPOD_WORKSPACE_URL}
- CODESANDBOX_HOST=${CODESANDBOX_HOST}
container_name: coolify
ports:
- target: 3000
published: 3000
protocol: tcp
mode: host
- target: 3001
published: 3001
protocol: tcp
mode: host
- target: 5555
published: 5555
protocol: tcp
mode: host
volumes:
- ./:/app
- '/var/run/docker.sock:/var/run/docker.sock'
- /tmp:/tmp
networks:
- coolify-infra
fluent-bit:
image: coollabsio/coolify-fluent-bit:1.0.0
command: /fluent-bit/bin/fluent-bit -c /fluent-bit/etc/fluent-bit-dev.conf
@ -8,7 +40,10 @@ services:
volumes:
- ./logs:/logs
ports:
- "24224:24224"
- target: 24224
published: 24224
protocol: tcp
mode: host
networks:
- coolify-infra
networks:

View File

@ -1,21 +1,24 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.10.14",
"version": "3.10.15",
"license": "Apache-2.0",
"repository": "github:coollabsio/coolify",
"scripts": {
"oc": "opencollective-setup",
"translate": "pnpm run --filter i18n-converter translate",
"db:studio": "pnpm run --filter api db:studio",
"db:studio:container": "docker exec coolify pnpm run --filter api db:studio",
"db:push": "pnpm run --filter api db:push",
"db:seed": "pnpm run --filter api db:seed",
"db:migrate": "pnpm run --filter api db:migrate",
"db:migrate:container": "docker exec coolify pnpm run --filter api db:migrate",
"format": "run-p -l -n format:*",
"format:api": "NODE_ENV=development pnpm run --filter api format",
"lint": "run-p -l -n lint:*",
"lint:api": "NODE_ENV=development pnpm run --filter api lint",
"dev": "run-p -l -n dev:*",
"dev:container": "docker-compose -f docker-compose-dev.yaml up --build || docker compose -f docker-compose-dev.yaml up --build",
"dev": "run-p -l -n dev:api dev:ui",
"dev:api": "NODE_ENV=development pnpm run --filter api dev",
"dev:ui": "NODE_ENV=development pnpm run --filter ui dev",
"build": "NODE_ENV=production run-p -n build:*",
@ -34,6 +37,7 @@
"docker",
"self-host",
"iaas",
"paas",
"heroku",
"netlify",
"open-source",

View File

@ -104,6 +104,7 @@ importers:
node-os-utils: 1.3.7
p-all: 4.0.0
p-throttle: 5.0.0
prisma: 4.4.0
public-ip: 6.0.1
pump: 3.0.0
ssh-config: 4.1.6
@ -120,7 +121,6 @@ importers:
eslint-plugin-prettier: 4.2.1_tgumt6uwl2md3n6uqnggd6wvce
nodemon: 2.0.20
prettier: 2.7.1
prisma: 4.4.0
rimraf: 3.0.2
tsconfig-paths: 4.1.0
typescript: 4.8.4
@ -159,6 +159,7 @@ importers:
flowbite: 1.5.2
flowbite-svelte: 0.26.2
js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0
postcss: 8.4.16
prettier: 2.7.1
@ -181,6 +182,7 @@ importers:
daisyui: 2.24.2_25hquoklqeoqwmt7fwvvcyxm5e
dayjs: 1.11.5
js-cookie: 3.0.1
js-yaml: 4.1.0
p-limit: 4.0.0
svelte-file-dropzone: 1.0.0
svelte-select: 4.4.7
@ -532,6 +534,7 @@ packages:
/@prisma/engines/4.4.0:
resolution: {integrity: sha512-Fpykccxlt9MHrAs/QpPGpI2nOiRxuLA+LiApgA59ibbf24YICZIMWd3SI2YD+q0IAIso0jCGiHhirAIbxK3RyQ==}
requiresBuild: true
dev: false
/@rollup/pluginutils/4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
@ -5078,6 +5081,7 @@ packages:
requiresBuild: true
dependencies:
'@prisma/engines': 4.4.0
dev: false
/private/0.1.8:
resolution: {integrity: sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==}