Compare commits

...

38 Commits

Author SHA1 Message Date
Andras Bacsai
470ff49a02 Merge pull request #981 from coollabsio/next
v3.12.26
2023-03-07 12:19:36 +01:00
Andras Bacsai
04d741581d Merge pull request #980 from hyenabyte/main
Fixing multiple remotes breaking the server overview
2023-03-07 12:12:59 +01:00
Andras Bacsai
038f210148 Merge branch 'main' into next 2023-03-07 11:47:29 +01:00
Andras Bacsai
2adad3a7bd fix: handle log format volumes 2023-03-07 11:46:23 +01:00
Andras Bacsai
05fb26a49b remove console logs 2023-03-07 11:15:43 +01:00
Andras Bacsai
1c237affb4 feat: add host path to any container 2023-03-07 11:15:05 +01:00
Andras Bacsai
3e81d7e9cb fix: replace . & .. & $PWD with ~ 2023-03-07 10:44:53 +01:00
David Koch Gregersen
edb66620c1 Adding a check when reading ssh config file
Also adds comments to the createRemoteEngineConfiguration function
2023-03-07 10:43:34 +01:00
Andras Bacsai
04f7e8e777 fix: host volumes 2023-03-07 10:31:10 +01:00
David Koch Gregersen
eee201013c Fixing multiple remotes breaking the server overview 2023-03-06 22:31:01 +01:00
Andras Bacsai
1190cb4ea1 Update deployApplication.ts 2023-03-04 18:49:34 +01:00
Andras Bacsai
507100ea0b Update package.json 2023-03-04 18:48:38 +01:00
Andras Bacsai
9b13912b6d Update common.ts 2023-03-04 18:48:28 +01:00
Andras Bacsai
ee65deebfd fix: nestjs buildpack 2023-03-04 18:05:01 +01:00
Andras Bacsai
ba9fa442d1 Merge pull request #973 from coollabsio/next
v3.12.23
2023-03-04 15:11:42 +01:00
Andras Bacsai
87da27f9bf fix: publishDirectory 2023-03-04 15:06:35 +01:00
Andras Bacsai
b5bc5fe2c6 Merge pull request #965 from coollabsio/next
v3.12.22
2023-03-03 09:13:56 +01:00
Andras Bacsai
d2329360d0 Merge pull request #950 from hemangjoshi37a/main
added `star-history`
2023-03-02 17:33:22 +01:00
Andras Bacsai
7ece0ae10a Merge pull request #955 from eltociear/patch-1
fix typo in _GitlabRepositories.svelte
2023-03-02 17:32:26 +01:00
Andras Bacsai
f931b47eb8 Merge pull request #957 from addianto/fix/pack
Fix PACK_VERSION build argument in Dockerfile
2023-03-02 17:31:55 +01:00
Andras Bacsai
7f7eb12ded fix: empty port in docker compose 2023-03-02 17:22:49 +01:00
Andras Bacsai
c0940f7a19 fix: cannot delete resource when you are not on root team 2023-03-02 17:12:29 +01:00
Andras Bacsai
9dfde11e35 possible fix: vaultwarden 2023-03-02 16:56:44 +01:00
Andras Bacsai
6f15cc2dbc fix: base directory not found 2023-03-02 16:52:55 +01:00
Daya Adianto
120308638f fix: set PACK_VERSION to 0.27.0
This commit removes the `v` prefix in the version identifier assigned to
PACK_VERSION build argument. The `pack` script actually available on
Coolify's CDN, but named without `v` prefix in the script's version identifier.

Related issue: #689, which reported that the `pack` script in the
container image is a HTML 404 file instead of the actual `pack`
executable.
2023-02-25 17:27:41 +07:00
Ikko Eltociear Ashimine
1d04ef99bb fix typo in _GitlabRepositories.svelte
occured -> occurred
2023-02-24 11:24:55 +09:00
Hemang Joshi
9b00d177ef added star-history
added `star-history`
2023-02-22 16:21:32 +05:30
Andras Bacsai
884524c448 Merge pull request #945 from coollabsio/next
v3.12.21
2023-02-21 13:55:29 +01:00
Andras Bacsai
3ae1e7e87d remove debug 2023-02-21 13:47:28 +01:00
Andras Bacsai
81f885311d debug 2023-02-21 13:24:23 +01:00
Andras Bacsai
d9362f09d8 debug 2023-02-21 13:23:34 +01:00
Andras Bacsai
906d181d1b debug 2023-02-21 13:15:17 +01:00
Andras Bacsai
44b8812a7b debug 2023-02-21 13:08:14 +01:00
Andras Bacsai
3308c45e88 Merge pull request #943 from coollabsio/next
v3.12.21
2023-02-21 13:02:39 +01:00
Andras Bacsai
e530ecf9f9 fix 2023-02-21 12:59:21 +01:00
Andras Bacsai
51b5edb04f hmm fix 2023-02-21 12:48:06 +01:00
Andras Bacsai
f0d89f850e fix 2023-02-21 12:45:22 +01:00
Andras Bacsai
b777e08542 fix: arm servics 2023-02-21 12:35:20 +01:00
26 changed files with 259 additions and 133 deletions

View File

@@ -22,7 +22,7 @@ ARG DOCKER_VERSION=20.10.18
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. # 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 ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0 ARG PACK_VERSION=0.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 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 apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
@@ -50,4 +50,4 @@ RUN pnpm install -p
EXPOSE 3000 EXPOSE 3000
ENV CHECKPOINT_DISABLE=1 ENV CHECKPOINT_DISABLE=1
CMD pnpm start CMD pnpm start

View File

@@ -9,7 +9,7 @@ ARG DOCKER_VERSION=20.10.18
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. # 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 ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0 ARG PACK_VERSION=0.27.0
WORKDIR /app WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
@@ -28,4 +28,4 @@ RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
EXPOSE 3000 EXPOSE 3000
ENV CHECKPOINT_DISABLE=1 ENV CHECKPOINT_DISABLE=1

View File

@@ -153,3 +153,6 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a> <a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;

View File

@@ -195,6 +195,7 @@ model ApplicationSettings {
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
applicationId String applicationId String
hostPath String?
path String path String
oldPath Boolean @default(false) oldPath Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@@ -110,6 +110,9 @@ import * as buildpacks from '../lib/buildPacks';
.replace(/\//gi, '-') .replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`; .replace('-app', '')}:${storage.path}`;
} }
if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}`
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
@@ -160,7 +163,11 @@ import * as buildpacks from '../lib/buildPacks';
port: exposePort ? `${exposePort}:${port}` : port port: exposePort ? `${exposePort}:${port}` : port
}); });
try { try {
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes.filter(v => {
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
return v;
}
}).map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
name: volume.split(':')[0] name: volume.split(':')[0]
@@ -381,12 +388,15 @@ import * as buildpacks from '../lib/buildPacks';
.replace(/\//gi, '-') .replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`; .replace('-app', '')}:${storage.path}`;
} }
if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}`
}
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
try { try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration); dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
} catch (error) {} } catch (error) { }
let deployNeeded = true; let deployNeeded = true;
let destinationType; let destinationType;
@@ -406,7 +416,7 @@ import * as buildpacks from '../lib/buildPacks';
installCommand = configuration.installCommand; installCommand = configuration.installCommand;
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory || '';
baseDirectory = configuration.baseDirectory || ''; baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation; dockerFileLocation = configuration.dockerFileLocation;
dockerComposeFileLocation = configuration.dockerComposeFileLocation; dockerComposeFileLocation = configuration.dockerComposeFileLocation;
@@ -453,7 +463,7 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
await prisma.build.update({ where: { id: buildId }, data: { commit } }); await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {} } catch (err) { }
if (!pullmergeRequestId) { if (!pullmergeRequestId) {
if (configHash !== currentHash) { if (configHash !== currentHash) {
@@ -494,9 +504,8 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
await executeCommand({ await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker ${ command: `docker ${location ? `--config ${location}` : ''
location ? `--config ${location}` : '' } pull ${imageName}:${customTag}`
} pull ${imageName}:${customTag}`
}); });
imageFoundRemotely = true; imageFoundRemotely = true;
} catch (error) { } catch (error) {
@@ -659,9 +668,8 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${ command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId
pullmergeRequestId ? imageId : applicationId }' --format {{.ID}}`
}' --format {{.ID}}`
}); });
if (containers) { if (containers) {
const containerArray = containers.split('\n'); const containerArray = containers.split('\n');
@@ -693,7 +701,11 @@ import * as buildpacks from '../lib/buildPacks';
await saveDockerRegistryCredentials({ url, username, password, workdir }); await saveDockerRegistryCredentials({ url, username, password, workdir });
} }
try { try {
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes.filter(v => {
if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
return v;
}
}).map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
name: volume.split(':')[0] name: volume.split(':')[0]

View File

@@ -429,7 +429,12 @@ export const setDefaultConfiguration = async (data: any) => {
startCommand = template?.startCommand || 'yarn start'; startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel') if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null; buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) {
publishDirectory = template?.publishDirectory || null;
} else {
if (!publishDirectory.startsWith('/')) publishDirectory = `/${publishDirectory}`;
if (publishDirectory.endsWith('/')) publishDirectory = publishDirectory.slice(0, -1);
}
if (baseDirectory) { if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (baseDirectory.endsWith('/') && baseDirectory !== '/') if (baseDirectory.endsWith('/') && baseDirectory !== '/')
@@ -702,9 +707,8 @@ export async function buildImage({
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker ${location ? `--config ${location}` : ''} build ${ command: `docker ${location ? `--config ${location}` : ''} build ${forceRebuild ? '--no-cache' : ''
forceRebuild ? '--no-cache' : '' } --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
}); });
const { status } = await prisma.build.findUnique({ where: { id: buildId } }); const { status } = await prisma.build.findUnique({ where: { id: buildId } });

View File

@@ -36,12 +36,13 @@ export default async function (data) {
if (volumes.length > 0) { if (volumes.length > 0) {
for (const volume of volumes) { for (const volume of volumes) {
let [v, path] = volume.split(':'); let [v, path] = volume.split(':');
composeVolumes[v] = { if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
name: v composeVolumes[v] = {
}; name: v
};
}
} }
} }
let networks = {}; let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) { for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`; value['container_name'] = `${applicationId}-${key}`;
@@ -77,17 +78,54 @@ export default async function (data) {
// TODO: If we support separated volume for each service, we need to add it here // TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) { if (value['volumes']?.length > 0) {
value['volumes'] = value['volumes'].map((volume) => { value['volumes'] = value['volumes'].map((volume) => {
let [v, path, permission] = volume.split(':'); if (typeof volume === 'string') {
if (!path) { let [v, path, permission] = volume.split(':');
path = v; if (
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; v.startsWith('.') ||
} else { v.startsWith('..') ||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; v.startsWith('/') ||
v.startsWith('~') ||
v.startsWith('$PWD')
) {
v = v.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
} else {
if (!path) {
path = v;
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
composeVolumes[v] = {
name: v
};
}
return `${v}:${path}${permission ? ':' + permission : ''}`;
} }
composeVolumes[v] = { if (typeof volume === 'object') {
name: v let { source, target, mode } = volume;
}; if (
return `${v}:${path}${permission ? ':' + permission : ''}`; source.startsWith('.') ||
source.startsWith('..') ||
source.startsWith('/') ||
source.startsWith('~') ||
source.startsWith('$PWD')
) {
source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
console.log({source})
} else {
if (!target) {
target = source;
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
}
return `${source}:${target}${mode ? ':' + mode : ''}`;
}
}); });
} }
if (volumes.length > 0) { if (volumes.length > 0) {
@@ -95,7 +133,7 @@ export default async function (data) {
value['volumes'].push(volume); value['volumes'].push(volume);
} }
} }
if (dockerComposeConfiguration[key].port) { if (dockerComposeConfiguration[key]?.port) {
value['expose'] = [dockerComposeConfiguration[key].port]; value['expose'] = [dockerComposeConfiguration[key].port];
} }
if (value['networks']?.length > 0) { if (value['networks']?.length > 0) {

View File

@@ -8,7 +8,7 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }

View File

@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common'; import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data; const { buildId, applicationId, tag, port, startCommand, workdir, publishDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm'); const isPnpm = startCommand.includes('pnpm');
@@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (isPnpm) { if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);

View File

@@ -42,7 +42,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -42,7 +42,7 @@ const createDockerfile = async (data, image): Promise<void> => {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }

View File

@@ -31,7 +31,7 @@ const createDockerfile = async (data, image): Promise<void> => {
}); });
} }
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
} else { } else {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
} }

View File

@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }

View File

@@ -8,7 +8,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }

View File

@@ -11,7 +11,7 @@ import { promises as dns } from 'dns';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import os from 'os'; import os from 'os';
import sshConfig from 'ssh-config'; import * as SSHConfig from 'ssh-config/src/ssh-config';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { checkContainer, removeContainer } from './docker'; import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
@@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
export const version = '3.12.20'; export const version = '3.12.26';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
export const proxyPort = process.env.COOLIFY_PROXY_PORT; export const proxyPort = process.env.COOLIFY_PROXY_PORT;
export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT; export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
@@ -498,33 +498,56 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
return false; return false;
} }
/**
* Update the ssh config file with a host
*
* @param id Destination ID
* @returns
*/
export async function createRemoteEngineConfiguration(id: string) { export async function createRemoteEngineConfiguration(id: string) {
const homedir = os.homedir();
const sshKeyFile = `/tmp/id_rsa-${id}`; const sshKeyFile = `/tmp/id_rsa-${id}`;
const localPort = await getFreeSSHLocalPort(id); const localPort = await getFreeSSHLocalPort(id);
const { const {
sshKey: { privateKey }, sshKey: { privateKey },
network,
remoteIpAddress, remoteIpAddress,
remotePort, remotePort,
remoteUser remoteUser
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }); } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
// Write new keyfile
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }); await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
const config = sshConfig.parse('');
const Host = `${remoteIpAddress}-remote`; const Host = `${remoteIpAddress}-remote`;
// Removes previous ssh-keys
try { try {
await executeCommand({ command: `ssh-keygen -R ${Host}` }); await executeCommand({ command: `ssh-keygen -R ${Host}` });
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` }); await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` }); await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
} catch (error) { } } catch (error) {
//
}
const homedir = os.homedir();
let currentConfigFileContent = '';
try {
// Read the current config file
currentConfigFileContent = (await fs.readFile(`${homedir}/.ssh/config`)).toString();
} catch (error) {
// File doesn't exist, so we do nothing, a new one is going to be created
}
// Parse the config file
const config = SSHConfig.parse(currentConfigFileContent);
// Remove current config for the given host
const found = config.find({ Host }); const found = config.find({ Host });
const foundIp = config.find({ Host: remoteIpAddress }); const foundIp = config.find({ Host: remoteIpAddress });
if (found) config.remove({ Host }); if (found) config.remove({ Host });
if (foundIp) config.remove({ Host: remoteIpAddress }); if (foundIp) config.remove({ Host: remoteIpAddress });
// Create the new config
config.append({ config.append({
Host, Host,
Hostname: remoteIpAddress, Hostname: remoteIpAddress,
@@ -537,13 +560,17 @@ export async function createRemoteEngineConfiguration(id: string) {
ControlPersist: '10m' ControlPersist: '10m'
}); });
// Check if .ssh folder exists, and if not create one
try { try {
await fs.stat(`${homedir}/.ssh/`); await fs.stat(`${homedir}/.ssh/`);
} catch (error) { } catch (error) {
await fs.mkdir(`${homedir}/.ssh/`); await fs.mkdir(`${homedir}/.ssh/`);
} }
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
// Write the config
return await fs.writeFile(`${homedir}/.ssh/config`, SSHConfig.stringify(config));
} }
export async function executeCommand({ export async function executeCommand({
command, command,
dockerId = null, dockerId = null,
@@ -1048,7 +1075,7 @@ export function generateDatabaseConfiguration(database: any): DatabaseConfigurat
} }
export function isARM() { export function isARM() {
const arch = process.arch; const arch = process.arch;
if (arch === 'arm' || arch === 'arm64') { if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
return true; return true;
} }
return false; return false;
@@ -1633,6 +1660,9 @@ export function errorHandler({
type?: string | null; type?: string | null;
}) { }) {
if (message.message) message = message.message; if (message.message) message = message.message;
if (message.includes('Unique constraint failed')) {
message = 'This data is unique and already exists. Please try again with a different value.';
}
if (type === 'normal') { if (type === 'normal') {
Sentry.captureException(message); Sentry.captureException(message);
} }

View File

@@ -50,24 +50,12 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
const config = {}; const config = {};
for (const s in template.services) { for (const s in template.services) {
let newEnvironments = [] let newEnvironments = []
if (arm) { if (template.services[s]?.environment?.length > 0) {
if (template.services[s]?.environmentArm?.length > 0) { for (const environment of template.services[s].environment) {
for (const environment of template.services[s].environmentArm) { let [env, ...value] = environment.split("=");
let [env, ...value] = environment.split("="); value = value.join("=")
value = value.join("=") if (!value.startsWith('$$secret') && value !== '') {
if (!value.startsWith('$$secret') && value !== '') { newEnvironments.push(`${env}=${value}`)
newEnvironments.push(`${env}=${value}`)
}
}
}
} else {
if (template.services[s]?.environment?.length > 0) {
for (const environment of template.services[s].environment) {
let [env, ...value] = environment.split("=");
value = value.join("=")
if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`)
}
} }
} }
} }
@@ -87,12 +75,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
} }
const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } }) const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } })
let volumes = new Set() let volumes = new Set()
if (arm) { if (arm && template.services[s]?.volumesArm?.length > 0) {
template.services[s]?.volumesArm && template.services[s].volumesArm.length > 0 && template.services[s].volumesArm.forEach(v => volumes.add(v)) template.services[s].volumesArm.forEach(v => volumes.add(v))
} else { } else {
template.services[s]?.volumes && template.services[s].volumes.length > 0 && template.services[s].volumes.forEach(v => volumes.add(v)) if (template.services[s]?.volumes?.length > 0) {
template.services[s].volumes.forEach(v => volumes.add(v))
}
} }
// Workaround: old plausible analytics service wrong volume id name // Workaround: old plausible analytics service wrong volume id name
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) { if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
let temp = Array.from(volumes) let temp = Array.from(volumes)

View File

@@ -736,7 +736,7 @@ export async function deleteApplication(
where: { id }, where: { id },
include: { destinationDocker: true, teams: true } include: { destinationDocker: true, teams: true }
}); });
if (teamId !== '0' || !application.teams.some((team) => team.id === teamId)) { if (teamId !== '0' && !application.teams.some((team) => team.id === teamId)) {
throw { status: 403, message: 'You are not allowed to delete this application.' }; throw { status: 403, message: 'You are not allowed to delete this application.' };
} }
if (application?.destinationDocker?.id && application.destinationDocker?.network) { if (application?.destinationDocker?.id && application.destinationDocker?.network) {
@@ -1340,16 +1340,16 @@ export async function getStorages(request: FastifyRequest<OnlyId>) {
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) { export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
try { try {
const { id } = request.params; const { id } = request.params;
const { path, newStorage, storageId } = request.body; const { hostPath, path, newStorage, storageId } = request.body;
if (newStorage) { if (newStorage) {
await prisma.applicationPersistentStorage.create({ await prisma.applicationPersistentStorage.create({
data: { path, application: { connect: { id } } } data: { hostPath, path, application: { connect: { id } } }
}); });
} else { } else {
await prisma.applicationPersistentStorage.update({ await prisma.applicationPersistentStorage.update({
where: { id: storageId }, where: { id: storageId },
data: { path } data: { hostPath, path }
}); });
} }
return reply.code(201).send(); return reply.code(201).send();

View File

@@ -96,6 +96,7 @@ export interface DeleteSecret extends OnlyId {
} }
export interface SaveStorage extends OnlyId { export interface SaveStorage extends OnlyId {
Body: { Body: {
hostPath?: string;
path: string; path: string;
newStorage: boolean; newStorage: boolean;
storageId: string; storageId: string;

View File

@@ -12,6 +12,7 @@
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import CopyVolumeField from '$lib/components/CopyVolumeField.svelte'; import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
const { id } = $page.params; const { id } = $page.params;
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
export let value: string; export let value: string;
@@ -33,11 +34,13 @@
storage.path.replace(/\/\//g, '/'); storage.path.replace(/\/\//g, '/');
await post(`/applications/${id}/storages`, { await post(`/applications/${id}/storages`, {
path: storage.path, path: storage.path,
hostPath: storage.hostPath,
storageId: storage.id, storageId: storage.id,
newStorage newStorage
}); });
dispatch('refresh'); dispatch('refresh');
if (isNew) { if (isNew) {
storage.hostPath = null;
storage.path = null; storage.path = null;
storage.id = null; storage.id = null;
} }
@@ -80,27 +83,42 @@
<div class="flex gap-4 pb-2" class:pt-8={isNew}> <div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId} {#if storage.applicationId}
{#if storage.oldPath} {#if storage.oldPath}
<CopyVolumeField
<CopyVolumeField value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/>
{:else if !storage.hostPath}
<CopyVolumeField
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}" value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/>
{:else}
<CopyVolumeField
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/> />
{/if} {/if}
{/if} {/if}
{#if isNew}
<div class="w-full">
<input
disabled={!isNew}
readonly={!isNew}
bind:value={storage.hostPath}
placeholder="Host path, example: ~/.directory"
/>
<SimpleExplainer
text="You can mount <span class='text-yellow-400 font-bold'>host paths</span> from the operating system.<br>Leave it empty to define a volume based volume."
/>
</div>
{:else if storage.hostPath}
<input disabled readonly value={storage.hostPath} />
{/if}
<input <input
disabled={!isNew} disabled={!isNew}
readonly={!isNew} readonly={!isNew}
class="w-full" class="w-full"
bind:value={storage.path} bind:value={storage.path}
required required
placeholder="eg: /data" placeholder="Mount point inside the container, example: /data"
/> />
<div class="flex items-center justify-center"> <div class="flex items-start justify-center">
{#if isNew} {#if isNew}
<div class="w-full lg:w-64"> <div class="w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)} <button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}

View File

@@ -427,29 +427,6 @@
</svg> Stop </svg> Stop
</button> </button>
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)} {:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{/if}
<button <button
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
@@ -493,6 +470,29 @@
: 'Redeploy Stack' : 'Redeploy Stack'
: 'Deploy'} : 'Deploy'}
</button> </button>
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{/if}
{/if} {/if}
{#if $location && $status.application.overallStatus === 'healthy'} {#if $location && $status.application.overallStatus === 'healthy'}
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary" <a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"

View File

@@ -406,7 +406,7 @@
> >
{#if tryAgain} {#if tryAgain}
<div class="p-5"> <div class="p-5">
An error occured during authenticating with GitLab. Please check your GitLab Source An error occurred during authenticating with GitLab. Please check your GitLab Source
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>
<button <button

View File

@@ -35,17 +35,46 @@
for (const [_, service] of Object.entries(composeJson.services)) { for (const [_, service] of Object.entries(composeJson.services)) {
if (service?.volumes) { if (service?.volumes) {
for (const [_, volumeName] of Object.entries(service.volumes)) { for (const [_, volumeName] of Object.entries(service.volumes)) {
let [volume, target] = volumeName.split(':'); if (typeof volumeName === 'string') {
if (volume === '.') { let [volume, target] = volumeName.split(':');
volume = target; if (
volume.startsWith('.') ||
volume.startsWith('..') ||
volume.startsWith('/') ||
volume.startsWith('~') ||
volume.startsWith('$PWD')
) {
volume = volume.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
} else {
if (!target) {
target = volume;
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
}
predefinedVolumes.push({ id: volume, path: target, predefined: true });
} }
if (!target) { if (typeof volumeName === 'object') {
target = volume; let { source, target } = volumeName;
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`; if (
} else { source.startsWith('.') ||
volume = `${application.id}${volume.replace(/\//gi, '-').replace(/\./gi, '')}`; source.startsWith('..') ||
source.startsWith('/') ||
source.startsWith('~') ||
source.startsWith('$PWD')
) {
source = source.replace(/^\./, `~`).replace(/^\.\./, '~').replace(/^\$PWD/, '~');
} else {
if (!target) {
target = source;
source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
source = `${application.id}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
}
predefinedVolumes.push({ id: source, path: target, predefined: true });
} }
predefinedVolumes.push({ id: volume, path: target, predefined: true });
} }
} }
} }
@@ -88,14 +117,14 @@
{/key} {/key}
{/each} {/each}
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<div class:pt-10={predefinedVolumes.length > 0}> <div class:pt-10={predefinedVolumes.length > 0}>
Add New Volume <Explainer Add New Volume <Explainer
position="dropdown-bottom" position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')} explanation={$t('application.storage.persistent_storage_explainer')}
/> />
</div> </div>
<Storage on:refresh={refreshStorage} isNew /> <Storage on:refresh={refreshStorage} isNew />
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -86,7 +86,7 @@
readonly readonly
class="w-full" class="w-full"
value={`${ value={`${
services.find((s) => s.id === storage.containerId).name || storage.containerId services.find((s) => s.id === storage.containerId)?.name || storage.containerId
}`} }`}
/> />
</div> </div>
@@ -111,19 +111,18 @@
name="containerId" name="containerId"
class="w-full lg:w-64" class="w-full lg:w-64"
disabled={storage.predefined} disabled={storage.predefined}
readonly={storage.predefined}
bind:value={storage.containerId} bind:value={storage.containerId}
> >
{#if services.length === 1} {#if services.length === 1}
{#if services[0].name} {#if services[0].name}
<option selected value={services[0].id}>{services[0].name}</option> <option selected value={services[0].id}>{services[0]?.name}</option>
{:else} {:else}
<option selected value={services[0]}>{services[0]}</option> <option selected value={services[0]}>{services[0]}</option>
{/if} {/if}
{:else} {:else}
{#each services as service} {#each services as service}
{#if service.name} {#if service.name}
<option value={service.id}>{service.name}</option> <option value={service.id}>{service?.name}</option>
{:else} {:else}
<option value={service}>{service}</option> <option value={service}>{service}</option>
{/if} {/if}
@@ -157,7 +156,7 @@
disabled disabled
readonly readonly
class="w-full" class="w-full"
value={`${services.find((s) => s.id === storage.containerId).name || storage.containerId}`} value={`${services.find((s) => s.id === storage.containerId)?.name || storage.containerId}`}
/> />
<input disabled readonly class="w-full" value={`${storage.volumeName}:${storage.path}`} /> <input disabled readonly class="w-full" value={`${storage.volumeName}:${storage.path}`} />
<button <button

View File

@@ -1,7 +1,7 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "3.12.20", "version": "3.12.26",
"license": "Apache-2.0", "license": "Apache-2.0",
"repository": "github:coollabsio/coolify", "repository": "github:coollabsio/coolify",
"scripts": { "scripts": {