diff --git a/package.json b/package.json index 148c4b441..a5bb1cae1 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.0.32", + "version": "2.1.0", "license": "AGPL-3.0", "scripts": { - "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", + "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev", "dev:stop": "docker-compose -f docker-compose-dev.yaml down", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "studio": "npx prisma studio", @@ -30,6 +30,7 @@ "@sveltejs/kit": "1.0.0-next.288", "@types/bcrypt": "5.0.0", "@types/js-cookie": "3.0.1", + "@types/js-yaml": "^4.0.5", "@types/node": "17.0.21", "@types/node-forge": "1.0.0", "@typescript-eslint/eslint-plugin": "4.31.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0382aae5a..f9f51fc15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,7 @@ specifiers: '@sveltejs/kit': 1.0.0-next.288 '@types/bcrypt': 5.0.0 '@types/js-cookie': 3.0.1 + '@types/js-yaml': ^4.0.5 '@types/node': 17.0.21 '@types/node-forge': 1.0.0 '@typescript-eslint/eslint-plugin': 4.31.1 @@ -84,6 +85,7 @@ devDependencies: '@sveltejs/kit': 1.0.0-next.288_svelte@3.46.4 '@types/bcrypt': 5.0.0 '@types/js-cookie': 3.0.1 + '@types/js-yaml': 4.0.5 '@types/node': 17.0.21 '@types/node-forge': 1.0.0 '@typescript-eslint/eslint-plugin': 4.31.1_386b67ad67ef29c6a0ccaf3e9b60f945 @@ -537,6 +539,13 @@ packages: } dev: true + /@types/js-yaml/4.0.5: + resolution: + { + integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA== + } + dev: true + /@types/json-schema/7.0.9: resolution: { diff --git a/prisma/migrations/20220322135800_persistent_storage/migration.sql b/prisma/migrations/20220322135800_persistent_storage/migration.sql new file mode 100644 index 000000000..d26ae3f8b --- /dev/null +++ b/prisma/migrations/20220322135800_persistent_storage/migration.sql @@ -0,0 +1,18 @@ +-- CreateTable +CREATE TABLE "ApplicationPersistentStorage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "path" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationPersistentStorage_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_key" ON "ApplicationPersistentStorage"("applicationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApplicationPersistentStorage_path_key" ON "ApplicationPersistentStorage"("path"); + +-- CreateIndex +CREATE UNIQUE INDEX "ApplicationPersistentStorage_applicationId_path_key" ON "ApplicationPersistentStorage"("applicationId", "path"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2a9a43c2b..95310d2aa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -72,9 +72,9 @@ model TeamInvitation { } model Application { - id String @id @default(cuid()) + id String @id @default(cuid()) name String - fqdn String? @unique + fqdn String? @unique repository String? configHash String? branch String? @@ -86,16 +86,17 @@ model Application { startCommand String? baseDirectory String? publishDirectory String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + phpModules String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt settings ApplicationSettings? teams Team[] destinationDockerId String? - destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) + destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id]) gitSourceId String? - gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) + gitSource GitSource? @relation(fields: [gitSourceId], references: [id]) secrets Secret[] - phpModules String? + persistentStorage ApplicationPersistentStorage[] } model ApplicationSettings { @@ -110,6 +111,17 @@ model ApplicationSettings { updatedAt DateTime @updatedAt } +model ApplicationPersistentStorage { + id String @id @default(cuid()) + application Application @relation(fields: [applicationId], references: [id]) + applicationId String @unique + path String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([applicationId, path]) +} + model Secret { id String @id @default(cuid()) name String diff --git a/src/app.d.ts b/src/app.d.ts index c3921e0f2..40f503598 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -7,7 +7,10 @@ declare namespace App { } interface Platform {} interface Session extends SessionData {} - interface Stuff {} + interface Stuff { + application: any; + isRunning: boolean; + } } interface SessionData { diff --git a/src/lib/buildPacks/common.ts b/src/lib/buildPacks/common.ts index 1d2fef25f..3f8b4b43e 100644 --- a/src/lib/buildPacks/common.ts +++ b/src/lib/buildPacks/common.ts @@ -29,10 +29,10 @@ export function makeLabelForStandaloneApplication({ fqdn = `${protocol}://${pullmergeRequestId}.${domain}`; } return [ - '--label coolify.managed=true', - `--label coolify.version=${version}`, - `--label coolify.type=standalone-application`, - `--label coolify.configuration=${base64Encode( + 'coolify.managed=true', + `coolify.version=${version}`, + `coolify.type=standalone-application`, + `coolify.configuration=${base64Encode( JSON.stringify({ applicationId, fqdn, @@ -135,6 +135,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap RewriteRule ^(.+)$ index.php [QSA,L] ` ); + await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`); saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); } else if (staticDeployments.includes(buildPack)) { await fs.writeFile( @@ -142,27 +143,35 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap `user nginx; worker_processes auto; - error_log /var/log/nginx/error.log warn; - pid /var/run/nginx.pid; + error_log /docker.stdout; + pid /run/nginx.pid; events { worker_connections 1024; } http { - include /etc/nginx/mime.types; - - access_log off; - sendfile on; - #tcp_nopush on; - keepalive_timeout 65; + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /docker.stdout main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; server { listen 80; server_name localhost; location / { - root /usr/share/nginx/html; + root /app; index index.html; try_files $uri $uri/index.html $uri/ /index.html =404; } @@ -173,7 +182,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap # error_page 500 502 503 504 /50x.html; location = /50x.html { - root /usr/share/nginx/html; + root /app; } } diff --git a/src/lib/buildPacks/gatsby.ts b/src/lib/buildPacks/gatsby.ts index 3e9c34a2d..38989626d 100644 --- a/src/lib/buildPacks/gatsby.ts +++ b/src/lib/buildPacks/gatsby.ts @@ -6,18 +6,17 @@ const createDockerfile = async (data, imageforBuild): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageforBuild}`); - Dockerfile.push('WORKDIR /usr/share/nginx/html'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); - Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'nginx:stable-alpine'; + const image = 'webdevops/nginx:alpine'; const imageForBuild = 'node:lts'; await buildCacheImageWithNode(data, imageForBuild); diff --git a/src/lib/buildPacks/nestjs.ts b/src/lib/buildPacks/nestjs.ts index b30c5ecd9..915bdd3d7 100644 --- a/src/lib/buildPacks/nestjs.ts +++ b/src/lib/buildPacks/nestjs.ts @@ -7,15 +7,13 @@ const createDockerfile = async (data, image): Promise => { const isPnpm = startCommand.includes('pnpm'); Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (isPnpm) { Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); Dockerfile.push('RUN pnpm add -g pnpm'); } - Dockerfile.push( - `COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./` - ); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`); Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`CMD ${startCommand}`); diff --git a/src/lib/buildPacks/nextjs.ts b/src/lib/buildPacks/nextjs.ts index c8d6a03bc..4f1f3b0d6 100644 --- a/src/lib/buildPacks/nextjs.ts +++ b/src/lib/buildPacks/nextjs.ts @@ -16,7 +16,7 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (secrets.length > 0) { secrets.forEach((secret) => { diff --git a/src/lib/buildPacks/node.ts b/src/lib/buildPacks/node.ts index cb99ecec8..54527a728 100644 --- a/src/lib/buildPacks/node.ts +++ b/src/lib/buildPacks/node.ts @@ -17,7 +17,7 @@ const createDockerfile = async (data, image): Promise => { const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (secrets.length > 0) { secrets.forEach((secret) => { diff --git a/src/lib/buildPacks/nuxtjs.ts b/src/lib/buildPacks/nuxtjs.ts index ca7c1809c..207afe864 100644 --- a/src/lib/buildPacks/nuxtjs.ts +++ b/src/lib/buildPacks/nuxtjs.ts @@ -16,7 +16,7 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (secrets.length > 0) { secrets.forEach((secret) => { diff --git a/src/lib/buildPacks/php.ts b/src/lib/buildPacks/php.ts index 4903213c6..abec33b3d 100644 --- a/src/lib/buildPacks/php.ts +++ b/src/lib/buildPacks/php.ts @@ -6,26 +6,17 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); Dockerfile.push(`LABEL coolify.image=true`); - if (data.phpModules?.length > 0) { - Dockerfile.push( - `ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/` - ); - Dockerfile.push(`RUN chmod +x /usr/local/bin/install-php-extensions`); - Dockerfile.push(`RUN /usr/local/bin/install-php-extensions ${data.phpModules.join(' ')}`); - } - Dockerfile.push('RUN a2enmod rewrite'); - Dockerfile.push('WORKDIR /var/www/html'); - Dockerfile.push(`COPY .${baseDirectory || ''} /var/www/html`); - Dockerfile.push(`COPY /.htaccess /var/www/html/.htaccess`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`COPY .${baseDirectory || ''} /app`); + Dockerfile.push(`COPY /.htaccess .`); + Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["apache2-foreground"]'); - Dockerfile.push('RUN chown -R www-data /var/www/html'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'php:apache'; + const image = 'webdevops/php-nginx'; await createDockerfile(data, image); await buildImage(data); } catch (error) { diff --git a/src/lib/buildPacks/react.ts b/src/lib/buildPacks/react.ts index 29c462d02..719f782eb 100644 --- a/src/lib/buildPacks/react.ts +++ b/src/lib/buildPacks/react.ts @@ -7,17 +7,16 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push(`FROM ${image}`); Dockerfile.push(`LABEL coolify.image=true`); - Dockerfile.push('WORKDIR /usr/share/nginx/html'); - Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'nginx:stable-alpine'; + const image = 'webdevops/nginx:alpine'; const imageForBuild = 'node:lts'; await buildCacheImageWithNode(data, imageForBuild); await createDockerfile(data, image); diff --git a/src/lib/buildPacks/rust.ts b/src/lib/buildPacks/rust.ts index 49eb59815..586d63140 100644 --- a/src/lib/buildPacks/rust.ts +++ b/src/lib/buildPacks/rust.ts @@ -7,23 +7,21 @@ const createDockerfile = async (data, image, name): Promise => { const { workdir, port, applicationId, tag } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); - Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/target target`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`); Dockerfile.push(`COPY . .`); Dockerfile.push(`RUN cargo build --release --bin ${name}`); Dockerfile.push('FROM debian:buster-slim'); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push( `RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*` ); Dockerfile.push(`RUN update-ca-certificates`); - Dockerfile.push( - `COPY --from=${applicationId}:${tag}-cache /usr/src/app/target/release/${name} ${name}` - ); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`); Dockerfile.push(`EXPOSE ${port}`); - Dockerfile.push(`CMD ["/usr/src/app/${name}"]`); + Dockerfile.push(`CMD ["/app/${name}"]`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/static.ts b/src/lib/buildPacks/static.ts index 075fad0aa..17900ced3 100644 --- a/src/lib/buildPacks/static.ts +++ b/src/lib/buildPacks/static.ts @@ -15,7 +15,7 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/share/nginx/html'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (secrets.length > 0) { secrets.forEach((secret) => { @@ -33,21 +33,18 @@ const createDockerfile = async (data, image): Promise => { }); } if (buildCommand) { - Dockerfile.push( - `COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./` - ); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); } else { Dockerfile.push(`COPY .${baseDirectory || ''} ./`); } Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'nginx:stable-alpine'; + const image = 'webdevops/nginx:alpine'; const imageForBuild = 'node:lts'; if (data.buildCommand) await buildCacheImageWithNode(data, imageForBuild); await createDockerfile(data, image); diff --git a/src/lib/buildPacks/svelte.ts b/src/lib/buildPacks/svelte.ts index e6821dafe..fbcf36abb 100644 --- a/src/lib/buildPacks/svelte.ts +++ b/src/lib/buildPacks/svelte.ts @@ -6,18 +6,17 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/share/nginx/html'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); - Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'nginx:stable-alpine'; + const image = 'webdevops/nginx:alpine'; const imageForBuild = 'node:lts'; await buildCacheImageWithNode(data, imageForBuild); diff --git a/src/lib/buildPacks/vuejs.ts b/src/lib/buildPacks/vuejs.ts index 4fe90038b..fa80ac435 100644 --- a/src/lib/buildPacks/vuejs.ts +++ b/src/lib/buildPacks/vuejs.ts @@ -6,35 +6,19 @@ const createDockerfile = async (data, image): Promise => { const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); - Dockerfile.push('WORKDIR /usr/share/nginx/html'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); - Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`EXPOSE 80`); - Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]'); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; export default async function (data) { try { - const image = 'nginx:stable-alpine'; + const image = 'webdevops/nginx:alpine'; const imageForBuild = 'node:lts'; await buildCacheImageWithNode(data, imageForBuild); - // await fs.writeFile(`${data.workdir}/default.conf`, `server { - // listen 80; - // server_name localhost; - - // location / { - // root /usr/share/nginx/html; - // try_files $uri $uri/ /index.html; - // } - - // error_page 500 502 503 504 /50x.html; - // location = /50x.html { - // root /usr/share/nginx/html; - // } - // } - // `); await createDockerfile(data, image); await buildImage(data); } catch (error) { diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index 687819c6d..890338597 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -9,7 +9,16 @@ export const dateOptions: DateTimeFormatOptions = { hour12: false }; -export const staticDeployments = ['react', 'vuejs', 'static', 'svelte', 'gatsby', 'php']; +export const staticDeployments = [ + 'react', + 'vuejs', + 'static', + 'svelte', + 'gatsby', + 'php', + 'astro', + 'eleventy' +]; export const notNodeDeployments = ['php', 'docker', 'rust']; export function getDomain(domain) { diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index d5285e79a..317793e9a 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -66,6 +66,7 @@ export async function removeApplication({ id, teamId }) { await prisma.buildLog.deleteMany({ where: { applicationId: id } }); await prisma.build.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } }); + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); } @@ -134,7 +135,8 @@ export async function getApplication({ id, teamId }) { destinationDocker: true, settings: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, - secrets: true + secrets: true, + persistentStorage: true } }); @@ -156,9 +158,6 @@ export async function getApplication({ id, teamId }) { return s; }); } - if (body?.phpModules) { - body.phpModules = body.phpModules.split(','); - } return { ...body }; } @@ -214,8 +213,7 @@ export async function configureApplication({ buildCommand, startCommand, baseDirectory, - publishDirectory, - phpModules + publishDirectory }) { return await prisma.application.update({ where: { id }, @@ -228,8 +226,7 @@ export async function configureApplication({ startCommand, baseDirectory, publishDirectory, - name, - phpModules + name } }); } @@ -268,3 +265,7 @@ export async function createBuild({ } }); } + +export async function getPersistentStorage(id) { + return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } }); +} diff --git a/src/lib/docker.ts b/src/lib/docker.ts index ccbc21b2a..717303040 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -20,7 +20,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) { const isPnpm = checkPnpm(installCommand, buildCommand); const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.image=true`); if (secrets.length > 0) { secrets.forEach((secret) => { @@ -65,14 +65,14 @@ export async function buildCacheImageWithCargo(data, imageForBuild) { } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('COPY . .'); Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json'); Dockerfile.push(`FROM ${imageForBuild}`); - Dockerfile.push('WORKDIR /usr/src/app'); + Dockerfile.push('WORKDIR /app'); Dockerfile.push('RUN cargo install cargo-chef'); - Dockerfile.push(`COPY --from=planner-${applicationId} /usr/src/app/recipe.json recipe.json`); + Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug }); diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index ff189845f..e85f539de 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -103,34 +103,38 @@ export async function generateSSLCerts() { orderBy: { createdAt: 'desc' } }); for (const application of applications) { - const { - fqdn, - id, - destinationDocker: { engine, network }, - settings: { previews } - } = application; - const isRunning = await checkContainer(engine, id); - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - if (isRunning) { - if (isHttps) ssls.push({ domain, id, isCoolify: false }); - } - if (previews) { - const host = getEngine(engine); - const { stdout } = await asyncExecShell( - `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` - ); - const containers = stdout - .trim() - .split('\n') - .filter((a) => a) - .map((c) => c.replace(/"/g, '')); - if (containers.length > 0) { - for (const container of containers) { - let previewDomain = `${container.split('-')[1]}.${domain}`; - if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); + try { + const { + fqdn, + id, + destinationDocker: { engine, network }, + settings: { previews } + } = application; + const isRunning = await checkContainer(engine, id); + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + if (isRunning) { + if (isHttps) ssls.push({ domain, id, isCoolify: false }); + } + if (previews) { + const host = getEngine(engine); + const { stdout } = await asyncExecShell( + `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` + ); + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + let previewDomain = `${container.split('-')[1]}.${domain}`; + if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); + } } } + } catch (error) { + console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`); } } const services = await db.prisma.service.findMany({ @@ -145,20 +149,24 @@ export async function generateSSLCerts() { }); for (const service of services) { - const { - fqdn, - id, - type, - destinationDocker: { engine } - } = service; - const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); - if (found) { - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - const isRunning = await checkContainer(engine, id); - if (isRunning) { - if (isHttps) ssls.push({ domain, id, isCoolify: false }); + try { + const { + fqdn, + id, + type, + destinationDocker: { engine } + } = service; + const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); + if (found) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isRunning = await checkContainer(engine, id); + if (isRunning) { + if (isHttps) ssls.push({ domain, id, isCoolify: false }); + } } + } catch (error) { + console.log(`Error during generateSSLCerts with ${service.fqdn}: ${error}`); } } const { fqdn } = await db.prisma.setting.findFirst(); diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index 0152cc9f2..1cb3d2a6a 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -19,6 +19,7 @@ import { makeLabelForStandaloneApplication, setDefaultConfiguration } from '$lib/buildPacks/common'; +import yaml from 'js-yaml'; export default async function (job) { /* @@ -49,7 +50,8 @@ export default async function (job) { type, pullmergeRequestId = null, sourceBranch = null, - settings + settings, + persistentStorage } = job.data; const { debug } = settings; @@ -66,8 +68,12 @@ export default async function (job) { }); let imageId = applicationId; let domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - + let volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ + buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; // Previews, we need to get the source branch and set subdomain if (pullmergeRequestId) { branch = sourceBranch; @@ -255,14 +261,53 @@ export default async function (job) { } try { saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); - const { stderr } = await asyncExecShell( - `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join( - ' ' - )} --name ${imageId} --network ${ - docker.network - } --restart always -d ${applicationId}:${tag}` + // for await (const volume of volumes) { + // const id = volume.split(':')[0]; + // try { + // await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`); + // } catch (error) { + // await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`); + // } + // } + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const compose = { + version: '3.8', + services: { + [imageId]: { + image: `${applicationId}:${tag}`, + container_name: imageId, + volumes, + env_file: envFound ? [`${workdir}/.env`] : [], + networks: [docker.network], + labels: labels, + depends_on: [], + restart: 'always' + } + }, + networks: { + [docker.network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` ); - if (stderr) console.log(stderr); + + // const { stderr } = await asyncExecShell( + // `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join( + // ' ' + // )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : '' + // } -d ${applicationId}:${tag}` + // ); saveBuildLog({ line: 'Deployment successful!', buildId, applicationId }); } catch (error) { saveBuildLog({ line: error, buildId, applicationId }); diff --git a/src/lib/queues/ssl.ts b/src/lib/queues/ssl.ts index d14fc0822..f041153d6 100644 --- a/src/lib/queues/ssl.ts +++ b/src/lib/queues/ssl.ts @@ -4,6 +4,7 @@ export default async function () { try { return await generateSSLCerts(); } catch (error) { + console.log(error); throw error; } } diff --git a/src/routes/applications/[id]/__layout.svelte b/src/routes/applications/[id]/__layout.svelte index fb944519b..c41f33282 100644 --- a/src/routes/applications/[id]/__layout.svelte +++ b/src/routes/applications/[id]/__layout.svelte @@ -271,6 +271,35 @@ + + { if (status === 401) return { status, body }; const { id } = event.params; + const { pullmergeRequestId = null, branch } = await event.request.json(); try { const buildId = cuid(); const applicationFound = await db.getApplication({ id, teamId }); @@ -42,7 +43,17 @@ export const post: RequestHandler = async (event) => { type: 'manual' } }); - await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); + if (pullmergeRequestId) { + await buildQueue.add(buildId, { + build_id: buildId, + type: 'manual', + ...applicationFound, + sourceBranch: branch, + pullmergeRequestId + }); + } else { + await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound }); + } return { status: 200, body: { diff --git a/src/routes/applications/[id]/index.json.ts b/src/routes/applications/[id]/index.json.ts index 47fc9e2ae..3b4405706 100644 --- a/src/routes/applications/[id]/index.json.ts +++ b/src/routes/applications/[id]/index.json.ts @@ -52,8 +52,7 @@ export const post: RequestHandler = async (event) => { buildCommand, startCommand, baseDirectory, - publishDirectory, - phpModules + publishDirectory } = await event.request.json(); if (port) port = Number(port); @@ -69,8 +68,7 @@ export const post: RequestHandler = async (event) => { buildCommand, startCommand, baseDirectory, - publishDirectory, - phpModules + publishDirectory }); return { status: 201 }; } catch (error) { diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index 221aefddc..49f800975 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -47,125 +47,7 @@ import { post } from '$lib/api'; import cuid from 'cuid'; import { browser } from '$app/env'; - import Select from 'svelte-select'; const { id } = $page.params; - let collection = [ - 'amqp', - 'apcu', - 'apcu_bc', - 'ast', - 'bcmath', - 'blackfire', - 'bz2', - 'calendar', - 'cmark', - 'csv', - 'dba', - 'decimal', - 'ds', - 'enchant', - 'ev', - 'event', - 'excimer', - 'exif', - 'ffi', - 'gd', - 'gearman', - 'geoip', - 'geospatial', - 'gettext', - 'gmagick', - 'gmp', - 'gnupg', - 'grpc', - 'http', - 'igbinary', - 'imagick', - 'imap', - 'inotify', - 'interbase', - 'intl', - 'ioncube_loader', - 'jsmin', - 'json_post', - 'ldap', - 'lzf', - 'mailparse', - 'maxminddb', - 'mcrypt', - 'memcache', - 'memcached', - 'mongo', - 'mongodb', - 'mosquitto', - 'msgpack', - 'mssql', - 'mysqli', - 'oauth', - 'oci8', - 'odbc', - 'opcache', - 'opencensus', - 'openswoole', - 'parallel', - 'pcntl', - 'pcov', - 'pdo_dblib', - 'pdo_firebird', - 'pdo_mysql', - 'pdo_oci', - 'pdo_odbc', - 'pdo_pgsql', - 'pdo_sqlsrv', - 'pgsql', - 'propro', - 'protobuf', - 'pspell', - 'pthreads', - 'raphf', - 'rdkafka', - 'recode', - 'redis', - 'seaslog', - 'shmop', - 'smbclient', - 'snmp', - 'snuffleupagus', - 'soap', - 'sockets', - 'solr', - 'sourceguardian', - 'spx', - 'sqlsrv', - 'ssh2', - 'stomp', - 'swoole', - 'sybase_ct', - 'sysvmsg', - 'sysvsem', - 'sysvshm', - 'tensor', - 'tidy', - 'timezonedb', - 'uopz', - 'uploadprogress', - 'uuid', - 'vips', - 'wddx', - 'xdebug', - 'xhprof', - 'xlswriter', - 'xmldiff', - 'xmlrpc', - 'xsl', - 'yac', - 'yaml', - 'yar', - 'zephir_parser', - 'zip', - 'zookeeper', - 'zstd' - ]; let domainEl: HTMLInputElement; @@ -225,9 +107,8 @@ async function handleSubmit() { loading = true; try { - const tempPhpModules = application.phpModules?.map((module) => module.value).toString() || ''; await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); - await post(`/applications/${id}.json`, { ...application, phpModules: tempPhpModules }); + await post(`/applications/${id}.json`, { ...application }); return window.location.reload(); } catch ({ error }) { if (error.startsWith('DNS not set')) { @@ -481,19 +362,6 @@ /> {/if} - {#if application.buildPack === 'php'} -
- -
- + + + {#if isNew} +
+ +
+ {:else} +
+
+ +
+
+ +
+
+ {/if} + diff --git a/src/routes/applications/[id]/storage/index.json.ts b/src/routes/applications/[id]/storage/index.json.ts new file mode 100644 index 000000000..c1581fd70 --- /dev/null +++ b/src/routes/applications/[id]/storage/index.json.ts @@ -0,0 +1,63 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { status, body, teamId } = await getUserDetails(event, false); + if (status === 401) return { status, body }; + + const { id } = event.params; + try { + const persistentStorages = await db.getPersistentStorage(id); + return { + body: { + persistentStorages + } + }; + } catch (error) { + return ErrorHandler(error); + } +}; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const { path, newStorage, storageId } = await event.request.json(); + try { + if (newStorage) { + await db.prisma.applicationPersistentStorage.create({ + data: { path, application: { connect: { id } } } + }); + } else { + await db.prisma.applicationPersistentStorage.update({ + where: { id: storageId }, + data: { path } + }); + } + return { + status: 201 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + +export const del: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + const { path } = await event.request.json(); + + try { + await db.prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } }); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/applications/[id]/storage/index.svelte b/src/routes/applications/[id]/storage/index.svelte new file mode 100644 index 000000000..75b129d68 --- /dev/null +++ b/src/routes/applications/[id]/storage/index.svelte @@ -0,0 +1,73 @@ + + + + +
+ +
+
+ This is useful for storing data such as a database (SQLite) or a cache.'} + /> +
+ + + + + + + + {#each persistentStorages as storage} + {#key storage.id} + + + + {/key} + {/each} + + + + +
Path
+