diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index e3e6a0750..44b5f7003 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -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() diff --git a/.github/workflows/staging-release.yml b/.github/workflows/staging-release.yml index 7a5e5474f..2280167d8 100644 --- a/.github/workflows/staging-release.yml +++ b/.github/workflows/staging-release.yml @@ -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 diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 78992243b..9f4bf9321 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -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. diff --git a/Dockerfile b/Dockerfile index b7bd56212..8cbc68037 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/Dockerfile-dev b/Dockerfile-dev new file mode 100644 index 000000000..198307656 --- /dev/null +++ b/Dockerfile-dev @@ -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 \ No newline at end of file diff --git a/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql b/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql new file mode 100644 index 000000000..bb93e1aaf --- /dev/null +++ b/apps/api/prisma/migrations/20221005120323_initial_docker_compose/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerComposeFile" TEXT; +ALTER TABLE "Application" ADD COLUMN "dockerComposeFileLocation" TEXT; diff --git a/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql b/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql new file mode 100644 index 000000000..e7368dc1a --- /dev/null +++ b/apps/api/prisma/migrations/20221005132352_docker_compose_configuration/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "dockerComposeConfiguration" TEXT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index eba0ac215..d782bceae 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -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 { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a40ea38f2..21185a6d4 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -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) + } +} \ No newline at end of file diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index a2790aadb..b2d2c8ec9 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -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) { diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts deleted file mode 100644 index 2bd9c329d..000000000 --- a/apps/api/src/jobs/infrastructure.ts +++ /dev/null @@ -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); -})(); diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 98a387f2f..fd36a2ea6 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -468,9 +468,9 @@ export const saveBuildLog = async ({ line = line.replace(regex, '@'); } 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 }); -} \ No newline at end of file +} diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts new file mode 100644 index 000000000..26a946596 --- /dev/null +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -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 }); +} diff --git a/apps/api/src/lib/buildPacks/deno.ts b/apps/api/src/lib/buildPacks/deno.ts index f255a5983..074464445 100644 --- a/apps/api/src/lib/buildPacks/deno.ts +++ b/apps/api/src/lib/buildPacks/deno.ts @@ -49,7 +49,7 @@ const createDockerfile = async (data, image): Promise => { 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')); }; diff --git a/apps/api/src/lib/buildPacks/index.ts b/apps/api/src/lib/buildPacks/index.ts index b8eb4d609..8e82047fe 100644 --- a/apps/api/src/lib/buildPacks/index.ts +++ b/apps/api/src/lib/buildPacks/index.ts @@ -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 }; diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 6ccd2f71a..d4d2e5d53 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -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 }) diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index dff59b7db..a4d36b7fc 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -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; } diff --git a/apps/api/src/lib/importers/github.ts b/apps/api/src/lib/importers/github.ts index 798931d7a..f42461939 100644 --- a/apps/api/src/lib/importers/github.ts +++ b/apps/api/src/lib/importers/github.ts @@ -73,6 +73,4 @@ export default async function ({ const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); return commit.replace('\n', ''); - - } diff --git a/apps/api/src/lib/scheduler.ts b/apps/api/src/lib/scheduler.ts index 743463757..20add2b06 100644 --- a/apps/api/src/lib/scheduler.ts +++ b/apps/api/src/lib/scheduler.ts @@ -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' }, ], }; diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index 3dcf0d5e8..bcaf07d36 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -1410,6 +1410,7 @@ async function startAppWriteService(request: FastifyRequest) { depends_on: [ `${id}-mariadb`, `${id}-redis`, + `${id}-influxdb`, ], environment: [ "_APP_ENV=production", @@ -1772,54 +1773,77 @@ async function startAppWriteService(request: FastifyRequest) { ], ...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) { } } 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` }) diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts index 64f049961..d84d1bd65 100644 --- a/apps/api/src/lib/services/supportedVersions.ts +++ b/apps/api/src/lib/services/supportedVersions.ts @@ -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'] }, ]; diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index be06a3519..d179aabfa 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -110,23 +110,64 @@ export async function getApplicationStatus(request: FastifyRequest) { 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, 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, baseImage, baseBuildImage, deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, ...defaultConfiguration, connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } } @@ -342,6 +388,9 @@ export async function saveApplication(request: FastifyRequest, baseImage, baseBuildImage, deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, ...defaultConfiguration } }); @@ -506,6 +555,21 @@ export async function stopApplication(request: FastifyRequest, 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) { try { const { id } = request.params @@ -1159,7 +1241,7 @@ export async function getPreviews(request: FastifyRequest) { export async function getApplicationLogs(request: FastifyRequest) { 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 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 => { fastify.get('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request)); fastify.post('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply)); - fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); + // fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); + fastify.get('/:id/logs/:containerId', async (request) => await getApplicationLogs(request)); fastify.get('/:id/logs/build', async (request) => await getBuilds(request)); fastify.get('/: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('/:id/deploy', async (request) => await deployApplication(request)) fastify.post('/:id/cancel', async (request, reply) => await cancelDeployment(request, reply)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 443deb00f..b282b0647 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -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, } diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index d16d3ed24..f05b8acb9 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -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 } } diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index 381869430..e6d4e474a 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -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 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 isRunning, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } if (previews) { @@ -649,7 +707,8 @@ export async function remoteTraefikConfiguration(request: FastifyRequest nakedDomain, isHttps, isWWW, - isDualCerts: dualCerts + isDualCerts: dualCerts, + isCustomSSL }); } } diff --git a/apps/ui/package.json b/apps/ui/package.json index 805539a5d..fdf37b8aa 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -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", diff --git a/apps/ui/src/lib/api.ts b/apps/ui/src/lib/api.ts index ed781b8a7..7341d580b 100644 --- a/apps/ui/src/lib/api.ts +++ b/apps/ui/src/lib/api.ts @@ -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'); } diff --git a/apps/ui/src/lib/components/Explainer.svelte b/apps/ui/src/lib/components/Explainer.svelte index f504798e1..43986a39d 100644 --- a/apps/ui/src/lib/components/Explainer.svelte +++ b/apps/ui/src/lib/components/Explainer.svelte @@ -12,6 +12,7 @@
+ diff --git a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte index 0980063c6..fc5dbe16c 100644 --- a/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte +++ b/apps/ui/src/lib/components/svg/applications/ApplicationIcons.svelte @@ -40,4 +40,6 @@ {:else if application.buildPack?.toLowerCase() === 'heroku'} +{:else if application.buildPack?.toLowerCase() === 'compose'} + {/if} diff --git a/apps/ui/src/lib/components/svg/applications/Compose.svelte b/apps/ui/src/lib/components/svg/applications/Compose.svelte new file mode 100644 index 000000000..86b9d622a --- /dev/null +++ b/apps/ui/src/lib/components/svg/applications/Compose.svelte @@ -0,0 +1,9 @@ + + +docker compose logo diff --git a/apps/ui/src/lib/components/svg/applications/index.ts b/apps/ui/src/lib/components/svg/applications/index.ts index ee2236357..7bbe7b55b 100644 --- a/apps/ui/src/lib/components/svg/applications/index.ts +++ b/apps/ui/src/lib/components/svg/applications/index.ts @@ -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'; diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index aa3493ca5..38a8eb4eb 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -56,6 +56,7 @@ export const isDeploymentEnabled: Writable = 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 = writable({ application: { - isRunning: false, - isExited: false, - isRestarting: false, + statuses: [], + overallStatus: 'stopped', loading: false, initialLoading: true }, diff --git a/apps/ui/src/lib/templates.ts b/apps/ui/src/lib/templates.ts index 671bf9d5a..d71fd9323 100644 --- a/apps/ui/src/lib/templates.ts +++ b/apps/ui/src/lib/templates.ts @@ -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': { diff --git a/apps/ui/src/routes/_NewResource.svelte b/apps/ui/src/routes/_NewResource.svelte index 84e115c5c..acdebb162 100644 --- a/apps/ui/src/routes/_NewResource.svelte +++ b/apps/ui/src/routes/_NewResource.svelte @@ -18,7 +18,7 @@ diff --git a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte index 13db5a24a..fb913b5a9 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte @@ -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) { diff --git a/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte b/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte index 43c31fd01..94fc11885 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte @@ -95,7 +95,7 @@ if (newWindow?.closed) { clearInterval(timer); $appSession.tokens.gitlab = localStorage.getItem('gitLabToken'); - localStorage.removeItem('gitLabToken'); + // localStorage.removeItem('gitLabToken'); resolve(); } }, 100); diff --git a/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte b/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte index 28d0ef1ce..32d5627d9 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte @@ -165,7 +165,7 @@ placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" bind:value={publicRepositoryLink} /> -
diff --git a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte index 3a4c41115..37e18a24d 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte @@ -12,6 +12,7 @@ const response = await get(`/applications/${params.id}/configuration/buildpack`); return { props: { + application, ...response } }; @@ -25,22 +26,6 @@ @@ -274,9 +373,17 @@
Coolify Base
- {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack} + {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'base') as buildPack}
- +
{/each}
@@ -284,7 +391,7 @@
Coolify Specific
- {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack} + {#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type === 'specific') as buildPack}
diff --git a/apps/ui/src/routes/applications/[id]/index.svelte b/apps/ui/src/routes/applications/[id]/index.svelte index 5f09c7db6..acfe3c686 100644 --- a/apps/ui/src/routes/applications/[id]/index.svelte +++ b/apps/ui/src/routes/applications/[id]/index.svelte @@ -28,10 +28,12 @@
-
+ handleSubmit()}>
General
@@ -345,9 +517,9 @@ {/if}
- + {#if isDisabled} - + {:else}
-
- changeSettings('isBot')} - title="Is your application a bot?" - description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." - disabled={$status.application.isRunning} - /> -
- {#if !isBot} + {#if application.buildPack !== 'compose'} +
+ changeSettings('isBot')} + title="Is your application a bot?" + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." + disabled={isDisabled} + /> +
+ {/if} + {#if !isBot && application.buildPack !== 'compose'}
- {#if isHttps} + {#if isHttps && application.buildPack !== 'compose'}
- -
Build & Deploy
-
- {#if application.buildCommand || application.buildPack === 'rust' || application.buildPack === 'laravel'} -
- -
- -
-
- {/if} - {#if application.buildPack !== 'docker' && (application.buildPack === 'nextjs' || application.buildPack === 'nuxtjs')} -
- -
- +
- {#if isDBBranching} - + - {#if application.connectedDatabase} -
- - -
-
- Connected to {application.connectedDatabase.databaseId} -
+
+ +
+
+ {/if} + {#if $features.beta} + {#if !application.settings.isBot && !application.settings.isPublicRepository} +
+ changeSettings('isDBBranching')} + title="Enable DB Branching" + description="Enable DB Branching" + /> +
+ {#if isDBBranching} + + {#if application.connectedDatabase} +
+ + +
+
+ Connected to {application.connectedDatabase.databaseId} +
+ {/if} {/if} {/if} {/if} - {/if} - {#if application.buildPack === 'python'} -
- -
- -
- {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} + {#if application.buildPack === 'python'}
- + +
+
+ {#if application.pythonWSGI?.toLowerCase() === 'gunicorn'} +
+ + +
+ {/if} + {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} +
+ + +
+ {/if} {/if} - {#if application.pythonWSGI?.toLowerCase() === 'uvicorn'} -
- - -
- {/if} - {/if} - {#if !staticDeployments.includes(application.buildPack)} -
- - -
- {/if} -
- - -
- {#if !notNodeDeployments.includes(application.buildPack)} -
- - -
-
- - -
-
- - -
- {/if} - {#if application.buildPack === 'deno'} -
- - -
-
- - -
- {/if} - {#if application.buildPack !== 'laravel'} -
-
-
- {/if} - {#if application.buildPack === 'docker'} + {/if}
-
- {/if} - {#if !notNodeDeployments.includes(application.buildPack)} -
-
- -
-
- {/if} -
+ {#if !notNodeDeployments.includes(application.buildPack)} +
+ + +
+
+ + +
+
+ + +
+ {/if} + {#if application.buildPack === 'deno'} +
+ + +
+
+ + +
+ {/if} + {#if application.buildPack !== 'laravel'} +
+
+ +
+ +
+ {/if} + {#if application.buildPack === 'docker'} +
+ +
+ + {#if application.baseDirectory} + + {/if} +
+
+ {/if} + {#if !notNodeDeployments.includes(application.buildPack)} +
+
+ +
+ + +
+ {/if} +
+ {:else} +
+ Stack + {#if $appSession.isAdmin} + + {/if} +
+
+ {#each dockerComposeServices as service} +
+
+ {service.name} + {statues[service.name] || 'Loading...'} +
+
{application.id}-{service.name}
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ {/each} +
+ {/if}
diff --git a/apps/ui/src/routes/applications/[id]/logs/index.svelte b/apps/ui/src/routes/applications/[id]/logs/index.svelte index 4ec6ab28d..2de662d12 100644 --- a/apps/ui/src/routes/applications/[id]/logs/index.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/index.svelte @@ -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); + }
@@ -96,50 +115,69 @@
Application Logs
-
- {#if logs.length === 0} -
{$t('application.build.waiting_logs')}
- {:else} -
-
- - {#if loadLogsInterval} -
-
- {#each logs as log} -

{log + '\n'}

- {/each} -
-
- {/if} +
+ {#each services as service} + + {/each}
+ +{#if selectedService} +
+ {#if logs.length === 0} + {#if noContainer} +
Container not found / exited.
+ {/if} + {:else} +
+
+ + {#if loadLogsInterval} +
+
+ {#each logs as log} +

{log + '\n'}

+ {/each} +
+
+ {/if} +
+{/if} diff --git a/apps/ui/src/routes/applications/[id]/usage.svelte b/apps/ui/src/routes/applications/[id]/usage.svelte index f9919ecee..395fd0bf1 100644 --- a/apps/ui/src/routes/applications/[id]/usage.svelte +++ b/apps/ui/src/routes/applications/[id]/usage.svelte @@ -1,11 +1,14 @@ @@ -38,21 +76,44 @@
Monitoring
-
-
-
-
Used Memory / Memory Limit
-
{usage?.MemUsage}
-
+
+ {#each services as service} + + {/each} +
+{#if selectedService} +
+ {#if usageLoading} +
+{/if} diff --git a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte index b3601a9e9..02c98e150 100644 --- a/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/apps/ui/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -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 ''; + } 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; } } @@ -115,9 +140,9 @@ {$t('forms.save')} {/if}
@@ -175,7 +200,7 @@ readonly disabled name="publicPort" - value={publicLoading + value={loading.public ? 'Loading...' : $status.database.isPublic ? database.publicPort @@ -198,8 +223,8 @@ {/if}
-
-
- + {#if publicUrl} + + {/if}
@@ -228,7 +256,7 @@
changeSettings('isPublic')} title={$t('database.set_public')} @@ -238,7 +266,7 @@ {#if database.type === 'redis'} changeSettings('appendOnly')} title={$t('database.change_append_only_mode')} diff --git a/apps/ui/src/routes/index.svelte b/apps/ui/src/routes/index.svelte index 59c70bdd2..1cd83bfe0 100644 --- a/apps/ui/src/routes/index.svelte +++ b/apps/ui/src/routes/index.svelte @@ -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 @@ {:else if status[application.id] === 'running'} + {:else if status[application.id] === 'degraded'} + Degraded {:else} {/if} @@ -613,7 +643,7 @@
{#if application?.fqdn}

{application?.fqdn.replace('https://', '').replace('http://', '')}

- {:else if !application.settings?.isBot && !application?.fqdn} + {:else if (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== 'compose'}

Not configured

{/if} {#if application.destinationDocker?.name} diff --git a/apps/ui/src/routes/services/[id]/configuration/type.svelte b/apps/ui/src/routes/services/[id]/configuration/type.svelte index bd4bbd74e..dcc6aee03 100644 --- a/apps/ui/src/routes/services/[id]/configuration/type.svelte +++ b/apps/ui/src/routes/services/[id]/configuration/type.svelte @@ -27,10 +27,12 @@ -
- {#each types as type} +
+
+
+ + + + + +
+ doSearch()} + /> +
+
+
+
+ {#each filteredTypes as type}
handleSubmit(type.name)}>
{/each}
+
diff --git a/apps/ui/static/docker-compose.png b/apps/ui/static/docker-compose.png new file mode 100644 index 000000000..b9cfb40e1 Binary files /dev/null and b/apps/ui/static/docker-compose.png differ diff --git a/apps/ui/tailwind.config.cjs b/apps/ui/tailwind.config.cjs index 082e6192e..6fdba38f5 100644 --- a/apps/ui/tailwind.config.cjs +++ b/apps/ui/tailwind.config.cjs @@ -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", diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index e1934322c..c3c6d8c54 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -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: diff --git a/package.json b/package.json index 6f580d533..6b17b3b66 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c7fb8aea..bac8b5276 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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==}