2022-07-06 11:02:36 +02:00
import child from 'child_process' ;
import util from 'util' ;
import fs from 'fs/promises' ;
import yaml from 'js-yaml' ;
import forge from 'node-forge' ;
import { uniqueNamesGenerator , adjectives , colors , animals } from 'unique-names-generator' ;
import type { Config } from 'unique-names-generator' ;
import generator from 'generate-password' ;
import crypto from 'crypto' ;
import { promises as dns } from 'dns' ;
import { PrismaClient } from '@prisma/client' ;
import cuid from 'cuid' ;
2022-07-20 13:35:26 +00:00
import os from 'os' ;
import sshConfig from 'ssh-config'
2022-07-06 11:02:36 +02:00
2022-07-25 12:42:10 +00:00
import { checkContainer , removeContainer } from './docker' ;
2022-07-06 11:02:36 +02:00
import { day } from './dayjs' ;
import * as serviceFields from './serviceFields'
2022-08-12 16:14:37 +00:00
export const version = '3.3.2' ;
2022-07-08 13:38:19 +02:00
export const isDev = process . env . NODE_ENV === 'development' ;
2022-07-06 11:02:36 +02:00
const algorithm = 'aes-256-ctr' ;
const customConfig : Config = {
dictionaries : [ adjectives , colors , animals ] ,
style : 'capital' ,
separator : ' ' ,
length : 3
} ;
export const defaultProxyImage = ` coolify-haproxy-alpine:latest ` ;
export const defaultProxyImageTcp = ` coolify-haproxy-tcp-alpine:latest ` ;
export const defaultProxyImageHttp = ` coolify-haproxy-http-alpine:latest ` ;
2022-07-22 12:01:07 +00:00
export const defaultTraefikImage = ` traefik:v2.8 ` ;
2022-07-12 13:08:47 +00:00
export function getAPIUrl() {
if ( process . env . GITPOD_WORKSPACE_URL ) {
const { href } = new URL ( process . env . GITPOD_WORKSPACE_URL )
const newURL = href . replace ( 'https://' , 'https://3001-' ) . replace ( /\/$/ , '' )
return newURL
}
2022-08-12 09:38:02 +02:00
if ( process . env . CODESANDBOX_HOST ) {
return ` https:// ${ process . env . CODESANDBOX_HOST . replace ( /\$PORT/ , '3001' ) } `
2022-08-11 08:18:17 +00:00
}
2022-07-12 13:08:47 +00:00
return isDev ? 'http://localhost:3001' : 'http://localhost:3000' ;
}
2022-08-11 08:18:17 +00:00
2022-07-12 13:08:47 +00:00
export function getUIUrl() {
if ( process . env . GITPOD_WORKSPACE_URL ) {
const { href } = new URL ( process . env . GITPOD_WORKSPACE_URL )
const newURL = href . replace ( 'https://' , 'https://3000-' ) . replace ( /\/$/ , '' )
return newURL
}
2022-08-12 09:38:02 +02:00
if ( process . env . CODESANDBOX_HOST ) {
return ` https:// ${ process . env . CODESANDBOX_HOST . replace ( /\$PORT/ , '3000' ) } `
2022-08-11 08:18:17 +00:00
}
2022-07-12 13:08:47 +00:00
return 'http://localhost:3000' ;
}
2022-07-06 11:02:36 +02:00
const mainTraefikEndpoint = isDev
2022-07-12 13:08:47 +00:00
? ` ${ getAPIUrl ( ) } /webhooks/traefik/main.json `
2022-07-06 11:02:36 +02:00
: 'http://coolify:3000/webhooks/traefik/main.json' ;
const otherTraefikEndpoint = isDev
2022-07-12 13:08:47 +00:00
? ` ${ getAPIUrl ( ) } /webhooks/traefik/other.json `
2022-07-06 11:02:36 +02:00
: 'http://coolify:3000/webhooks/traefik/other.json' ;
export const include : any = {
destinationDocker : true ,
persistentStorage : true ,
serviceSecret : true ,
minio : true ,
plausibleAnalytics : true ,
vscodeserver : true ,
wordpress : true ,
ghost : true ,
meiliSearch : true ,
umami : true ,
hasura : true ,
2022-07-15 12:31:54 +00:00
fider : true ,
2022-08-15 09:56:34 +00:00
glitchTip : true ,
2022-07-06 11:02:36 +02:00
} ;
export const uniqueName = ( ) : string = > uniqueNamesGenerator ( customConfig ) ;
export const asyncExecShell = util . promisify ( child . exec ) ;
export const asyncSleep = ( delay : number ) : Promise < unknown > = >
new Promise ( ( resolve ) = > setTimeout ( resolve , delay ) ) ;
export const prisma = new PrismaClient ( {
errorFormat : 'minimal'
} ) ;
export const base64Encode = ( text : string ) : string = > {
return Buffer . from ( text ) . toString ( 'base64' ) ;
} ;
export const base64Decode = ( text : string ) : string = > {
return Buffer . from ( text , 'base64' ) . toString ( 'ascii' ) ;
} ;
export const decrypt = ( hashString : string ) = > {
if ( hashString ) {
const hash = JSON . parse ( hashString ) ;
const decipher = crypto . createDecipheriv (
algorithm ,
process . env [ 'COOLIFY_SECRET_KEY' ] ,
Buffer . from ( hash . iv , 'hex' )
) ;
const decrpyted = Buffer . concat ( [
decipher . update ( Buffer . from ( hash . content , 'hex' ) ) ,
decipher . final ( )
] ) ;
return decrpyted . toString ( ) ;
}
} ;
export const encrypt = ( text : string ) = > {
if ( text ) {
const iv = crypto . randomBytes ( 16 ) ;
const cipher = crypto . createCipheriv ( algorithm , process . env [ 'COOLIFY_SECRET_KEY' ] , iv ) ;
const encrypted = Buffer . concat ( [ cipher . update ( text ) , cipher . final ( ) ] ) ;
return JSON . stringify ( {
iv : iv.toString ( 'hex' ) ,
content : encrypted.toString ( 'hex' )
} ) ;
}
} ;
2022-07-15 15:40:26 +02:00
export const supportedServiceTypesAndVersions = [
2022-07-20 13:35:26 +00:00
{
name : 'plausibleanalytics' ,
fancyName : 'Plausible Analytics' ,
baseImage : 'plausible/analytics' ,
images : [ 'bitnami/postgresql:13.2.0' , 'yandex/clickhouse-server:21.3.2.5' ] ,
versions : [ 'latest' , 'stable' ] ,
recommendedVersion : 'stable' ,
ports : {
main : 8000
}
} ,
{
name : 'nocodb' ,
fancyName : 'NocoDB' ,
baseImage : 'nocodb/nocodb' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 8080
}
} ,
{
name : 'minio' ,
fancyName : 'MinIO' ,
baseImage : 'minio/minio' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 9001
}
} ,
{
name : 'vscodeserver' ,
fancyName : 'VSCode Server' ,
baseImage : 'codercom/code-server' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 8080
}
} ,
{
name : 'wordpress' ,
fancyName : 'Wordpress' ,
baseImage : 'wordpress' ,
images : [ 'bitnami/mysql:5.7' ] ,
versions : [ 'latest' , 'php8.1' , 'php8.0' , 'php7.4' , 'php7.3' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 80
}
} ,
{
name : 'vaultwarden' ,
fancyName : 'Vaultwarden' ,
baseImage : 'vaultwarden/server' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 80
}
} ,
{
name : 'languagetool' ,
fancyName : 'LanguageTool' ,
baseImage : 'silviof/docker-languagetool' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 8010
}
} ,
{
name : 'n8n' ,
fancyName : 'n8n' ,
baseImage : 'n8nio/n8n' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 5678
}
} ,
{
name : 'uptimekuma' ,
fancyName : 'Uptime Kuma' ,
baseImage : 'louislam/uptime-kuma' ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 3001
}
} ,
{
name : 'ghost' ,
fancyName : 'Ghost' ,
baseImage : 'bitnami/ghost' ,
images : [ 'bitnami/mariadb' ] ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 2368
}
} ,
{
name : 'meilisearch' ,
fancyName : 'Meilisearch' ,
baseImage : 'getmeili/meilisearch' ,
images : [ ] ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 7700
}
} ,
{
name : 'umami' ,
fancyName : 'Umami' ,
baseImage : 'ghcr.io/mikecao/umami' ,
images : [ 'postgres:12-alpine' ] ,
versions : [ 'postgresql-latest' ] ,
recommendedVersion : 'postgresql-latest' ,
ports : {
main : 3000
}
} ,
{
name : 'hasura' ,
fancyName : 'Hasura' ,
baseImage : 'hasura/graphql-engine' ,
images : [ 'postgres:12-alpine' ] ,
2022-07-25 19:07:29 +00:00
versions : [ 'latest' , 'v2.8.4' , 'v2.5.1' ] ,
recommendedVersion : 'v2.8.4' ,
2022-07-20 13:35:26 +00:00
ports : {
main : 8080
}
} ,
{
name : 'fider' ,
fancyName : 'Fider' ,
baseImage : 'getfider/fider' ,
images : [ 'postgres:12-alpine' ] ,
versions : [ 'stable' ] ,
recommendedVersion : 'stable' ,
ports : {
main : 3000
}
} ,
// {
// name: 'moodle',
// fancyName: 'Moodle',
// baseImage: 'bitnami/moodle',
// images: [],
// versions: ['latest', 'v4.0.2'],
// recommendedVersion: 'latest',
// ports: {
// main: 8080
// }
// }
2022-08-15 09:56:34 +00:00
{
name : 'glitchTip' ,
fancyName : 'GlitchTip' ,
baseImage : 'glitchtip/glitchtip' ,
images : [ 'postgres:14-alpine' , 'redis:7-alpine' ] ,
versions : [ 'latest' ] ,
recommendedVersion : 'latest' ,
ports : {
main : 8000
}
} ,
2022-07-15 15:40:26 +02:00
] ;
2022-07-06 11:02:36 +02:00
export async function checkDoubleBranch ( branch : string , projectId : number ) : Promise < boolean > {
const applications = await prisma . application . findMany ( { where : { branch , projectId } } ) ;
return applications . length > 1 ;
}
export async function isDNSValid ( hostname : any , domain : string ) : Promise < any > {
const { isIP } = await import ( 'is-ip' ) ;
let resolves = [ ] ;
try {
if ( isIP ( hostname ) ) {
resolves = [ hostname ] ;
} else {
resolves = await dns . resolve4 ( hostname ) ;
}
} catch ( error ) {
throw 'Invalid DNS.'
}
try {
let ipDomainFound = false ;
dns . setServers ( [ '1.1.1.1' , '8.8.8.8' ] ) ;
const dnsResolve = await dns . resolve4 ( domain ) ;
if ( dnsResolve . length > 0 ) {
for ( const ip of dnsResolve ) {
if ( resolves . includes ( ip ) ) {
ipDomainFound = true ;
}
}
}
if ( ! ipDomainFound ) throw false ;
} catch ( error ) {
throw 'DNS not set'
}
}
export function getDomain ( domain : string ) : string {
return domain ? . replace ( 'https://' , '' ) . replace ( 'http://' , '' ) ;
}
export async function isDomainConfigured ( {
id ,
fqdn ,
2022-07-25 10:16:25 +00:00
checkOwn = false ,
2022-08-06 09:21:16 +00:00
remoteIpAddress = undefined
2022-07-06 11:02:36 +02:00
} : {
id : string ;
fqdn : string ;
checkOwn? : boolean ;
2022-08-06 09:21:16 +00:00
remoteIpAddress? : string ;
2022-07-06 11:02:36 +02:00
} ) : Promise < boolean > {
const domain = getDomain ( fqdn ) ;
const nakedDomain = domain . replace ( 'www.' , '' ) ;
const foundApp = await prisma . application . findFirst ( {
where : {
OR : [
{ fqdn : { endsWith : ` // ${ nakedDomain } ` } } ,
{ fqdn : { endsWith : ` //www. ${ nakedDomain } ` } }
] ,
2022-07-25 10:16:25 +00:00
id : { not : id } ,
destinationDocker : {
2022-08-06 09:21:16 +00:00
remoteIpAddress ,
2022-07-25 10:16:25 +00:00
}
2022-07-06 11:02:36 +02:00
} ,
select : { fqdn : true }
} ) ;
const foundService = await prisma . service . findFirst ( {
where : {
OR : [
{ fqdn : { endsWith : ` // ${ nakedDomain } ` } } ,
{ fqdn : { endsWith : ` //www. ${ nakedDomain } ` } } ,
{ minio : { apiFqdn : { endsWith : ` // ${ nakedDomain } ` } } } ,
{ minio : { apiFqdn : { endsWith : ` //www. ${ nakedDomain } ` } } }
] ,
2022-07-25 10:16:25 +00:00
id : { not : checkOwn ? undefined : id } ,
destinationDocker : {
2022-08-06 09:21:16 +00:00
remoteIpAddress
2022-07-25 10:16:25 +00:00
}
2022-07-06 11:02:36 +02:00
} ,
select : { fqdn : true }
} ) ;
const coolifyFqdn = await prisma . setting . findFirst ( {
where : {
OR : [
{ fqdn : { endsWith : ` // ${ nakedDomain } ` } } ,
{ fqdn : { endsWith : ` //www. ${ nakedDomain } ` } }
] ,
id : { not : id }
} ,
select : { fqdn : true }
} ) ;
return ! ! ( foundApp || foundService || coolifyFqdn ) ;
}
2022-07-20 13:35:26 +00:00
export async function getContainerUsage ( dockerId : string , container : string ) : Promise < any > {
2022-07-06 11:02:36 +02:00
try {
2022-07-20 13:35:26 +00:00
const { stdout } = await executeDockerCmd ( { dockerId , command : ` docker container stats ${ container } --no-stream --no-trunc --format "{{json .}}" ` } )
2022-07-06 11:02:36 +02:00
return JSON . parse ( stdout ) ;
} catch ( err ) {
return {
MemUsage : 0 ,
CPUPerc : 0 ,
NetIO : 0
} ;
}
}
export async function checkDomainsIsValidInDNS ( { hostname , fqdn , dualCerts } ) : Promise < any > {
const { isIP } = await import ( 'is-ip' ) ;
const domain = getDomain ( fqdn ) ;
const domainDualCert = domain . includes ( 'www.' ) ? domain . replace ( 'www.' , '' ) : ` www. ${ domain } ` ;
dns . setServers ( [ '1.1.1.1' , '8.8.8.8' ] ) ;
let resolves = [ ] ;
try {
if ( isIP ( hostname ) ) {
resolves = [ hostname ] ;
} else {
resolves = await dns . resolve4 ( hostname ) ;
}
} catch ( error ) {
2022-07-06 19:34:16 +02:00
throw { status : 500 , message : ` Could not determine IP address for ${ hostname } . ` }
2022-07-06 11:02:36 +02:00
}
if ( dualCerts ) {
try {
const ipDomain = await dns . resolve4 ( domain ) ;
const ipDomainDualCert = await dns . resolve4 ( domainDualCert ) ;
let ipDomainFound = false ;
let ipDomainDualCertFound = false ;
for ( const ip of ipDomain ) {
if ( resolves . includes ( ip ) ) {
ipDomainFound = true ;
}
}
for ( const ip of ipDomainDualCert ) {
if ( resolves . includes ( ip ) ) {
ipDomainDualCertFound = true ;
}
}
if ( ipDomainFound && ipDomainDualCertFound ) return { status : 200 } ;
2022-07-06 19:04:54 +02:00
throw { status : 500 , message : ` DNS not set correctly or propogated.<br>Please check your DNS settings. ` }
2022-07-06 11:02:36 +02:00
} catch ( error ) {
2022-07-06 19:04:54 +02:00
throw { status : 500 , message : ` DNS not set correctly or propogated.<br>Please check your DNS settings. ` }
2022-07-06 11:02:36 +02:00
}
} else {
try {
const ipDomain = await dns . resolve4 ( domain ) ;
let ipDomainFound = false ;
for ( const ip of ipDomain ) {
if ( resolves . includes ( ip ) ) {
ipDomainFound = true ;
}
}
if ( ipDomainFound ) return { status : 200 } ;
2022-07-06 19:04:54 +02:00
throw { status : 500 , message : ` DNS not set correctly or propogated.<br>Please check your DNS settings. ` }
2022-07-06 11:02:36 +02:00
} catch ( error ) {
2022-07-06 19:04:54 +02:00
throw { status : 500 , message : ` DNS not set correctly or propogated.<br>Please check your DNS settings. ` }
2022-07-06 11:02:36 +02:00
}
}
}
export function generateTimestamp ( ) : string {
return ` ${ day ( ) . format ( 'HH:mm:ss.SSS' ) } ` ;
}
export async function listServicesWithIncludes ( ) : Promise < any > {
return await prisma . service . findMany ( {
include ,
orderBy : { createdAt : 'desc' }
} ) ;
}
export const supportedDatabaseTypesAndVersions = [
{
name : 'mongodb' ,
fancyName : 'MongoDB' ,
baseImage : 'bitnami/mongodb' ,
2022-08-12 12:13:52 +02:00
baseImageARM : 'mongo' ,
versions : [ '5.0' , '4.4' , '4.2' ] ,
versionsARM : [ '5.0' , '4.4' , '4.2' ]
2022-07-06 11:02:36 +02:00
} ,
2022-08-12 11:48:38 +02:00
{
name : 'mysql' ,
fancyName : 'MySQL' ,
baseImage : 'bitnami/mysql' ,
baseImageARM : 'mysql' ,
versions : [ '8.0' , '5.7' ] ,
versionsARM : [ '8.0' , '5.7' ]
} ,
2022-07-06 11:02:36 +02:00
{
name : 'mariadb' ,
fancyName : 'MariaDB' ,
baseImage : 'bitnami/mariadb' ,
2022-08-12 11:48:38 +02:00
baseImageARM : 'mariadb' ,
versions : [ '10.8' , '10.7' , '10.6' , '10.5' , '10.4' , '10.3' , '10.2' ] ,
versionsARM : [ '10.8' , '10.7' , '10.6' , '10.5' , '10.4' , '10.3' , '10.2' ]
2022-07-06 11:02:36 +02:00
} ,
{
name : 'postgresql' ,
fancyName : 'PostgreSQL' ,
baseImage : 'bitnami/postgresql' ,
2022-08-12 11:48:38 +02:00
baseImageARM : 'postgres' ,
versions : [ '14.5.0' , '13.8.0' , '12.12.0' , '11.17.0' , '10.22.0' ] ,
versionsARM : [ '14.5' , '13.8' , '12.12' , '11.17' , '10.22' ]
2022-07-06 11:02:36 +02:00
} ,
{
name : 'redis' ,
fancyName : 'Redis' ,
baseImage : 'bitnami/redis' ,
2022-08-12 11:48:38 +02:00
baseImageARM : 'redis' ,
versions : [ '7.0' , '6.2' , '6.0' , '5.0' ] ,
versionsARM : [ '7.0' , '6.2' , '6.0' , '5.0' ]
2022-07-06 11:02:36 +02:00
} ,
2022-08-12 11:48:38 +02:00
{
name : 'couchdb' ,
fancyName : 'CouchDB' ,
baseImage : 'bitnami/couchdb' ,
baseImageARM : 'couchdb' ,
versions : [ '3.2.2' , '3.1.2' , '2.3.1' ] ,
versionsARM : [ '3.2.2' , '3.1.2' , '2.3.1' ]
}
2022-07-06 11:02:36 +02:00
] ;
2022-08-07 12:42:20 +00:00
export async function getFreeSSHLocalPort ( id : string ) : Promise < number > {
const { default : getPort , portNumbers } = await import ( 'get-port' ) ;
const { remoteIpAddress , sshLocalPort } = await prisma . destinationDocker . findUnique ( { where : { id } } )
if ( sshLocalPort ) {
return Number ( sshLocalPort )
}
const ports = await prisma . destinationDocker . findMany ( { where : { sshLocalPort : { not : null } , remoteIpAddress : { not : remoteIpAddress } } } )
const alreadyConfigured = await prisma . destinationDocker . findFirst ( { where : { remoteIpAddress , id : { not : id } , sshLocalPort : { not : null } } } )
if ( alreadyConfigured ? . sshLocalPort ) {
await prisma . destinationDocker . update ( { where : { id } , data : { sshLocalPort : alreadyConfigured.sshLocalPort } } )
return Number ( alreadyConfigured . sshLocalPort )
}
const availablePort = await getPort ( { port : portNumbers ( 10000 , 10100 ) , exclude : ports.map ( p = > p . sshLocalPort ) } )
await prisma . destinationDocker . update ( { where : { id } , data : { sshLocalPort : Number ( availablePort ) } } )
return Number ( availablePort )
}
2022-07-20 13:35:26 +00:00
export async function createRemoteEngineConfiguration ( id : string ) {
const homedir = os . homedir ( ) ;
const sshKeyFile = ` /tmp/id_rsa- ${ id } `
2022-08-07 12:42:20 +00:00
const localPort = await getFreeSSHLocalPort ( id ) ;
2022-07-20 13:35:26 +00:00
const { sshKey : { privateKey } , remoteIpAddress , remotePort , remoteUser } = await prisma . destinationDocker . findFirst ( { where : { id } , include : { sshKey : true } } )
await fs . writeFile ( sshKeyFile , decrypt ( privateKey ) + '\n' , { encoding : 'utf8' , mode : 400 } )
2022-07-21 12:43:53 +00:00
// Needed for remote docker compose
2022-07-27 13:57:42 +00:00
const { stdout : numberOfSSHAgentsRunning } = await asyncExecShell ( ` ps ax | grep [s]sh-agent | grep ssh-agent.pid | grep -v grep | wc -l ` )
2022-07-27 13:11:46 +00:00
if ( numberOfSSHAgentsRunning !== '' && Number ( numberOfSSHAgentsRunning . trim ( ) ) == 0 ) {
2022-07-27 13:57:42 +00:00
await asyncExecShell ( ` eval $ (ssh-agent -sa /tmp/ssh-agent.pid) ` )
2022-07-27 13:11:46 +00:00
}
2022-07-27 13:57:42 +00:00
await asyncExecShell ( ` SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${ sshKeyFile } ` )
2022-08-07 12:42:20 +00:00
const { stdout : numberOfSSHTunnelsRunning } = await asyncExecShell ( ` ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${ localPort } :localhost: ${ remotePort } ' | grep -v grep | wc -l ` )
2022-07-27 13:57:42 +00:00
if ( numberOfSSHTunnelsRunning !== '' && Number ( numberOfSSHTunnelsRunning . trim ( ) ) == 0 ) {
try {
2022-08-07 12:42:20 +00:00
await asyncExecShell ( ` SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${ localPort } :localhost: ${ remotePort } ${ remoteUser } @ ${ remoteIpAddress } ` )
2022-07-27 13:57:42 +00:00
2022-08-07 12:42:20 +00:00
} catch ( error ) {
2022-07-27 13:57:42 +00:00
console . log ( error )
}
}
2022-07-20 13:35:26 +00:00
const config = sshConfig . parse ( '' )
const found = config . find ( { Host : remoteIpAddress } )
if ( ! found ) {
config . append ( {
Host : remoteIpAddress ,
2022-07-27 13:57:42 +00:00
Hostname : 'localhost' ,
2022-08-07 12:42:20 +00:00
Port : Number ( localPort ) ,
2022-07-20 13:35:26 +00:00
User : remoteUser ,
IdentityFile : sshKeyFile ,
StrictHostKeyChecking : 'no'
} )
}
try {
await fs . stat ( ` ${ homedir } /.ssh/ ` )
} catch ( error ) {
await fs . mkdir ( ` ${ homedir } /.ssh/ ` )
}
2022-08-07 12:42:20 +00:00
return await fs . writeFile ( ` ${ homedir } /.ssh/config ` , sshConfig . stringify ( config ) )
2022-07-20 13:35:26 +00:00
}
export async function executeDockerCmd ( { dockerId , command } : { dockerId : string , command : string } ) {
2022-07-27 13:57:42 +00:00
let { remoteEngine , remoteIpAddress , engine } = await prisma . destinationDocker . findUnique ( { where : { id : dockerId } } )
2022-07-25 12:42:10 +00:00
if ( remoteEngine ) {
2022-07-20 13:35:26 +00:00
await createRemoteEngineConfiguration ( dockerId )
2022-07-27 13:57:42 +00:00
engine = ` ssh:// ${ remoteIpAddress } `
2022-07-25 12:42:10 +00:00
} else {
engine = 'unix:///var/run/docker.sock'
2022-07-20 13:35:26 +00:00
}
return await asyncExecShell (
2022-07-25 12:42:10 +00:00
` DOCKER_BUILDKIT=1 DOCKER_HOST=" ${ engine } " ${ command } `
2022-07-20 13:35:26 +00:00
) ;
2022-07-27 13:57:42 +00:00
2022-07-20 13:35:26 +00:00
}
export async function startTraefikProxy ( id : string ) : Promise < void > {
2022-07-22 20:23:16 +00:00
const { engine , network , remoteEngine , remoteIpAddress } = await prisma . destinationDocker . findUnique ( { where : { id } } )
2022-07-20 13:35:26 +00:00
const found = await checkContainer ( { dockerId : id , container : 'coolify-proxy' , remove : true } ) ;
2022-07-22 21:14:12 +00:00
const { id : settingsId , ipv4 , ipv6 } = await listSettings ( ) ;
2022-07-06 11:02:36 +02:00
if ( ! found ) {
2022-07-20 13:35:26 +00:00
const { stdout : Config } = await executeDockerCmd ( { dockerId : id , command : ` docker network inspect ${ network } --format '{{json .IPAM.Config }}' ` } )
2022-07-06 11:02:36 +02:00
const ip = JSON . parse ( Config ) [ 0 ] . Gateway ;
2022-07-22 20:23:16 +00:00
let traefikUrl = mainTraefikEndpoint
if ( remoteEngine ) {
let ip = null
if ( isDev ) {
ip = getAPIUrl ( )
} else {
2022-07-22 21:28:01 +00:00
ip = ` http:// ${ ipv4 || ipv6 } :3000 `
2022-07-22 20:23:16 +00:00
}
traefikUrl = ` ${ ip } /webhooks/traefik/remote/ ${ id } `
}
2022-07-20 13:35:26 +00:00
await executeDockerCmd ( {
dockerId : id ,
command : ` docker run --restart always \
2022-07-06 11:02:36 +02:00
-- add - host 'host.docker.internal:host-gateway' \
2022-07-20 13:35:26 +00:00
$ { ip ? ` --add-host 'host.docker.internal: ${ ip } ' ` : '' } \
2022-07-06 11:02:36 +02:00
- v coolify - traefik - letsencrypt : / e t c / t r a e f i k / a c m e \
- v / var / r u n / d o c k e r . s o c k : / v a r / r u n / d o c k e r . s o c k \
-- network coolify - infra \
- p "80:80" \
- p "443:443" \
-- name coolify - proxy \
- d $ { defaultTraefikImage } \
-- entrypoints . web . address = : 80 \
-- entrypoints . web . forwardedHeaders . insecure = true \
-- entrypoints . websecure . address = : 443 \
-- entrypoints . websecure . forwardedHeaders . insecure = true \
-- providers . docker = true \
-- providers . docker . exposedbydefault = false \
2022-07-22 20:23:16 +00:00
-- providers . http . endpoint = $ { traefikUrl } \
2022-07-06 11:02:36 +02:00
-- providers . http . pollTimeout = 5 s \
-- certificatesresolvers . letsencrypt . acme . httpchallenge = true \
-- certificatesresolvers . letsencrypt . acme . storage = / e t c / t r a e f i k / a c m e / a c m e . j s o n \
-- certificatesresolvers . letsencrypt . acme . httpchallenge . entrypoint = web \
-- log . level = error `
2022-07-20 13:35:26 +00:00
} )
await prisma . setting . update ( { where : { id : settingsId } , data : { proxyHash : null } } ) ;
await prisma . destinationDocker . update ( {
where : { id } ,
2022-07-06 11:02:36 +02:00
data : { isCoolifyProxyUsed : true }
} ) ;
}
2022-07-22 20:23:16 +00:00
// Configure networks for local docker engine
if ( engine ) {
const destinations = await prisma . destinationDocker . findMany ( { where : { engine } } ) ;
for ( const destination of destinations ) {
await configureNetworkTraefikProxy ( destination ) ;
}
}
// Configure networks for remote docker engine
if ( remoteEngine ) {
const destinations = await prisma . destinationDocker . findMany ( { where : { remoteIpAddress } } ) ;
for ( const destination of destinations ) {
await configureNetworkTraefikProxy ( destination ) ;
}
}
2022-07-06 11:02:36 +02:00
}
2022-07-22 20:23:16 +00:00
export async function configureNetworkTraefikProxy ( destination : any ) : Promise < void > {
const { id } = destination
2022-07-20 13:35:26 +00:00
const { stdout : networks } = await executeDockerCmd ( {
2022-07-21 12:43:53 +00:00
dockerId : id ,
2022-07-20 13:35:26 +00:00
command :
` docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}' `
} ) ;
2022-07-06 11:02:36 +02:00
const configuredNetworks = networks . replace ( /"/g , '' ) . replace ( '\n' , '' ) . split ( ',' ) ;
2022-07-22 20:23:16 +00:00
if ( ! configuredNetworks . includes ( destination . network ) ) {
await executeDockerCmd ( { dockerId : destination.id , command : ` docker network connect ${ destination . network } coolify-proxy ` } )
2022-07-06 11:02:36 +02:00
}
}
export async function stopTraefikProxy (
2022-07-20 13:35:26 +00:00
id : string
2022-07-06 11:02:36 +02:00
) : Promise < { stdout : string ; stderr : string } | Error > {
2022-07-20 13:35:26 +00:00
const found = await checkContainer ( { dockerId : id , container : 'coolify-proxy' } ) ;
await prisma . destinationDocker . update ( {
where : { id } ,
2022-07-06 11:02:36 +02:00
data : { isCoolifyProxyUsed : false }
} ) ;
2022-07-20 13:35:26 +00:00
const { id : settingsId } = await prisma . setting . findFirst ( { } ) ;
await prisma . setting . update ( { where : { id : settingsId } , data : { proxyHash : null } } ) ;
2022-07-06 11:02:36 +02:00
try {
if ( found ) {
2022-07-20 13:35:26 +00:00
await executeDockerCmd ( {
dockerId : id ,
command :
` docker stop -t 0 coolify-proxy && docker rm coolify-proxy `
} ) ;
2022-07-06 11:02:36 +02:00
}
} catch ( error ) {
return error ;
}
}
export async function listSettings ( ) : Promise < any > {
const settings = await prisma . setting . findFirst ( { } ) ;
if ( settings . proxyPassword ) settings . proxyPassword = decrypt ( settings . proxyPassword ) ;
return settings ;
}
export function generatePassword ( length = 24 , symbols = false ) : string {
return generator . generate ( {
length ,
numbers : true ,
strict : true ,
symbols
} ) ;
}
2022-08-12 11:48:38 +02:00
export function generateDatabaseConfiguration ( database : any , arch : string ) :
2022-07-06 11:02:36 +02:00
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
MYSQL_DATABASE : string ;
MYSQL_PASSWORD : string ;
MYSQL_ROOT_USER : string ;
MYSQL_USER : string ;
MYSQL_ROOT_PASSWORD : string ;
} ;
}
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
2022-08-12 12:13:52 +02:00
MONGO_INITDB_ROOT_USERNAME? : string ;
MONGO_INITDB_ROOT_PASSWORD? : string ;
MONGODB_ROOT_USER? : string ;
MONGODB_ROOT_PASSWORD? : string ;
2022-07-06 11:02:36 +02:00
} ;
}
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
MARIADB_ROOT_USER : string ;
MARIADB_ROOT_PASSWORD : string ;
MARIADB_USER : string ;
MARIADB_PASSWORD : string ;
MARIADB_DATABASE : string ;
} ;
}
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
POSTGRESQL_POSTGRES_PASSWORD : string ;
POSTGRESQL_USERNAME : string ;
POSTGRESQL_PASSWORD : string ;
POSTGRESQL_DATABASE : string ;
} ;
}
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
POSTGRES_USER : string ;
POSTGRES_PASSWORD : string ;
POSTGRES_DB : string ;
} ;
}
| {
volume : string ;
image : string ;
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
REDIS_AOF_ENABLED : string ;
REDIS_PASSWORD : string ;
} ;
}
| {
volume : string ;
image : string ;
2022-08-12 11:48:38 +02:00
command? : string ;
2022-07-06 11:02:36 +02:00
ulimits : Record < string , unknown > ;
privatePort : number ;
environmentVariables : {
COUCHDB_PASSWORD : string ;
COUCHDB_USER : string ;
} ;
} {
const {
id ,
dbUser ,
dbUserPassword ,
rootUser ,
rootUserPassword ,
defaultDatabase ,
version ,
type ,
settings : { appendOnly }
} = database ;
2022-08-12 11:48:38 +02:00
const baseImage = getDatabaseImage ( type , arch ) ;
2022-07-06 11:02:36 +02:00
if ( type === 'mysql' ) {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 3306 ,
environmentVariables : {
MYSQL_USER : dbUser ,
MYSQL_PASSWORD : dbUserPassword ,
MYSQL_ROOT_PASSWORD : rootUserPassword ,
MYSQL_ROOT_USER : rootUser ,
MYSQL_DATABASE : defaultDatabase
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/mysql/data ` ,
ulimits : { }
2022-08-12 11:48:38 +02:00
}
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
configuration . volume = ` ${ id } - ${ type } -data:/var/lib/mysql ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if ( type === 'mariadb' ) {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 3306 ,
environmentVariables : {
MARIADB_ROOT_USER : rootUser ,
MARIADB_ROOT_PASSWORD : rootUserPassword ,
MARIADB_USER : dbUser ,
MARIADB_PASSWORD : dbUserPassword ,
MARIADB_DATABASE : defaultDatabase
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/mariadb ` ,
ulimits : { }
} ;
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
configuration . volume = ` ${ id } - ${ type } -data:/var/lib/mysql ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if ( type === 'mongodb' ) {
2022-08-12 12:13:52 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 27017 ,
environmentVariables : {
MONGODB_ROOT_USER : rootUser ,
MONGODB_ROOT_PASSWORD : rootUserPassword
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/mongodb ` ,
ulimits : { }
} ;
2022-08-12 12:13:52 +02:00
if ( isARM ( arch ) ) {
configuration . environmentVariables = {
MONGO_INITDB_ROOT_USERNAME : rootUser ,
MONGO_INITDB_ROOT_PASSWORD : rootUserPassword
}
configuration . volume = ` ${ id } - ${ type } -data:/data/db ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if ( type === 'postgresql' ) {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 5432 ,
environmentVariables : {
POSTGRESQL_POSTGRES_PASSWORD : rootUserPassword ,
POSTGRESQL_PASSWORD : dbUserPassword ,
POSTGRESQL_USERNAME : dbUser ,
POSTGRESQL_DATABASE : defaultDatabase
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/postgresql ` ,
ulimits : { }
2022-08-12 11:48:38 +02:00
}
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
configuration . volume = ` ${ id } - ${ type } -data:/var/lib/postgresql ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if ( type === 'redis' ) {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 6379 ,
2022-08-12 11:48:38 +02:00
command : undefined ,
2022-07-06 11:02:36 +02:00
environmentVariables : {
REDIS_PASSWORD : dbUserPassword ,
REDIS_AOF_ENABLED : appendOnly ? 'yes' : 'no'
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/redis/data ` ,
ulimits : { }
} ;
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
configuration . volume = ` ${ id } - ${ type } -data:/data ` ;
configuration . command = ` /usr/local/bin/redis-server --appendonly ${ appendOnly ? 'yes' : 'no' } --requirepass ${ dbUserPassword } ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if ( type === 'couchdb' ) {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort : 5984 ,
environmentVariables : {
COUCHDB_PASSWORD : dbUserPassword ,
COUCHDB_USER : dbUser
} ,
image : ` ${ baseImage } : ${ version } ` ,
volume : ` ${ id } - ${ type } -data:/bitnami/couchdb ` ,
ulimits : { }
} ;
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
configuration . volume = ` ${ id } - ${ type } -data:/opt/couchdb/data ` ;
}
return configuration
2022-07-06 11:02:36 +02:00
}
}
2022-08-12 11:48:38 +02:00
export function isARM ( arch ) {
if ( arch === 'arm' || arch === 'arm64' ) {
return true
}
return false
}
export function getDatabaseImage ( type : string , arch : string ) : string {
2022-07-06 11:02:36 +02:00
const found = supportedDatabaseTypesAndVersions . find ( ( t ) = > t . name === type ) ;
if ( found ) {
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
return found . baseImageARM || found . baseImage
}
2022-07-06 11:02:36 +02:00
return found . baseImage ;
}
return '' ;
}
2022-08-12 11:48:38 +02:00
export function getDatabaseVersions ( type : string , arch : string ) : string [ ] {
2022-07-06 11:02:36 +02:00
const found = supportedDatabaseTypesAndVersions . find ( ( t ) = > t . name === type ) ;
if ( found ) {
2022-08-12 11:54:06 +02:00
if ( isARM ( arch ) ) {
2022-08-12 11:48:38 +02:00
return found . versionsARM || found . versions
}
2022-07-06 11:02:36 +02:00
return found . versions ;
}
return [ ] ;
}
export type ComposeFile = {
version : ComposerFileVersion ;
services : Record < string , ComposeFileService > ;
networks : Record < string , ComposeFileNetwork > ;
volumes? : Record < string , ComposeFileVolume > ;
} ;
export type ComposeFileService = {
container_name : string ;
image? : string ;
networks : string [ ] ;
environment? : Record < string , unknown > ;
volumes? : string [ ] ;
ulimits? : unknown ;
labels? : string [ ] ;
env_file? : string [ ] ;
extra_hosts? : string [ ] ;
restart : ComposeFileRestartOption ;
depends_on? : string [ ] ;
command? : string ;
ports? : string [ ] ;
build ? : {
context : string ;
dockerfile : string ;
args? : Record < string , unknown > ;
2022-07-14 12:47:26 +00:00
} | string ;
2022-07-06 11:02:36 +02:00
deploy ? : {
restart_policy ? : {
condition? : string ;
delay? : string ;
max_attempts? : number ;
window ? : string ;
} ;
} ;
} ;
export type ComposerFileVersion =
| '3.8'
| '3.7'
| '3.6'
| '3.5'
| '3.4'
| '3.3'
| '3.2'
| '3.1'
| '3.0'
| '2.4'
| '2.3'
| '2.2'
| '2.1'
| '2.0' ;
export type ComposeFileRestartOption = 'no' | 'always' | 'on-failure' | 'unless-stopped' ;
export type ComposeFileNetwork = {
external : boolean ;
} ;
export type ComposeFileVolume = {
external? : boolean ;
name? : string ;
} ;
export async function makeLabelForStandaloneDatabase ( { id , image , volume } ) {
const database = await prisma . database . findFirst ( { where : { id } } ) ;
delete database . destinationDockerId ;
delete database . createdAt ;
delete database . updatedAt ;
return [
'coolify.managed=true' ,
` coolify.version= ${ version } ` ,
` coolify.type=standalone-database ` ,
` coolify.configuration= ${ base64Encode (
JSON . stringify ( {
version ,
image ,
volume ,
. . . database
} )
) } `
] ;
}
export const createDirectories = async ( {
repository ,
buildId
} : {
repository : string ;
buildId : string ;
} ) : Promise < { workdir : string ; repodir : string } > = > {
const repodir = ` /tmp/build-sources/ ${ repository } / ` ;
const workdir = ` /tmp/build-sources/ ${ repository } / ${ buildId } ` ;
await asyncExecShell ( ` mkdir -p ${ workdir } ` ) ;
return {
workdir ,
repodir
} ;
} ;
export async function stopDatabaseContainer (
database : any
) : Promise < boolean > {
let everStarted = false ;
const {
id ,
destinationDockerId ,
2022-07-20 13:35:26 +00:00
destinationDocker : { engine , id : dockerId }
2022-07-06 11:02:36 +02:00
} = database ;
if ( destinationDockerId ) {
try {
2022-07-20 13:35:26 +00:00
const { stdout } = await executeDockerCmd ( { dockerId , command : ` docker inspect --format '{{json .State}}' ${ id } ` } )
2022-07-06 11:02:36 +02:00
if ( stdout ) {
everStarted = true ;
2022-07-20 13:35:26 +00:00
await removeContainer ( { id , dockerId } ) ;
2022-07-06 11:02:36 +02:00
}
} catch ( error ) {
//
}
}
return everStarted ;
}
export async function stopTcpHttpProxy (
id : string ,
destinationDocker : any ,
publicPort : number ,
forceName : string = null
) : Promise < { stdout : string ; stderr : string } | Error > {
2022-07-20 13:35:26 +00:00
const { id : dockerId } = destinationDocker ;
let container = ` ${ id } - ${ publicPort } ` ;
if ( forceName ) container = forceName ;
const found = await checkContainer ( { dockerId , container } ) ;
2022-07-06 11:02:36 +02:00
try {
if ( found ) {
2022-07-20 13:35:26 +00:00
return await executeDockerCmd ( {
dockerId ,
command :
` docker stop -t 0 ${ container } && docker rm ${ container } `
} ) ;
2022-07-06 11:02:36 +02:00
}
} catch ( error ) {
return error ;
}
}
export async function updatePasswordInDb ( database , user , newPassword , isRoot ) {
const {
id ,
type ,
rootUser ,
rootUserPassword ,
dbUser ,
dbUserPassword ,
defaultDatabase ,
destinationDockerId ,
2022-07-20 13:53:39 +00:00
destinationDocker : { id : dockerId }
2022-07-06 11:02:36 +02:00
} = database ;
if ( destinationDockerId ) {
if ( type === 'mysql' ) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } mysql -u ${ rootUser } -p ${ rootUserPassword } -e \ "ALTER USER ' ${ user } '@'%' IDENTIFIED WITH caching_sha2_password BY ' ${ newPassword } '; \ " `
} )
2022-07-06 11:02:36 +02:00
} else if ( type === 'mariadb' ) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } mysql -u ${ rootUser } -p ${ rootUserPassword } -e \ "SET PASSWORD FOR ' ${ user } '@'%' = PASSWORD(' ${ newPassword } '); \ " `
} )
2022-07-06 11:02:36 +02:00
} else if ( type === 'postgresql' ) {
if ( isRoot ) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } psql postgresql://postgres: ${ rootUserPassword } @ ${ id } :5432/ ${ defaultDatabase } -c "ALTER role postgres WITH PASSWORD ' ${ newPassword } '" `
} )
2022-07-06 11:02:36 +02:00
} else {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } psql postgresql:// ${ dbUser } : ${ dbUserPassword } @ ${ id } :5432/ ${ defaultDatabase } -c "ALTER role ${ user } WITH PASSWORD ' ${ newPassword } '" `
} )
2022-07-06 11:02:36 +02:00
}
} else if ( type === 'mongodb' ) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } mongo 'mongodb:// ${ rootUser } : ${ rootUserPassword } @ ${ id } :27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword(' ${ user } ',' ${ newPassword } ')" `
} )
2022-07-06 11:02:36 +02:00
} else if ( type === 'redis' ) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker exec ${ id } redis-cli -u redis:// ${ dbUserPassword } @ ${ id } :6379 --raw CONFIG SET requirepass ${ newPassword } `
} )
2022-07-06 11:02:36 +02:00
}
}
}
2022-07-22 12:01:07 +00:00
export async function getFreeExposedPort ( id , exposePort , dockerId , remoteIpAddress ) {
2022-07-22 08:57:11 +00:00
const { default : getPort } = await import ( 'get-port' ) ;
const applicationUsed = await (
await prisma . application . findMany ( {
2022-07-22 12:01:07 +00:00
where : { exposePort : { not : null } , id : { not : id } , destinationDockerId : dockerId } ,
2022-07-22 08:57:11 +00:00
select : { exposePort : true }
} )
) . map ( ( a ) = > a . exposePort ) ;
const serviceUsed = await (
await prisma . service . findMany ( {
2022-07-22 12:01:07 +00:00
where : { exposePort : { not : null } , id : { not : id } , destinationDockerId : dockerId } ,
2022-07-22 08:57:11 +00:00
select : { exposePort : true }
} )
) . map ( ( a ) = > a . exposePort ) ;
const usedPorts = [ . . . applicationUsed , . . . serviceUsed ] ;
2022-07-22 12:01:07 +00:00
if ( remoteIpAddress ) {
const { default : checkPort } = await import ( 'is-port-reachable' ) ;
const found = await checkPort ( exposePort , { host : remoteIpAddress } ) ;
if ( ! found ) {
return exposePort
}
return false
}
return await getPort ( { port : Number ( exposePort ) , exclude : usedPorts } ) ;
2022-07-22 08:57:11 +00:00
}
2022-07-22 12:01:07 +00:00
export async function getFreePublicPort ( id , dockerId ) {
2022-07-06 11:02:36 +02:00
const { default : getPort , portNumbers } = await import ( 'get-port' ) ;
const data = await prisma . setting . findFirst ( ) ;
const { minPort , maxPort } = data ;
const dbUsed = await (
await prisma . database . findMany ( {
2022-07-22 12:01:07 +00:00
where : { publicPort : { not : null } , id : { not : id } , destinationDockerId : dockerId } ,
2022-07-06 11:02:36 +02:00
select : { publicPort : true }
} )
) . map ( ( a ) = > a . publicPort ) ;
const wpFtpUsed = await (
await prisma . wordpress . findMany ( {
2022-07-22 12:01:07 +00:00
where : { ftpPublicPort : { not : null } , id : { not : id } , service : { destinationDockerId : dockerId } } ,
2022-07-06 11:02:36 +02:00
select : { ftpPublicPort : true }
} )
) . map ( ( a ) = > a . ftpPublicPort ) ;
const wpUsed = await (
await prisma . wordpress . findMany ( {
2022-07-22 12:01:07 +00:00
where : { mysqlPublicPort : { not : null } , id : { not : id } , service : { destinationDockerId : dockerId } } ,
2022-07-06 11:02:36 +02:00
select : { mysqlPublicPort : true }
} )
) . map ( ( a ) = > a . mysqlPublicPort ) ;
const minioUsed = await (
await prisma . minio . findMany ( {
2022-07-22 12:01:07 +00:00
where : { publicPort : { not : null } , id : { not : id } , service : { destinationDockerId : dockerId } } ,
2022-07-06 11:02:36 +02:00
select : { publicPort : true }
} )
) . map ( ( a ) = > a . publicPort ) ;
const usedPorts = [ . . . dbUsed , . . . wpFtpUsed , . . . wpUsed , . . . minioUsed ] ;
return await getPort ( { port : portNumbers ( minPort , maxPort ) , exclude : usedPorts } ) ;
}
export async function startTraefikTCPProxy (
destinationDocker : any ,
id : string ,
publicPort : number ,
privatePort : number ,
type ? : string
) : Promise < { stdout : string ; stderr : string } | Error > {
2022-07-22 21:14:12 +00:00
const { network , id : dockerId , remoteEngine } = destinationDocker ;
2022-07-20 13:35:26 +00:00
const container = ` ${ id } - ${ publicPort } ` ;
const found = await checkContainer ( { dockerId , container , remove : true } ) ;
2022-07-25 12:42:10 +00:00
const { ipv4 , ipv6 } = await listSettings ( ) ;
2022-07-22 21:14:12 +00:00
2022-07-06 11:02:36 +02:00
let dependentId = id ;
if ( type === 'wordpressftp' ) dependentId = ` ${ id } -ftp ` ;
2022-07-20 13:35:26 +00:00
const foundDependentContainer = await checkContainer ( { dockerId , container : dependentId , remove : true } ) ;
2022-07-06 11:02:36 +02:00
try {
if ( foundDependentContainer && ! found ) {
2022-07-20 13:35:26 +00:00
const { stdout : Config } = await executeDockerCmd ( {
dockerId ,
command : ` docker network inspect ${ network } --format '{{json .IPAM.Config }}' `
} )
2022-07-06 11:02:36 +02:00
const ip = JSON . parse ( Config ) [ 0 ] . Gateway ;
2022-07-22 21:14:12 +00:00
let traefikUrl = otherTraefikEndpoint
if ( remoteEngine ) {
let ip = null
if ( isDev ) {
ip = getAPIUrl ( )
} else {
2022-07-22 21:34:31 +00:00
ip = ` http:// ${ ipv4 || ipv6 } :3000 `
2022-07-22 21:14:12 +00:00
}
traefikUrl = ` ${ ip } /webhooks/traefik/other.json `
}
2022-07-22 21:28:01 +00:00
console . log ( traefikUrl )
2022-07-06 11:02:36 +02:00
const tcpProxy = {
2022-07-22 12:01:07 +00:00
version : '3.8' ,
2022-07-06 11:02:36 +02:00
services : {
[ ` ${ id } - ${ publicPort } ` ] : {
2022-07-20 13:35:26 +00:00
container_name : container ,
2022-07-22 21:14:12 +00:00
image : defaultTraefikImage ,
2022-07-06 11:02:36 +02:00
command : [
2022-07-22 12:01:07 +00:00
` --entrypoints.tcp.address=: ${ publicPort } ` ,
` --entryPoints.tcp.forwardedHeaders.insecure=true ` ,
2022-07-22 21:21:40 +00:00
` --providers.http.endpoint= ${ traefikUrl } ?id= ${ id } &privatePort= ${ privatePort } &publicPort= ${ publicPort } &type=tcp&address= ${ dependentId } ` ,
2022-07-06 11:02:36 +02:00
'--providers.http.pollTimeout=2s' ,
'--log.level=error'
] ,
2022-07-22 12:01:07 +00:00
ports : [ ` ${ publicPort } : ${ publicPort } ` ] ,
2022-07-20 13:35:26 +00:00
extra_hosts : [ 'host.docker.internal:host-gateway' , ` host.docker.internal: ${ ip } ` ] ,
2022-07-06 11:02:36 +02:00
volumes : [ '/var/run/docker.sock:/var/run/docker.sock' ] ,
networks : [ 'coolify-infra' , network ]
}
} ,
networks : {
[ network ] : {
external : false ,
name : network
} ,
'coolify-infra' : {
external : false ,
name : 'coolify-infra'
}
}
} ;
await fs . writeFile ( ` /tmp/docker-compose- ${ id } .yaml ` , yaml . dump ( tcpProxy ) ) ;
2022-07-20 13:35:26 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker compose -f /tmp/docker-compose- ${ id } .yaml up -d `
} )
2022-07-06 11:02:36 +02:00
await fs . rm ( ` /tmp/docker-compose- ${ id } .yaml ` ) ;
}
if ( ! foundDependentContainer && found ) {
2022-07-20 13:35:26 +00:00
await executeDockerCmd ( {
dockerId ,
command : ` docker stop -t 0 ${ container } && docker rm ${ container } `
} )
2022-07-06 11:02:36 +02:00
}
} catch ( error ) {
console . log ( error ) ;
return error ;
}
}
export async function getServiceFromDB ( { id , teamId } : { id : string ; teamId : string } ) : Promise < any > {
const settings = await prisma . setting . findFirst ( ) ;
const body = await prisma . service . findFirst ( {
where : { id , teams : { some : { id : teamId === '0' ? undefined : teamId } } } ,
include
} ) ;
let { type } = body
type = fixType ( type )
if ( body ? . serviceSecret . length > 0 ) {
body . serviceSecret = body . serviceSecret . map ( ( s ) = > {
s . value = decrypt ( s . value ) ;
return s ;
} ) ;
}
body [ type ] = { . . . body [ type ] , . . . getUpdateableFields ( type , body [ type ] ) }
return { . . . body , settings } ;
}
export function getServiceImage ( type : string ) : string {
const found = supportedServiceTypesAndVersions . find ( ( t ) = > t . name === type ) ;
if ( found ) {
return found . baseImage ;
}
return '' ;
}
export function getServiceImages ( type : string ) : string [ ] {
const found = supportedServiceTypesAndVersions . find ( ( t ) = > t . name === type ) ;
if ( found ) {
return found . images ;
}
return [ ] ;
}
export async function configureServiceType ( {
id ,
type
} : {
id : string ;
type : string ;
} ) : Promise < void > {
if ( type === 'plausibleanalytics' ) {
const password = encrypt ( generatePassword ( ) ) ;
const postgresqlUser = cuid ( ) ;
const postgresqlPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlDatabase = 'plausibleanalytics' ;
const secretKeyBase = encrypt ( generatePassword ( 64 ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
plausibleAnalytics : {
create : {
postgresqlDatabase ,
postgresqlUser ,
postgresqlPassword ,
password ,
secretKeyBase
}
}
}
} ) ;
} else if ( type === 'nocodb' ) {
await prisma . service . update ( {
where : { id } ,
data : { type }
} ) ;
} else if ( type === 'minio' ) {
const rootUser = cuid ( ) ;
const rootUserPassword = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : { type , minio : { create : { rootUser , rootUserPassword } } }
} ) ;
} else if ( type === 'vscodeserver' ) {
const password = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : { type , vscodeserver : { create : { password } } }
} ) ;
} else if ( type === 'wordpress' ) {
const mysqlUser = cuid ( ) ;
const mysqlPassword = encrypt ( generatePassword ( ) ) ;
const mysqlRootUser = cuid ( ) ;
const mysqlRootUserPassword = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
wordpress : { create : { mysqlPassword , mysqlRootUserPassword , mysqlRootUser , mysqlUser } }
}
} ) ;
} else if ( type === 'vaultwarden' ) {
await prisma . service . update ( {
where : { id } ,
data : {
type
}
} ) ;
} else if ( type === 'languagetool' ) {
await prisma . service . update ( {
where : { id } ,
data : {
type
}
} ) ;
} else if ( type === 'n8n' ) {
await prisma . service . update ( {
where : { id } ,
data : {
type
}
} ) ;
} else if ( type === 'uptimekuma' ) {
await prisma . service . update ( {
where : { id } ,
data : {
type
}
} ) ;
} else if ( type === 'ghost' ) {
const defaultEmail = ` ${ cuid ( ) } @example.com ` ;
const defaultPassword = encrypt ( generatePassword ( ) ) ;
const mariadbUser = cuid ( ) ;
const mariadbPassword = encrypt ( generatePassword ( ) ) ;
const mariadbRootUser = cuid ( ) ;
const mariadbRootUserPassword = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
ghost : {
create : {
defaultEmail ,
defaultPassword ,
mariadbUser ,
mariadbPassword ,
mariadbRootUser ,
mariadbRootUserPassword
}
}
}
} ) ;
} else if ( type === 'meilisearch' ) {
const masterKey = encrypt ( generatePassword ( 32 ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
meiliSearch : { create : { masterKey } }
}
} ) ;
} else if ( type === 'umami' ) {
const umamiAdminPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlUser = cuid ( ) ;
const postgresqlPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlDatabase = 'umami' ;
const hashSalt = encrypt ( generatePassword ( 64 ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
umami : {
create : {
umamiAdminPassword ,
postgresqlDatabase ,
postgresqlPassword ,
postgresqlUser ,
hashSalt
}
}
}
} ) ;
} else if ( type === 'hasura' ) {
const postgresqlUser = cuid ( ) ;
const postgresqlPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlDatabase = 'hasura' ;
const graphQLAdminPassword = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
hasura : {
create : {
postgresqlDatabase ,
postgresqlPassword ,
postgresqlUser ,
graphQLAdminPassword
}
}
}
} ) ;
} else if ( type === 'fider' ) {
const postgresqlUser = cuid ( ) ;
const postgresqlPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlDatabase = 'fider' ;
const jwtSecret = encrypt ( generatePassword ( 64 , true ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
fider : {
create : {
postgresqlDatabase ,
postgresqlPassword ,
postgresqlUser ,
jwtSecret
}
}
}
} ) ;
2022-07-15 12:31:54 +00:00
} else if ( type === 'moodle' ) {
const defaultUsername = cuid ( ) ;
const defaultPassword = encrypt ( generatePassword ( ) ) ;
2022-07-20 13:35:26 +00:00
const defaultEmail = ` ${ cuid ( ) } @example.com ` ;
2022-07-15 12:31:54 +00:00
const mariadbUser = cuid ( ) ;
const mariadbPassword = encrypt ( generatePassword ( ) ) ;
const mariadbDatabase = 'moodle_db' ;
const mariadbRootUser = cuid ( ) ;
const mariadbRootUserPassword = encrypt ( generatePassword ( ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
moodle : {
create : {
defaultUsername ,
defaultPassword ,
defaultEmail ,
mariadbUser ,
mariadbPassword ,
mariadbDatabase ,
mariadbRootUser ,
mariadbRootUserPassword
}
}
}
} ) ;
2022-08-15 09:56:34 +00:00
} else if ( type === 'glitchTip' ) {
const defaultUsername = cuid ( ) ;
const defaultEmail = ` ${ defaultUsername } @example.com ` ;
const defaultPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlUser = cuid ( ) ;
const postgresqlPassword = encrypt ( generatePassword ( ) ) ;
const postgresqlDatabase = 'glitchTip' ;
const secretKeyBase = encrypt ( generatePassword ( 64 ) ) ;
await prisma . service . update ( {
where : { id } ,
data : {
type ,
glitchTip : {
create : {
postgresqlDatabase ,
postgresqlUser ,
postgresqlPassword ,
secretKeyBase ,
defaultEmail ,
defaultUsername ,
defaultPassword ,
}
}
}
} ) ;
} else {
2022-07-15 12:31:54 +00:00
await prisma . service . update ( {
where : { id } ,
data : {
type
}
} ) ;
2022-07-06 11:02:36 +02:00
}
}
export async function removeService ( { id } : { id : string } ) : Promise < void > {
await prisma . servicePersistentStorage . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . meiliSearch . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . fider . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . ghost . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . umami . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . hasura . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . plausibleAnalytics . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . minio . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . vscodeserver . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . wordpress . deleteMany ( { where : { serviceId : id } } ) ;
2022-08-15 09:56:34 +00:00
await prisma . glitchTip . deleteMany ( { where : { serviceId : id } } ) ;
2022-07-06 11:02:36 +02:00
await prisma . serviceSecret . deleteMany ( { where : { serviceId : id } } ) ;
await prisma . service . delete ( { where : { id } } ) ;
}
export function saveUpdateableFields ( type : string , data : any ) {
2022-08-11 08:18:17 +00:00
const update = { } ;
2022-07-06 11:02:36 +02:00
if ( type && serviceFields [ type ] ) {
serviceFields [ type ] . map ( ( k ) = > {
let temp = data [ k . name ]
if ( temp ) {
if ( k . isEncrypted ) {
temp = encrypt ( temp )
}
if ( k . isLowerCase ) {
temp = temp . toLowerCase ( )
}
if ( k . isNumber ) {
temp = Number ( temp )
}
if ( k . isBoolean ) {
temp = Boolean ( temp )
}
}
update [ k . name ] = temp
} ) ;
}
return update
}
export function getUpdateableFields ( type : string , data : any ) {
2022-08-11 08:18:17 +00:00
const update = { } ;
2022-07-06 11:02:36 +02:00
if ( type && serviceFields [ type ] ) {
serviceFields [ type ] . map ( ( k ) = > {
let temp = data [ k . name ]
if ( temp ) {
if ( k . isEncrypted ) {
temp = decrypt ( temp )
}
update [ k . name ] = temp
}
update [ k . name ] = temp
} ) ;
}
return update
}
export function fixType ( type ) {
// Hack to fix the type case sensitivity...
if ( type === 'plausibleanalytics' ) type = 'plausibleAnalytics' ;
if ( type === 'meilisearch' ) type = 'meiliSearch' ;
return type
}
export const getServiceMainPort = ( service : string ) = > {
const serviceType = supportedServiceTypesAndVersions . find ( ( s ) = > s . name === service ) ;
if ( serviceType ) {
return serviceType . ports . main ;
}
return null ;
} ;
export function makeLabelForServices ( type ) {
return [
'coolify.managed=true' ,
2022-07-20 13:35:26 +00:00
` coolify.version = ${ version } ` ,
` coolify.type = service ` ,
` coolify.service.type = ${ type } `
2022-07-06 11:02:36 +02:00
] ;
}
export function errorHandler ( { status = 500 , message = 'Unknown error.' } : { status : number , message : string | any } ) {
if ( message . message ) message = message . message
throw { status , message } ;
}
export async function generateSshKeyPair ( ) : Promise < { publicKey : string ; privateKey : string } > {
return await new Promise ( ( resolve , reject ) = > {
forge . pki . rsa . generateKeyPair ( { bits : 4096 , workers : - 1 } , function ( err , keys ) {
if ( keys ) {
resolve ( {
publicKey : forge.ssh.publicKeyToOpenSSH ( keys . publicKey ) ,
privateKey : forge.ssh.privateKeyToOpenSSH ( keys . privateKey )
} ) ;
} else {
reject ( keys ) ;
}
} ) ;
} ) ;
}
export async function stopBuild ( buildId , applicationId ) {
let count = 0 ;
await new Promise < void > ( async ( resolve , reject ) = > {
const { destinationDockerId , status } = await prisma . build . findFirst ( { where : { id : buildId } } ) ;
2022-07-20 13:35:26 +00:00
const { engine , id : dockerId } = await prisma . destinationDocker . findFirst ( { where : { id : destinationDockerId } } ) ;
2022-08-11 08:18:17 +00:00
const interval = setInterval ( async ( ) = > {
2022-07-06 11:02:36 +02:00
try {
if ( status === 'failed' ) {
clearInterval ( interval ) ;
return resolve ( ) ;
}
if ( count > 100 ) {
clearInterval ( interval ) ;
return reject ( new Error ( 'Build canceled' ) ) ;
}
2022-07-25 12:42:10 +00:00
const { stdout : buildContainers } = await executeDockerCmd ( { dockerId , command : ` docker container ls--filter "label=coolify.buildId= ${ buildId } " --format '{{json .}}' ` } )
2022-07-06 11:02:36 +02:00
if ( buildContainers ) {
const containersArray = buildContainers . trim ( ) . split ( '\n' ) ;
for ( const container of containersArray ) {
const containerObj = JSON . parse ( container ) ;
const id = containerObj . ID ;
2022-07-20 13:35:26 +00:00
if ( ! containerObj . Names . startsWith ( ` ${ applicationId } ` ) ) {
await removeContainer ( { id , dockerId } ) ;
2022-07-06 11:02:36 +02:00
await cleanupDB ( buildId ) ;
clearInterval ( interval ) ;
return resolve ( ) ;
}
}
}
count ++ ;
} catch ( error ) { }
} , 100 ) ;
} ) ;
}
async function cleanupDB ( buildId : string ) {
const data = await prisma . build . findUnique ( { where : { id : buildId } } ) ;
if ( data ? . status === 'queued' || data ? . status === 'running' ) {
await prisma . build . update ( { where : { id : buildId } , data : { status : 'failed' } } ) ;
}
2022-07-13 13:40:41 +00:00
}
export function convertTolOldVolumeNames ( type ) {
if ( type === 'nocodb' ) {
return 'nc'
}
}
2022-07-15 09:18:16 +00:00
// export async function getAvailableServices(): Promise<any> {
// const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`)
// return data
2022-07-22 20:23:16 +00:00
//
2022-07-22 15:16:51 +00:00
export async function cleanupDockerStorage ( dockerId , lowDiskSpace , force ) {
2022-07-13 13:40:41 +00:00
// Cleanup old coolify images
try {
2022-07-22 15:16:51 +00:00
let { stdout : images } = await executeDockerCmd ( { dockerId , command : ` docker images coollabsio/coolify --filter before="coollabsio/coolify: ${ version } " -q | xargs ` } )
2022-07-13 13:40:41 +00:00
images = images . trim ( ) ;
if ( images ) {
2022-07-22 15:16:51 +00:00
await executeDockerCmd ( { dockerId , command : ` docker rmi -f ${ images } " -q | xargs ` } )
2022-07-13 13:40:41 +00:00
}
} catch ( error ) {
//console.log(error);
}
if ( lowDiskSpace || force ) {
if ( isDev ) {
if ( ! force ) console . log ( ` [DEV MODE] Low disk space: ${ lowDiskSpace } ` ) ;
return
}
try {
2022-08-11 12:08:44 +00:00
await executeDockerCmd ( { dockerId , command : ` docker container prune -f --filter "label=coolify.managed=true" ` } )
2022-07-13 13:40:41 +00:00
} catch ( error ) {
//console.log(error);
}
try {
2022-07-22 15:16:51 +00:00
await executeDockerCmd ( { dockerId , command : ` docker image prune -f ` } )
2022-07-13 13:40:41 +00:00
} catch ( error ) {
//console.log(error);
}
try {
2022-07-22 15:16:51 +00:00
await executeDockerCmd ( { dockerId , command : ` docker image prune -a -f ` } )
2022-07-13 13:40:41 +00:00
} catch ( error ) {
//console.log(error);
}
}
2022-08-08 11:54:05 +00:00
}
export function persistentVolumes ( id , persistentStorage , config ) {
const persistentVolume =
persistentStorage ? . map ( ( storage ) = > {
return ` ${ id } ${ storage . path . replace ( /\//gi , '-' ) } : ${ storage . path } ` ;
} ) || [ ] ;
2022-08-12 09:38:02 +02:00
let volumes = [ . . . persistentVolume ]
2022-08-10 11:55:27 +00:00
if ( config . volume ) volumes = [ config . volume , . . . volumes ]
const composeVolumes = volumes . length > 0 && volumes . map ( ( volume ) = > {
2022-08-08 11:54:05 +00:00
return {
[ ` ${ volume . split ( ':' ) [ 0 ] } ` ] : {
name : volume.split ( ':' ) [ 0 ]
}
} ;
2022-08-10 11:55:27 +00:00
} ) || [ ]
const volumeMounts = config . volume && Object . assign (
2022-08-08 11:54:05 +00:00
{ } ,
{
[ config . volume . split ( ':' ) [ 0 ] ] : {
name : config.volume.split ( ':' ) [ 0 ]
}
} ,
. . . composeVolumes
2022-08-10 11:55:27 +00:00
) || { }
2022-08-08 11:54:05 +00:00
return { volumes , volumeMounts }
2022-07-06 11:02:36 +02:00
}