2015-07-20 00:09:47 -07:00
'use strict' ;
2015-10-19 11:24:21 -07:00
exports = module . exports = {
2020-11-18 23:24:34 -08:00
ping ,
info ,
2022-10-11 22:38:26 +02:00
df ,
2020-11-18 23:24:34 -08:00
downloadImage ,
createContainer ,
startContainer ,
restartContainer ,
stopContainer ,
stopContainers ,
deleteContainer ,
deleteImage ,
deleteContainers ,
createSubcontainer ,
inspect ,
getContainerIp ,
getEvents ,
2025-05-21 15:37:31 +02:00
stats ,
2022-05-16 10:26:30 -07:00
2021-08-19 12:32:23 -07:00
update ,
2022-05-16 10:26:30 -07:00
2024-12-14 14:00:05 +01:00
parseImageRef ,
2023-08-08 10:42:16 +05:30
2022-05-16 10:26:30 -07:00
createExec ,
startExec ,
getExec ,
resizeExec
2015-10-19 11:24:21 -07:00
} ;
2015-10-19 11:08:23 -07:00
2021-01-21 11:31:35 -08:00
const apps = require ( './apps.js' ) ,
2016-04-18 16:30:58 -07:00
assert = require ( 'assert' ) ,
2019-09-23 12:13:21 -07:00
BoxError = require ( './boxerror.js' ) ,
2016-04-18 16:30:58 -07:00
constants = require ( './constants.js' ) ,
2023-08-11 19:41:05 +05:30
dashboard = require ( './dashboard.js' ) ,
2019-12-06 13:52:43 -08:00
debug = require ( 'debug' ) ( 'box:docker' ) ,
2019-12-04 13:17:58 -08:00
Docker = require ( 'dockerode' ) ,
2025-05-07 14:09:10 +02:00
dockerRegistries = require ( './dockerregistries.js' ) ,
2024-02-20 23:09:49 +01:00
fs = require ( 'fs' ) ,
2025-03-08 12:04:13 +01:00
mailServer = require ( './mailserver.js' ) ,
2024-04-10 17:38:49 +02:00
os = require ( 'os' ) ,
2022-11-28 22:32:34 +01:00
paths = require ( './paths.js' ) ,
2023-03-09 21:06:54 +01:00
promiseRetry = require ( './promise-retry.js' ) ,
2021-01-21 11:31:35 -08:00
services = require ( './services.js' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'docker' ) ,
2019-08-06 09:45:16 -07:00
safe = require ( 'safetydance' ) ,
2023-05-14 10:53:50 +02:00
timers = require ( 'timers/promises' ) ,
2023-05-25 11:27:23 +02:00
volumes = require ( './volumes.js' ) ;
2016-04-18 16:30:58 -07:00
2025-05-07 14:09:10 +02:00
const gConnection = new Docker ( { socketPath : paths . DOCKER _SOCKET _PATH } ) ;
2019-10-22 22:07:44 -07:00
2024-12-14 14:53:08 +01:00
function parseImageRef ( imageRef ) {
assert . strictEqual ( typeof imageRef , 'string' ) ;
// a ref is like registry.docker.com/cloudron/base:4.2.0@sha256:46da2fffb36353ef714f97ae8e962bd2c212ca091108d768ba473078319a47f4
2025-01-03 10:09:00 +01:00
// registry.docker.com is registry name . cloudron is (optional) namespace . base is image name . cloudron/base is repository path
2024-12-14 14:53:08 +01:00
// registry.docker.com/cloudron/base is fullRepositoryName
const result = { fullRepositoryName : null , registry : null , tag : null , digest : null } ;
result . fullRepositoryName = imageRef . split ( /[:@]/ ) [ 0 ] ;
const parts = result . fullRepositoryName . split ( '/' ) ;
2025-01-03 10:09:00 +01:00
result . registry = parts [ 0 ] . includes ( '.' ) ? parts [ 0 ] : null ; // https://docs.docker.com/admin/faqs/general-faqs/#what-is-a-docker-id
2024-12-14 14:53:08 +01:00
let remaining = imageRef . substr ( result . fullRepositoryName . length ) ;
if ( remaining . startsWith ( ':' ) ) {
result . tag = remaining . substr ( 1 ) . split ( '@' , 1 ) [ 0 ] ;
remaining = remaining . substr ( result . tag . length + 1 ) ; // also ':'
}
if ( remaining . startsWith ( '@sha256:' ) ) result . digest = remaining . substr ( 8 ) ;
return result ;
}
2021-08-26 21:14:49 -07:00
async function ping ( ) {
2018-11-23 15:49:47 +01:00
// do not let the request linger
2025-05-07 14:09:10 +02:00
const connection = new Docker ( { socketPath : paths . DOCKER _SOCKET _PATH , timeout : 1000 } ) ;
2018-11-19 10:19:46 +01:00
2021-08-26 21:14:49 -07:00
const [ error , result ] = await safe ( connection . ping ( ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
if ( Buffer . isBuffer ( result ) && result . toString ( 'utf8' ) === 'OK' ) return ; // sometimes it returns buffer
if ( result === 'OK' ) return ;
2018-11-19 10:19:46 +01:00
2021-08-26 21:14:49 -07:00
throw new BoxError ( BoxError . DOCKER _ERROR , 'Unable to ping the docker daemon' ) ;
2018-11-19 10:19:46 +01:00
}
2024-12-14 14:53:08 +01:00
async function getAuthConfig ( imageRef ) {
assert . strictEqual ( typeof imageRef , 'string' ) ;
const parsedRef = parseImageRef ( imageRef ) ;
2024-12-14 14:04:40 +01:00
2024-12-14 14:21:15 +01:00
// images in our cloudron namespace are always unauthenticated to not interfere with any user limits
2024-12-14 14:53:08 +01:00
if ( parsedRef . registry === null && parsedRef . fullRepositoryName . startsWith ( 'cloudron/' ) ) return null ;
2019-10-27 12:14:27 -07:00
2025-05-07 14:09:10 +02:00
const registries = await dockerRegistries . list ( ) ;
2024-12-14 14:21:15 +01:00
2025-05-07 14:09:10 +02:00
for ( const registry of registries ) {
if ( registry . serverAddress !== parsedRef . registry ) { // ideally they match but there's too many docker registry domains!
if ( ! registry . serverAddress . includes ( '.docker.' ) ) continue ;
if ( parsedRef . registry !== null && ! parsedRef . includes ( '.docker.' ) ) continue ;
}
2019-10-27 12:14:27 -07:00
2025-05-07 14:09:10 +02:00
// https://github.com/apocas/dockerode#pull-from-private-repos
const authConfig = {
username : registry . username ,
password : registry . password ,
auth : registry . auth || '' , // the auth token at login time
email : registry . email || '' ,
serveraddress : registry . serverAddress
} ;
return authConfig ;
}
2015-10-19 11:40:19 -07:00
2025-05-07 14:09:10 +02:00
return null ;
2021-08-25 19:41:46 -07:00
}
2015-10-19 11:40:19 -07:00
2024-12-14 14:53:08 +01:00
async function pullImage ( imageRef ) {
assert . strictEqual ( typeof imageRef , 'string' ) ;
2019-10-27 12:14:27 -07:00
2024-12-14 14:53:08 +01:00
const authConfig = await getAuthConfig ( imageRef ) ;
2019-10-27 12:14:27 -07:00
2024-12-14 14:53:08 +01:00
debug ( ` pullImage: will pull ${ imageRef } . auth: ${ authConfig ? 'yes' : 'no' } ` ) ;
2024-12-14 14:05:53 +01:00
2024-12-14 14:53:08 +01:00
const [ error , stream ] = await safe ( gConnection . pull ( imageRef , { authconfig : authConfig } ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to pull image ${ imageRef } . message: ${ error . message } statusCode: ${ error . statusCode } ` ) ;
2024-12-14 14:21:15 +01:00
// toomanyrequests is flagged as a 500. dockerhub appears to have 10 pulls her hour per IP limit
2024-12-14 14:53:08 +01:00
if ( error && error . statusCode === 500 ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Unable to pull image ${ imageRef } . registry error: ${ JSON . stringify ( error ) } ` ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Unable to pull image ${ imageRef } . Please check the network or if the image needs authentication. statusCode: ${ error . statusCode } ` ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
return new Promise ( ( resolve , reject ) => {
2023-03-09 21:06:54 +01:00
// https://github.com/dotcloud/docker/issues/1074 says each status message is emitted as a chunk
let layerError = null ;
2021-08-25 19:41:46 -07:00
stream . on ( 'data' , function ( chunk ) {
2022-04-14 17:41:41 -05:00
const data = safe . JSON . parse ( chunk ) || { } ;
2021-08-25 19:41:46 -07:00
debug ( 'pullImage: %j' , data ) ;
2019-10-27 12:14:27 -07:00
2021-08-25 19:41:46 -07:00
// The data.status here is useless because this is per layer as opposed to per image
2023-03-09 21:06:54 +01:00
if ( ! data . status && data . error ) { // data is { errorDetail: { message: xx } , error: xx }
2024-12-14 14:53:08 +01:00
debug ( ` pullImage error ${ imageRef } : ${ data . errorDetail . message } ` ) ;
2023-03-09 21:06:54 +01:00
layerError = data . errorDetail ;
2021-08-25 19:41:46 -07:00
}
} ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
stream . on ( 'end' , function ( ) {
2024-12-14 14:53:08 +01:00
debug ( ` downloaded image ${ imageRef } . error: ${ ! ! layerError } ` ) ;
2023-03-09 21:06:54 +01:00
if ( ! layerError ) return resolve ( ) ;
2023-10-09 07:38:39 +05:30
reject ( new BoxError ( layerError . message . includes ( 'no space' ) ? BoxError . FS _ERROR : BoxError . DOCKER _ERROR , layerError . message ) ) ;
2021-08-25 19:41:46 -07:00
} ) ;
2015-10-19 11:40:19 -07:00
2023-03-09 21:06:54 +01:00
stream . on ( 'error' , function ( error ) { // this is only hit for stream error and not for some download error
2024-12-14 14:53:08 +01:00
debug ( ` error pulling image ${ imageRef } : %o ` , error ) ;
2021-08-25 19:41:46 -07:00
reject ( new BoxError ( BoxError . DOCKER _ERROR , error . message ) ) ;
2015-10-19 11:40:19 -07:00
} ) ;
} ) ;
}
2021-08-25 19:41:46 -07:00
async function downloadImage ( manifest ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof manifest , 'object' ) ;
2024-12-14 14:53:08 +01:00
debug ( ` downloadImage: ${ manifest . dockerImage } ` ) ;
2015-10-19 11:40:19 -07:00
2020-10-19 22:11:23 -07:00
const image = gConnection . getImage ( manifest . dockerImage ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( image . inspect ( ) ) ;
if ( ! error && result ) return ; // image is already present locally
2015-10-19 11:40:19 -07:00
2024-12-14 14:53:08 +01:00
const parsedManifestRef = parseImageRef ( manifest . dockerImage ) ;
await promiseRetry ( { times : 10 , interval : 5000 , debug , retry : ( pullError ) => pullError . reason !== BoxError . FS _ERROR } , async ( ) => {
2024-12-14 23:28:00 +01:00
if ( parsedManifestRef . registry !== null || ! parsedManifestRef . fullRepositoryName . startsWith ( 'cloudron/' ) ) return await pullImage ( manifest . dockerImage ) ;
2024-12-14 14:53:08 +01:00
2024-12-14 18:55:52 +01:00
let upstreamRef = null ;
for ( const registry of [ 'registry.docker.com' , 'registry.ipv4.docker.com' , 'quay.io' ] ) {
upstreamRef = ` ${ registry } / ${ manifest . dockerImage } ` ;
const [ pullError ] = await safe ( pullImage ( upstreamRef ) ) ;
if ( ! pullError ) break ;
2024-12-14 14:53:08 +01:00
}
2024-12-14 18:55:52 +01:00
if ( ! upstreamRef ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Unable to pull image ${ manifest . dockerImage } from dockerhub or quay ` ) ;
2024-12-14 14:53:08 +01:00
// retag the downloaded image to not have the registry name. this prevents 'docker run' from redownloading it
debug ( ` downloadImage: tagging ${ upstreamRef } as ${ parsedManifestRef . fullRepositoryName } : ${ parsedManifestRef . tag } ` ) ;
await gConnection . getImage ( upstreamRef ) . tag ( { repo : parsedManifestRef . fullRepositoryName , tag : parsedManifestRef . tag } ) ;
debug ( ` downloadImage: untagging ${ upstreamRef } ` ) ;
await deleteImage ( upstreamRef ) ;
2023-03-09 21:06:54 +01:00
} ) ;
2015-10-19 11:40:19 -07:00
}
2021-05-11 17:50:48 -07:00
async function getVolumeMounts ( app ) {
2020-04-29 21:55:21 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2020-10-27 22:39:05 -07:00
2021-05-11 17:50:48 -07:00
if ( app . mounts . length === 0 ) return [ ] ;
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
const result = await volumes . list ( ) ;
2024-06-06 15:22:33 +02:00
const volumesById = { } ;
2021-05-11 17:50:48 -07:00
result . forEach ( r => volumesById [ r . id ] = r ) ;
2020-10-27 22:39:05 -07:00
2024-06-06 15:22:33 +02:00
const mounts = [ ] ;
2021-05-11 17:50:48 -07:00
for ( const mount of app . mounts ) {
const volume = volumesById [ mount . volumeId ] ;
2021-02-17 22:53:50 -08:00
2021-05-11 17:50:48 -07:00
mounts . push ( {
Source : volume . hostPath ,
Target : ` /media/ ${ volume . name } ` ,
Type : 'bind' ,
ReadOnly : mount . readOnly
} ) ;
}
2020-04-29 21:55:21 -07:00
2021-05-11 17:50:48 -07:00
return mounts ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getAddonMounts ( app ) {
2021-02-17 22:53:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2024-06-06 15:22:33 +02:00
const mounts = [ ] ;
2021-02-17 22:53:50 -08:00
const addons = app . manifest . addons ;
2021-08-25 19:41:46 -07:00
if ( ! addons ) return mounts ;
2021-02-17 22:53:50 -08:00
2021-08-25 19:41:46 -07:00
for ( const addon of Object . keys ( addons ) ) {
2021-02-17 22:53:50 -08:00
switch ( addon ) {
2022-06-06 15:59:50 -07:00
case 'localstorage' : {
const storageDir = await apps . getStorageDir ( app ) ;
2021-02-17 22:53:50 -08:00
mounts . push ( {
Target : '/app/data' ,
2022-06-06 15:59:50 -07:00
Source : storageDir ,
Type : 'bind' ,
2021-02-17 22:53:50 -08:00
ReadOnly : false
} ) ;
2021-08-25 19:41:46 -07:00
break ;
2022-06-06 15:59:50 -07:00
}
2021-08-17 14:04:29 -07:00
case 'tls' : {
2022-11-28 22:32:34 +01:00
const certificateDir = ` ${ paths . PLATFORM _DATA _DIR } /tls/ ${ app . id } ` ;
2021-08-17 14:04:29 -07:00
mounts . push ( {
2022-11-28 22:32:34 +01:00
Target : '/etc/certs' ,
Source : certificateDir ,
2021-08-17 14:04:29 -07:00
Type : 'bind' ,
ReadOnly : true
2021-02-17 22:53:50 -08:00
} ) ;
2021-08-25 19:41:46 -07:00
break ;
2021-08-17 14:04:29 -07:00
}
2021-02-17 22:53:50 -08:00
default :
2021-08-25 19:41:46 -07:00
break ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
}
return mounts ;
2021-02-17 22:53:50 -08:00
}
2021-08-25 19:41:46 -07:00
async function getMounts ( app ) {
2021-02-17 22:53:50 -08:00
assert . strictEqual ( typeof app , 'object' ) ;
2021-08-25 19:41:46 -07:00
const volumeMounts = await getVolumeMounts ( app ) ;
const addonMounts = await getAddonMounts ( app ) ;
return volumeMounts . concat ( addonMounts ) ;
2020-04-29 21:55:21 -07:00
}
2023-12-14 17:59:40 +01:00
// This only returns ipv4 addresses
// We dont bind to ipv6 interfaces, public prefix changes and container restarts wont work
2024-02-20 23:09:49 +01:00
async function getAddressesForPort53 ( ) {
const [ error , deviceLinks ] = await safe ( fs . promises . readdir ( '/sys/class/net' ) ) ; // https://man7.org/linux/man-pages/man5/sysfs.5.html
if ( error ) return [ ] ;
2021-08-10 21:59:58 -07:00
const devices = deviceLinks . map ( d => { return { name : d , link : safe . fs . readlinkSync ( ` /sys/class/net/ ${ d } ` ) } ; } ) ;
const physicalDevices = devices . filter ( d => d . link && ! d . link . includes ( 'virtual' ) ) ;
const addresses = [ ] ;
for ( const phy of physicalDevices ) {
2024-11-05 14:24:40 +01:00
const [ error , output ] = await safe ( shell . spawn ( 'ip' , [ '-f' , 'inet' , '-j' , 'addr' , 'show' , 'dev' , phy . name , 'scope' , 'global' ] , { encoding : 'utf8' } ) ) ;
2024-02-20 23:09:49 +01:00
if ( error ) continue ;
const inet = safe . JSON . parse ( output ) || [ ] ;
2022-02-17 11:56:08 -08:00
for ( const r of inet ) {
const address = safe . query ( r , 'addr_info[0].local' ) ;
if ( address ) addresses . push ( address ) ;
}
2020-11-18 11:43:28 -08:00
}
2021-08-10 21:59:58 -07:00
return addresses ;
2020-11-18 11:43:28 -08:00
}
2021-08-25 19:41:46 -07:00
async function createSubcontainer ( app , name , cmd , options ) {
2015-10-19 11:40:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2015-11-02 09:34:31 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
2021-05-02 11:26:08 -07:00
assert ( ! cmd || Array . isArray ( cmd ) ) ;
2020-04-27 22:55:43 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-10-19 11:40:19 -07:00
2024-06-06 15:22:33 +02:00
const isAppContainer = ! cmd ; // non app-containers are like scheduler
2021-08-25 19:41:46 -07:00
const manifest = app . manifest ;
const exposedPorts = { } , dockerPortBindings = { } ;
const domain = app . fqdn ;
2023-08-11 19:41:05 +05:30
const { fqdn : dashboardFqdn } = await dashboard . getLocation ( ) ;
2019-06-03 13:45:03 -07:00
2021-08-25 19:41:46 -07:00
const stdEnv = [
2015-10-19 11:40:19 -07:00
'CLOUDRON=1' ,
2018-11-22 16:50:02 -08:00
'CLOUDRON_PROXY_IP=172.18.0.1' ,
2019-06-26 14:18:39 -07:00
` CLOUDRON_APP_HOSTNAME= ${ app . id } ` ,
2023-08-11 19:41:05 +05:30
` CLOUDRON_WEBADMIN_ORIGIN=https:// ${ dashboardFqdn } ` ,
` CLOUDRON_API_ORIGIN=https:// ${ dashboardFqdn } ` ,
2021-12-06 17:43:50 -08:00
` CLOUDRON_APP_ORIGIN=https:// ${ domain } ` ,
` CLOUDRON_APP_DOMAIN= ${ domain } `
2015-10-19 11:40:19 -07:00
] ;
2023-05-25 11:25:55 +02:00
if ( app . manifest . multiDomain ) stdEnv . push ( ` CLOUDRON_ALIAS_DOMAINS= ${ app . aliasDomains . map ( ad => ad . fqdn ) . join ( ',' ) } ` ) ;
2022-01-21 11:24:51 -08:00
const secondaryDomainsEnv = app . secondaryDomains . map ( sd => ` ${ sd . environmentVariable } = ${ sd . fqdn } ` ) ;
2021-08-25 19:41:46 -07:00
const portEnv = [ ] ;
for ( const portName in app . portBindings ) {
2024-07-16 22:21:36 +02:00
const { hostPort , type : portType , count : portCount } = app . portBindings [ portName ] ;
const portSpec = portType == 'tcp' ? manifest . tcpPorts : manifest . udpPorts ;
const containerPort = portSpec [ portName ] . containerPort || hostPort ;
2024-09-18 14:10:33 +02:00
// port 53 is special. systemd-resolved is listening on 127.0.0.x port 53 and another process cannot listen to 0.0.0.0 port 53
// for port 53 alone, we listen explicitly on the server's interface IP
const hostIps = hostPort === 53 ? await getAddressesForPort53 ( ) : [ '0.0.0.0' , '::0' ] ;
2018-08-12 22:47:59 -07:00
2018-08-12 19:33:11 -07:00
portEnv . push ( ` ${ portName } = ${ hostPort } ` ) ;
2024-02-09 15:49:29 +01:00
if ( portCount > 1 ) portEnv . push ( ` ${ portName } _COUNT= ${ portCount } ` ) ;
2015-10-19 11:40:19 -07:00
2024-02-06 16:10:34 +01:00
// docker portBindings requires ports to be exposed
for ( let i = 0 ; i < portCount ; ++ i ) {
exposedPorts [ ` ${ containerPort + i } / ${ portType } ` ] = { } ;
2024-07-16 22:21:36 +02:00
dockerPortBindings [ ` ${ containerPort + i } / ${ portType } ` ] = hostIps . map ( hip => { return { HostIp : hip , HostPort : String ( hostPort + i ) } ; } ) ;
2024-02-06 16:10:34 +01:00
}
2015-10-19 11:40:19 -07:00
}
2018-10-11 16:18:38 -07:00
2021-08-25 19:41:46 -07:00
const appEnv = [ ] ;
2018-10-11 14:07:43 -07:00
Object . keys ( app . env ) . forEach ( function ( name ) { appEnv . push ( ` ${ name } = ${ app . env [ name ] } ` ) ; } ) ;
2015-10-19 11:40:19 -07:00
2021-01-20 12:12:14 -08:00
let memoryLimit = apps . getMemoryLimit ( app ) ;
2016-02-11 17:00:21 +01:00
2018-02-27 10:51:21 -08:00
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if ( ! isAppContainer ) memoryLimit *= 2 ;
2021-08-25 19:41:46 -07:00
const mounts = await getMounts ( app ) ;
const addonEnv = await services . getEnvironment ( app ) ;
2022-10-24 22:34:06 +02:00
const runtimeVolumes = {
'/tmp' : { } ,
2022-10-24 23:58:20 +02:00
'/run' : { } ,
2022-10-24 22:34:06 +02:00
} ;
if ( app . manifest . runtimeDirs ) {
app . manifest . runtimeDirs . forEach ( dir => runtimeVolumes [ dir ] = { } ) ;
}
2021-08-25 19:41:46 -07:00
2023-05-25 11:27:23 +02:00
const containerOptions = {
2021-08-25 19:41:46 -07:00
name : name , // for referencing containers
Tty : isAppContainer ,
Image : app . manifest . dockerImage ,
Cmd : ( isAppContainer && app . debugMode && app . debugMode . cmd ) ? app . debugMode . cmd : cmd ,
2022-01-21 11:24:51 -08:00
Env : stdEnv . concat ( addonEnv ) . concat ( portEnv ) . concat ( appEnv ) . concat ( secondaryDomainsEnv ) ,
2021-08-25 19:41:46 -07:00
ExposedPorts : isAppContainer ? exposedPorts : { } ,
2022-10-24 22:34:06 +02:00
Volumes : runtimeVolumes ,
2021-08-25 19:41:46 -07:00
Labels : {
'fqdn' : app . fqdn ,
'appId' : app . id ,
'isSubcontainer' : String ( ! isAppContainer ) ,
'isCloudronManaged' : String ( true )
} ,
HostConfig : {
Mounts : mounts ,
LogConfig : {
Type : 'syslog' ,
Config : {
'tag' : app . id ,
2024-03-21 17:30:50 +01:00
'syslog-address' : ` unix:// ${ paths . SYSLOG _SOCKET _FILE } ` ,
2021-08-25 19:41:46 -07:00
'syslog-format' : 'rfc5424'
}
2021-08-19 21:39:27 -07:00
} ,
2024-04-09 18:59:40 +02:00
Memory : memoryLimit ,
MemorySwap : - 1 , // Unlimited swap
2024-07-16 22:21:36 +02:00
PortBindings : isAppContainer ? dockerPortBindings : { } ,
2021-08-25 19:41:46 -07:00
PublishAllPorts : false ,
ReadonlyRootfs : app . debugMode ? ! ! app . debugMode . readonlyRootfs : true ,
RestartPolicy : {
'Name' : isAppContainer ? 'unless-stopped' : 'no' ,
'MaximumRetryCount' : 0
2021-08-19 21:39:27 -07:00
} ,
2024-04-10 17:38:49 +02:00
// CpuPeriod (100000 microseconds) and CpuQuota(app.cpuQuota% of CpuPeriod)
// 1000000000 is one core https://github.com/moby/moby/issues/24713#issuecomment-233167619 and https://stackoverflow.com/questions/52391877/set-the-number-of-cpu-cores-of-a-container-using-docker-engine-api
2024-08-28 11:45:50 +02:00
NanoCPUs : app . cpuQuota === 100 ? 0 : Math . round ( os . cpus ( ) . length * app . cpuQuota / 100 * 1000000000 ) ,
2021-08-25 19:41:46 -07:00
VolumesFrom : isAppContainer ? null : [ app . containerId + ':rw' ] ,
SecurityOpt : [ 'apparmor=docker-cloudron-app' ] ,
2022-04-28 17:04:23 -07:00
CapAdd : [ ] ,
2022-03-10 11:59:41 -08:00
CapDrop : [ ] ,
2025-03-08 12:04:13 +01:00
Sysctls : { } ,
ExtraHosts : [ ]
2021-08-25 19:41:46 -07:00
}
} ;
2017-08-11 23:22:48 +01:00
2021-08-25 19:41:46 -07:00
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if ( isAppContainer ) {
containerOptions . Hostname = app . id ;
containerOptions . HostConfig . NetworkMode = 'cloudron' ; // user defined bridge network
2024-09-18 14:10:33 +02:00
// Do not inject for AdGuard. It ends up resolving the dashboard domain as the docker bridge IP
2025-03-08 12:04:13 +01:00
if ( manifest . id !== 'com.adguard.home.cloudronapp' ) containerOptions . HostConfig . ExtraHosts . push ( ` ${ dashboardFqdn } :172.18.0.1 ` ) ;
if ( manifest . addons ? . sendmail ? . requiresValidCertificate ) {
const { fqdn : mailFqdn } = await mailServer . getLocation ( ) ;
containerOptions . HostConfig . ExtraHosts . push ( ` ${ mailFqdn } : ${ constants . MAIL _SERVICE _IPv4 } ` ) ;
}
2021-08-25 19:41:46 -07:00
containerOptions . NetworkingConfig = {
EndpointsConfig : {
cloudron : {
IPAMConfig : {
IPv4Address : app . containerIp
} ,
Aliases : [ name ] // adds hostname entry with container name
2021-08-19 21:39:27 -07:00
}
2021-08-25 19:41:46 -07:00
}
} ;
} else {
containerOptions . HostConfig . NetworkMode = ` container: ${ app . containerId } ` ; // scheduler containers must have same IP as app for various addon auth
}
2020-10-27 22:39:05 -07:00
2021-08-25 19:41:46 -07:00
const capabilities = manifest . capabilities || [ ] ;
2020-06-30 07:31:24 -07:00
2021-08-25 19:41:46 -07:00
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
2022-04-28 17:04:23 -07:00
if ( capabilities . includes ( 'net_admin' ) ) {
containerOptions . HostConfig . CapAdd . push ( 'NET_ADMIN' , 'NET_RAW' ) ;
// ipv6 for new interfaces is disabled in the container. this prevents the openvpn tun device having ipv6
// See https://github.com/moby/moby/issues/20569 and https://github.com/moby/moby/issues/33099
containerOptions . HostConfig . Sysctls [ 'net.ipv6.conf.all.disable_ipv6' ] = '0' ;
2022-05-19 17:10:05 -07:00
containerOptions . HostConfig . Sysctls [ 'net.ipv6.conf.all.forwarding' ] = '1' ;
2022-04-28 17:04:23 -07:00
}
if ( capabilities . includes ( 'mlock' ) ) containerOptions . HostConfig . CapAdd . push ( 'IPC_LOCK' ) ; // mlock prevents swapping
if ( ! capabilities . includes ( 'ping' ) ) containerOptions . HostConfig . CapDrop . push ( 'NET_RAW' ) ; // NET_RAW is included by default by Docker
2024-12-05 13:47:59 +01:00
containerOptions . HostConfig . Devices = Object . keys ( app . devices ) . map ( ( d ) => {
if ( ! safe . fs . existsSync ( d ) ) {
debug ( ` createSubcontainer: device ${ d } does not exist. Skipping... ` ) ;
return null ;
}
return { PathOnHost : d , PathInContainer : d , CgroupPermissions : 'rwm' } ;
} ) . filter ( d => d ) ;
2022-04-28 17:04:23 -07:00
if ( capabilities . includes ( 'vaapi' ) && safe . fs . existsSync ( '/dev/dri' ) ) {
2024-12-05 13:47:59 +01:00
containerOptions . HostConfig . Devices . push ( { PathOnHost : '/dev/dri' , PathInContainer : '/dev/dri' , CgroupPermissions : 'rwm' } ) ;
2022-04-28 17:04:23 -07:00
}
2020-08-14 18:48:53 -07:00
2023-05-25 11:27:23 +02:00
const mergedOptions = Object . assign ( { } , containerOptions , options ) ;
2020-04-27 22:55:43 -07:00
2023-05-25 11:27:23 +02:00
const [ createError , container ] = await safe ( gConnection . createContainer ( mergedOptions ) ) ;
2021-08-25 19:41:46 -07:00
if ( createError && createError . statusCode === 409 ) throw new BoxError ( BoxError . ALREADY _EXISTS , createError ) ;
if ( createError ) throw new BoxError ( BoxError . DOCKER _ERROR , createError ) ;
2019-10-22 21:46:32 -07:00
2021-08-25 19:41:46 -07:00
return container ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function createContainer ( app ) {
return await createSubcontainer ( app , app . id /* name */ , null /* cmd */ , { } /* options */ ) ;
2015-10-19 21:33:53 -07:00
}
2021-08-25 19:41:46 -07:00
async function startContainer ( containerId ) {
2015-10-19 15:51:02 -07:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . start ( ) ) ;
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Container ${ containerId } not found ` ) ;
2021-08-25 19:41:46 -07:00
if ( error && error . statusCode === 400 ) throw new BoxError ( BoxError . BAD _FIELD , error ) ; // e.g start.sh is not executable
if ( error && error . statusCode !== 304 ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ; // 304 means already started
2019-12-20 10:29:29 -08:00
}
2021-08-25 19:41:46 -07:00
async function restartContainer ( containerId ) {
2019-12-20 10:29:29 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . restart ( ) ) ;
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Contanier ${ containerId } not found ` ) ;
2021-08-25 19:41:46 -07:00
if ( error && error . statusCode === 400 ) throw new BoxError ( BoxError . BAD _FIELD , error ) ; // e.g start.sh is not executable
if ( error && error . statusCode !== 204 ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainer ( containerId ) {
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
2015-10-19 15:39:26 -07:00
if ( ! containerId ) {
debug ( 'No previous container to stop' ) ;
2021-08-25 19:41:46 -07:00
return ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const options = {
2015-10-19 11:40:19 -07:00
t : 10 // wait for 10 seconds before killing it
} ;
2021-08-25 19:41:46 -07:00
let [ error ] = await safe ( container . stop ( options ) ) ;
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error stopping container:' + error . message ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
[ error ] = await safe ( container . wait ( ) ) ;
if ( error && ( error . statusCode !== 304 && error . statusCode !== 404 ) ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error waiting on container:' + error . message ) ;
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainer ( containerId ) { // id can also be name
2015-10-19 15:51:02 -07:00
assert ( ! containerId || typeof containerId === 'string' ) ;
2021-08-25 19:41:46 -07:00
if ( containerId === null ) return null ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2015-10-19 11:40:19 -07:00
2021-08-25 19:41:46 -07:00
const removeOptions = {
2015-10-19 11:40:19 -07:00
force : true , // kill container if it's running
v : true // removes volumes associated with the container (but not host mounts)
} ;
2021-08-25 19:41:46 -07:00
const [ error ] = await safe ( container . remove ( removeOptions ) ) ;
if ( error && error . statusCode === 404 ) return ;
2015-10-19 15:39:26 -07:00
2021-08-25 19:41:46 -07:00
if ( error ) {
2023-04-16 10:49:59 +02:00
debug ( 'Error removing container %s : %o' , containerId , error ) ;
2021-08-25 19:41:46 -07:00
throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2015-10-19 11:40:19 -07:00
}
2021-08-25 19:41:46 -07:00
async function deleteContainers ( appId , options ) {
2015-10-19 18:48:56 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2019-01-17 23:32:24 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2015-10-19 18:48:56 -07:00
2021-08-25 19:41:46 -07:00
const labels = [ 'appId=' + appId ] ;
2019-01-17 23:32:24 -08:00
if ( options . managedOnly ) labels . push ( 'isCloudronManaged=true' ) ;
2025-05-27 13:55:36 +02:00
const [ error , containers ] = await safe ( gConnection . listContainers ( { all : typeof options . all === 'undefined' ? 1 : options . all , filters : JSON . stringify ( { label : labels } ) } ) ) ;
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-19 18:48:56 -07:00
2021-08-25 19:41:46 -07:00
for ( const container of containers ) {
await deleteContainer ( container . Id ) ;
}
2015-10-19 18:48:56 -07:00
}
2021-08-25 19:41:46 -07:00
async function stopContainers ( appId ) {
2015-10-20 00:05:07 -07:00
assert . strictEqual ( typeof appId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const [ error , containers ] = await safe ( gConnection . listContainers ( { all : 1 , filters : JSON . stringify ( { label : [ 'appId=' + appId ] } ) } ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2015-10-20 00:05:07 -07:00
2021-08-25 19:41:46 -07:00
for ( const container of containers ) {
await stopContainer ( container . Id ) ;
}
2015-10-20 00:05:07 -07:00
}
2024-12-14 14:53:08 +01:00
async function deleteImage ( imageRef ) {
assert . strictEqual ( typeof imageRef , 'string' ) ;
2015-10-19 15:51:02 -07:00
2024-12-14 14:53:08 +01:00
if ( ! imageRef ) return ;
if ( imageRef . includes ( '//' ) || imageRef . startsWith ( '/' ) ) return ; // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module (https://github.com/apocas/dockerode/issues/548)
2015-10-19 11:40:19 -07:00
2021-06-16 11:46:18 -07:00
const removeOptions = {
2016-01-21 14:59:24 -08:00
force : false , // might be shared with another instance of this app
noprune : false // delete untagged parents
} ;
2015-10-19 11:40:19 -07:00
2016-01-21 14:59:24 -08:00
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
// not created anymore after https://github.com/docker/docker/pull/10571
2024-12-14 20:47:35 +01:00
debug ( ` deleteImage: removing ${ imageRef } ` ) ;
const [ error ] = await safe ( gConnection . getImage ( imageRef . replace ( /@sha256:.*/ , '' ) ) . remove ( removeOptions ) ) ; // can't have the manifest id. won't remove anythin
2021-08-26 18:34:32 -07:00
if ( error && error . statusCode === 400 ) return ; // invalid image format. this can happen if user installed with a bad --docker-image
if ( error && error . statusCode === 404 ) return ; // not found
if ( error && error . statusCode === 409 ) return ; // another container using the image
2015-10-19 11:40:19 -07:00
2021-08-26 18:34:32 -07:00
if ( error ) {
2024-12-14 14:53:08 +01:00
debug ( ` Error removing image ${ imageRef } : %o ` , error ) ;
2021-08-26 18:34:32 -07:00
throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2015-10-19 11:40:19 -07:00
}
2016-02-18 15:39:27 +01:00
2021-08-25 19:41:46 -07:00
async function inspect ( containerId ) {
2017-08-11 22:04:40 +02:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2017-08-11 22:04:40 +02:00
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( container . inspect ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to find container ${ containerId } ` ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2018-11-19 10:19:46 +01:00
2021-08-25 19:41:46 -07:00
return result ;
2018-11-28 10:39:12 +01:00
}
2021-08-25 19:41:46 -07:00
async function getContainerIp ( containerId ) {
2020-11-18 23:24:34 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
if ( constants . TEST ) return '127.0.5.5' ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
const result = await inspect ( containerId ) ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
const ip = safe . query ( result , 'NetworkSettings.Networks.cloudron.IPAddress' , null ) ;
if ( ! ip ) throw new BoxError ( BoxError . DOCKER _ERROR , 'Error getting container IP' ) ;
2020-11-18 23:24:34 -08:00
2021-08-25 19:41:46 -07:00
return ip ;
2020-11-18 23:24:34 -08:00
}
2022-05-16 10:26:30 -07:00
async function createExec ( containerId , options ) {
2019-12-04 13:17:58 -08:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2019-03-06 11:54:37 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2022-05-16 10:26:30 -07:00
const [ error , exec ] = await safe ( container . exec ( options ) ) ;
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Container ${ containerId } not found ` ) ;
2021-08-25 19:41:46 -07:00
if ( error && error . statusCode === 409 ) throw new BoxError ( BoxError . BAD _STATE , error . message ) ; // container restarting/not running
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
return exec . id ;
}
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
async function startExec ( execId , options ) {
assert . strictEqual ( typeof execId , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
2019-12-04 13:17:58 -08:00
2022-05-16 10:26:30 -07:00
const exec = gConnection . getExec ( execId ) ;
const [ error , stream ] = await safe ( exec . start ( options ) ) ; /* in hijacked mode, stream is a net.socket */
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Exec container ${ execId } not found ` ) ;
2022-05-16 10:26:30 -07:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2021-08-25 19:41:46 -07:00
return stream ;
2019-12-04 13:17:58 -08:00
}
2022-05-16 10:26:30 -07:00
async function getExec ( execId ) {
assert . strictEqual ( typeof execId , 'string' ) ;
const exec = gConnection . getExec ( execId ) ;
const [ error , result ] = await safe ( exec . inspect ( ) ) ;
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Unable to find exec container ${ execId } ` ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
return { exitCode : result . ExitCode , running : result . Running } ;
}
async function resizeExec ( execId , options ) {
assert . strictEqual ( typeof execId , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
const exec = gConnection . getExec ( execId ) ;
const [ error ] = await safe ( exec . resize ( options ) ) ; // { h, w }
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Exec container ${ execId } not found ` ) ;
2022-05-16 10:26:30 -07:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
}
2021-08-25 19:41:46 -07:00
async function getEvents ( options ) {
2019-12-04 13:17:58 -08:00
assert . strictEqual ( typeof options , 'object' ) ;
2021-08-25 19:41:46 -07:00
const [ error , stream ] = await safe ( gConnection . getEvents ( options ) ) ;
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
return stream ;
2019-03-06 11:54:37 -08:00
}
2025-05-21 15:37:31 +02:00
async function stats ( containerId ) {
2018-11-28 10:39:12 +01:00
assert . strictEqual ( typeof containerId , 'string' ) ;
2021-08-25 19:41:46 -07:00
const container = gConnection . getContainer ( containerId ) ;
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
const [ error , result ] = await safe ( container . stats ( { stream : false } ) ) ;
2024-10-30 16:21:21 +01:00
if ( error && error . statusCode === 404 ) throw new BoxError ( BoxError . NOT _FOUND , ` Container ${ containerId } not found ` ) ;
2021-08-25 19:41:46 -07:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , error ) ;
2018-11-28 10:39:12 +01:00
2021-08-25 19:41:46 -07:00
return result ;
2017-08-11 22:04:40 +02:00
}
2021-08-25 19:41:46 -07:00
async function info ( ) {
const [ error , result ] = await safe ( gConnection . info ( ) ) ;
2025-02-05 16:23:31 +01:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Error connecting to docker: ${ error . message } ` ) ;
2021-08-25 19:41:46 -07:00
return result ;
2019-08-12 20:38:24 -07:00
}
2021-01-20 12:01:15 -08:00
2022-10-11 22:38:26 +02:00
async function df ( ) {
const [ error , result ] = await safe ( gConnection . df ( ) ) ;
2025-02-05 16:23:31 +01:00
if ( error ) throw new BoxError ( BoxError . DOCKER _ERROR , ` Error connecting to docker: ${ error . message } ` ) ;
2022-10-11 22:38:26 +02:00
return result ;
}
2024-04-09 18:59:40 +02:00
async function update ( name , memory ) {
2021-01-20 12:01:15 -08:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof memory , 'number' ) ;
// scale back db containers, if possible. this is retried because updating memory constraints can fail
// with failed to write to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/xx/memory.memsw.limit_in_bytes: device or resource busy
2021-08-25 19:41:46 -07:00
for ( let times = 0 ; times < 10 ; times ++ ) {
2024-10-15 10:10:15 +02:00
const [ error ] = await safe ( shell . spawn ( 'docker' , [ 'update' , '--memory' , memory , '--memory-swap' , '-1' , name ] , { } ) ) ;
2021-08-25 19:41:46 -07:00
if ( ! error ) return ;
2023-05-14 10:53:50 +02:00
await timers . setTimeout ( 60 * 1000 ) ;
2021-08-25 19:41:46 -07:00
}
throw new BoxError ( BoxError . DOCKER _ERROR , 'Unable to update container' ) ;
2021-01-20 12:01:15 -08:00
}