├── .eslintignore ├── .dockerignore ├── Icons.sketch ├── logo512.png ├── nodemon.json ├── Dockerfile ├── .vscode └── settings.json ├── src ├── Logger.ts ├── DockerManager.ts ├── index.ts ├── ConfigManager.ts └── ProxyHost.ts ├── .eslintrc ├── config └── config.yml.example ├── LICENSE ├── package.json ├── CHANGELOG.md ├── .gitignore ├── views └── placeholder.ejs ├── README.md └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | src/ 4 | config/config.yml 5 | .env -------------------------------------------------------------------------------- /Icons.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsEcholot/ContainerNursery/HEAD/Icons.sketch -------------------------------------------------------------------------------- /logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsEcholot/ContainerNursery/HEAD/logo512.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": ".ts,.js", 4 | "ignore": [], 5 | "exec": "ts-node ./src/index.ts -r dotenv/config" 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:17-alpine 2 | 3 | WORKDIR /usr/src/app 4 | COPY package*.json ./ 5 | RUN npm install --only=production 6 | 7 | COPY . . 8 | EXPOSE 80 9 | CMD ["node", "build/index.js"] -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "typescript" 5 | ], 6 | "editor.formatOnSave": false, 7 | "editor.tabSize": 2, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | } 11 | } -------------------------------------------------------------------------------- /src/Logger.ts: -------------------------------------------------------------------------------- 1 | import Pino from 'pino'; 2 | 3 | const transport = Pino.transport({ 4 | target: 'pino-pretty', 5 | options: { 6 | destination: 1, 7 | levelFirst: true, 8 | colorize: true, 9 | translateTime: 'SYS:standard', 10 | ignore: 'pid,hostname' 11 | } 12 | }); 13 | 14 | export default Pino({ 15 | level: process.env.CN_LOG_LEVEL || 'info' 16 | }, process.env.CN_LOG_JSON === 'true' ? undefined : transport); 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "airbnb-base/legacy" 12 | ], 13 | "rules": { 14 | "no-console": "off", 15 | "lines-between-class-members": "off", 16 | "max-len": ["warn", {"code": 120, "ignoreStrings": true}] 17 | }, 18 | "globals": { 19 | "NodeJS": true 20 | } 21 | } -------------------------------------------------------------------------------- /config/config.yml.example: -------------------------------------------------------------------------------- 1 | proxyListeningPort: 80 2 | proxyHosts: 3 | - domain: handbrake.yourdomain.io 4 | containerName: handbrake 5 | displayName: Handbrake 6 | proxyHost: localhost 7 | proxyPort: 5800 8 | timeoutSeconds: 15 9 | stopOnTimeoutIfCpuUsageBelow: 50 10 | - domain: 11 | - wordpress.yourdomain.io 12 | - wordpress.otherdomain.io 13 | containerName: 14 | - wordpress 15 | - mariadb 16 | proxyHost: wordpress 17 | proxyPort: 3000 18 | proxyUseHttps: true 19 | timeoutSeconds: 1800 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Marc Berchtold 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "containernursery", 3 | "version": "1.9.0", 4 | "description": "Puts Docker Containers to sleep and wakes them back up when they're needed", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "start": "node build/index.js", 8 | "watch": "nodemon", 9 | "build": "rimraf ./build && tsc", 10 | "lint": "eslint ." 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/ItsEcholot/ContainerNursery.git" 15 | }, 16 | "author": "Marc Berchtold", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/ItsEcholot/ContainerNursery/issues" 20 | }, 21 | "homepage": "https://github.com/ItsEcholot/ContainerNursery#readme", 22 | "devDependencies": { 23 | "@types/dockerode": "^3.2.7", 24 | "@types/ejs": "^3.1.0", 25 | "@types/express": "^4.17.13", 26 | "@types/http-proxy": "^1.17.7", 27 | "@types/node": "^16.7.10", 28 | "@types/node-fetch": "^2.5.12", 29 | "@types/pino": "^6.3.11", 30 | "@typescript-eslint/eslint-plugin": "^4.30.0", 31 | "@typescript-eslint/parser": "^4.30.0", 32 | "eslint": "^7.32.0", 33 | "eslint-config-airbnb-base": "^14.2.1", 34 | "eslint-plugin-import": "^2.24.2", 35 | "nodemon": "^2.0.12", 36 | "rimraf": "^3.0.2", 37 | "ts-node": "^10.2.1", 38 | "typescript": "^4.4.2" 39 | }, 40 | "dependencies": { 41 | "chokidar": "^3.5.2", 42 | "dockerode": "^3.3.1", 43 | "dotenv": "^10.0.0", 44 | "ejs": "^3.1.6", 45 | "express": "^4.17.1", 46 | "http-proxy": "^1.18.1", 47 | "node-fetch": "^2.6.6", 48 | "pino": "^7.4.1", 49 | "pino-pretty": "^7.2.0", 50 | "yaml": "^1.10.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | **13.12.2023 - v1.9.0** Added the ability to define the name which should be displayed on the waiting page. Contributed by @Samurai1201 in #55. 4 | 5 | **22.10.2023 - v1.8.0** Added the ability to define which HTTP method should be used during the ready check. Contributed by @Howard3 in #53. 6 | 7 | **30.08.2022 - v1.7.0** Added the ability to proxy traffic to containers using HTTPS, while ignoring their certificate. This is for services which only listen for SSL requests. This does **not** provide any security over normal http proxying. 8 | 9 | **08.02.2022 - v1.6.0** Added the ability to add multiple domains that point to the same proxy host. 10 | 11 | **29.11.2021 - v1.5.2** Fixed a bug where misformed docker event payload would crash ContainerNursery when trying to parse JSON. Thanks to Alfy1080 on the Unraid Forums for the Bug report. 12 | 13 | **12.10.2021 - v1.5.1** Fixed a bug where the loading page wouldn't be displayed when a path other than `/` is requested. Thanks to @JamesDAdams on GitHub for the Bug report. 14 | 15 | **29.09.2021 - v1.5.0** Added the ability to stop (and start) multiple containers per proxy host. This is useful if the application supports multiple containers. The first container in the list is the main container, which is used to check if the container is ready and reload the loading page. For usage information check the README.md file on GitHub. 16 | 17 | **24.09.2021 - v1.4.2** Handle SIGTERM. The ContainerNursery container should now stop (and thus also restart) much quicker. 18 | 19 | **23.09.2021 - v1.4.1** Fixed an issue where certain editors broke the live config reload functionality by introducing a small delay before reading the config file. 20 | 21 | **23.09.2021 - v1.4.0** Added stopOnTimeoutIfCpuUsageBelow setting to proxyHosts which prevents ContainerNursery from stoping containers if they're still busy (using more CPU than the limit). For usage information check the README.md file on GitHub. -------------------------------------------------------------------------------- /src/DockerManager.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import Docker from 'dockerode'; 3 | import logger from './Logger'; 4 | 5 | export default class DockerManager { 6 | private docker: Docker; 7 | 8 | constructor() { 9 | this.docker = new Docker({ socketPath: '/var/run/docker.sock' }); 10 | } 11 | 12 | private findContainerByName(name: string): Docker.Container { 13 | return this.docker.getContainer(name); 14 | } 15 | 16 | public async isContainerRunning(name: string): Promise { 17 | return (await this.findContainerByName(name).inspect()).State.Running; 18 | } 19 | 20 | public async startContainer(name: string): Promise { 21 | return this.findContainerByName(name).start(); 22 | } 23 | 24 | public async stopContainer(name: string): Promise { 25 | return this.findContainerByName(name).stop(); 26 | } 27 | 28 | public async getContainerEventEmitter(names: string[]): Promise { 29 | const eventEmitter = new EventEmitter(); 30 | const readableStream = await this.docker.getEvents({ 31 | filters: { 32 | container: names 33 | } 34 | }); 35 | 36 | readableStream.on('data', chunk => { 37 | try { 38 | eventEmitter.emit('update', JSON.parse(chunk.toString('utf-8'))); 39 | } catch (err) { 40 | logger.error(err, 'JSON parsing of Docker Event failed'); 41 | } 42 | }); 43 | 44 | eventEmitter.on('stop-stream', () => { 45 | readableStream.removeAllListeners(); 46 | }); 47 | 48 | return eventEmitter; 49 | } 50 | 51 | public async getContainerStatsEventEmitter(name: string): Promise { 52 | const eventEmitter = new EventEmitter(); 53 | const statsStream = await this.findContainerByName(name) 54 | .stats({ stream: true }) as unknown as NodeJS.ReadableStream; 55 | 56 | statsStream.on('data', chunk => { 57 | try { 58 | eventEmitter.emit('update', JSON.parse(chunk.toString('utf-8'))); 59 | } catch (err) { 60 | logger.error(err, 'JSON parsing of Docker Event failed'); 61 | } 62 | }); 63 | 64 | eventEmitter.on('stop-stream', () => { 65 | statsStream.removeAllListeners(); 66 | }); 67 | 68 | return eventEmitter; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Builds 107 | build/ 108 | 109 | # Config file 110 | config/config.yml 111 | 112 | # Dev Env 113 | .devcontainer 114 | 115 | # IntelliJ configuration 116 | .idea 117 | -------------------------------------------------------------------------------- /views/placeholder.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 107 | 108 | 121 | 122 | 123 | 124 |
125 |
126 |

Waking up <%= containerName %>...

127 |
128 | 129 |
130 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 |
143 |
144 | 145 | 146 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import { createServer } from 'http'; 3 | import { createProxyServer } from 'http-proxy'; 4 | import express from 'express'; 5 | import logger from './Logger'; 6 | import ConfigManager from './ConfigManager'; 7 | import ProxyHost from './ProxyHost'; 8 | 9 | // Disable TLS certificate verification for the targets of the proxy server 10 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; 11 | 12 | const proxyHosts: Map = new Map(); 13 | // eslint-disable-next-line no-unused-vars 14 | const configManager = new ConfigManager(proxyHosts); 15 | const proxy = createProxyServer({ 16 | xfwd: true, 17 | secure: false 18 | }); 19 | 20 | let proxyListeningPort = configManager.getProxyListeningPort(); 21 | const placeholderServerListeningPort = 8080; 22 | const placeholderServerListeningHost = '127.0.0.1'; 23 | 24 | // Remove secure tag from cookies 25 | proxy.on('proxyRes', (proxyRes) => { 26 | const sc = proxyRes.headers['set-cookie']; 27 | if (Array.isArray(sc)) { 28 | // eslint-disable-next-line no-param-reassign 29 | proxyRes.headers['set-cookie'] = sc.map(str => { 30 | return str.split(';') 31 | .filter(v => v.trim().toLowerCase() !== 'secure') 32 | .join('; '); 33 | }); 34 | } 35 | }); 36 | 37 | const stripPortHostHeader = (host: string | undefined): string | undefined => { 38 | if (!host) return host; 39 | return host.replace(/(:\d+)/i, ''); 40 | }; 41 | 42 | proxy.on('error', (err, req, res) => { 43 | req.headers.host = stripPortHostHeader(req.headers.host); 44 | logger.debug({ host: req.headers.host, error: err }, 'Error in proxying request'); 45 | if (!req.headers.host) { 46 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 47 | res.write('Error: Request header host wasn\t specified'); 48 | res.end(); 49 | return; 50 | } 51 | const proxyHost = proxyHosts.get(req.headers.host as string); 52 | if (res.writeHead) { 53 | res.writeHead(500, { 'Content-Type': 'text/plain' }); 54 | res.write(`Error: Host is not reachable ${JSON.stringify(proxyHost?.getTarget())}`); 55 | res.end(); 56 | } 57 | logger.warn({ host: req.headers.host, target: proxyHost?.getTarget() }, 'Host not reachable'); 58 | }); 59 | 60 | const proxyServer = createServer((req, res) => { 61 | req.headers.host = stripPortHostHeader(req.headers.host); 62 | if (!req.headers.host) { 63 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 64 | res.write('Error: Request header host wasn\t specified'); 65 | res.end(); 66 | return; 67 | } 68 | const proxyHost = proxyHosts.get(req.headers.host); 69 | if (!proxyHost) { 70 | res.writeHead(400, { 'Content-Type': 'text/plain' }); 71 | res.write(`Error: Proxy configuration is missing for ${req.headers.host}`); 72 | res.end(); 73 | logger.warn({ host: req.headers.host }, 'Proxy configuration missing'); 74 | return; 75 | } 76 | 77 | proxyHost.newConnection(); 78 | proxy.web(req, res, { 79 | target: proxyHost.getTarget(), 80 | headers: proxyHost.getHeaders() 81 | }); 82 | logger.debug({ host: req.headers.host, target: proxyHost.getTarget(), headers: proxyHost.getHeaders() }, 'Proxied request'); 83 | }); 84 | 85 | proxyServer.on('upgrade', (req, socket, head) => { 86 | req.headers.host = stripPortHostHeader(req.headers.host); 87 | if (!req.headers.host) { 88 | logger.warn('Socket upgrade failed, request header host not specified'); 89 | return; 90 | } 91 | const proxyHost = proxyHosts.get(req.headers.host); 92 | if (!proxyHost) { 93 | logger.warn({ host: req.headers.host }, 'Socket upgrade failed, proxy configuration missing'); 94 | return; 95 | } 96 | 97 | proxyHost.newSocketConnection(socket); 98 | proxy.ws(req, socket, head, { 99 | target: proxyHost.getTarget(), 100 | headers: proxyHost.getHeaders() 101 | }); 102 | logger.debug({ host: req.headers.host, target: proxyHost.getTarget(), headers: proxyHost.getHeaders() }, 'Proxied Upgrade request'); 103 | }); 104 | 105 | proxyServer.listen(proxyListeningPort); 106 | logger.info({ port: proxyListeningPort }, 'Proxy listening'); 107 | 108 | configManager.on('port-update', () => { 109 | proxyListeningPort = configManager.getProxyListeningPort(); 110 | proxyServer.close(() => { 111 | proxyServer.listen(proxyListeningPort); 112 | logger.info({ port: proxyListeningPort }, 'Proxy listening'); 113 | }); 114 | }); 115 | 116 | const placeholderServer = express(); 117 | placeholderServer.set('views', 'views'); 118 | placeholderServer.set('view engine', 'ejs'); 119 | placeholderServer.use((_, res, next) => { 120 | res.setHeader('x-powered-by', 'ContainerNursery'); 121 | next(); 122 | }); 123 | placeholderServer.get('*', (req, res) => { 124 | res.render('placeholder', { containerName: req.headers['x-container-nursery-container-name'] }); 125 | }); 126 | placeholderServer.listen(placeholderServerListeningPort, placeholderServerListeningHost); 127 | logger.info({ port: placeholderServerListeningPort, host: placeholderServerListeningHost }, 'Proxy placeholder server listening'); 128 | 129 | process.on('SIGTERM', () => { 130 | process.exit(0); 131 | }); 132 | -------------------------------------------------------------------------------- /src/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import Chokidar from 'chokidar'; 3 | import YAML from 'yaml'; 4 | import logger from './Logger'; 5 | import ProxyHost from './ProxyHost'; 6 | import EventEmitter from 'events'; 7 | 8 | const defaultProxyListeningPort = 80; 9 | const placeholderServerListeningPort = 8080; 10 | 11 | type ProxyHostConfig = { 12 | domain: string | string[] 13 | containerName: string | string[] 14 | displayName: string | string[] 15 | proxyHost: string 16 | proxyPort: number 17 | proxyUseHttps: boolean 18 | proxyUseCustomMethod: string 19 | timeoutSeconds: number 20 | stopOnTimeoutIfCpuUsageBelow?: number 21 | } 22 | type ApplicationConfig = { 23 | proxyListeningPort: number, 24 | proxyHosts: ProxyHostConfig[] 25 | } 26 | 27 | export default class ConfigManager { 28 | private configFile = 'config/config.yml'; 29 | private proxyHosts: Map; 30 | private proxyListeningPort: number | null; 31 | private eventEmitter: EventEmitter; 32 | 33 | constructor(proxyHosts: Map) { 34 | this.proxyHosts = proxyHosts; 35 | this.proxyListeningPort = null; 36 | this.eventEmitter = new EventEmitter(); 37 | this.createIfNotExist(); 38 | this.parseConfig(); 39 | this.watch(); 40 | } 41 | 42 | private static parsePort(p: number): number | null { 43 | if (Number.isInteger(p) && p >= 0 && p <= 49151) { 44 | return p; 45 | } 46 | 47 | logger.warn({ portString: p }, 'Parsing proxy listening port failed! Is a valid value used (0-49151)?'); 48 | return null; 49 | } 50 | 51 | // eslint-disable-next-line no-unused-vars 52 | public on(event: string, cb: (...args: unknown[]) => void): void { 53 | this.eventEmitter.on(event, cb); 54 | } 55 | 56 | public getProxyListeningPort(): number { 57 | if (this.proxyListeningPort 58 | && this.proxyListeningPort !== placeholderServerListeningPort) { 59 | return this.proxyListeningPort; 60 | } 61 | if (this.proxyListeningPort === placeholderServerListeningPort) { 62 | logger.warn({ placeholderServerListeningPort, desiredProxyListeningPort: this.proxyListeningPort }, "Can't use the same port as the internal placeholder server uses"); 63 | } 64 | 65 | if (process.env.CN_PORT) { 66 | this.proxyListeningPort = ConfigManager.parsePort(parseInt(process.env.CN_PORT, 10)); 67 | if (this.proxyListeningPort 68 | && this.proxyListeningPort !== placeholderServerListeningPort) { 69 | return this.proxyListeningPort; 70 | } 71 | } 72 | 73 | logger.warn({ port: defaultProxyListeningPort }, 'Using default proxy listening port'); 74 | this.proxyListeningPort = defaultProxyListeningPort; 75 | return this.proxyListeningPort; 76 | } 77 | 78 | private createIfNotExist(): void { 79 | if (!fs.existsSync(this.configFile)) { 80 | fs.closeSync(fs.openSync(this.configFile, 'w')); 81 | logger.error('config.yml is missing, empty config file was created'); 82 | } 83 | } 84 | 85 | private watch(): void { 86 | Chokidar.watch(this.configFile).on('change', () => { 87 | logger.info('Config changed, reloading hosts'); 88 | setTimeout(() => this.parseConfig(), 500); 89 | }); 90 | } 91 | 92 | private parseConfig(): void { 93 | const fileContent = fs.readFileSync(this.configFile, 'utf-8'); 94 | const config: ApplicationConfig = YAML.parse(fileContent); 95 | 96 | if (!config || !config.proxyHosts) { 97 | logger.error({ invalidProperty: 'proxyHosts' }, 'Config is invalid, missing property'); 98 | } else { 99 | this.loadProxyHosts(config.proxyHosts); 100 | if (config.proxyListeningPort) { 101 | const prevPort = this.proxyListeningPort; 102 | this.proxyListeningPort = ConfigManager.parsePort(config.proxyListeningPort); 103 | if (prevPort !== null && prevPort !== this.proxyListeningPort) { 104 | this.eventEmitter.emit('port-update'); 105 | } 106 | } 107 | } 108 | } 109 | 110 | private loadProxyHosts(proxyHosts: ProxyHostConfig[]): void { 111 | logger.info('(Re)loading hosts, clearing all existing hosts first'); 112 | this.clearOldProxyHosts(); 113 | proxyHosts.forEach(proxyHostConfig => { 114 | if (!ConfigManager.validateProxyHost(proxyHostConfig)) { 115 | logger.error({ proxyHost: proxyHostConfig }, 'Config contains invalid proxyHost object'); 116 | } else { 117 | const proxyHost = new ProxyHost( // TODO 118 | proxyHostConfig.domain instanceof Array 119 | ? proxyHostConfig.domain 120 | : [proxyHostConfig.domain], 121 | proxyHostConfig.containerName instanceof Array 122 | ? proxyHostConfig.containerName 123 | : [proxyHostConfig.containerName], 124 | proxyHostConfig.displayName instanceof Array 125 | ? proxyHostConfig.displayName 126 | : [proxyHostConfig.displayName], 127 | proxyHostConfig.proxyHost, 128 | proxyHostConfig.proxyPort, 129 | proxyHostConfig.timeoutSeconds 130 | ); 131 | 132 | if (proxyHostConfig.proxyUseHttps) { 133 | proxyHost.proxyUseHttps = proxyHostConfig.proxyUseHttps; 134 | } 135 | 136 | if (proxyHostConfig.proxyUseCustomMethod) { 137 | proxyHost.proxyUseCustomMethod = proxyHostConfig.proxyUseCustomMethod; 138 | } 139 | 140 | if (proxyHostConfig.stopOnTimeoutIfCpuUsageBelow) { 141 | proxyHost.stopOnTimeoutIfCpuUsageBelow = proxyHostConfig.stopOnTimeoutIfCpuUsageBelow as number; 142 | } 143 | 144 | if (proxyHostConfig.domain instanceof Array) { 145 | proxyHostConfig.domain.forEach(domain => { 146 | this.proxyHosts.set( 147 | domain, 148 | proxyHost 149 | ); 150 | }); 151 | } else { 152 | this.proxyHosts.set( 153 | proxyHostConfig.domain as string, 154 | proxyHost 155 | ); 156 | } 157 | } 158 | }); 159 | } 160 | 161 | private clearOldProxyHosts(): void { 162 | this.proxyHosts.forEach(proxyHost => { 163 | proxyHost.stopConnectionTimeout(); 164 | proxyHost.stopContainerEventEmitter(); 165 | }); 166 | this.proxyHosts.clear(); 167 | } 168 | 169 | private static validateProxyHost(proxyHostConfig: Record): boolean { 170 | // TODO 171 | if (!proxyHostConfig.domain) return false; 172 | if (!proxyHostConfig.containerName) return false; 173 | if (!proxyHostConfig.proxyHost) return false; 174 | if (!proxyHostConfig.proxyPort) return false; 175 | if (!proxyHostConfig.timeoutSeconds) return false; 176 | 177 | return true; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Container Nursery 2 |

3 | 4 |

5 | GitHub package.json version 6 | Maintenance 7 |

8 | 9 | Written in Node.js, this application acts as a HTTP reverse proxy and stops Docker containers which haven't been accessed recently and starts them again when a new request comes in. ContainerNursery also makes sure there are no more active WebSocket connections before stopping the container. 10 | 11 | To improve the user experience a loading page is presented, which automatically reloads when the containers webserver is ready. 12 | 13 | The application listens on port `80` by default for HTTP traffic and uses the socket at `/var/run/docker.sock` to connect to the docker daemon. 14 | 15 | You can find the changelog in the [CHANGELOG.md](CHANGELOG.md) file. 16 | 17 | ## Demo 18 | 19 | 20 | https://user-images.githubusercontent.com/2771251/132314400-817971fd-b364-4c78-9fed-650138960530.mp4 21 | 22 | 23 | ## Installation 24 | I ***heavily*** recommend using another reverse proxy in front of ContainerNursery (for HTTPS, caching, etc.) pointing to your configured listening port (default `80`). 25 | 26 | I also recommend running this application in a Docker container. Pull the latest image using: 27 | 28 | ```docker pull ghcr.io/itsecholot/containernursery:latest``` 29 | 30 | More information about the available tags and versions can be found on the [GitHub packages page](https://github.com/ItsEcholot/ContainerNursery/pkgs/container/containernursery). 31 | 32 | ### Example 33 | 34 | ```bash 35 | docker run \ 36 | --name='ContainerNursery' \ 37 | -v /var/run/docker.sock:/var/run/docker.sock \ 38 | -v /mnt/ContainerNursery/config:/usr/src/app/config \ 39 | ghcr.io/itsecholot/containernursery:latest 40 | ``` 41 | 42 | ## Configuration 43 | To configure the proxy, edit the `config.yml` file in the `config` directory. The configuration file is automatically reloaded by the application when changes are made. 44 | If no `config.yml` file is found an empty one is automatically created on application start. 45 | 46 | The following top-level properties can be configured: 47 | Property | Meaning 48 | ---------|--------| 49 | `proxyListeningPort` | The port ContainerNursery should listen on for new http connections. Defaults to `80`. 50 | 51 | The virtual hosts the proxy should handle can be configured by adding an object to the `proxyHosts` key. 52 | 53 | The following properties are required: 54 | 55 | | Property | Meaning | 56 | |------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 57 | | `domain` | Array or string containing domain(s) to listen for (equals the `host` header) | 58 | | `containerName` | Array or string of which container(s) (by name or id) to start and stop. ContainerNursery can start and stop multiple containers for a single proxy host. The first container in the list (main container) is used to check if the application is ready and reload the loading page. Note however that CN doesn't manage the timing of how the containers are started (database before app etc.). | 59 | | `proxyHost` | Domain / IP of container (use custom Docker bridge networks for dynDNS using the name of the container) | 60 | | `proxyPort` | Port on which the containers webserver listens on | 61 | | `timeoutSeconds` | Seconds after which the container should be stopped. The internal timeout gets reset to this configured value every time a new HTTP request is made, or when the timer runs out while a Websocket connection is still active. | 62 | 63 | The following properties are optional: 64 | 65 | | Property | Meaning | 66 | |--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 67 | | `displayName` | The name (of the service) that should be displayed on the waiting page | 68 | | `proxyUseHttps` | Boolean indicating if the proxy should use HTTPS to connect to the container. Defaults to `false`. This should only be used if the container only accepts HTTPS requests. It provides no additional security. | 69 | | `stopOnTimeoutIfCpuUsageBelow` | If set, prevents the container from stopping when reaching the configured timeout if the averaged CPU usage (percentage between 0 and 100*core count) of the **main** container (first in the list of container names) is above this value. This is great for containers that should remain running while their doing intensive work even when nobody is doing any http requests, for example handbrake. | 70 | | `proxyUseCustomMethod` | Can be set to a HTTP method (`HEAD`,`GET`, ...) which should be used for the ready check. Some services only respond to certain HTTP methods correctly. | 71 | 72 | ### Example Configuration 73 | ```yaml 74 | proxyListeningPort: 80 75 | proxyHosts: 76 | - domain: handbrake.yourdomain.io 77 | containerName: handbrake 78 | displayName: Handbrake 79 | proxyHost: localhost 80 | proxyPort: 5800 81 | timeoutSeconds: 15 82 | stopOnTimeoutIfCpuUsageBelow: 50 83 | proxyUseCustomMethod: GET 84 | - domain: 85 | - wordpress.yourdomain.io 86 | - wordpress.otherdomain.io 87 | containerName: 88 | - wordpress 89 | - mariadb 90 | proxyHost: wordpress 91 | proxyPort: 3000 92 | proxyUseHttps: true 93 | timeoutSeconds: 1800 94 | ``` 95 | 96 | Now point your existing reverse proxy for the hosts you configured in the previous step to the ContainerNursery IP/Domain Name on port 80. 97 | 98 | **Important:** If you use the (otherwise) excellent [NginxProxyManager](https://github.com/jc21/nginx-proxy-manager) ***disable*** the caching for proxy hosts that are routed through ContainerNursery. Because the built-in caching config sadly also caches 404s this will lead to NginxProxyManager caching error messages while your app container is starting up and then refusing to serve the real `.js/.css` file once the startup is complete. 99 | 100 | ### Example Configuration for [NginxProxyManager](https://github.com/jc21/nginx-proxy-manager) 101 | 102 | ![NginxProxyManager Config](https://user-images.githubusercontent.com/2771251/132512090-621926eb-70b5-4801-a477-70cc300ab2a1.jpeg) 103 | 104 | ### Environment Variables 105 | Additionally to the configuration done in the `congif.yml` file, there are a few settings which can only be configured by using environment variables. 106 | 107 | | Name | Valid Values | Description | 108 | |----------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 109 | | `CN_LOG_JSON` | `true` / `false` | If set to `true` all logging is done in a machine readable format (JSON). Defaults to `false`. | 110 | | `CN_LOG_LEVEL` | `debug` / `info` / `warn` / `error` | Sets the minimum log level. Log entries below this importance level won't be printed to the console. Defaults to `info`. | 111 | | `CN_PORT` | `integer` | Sets the port ContainerNursery listens on for new http connections. The `proxyListeningPort` option in the `config.yml` file takes precedence if both are set. Defaults to `80` if no value is set. | 112 | -------------------------------------------------------------------------------- /src/ProxyHost.ts: -------------------------------------------------------------------------------- 1 | import { ProxyTarget } from 'http-proxy'; 2 | import internal, { EventEmitter } from 'stream'; 3 | import fetch from 'node-fetch'; 4 | import logger from './Logger'; 5 | import DockerManager from './DockerManager'; 6 | 7 | const dockerManager = new DockerManager(); 8 | 9 | export default class ProxyHost { 10 | private domain: string[]; 11 | private containerName: string[]; 12 | private displayName: string[]; 13 | private proxyHost: string; 14 | private proxyPort: number; 15 | public proxyUseHttps = false; 16 | public proxyUseCustomMethod: string | undefined; 17 | private timeoutSeconds: number; 18 | public stopOnTimeoutIfCpuUsageBelow = Infinity; 19 | 20 | private activeSockets: Set = new Set(); 21 | private containerEventEmitter: EventEmitter | null = null; 22 | private connectionTimeoutId: NodeJS.Timeout | null = null; 23 | private containerRunning: boolean | undefined = undefined; 24 | private containerReadyChecking = false; 25 | private startingHost = false; 26 | private stoppingHost = false; 27 | 28 | private cpuAverage = 0; 29 | private cpuAverageCounter = 0; 30 | private lastContainerCPUUsage = 0; 31 | private lastSystemCPUUsage = 0; 32 | 33 | constructor( 34 | domain: string[], 35 | containerName: string[], 36 | displayName: string[], 37 | proxyHost: string, 38 | proxyPort: number, 39 | timeoutSeconds: number 40 | ) { 41 | logger.info({ 42 | host: domain, 43 | container: containerName, 44 | proxy: { 45 | host: proxyHost, 46 | port: proxyPort, 47 | useHttps: this.proxyUseHttps 48 | }, 49 | timeoutSeconds: timeoutSeconds 50 | }, 'Added proxy host'); 51 | 52 | this.domain = domain; 53 | this.containerName = containerName; 54 | this.displayName = displayName; 55 | this.proxyHost = proxyHost; 56 | this.proxyPort = proxyPort; 57 | this.timeoutSeconds = timeoutSeconds; 58 | dockerManager.isContainerRunning(this.containerName[0]).then(async (res) => { 59 | if (res) this.resetConnectionTimeout(); 60 | this.containerRunning = res; 61 | 62 | const otherContainers = this.containerName.slice(1); 63 | const otherContainerChecks: Promise[] = []; 64 | otherContainers.forEach(otherContainerName => { 65 | otherContainerChecks.push(dockerManager.isContainerRunning(otherContainerName)); 66 | }); 67 | (await Promise.all(otherContainerChecks)).forEach((otherContainerRunning, i) => { 68 | if (this.containerRunning === otherContainerRunning) return; 69 | if (otherContainerRunning) { 70 | logger.debug({ mainContainer: otherContainers[i], container: otherContainers[i] }, 'Stopping other container because main container isn\'t running'); 71 | dockerManager.stopContainer(otherContainers[i]); 72 | } else { 73 | logger.debug({ mainContainer: otherContainers[i], container: otherContainers[i] }, 'Starting other container because main container is running'); 74 | dockerManager.startContainer(otherContainers[i]); 75 | } 76 | }); 77 | 78 | logger.debug({ container: this.containerName, running: res }, 'Initial docker state check done'); 79 | }); 80 | 81 | dockerManager.getContainerEventEmitter(this.containerName).then(eventEmitter => { 82 | this.containerEventEmitter = eventEmitter; 83 | eventEmitter.on('update', data => { 84 | logger.debug({ container: this.containerName, data }, 'Received container event'); 85 | if (data.status === 'stop') { 86 | this.stopHost(); 87 | } else if (data.status === 'start') { 88 | this.startHost(); 89 | } 90 | }); 91 | }); 92 | 93 | dockerManager.getContainerStatsEventEmitter(this.containerName[0]).then(eventEmitter => { 94 | eventEmitter.on('update', data => { 95 | if (!this.containerRunning || !data.cpu_stats.cpu_usage.percpu_usage) return; 96 | 97 | this.calculateCPUAverage( 98 | data.cpu_stats.cpu_usage.total_usage, 99 | data.cpu_stats.system_cpu_usage, 100 | data.cpu_stats.cpu_usage.percpu_usage.length 101 | ); 102 | }); 103 | }); 104 | } 105 | 106 | private async stopHost(): Promise { 107 | if (this.stoppingHost) return; 108 | this.stoppingHost = true; 109 | 110 | this.containerRunning = false; 111 | this.stopConnectionTimeout(); 112 | 113 | await Promise.all(this.containerName.map(async (container) => { 114 | if (await dockerManager.isContainerRunning(container)) { 115 | logger.info({ container, cpuUsageAverage: container === this.containerName[0] ? this.cpuAverage : undefined }, 'Stopping container'); 116 | await dockerManager.stopContainer(container); 117 | logger.debug({ container }, 'Stopping container complete'); 118 | } 119 | })); 120 | 121 | this.stoppingHost = false; 122 | } 123 | 124 | private async startHost(): Promise { 125 | if (this.startingHost) return; 126 | this.startingHost = true; 127 | 128 | await Promise.all(this.containerName.map(async (container) => { 129 | if (!(await dockerManager.isContainerRunning(container))) { 130 | logger.info({ container }, 'Starting container'); 131 | await dockerManager.startContainer(container); 132 | logger.debug({ container }, 'Starting container complete'); 133 | } 134 | })); 135 | 136 | this.checkContainerReady(); 137 | this.startingHost = false; 138 | } 139 | 140 | private checkContainerReady() { 141 | if (this.containerReadyChecking) return; 142 | 143 | this.containerReadyChecking = true; 144 | const checkInterval = setInterval(() => { 145 | this.resetConnectionTimeout(); 146 | 147 | fetch(`http${this.proxyUseHttps ? 's' : ''}://${this.proxyHost}:${this.proxyPort}`, { 148 | method: this.proxyUseCustomMethod ?? 'HEAD' 149 | }).then(res => { 150 | logger.debug({ 151 | domain: this.domain, 152 | proxyHost: this.proxyHost, 153 | proxyPort: this.proxyPort, 154 | status: res.status, 155 | headers: res.headers 156 | }, 'Checked if target is ready'); 157 | 158 | if (res.status === 200 || (res.status >= 300 && res.status <= 399)) { 159 | clearInterval(checkInterval); 160 | this.containerReadyChecking = false; 161 | this.containerRunning = true; 162 | 163 | logger.debug({ 164 | domain: this.domain, 165 | proxyHost: this.proxyHost, 166 | proxyPort: this.proxyPort 167 | }, 'Target is ready'); 168 | } 169 | }).catch(err => logger.debug({ error: err }, 'Container readiness check failed')); 170 | }, 250); 171 | } 172 | 173 | private startConnectionTimeout(): void { 174 | this.connectionTimeoutId = setTimeout( 175 | () => this.onConnectionTimeout(), this.timeoutSeconds * 1000 176 | ); 177 | } 178 | 179 | private resetConnectionTimeout(): void { 180 | logger.debug({ 181 | domain: this.domain, 182 | timeoutSeconds: this.timeoutSeconds 183 | }, 'Resetting connection timeout'); 184 | this.stopConnectionTimeout(); 185 | this.startConnectionTimeout(); 186 | this.resetCPUAverage(); 187 | } 188 | 189 | private onConnectionTimeout(): void { 190 | if (this.activeSockets.size > 0) { 191 | logger.debug({ 192 | domain: this.domain, 193 | activeSocketCount: this.activeSockets.size 194 | }, 'Reached timeout but there are still active sockets'); 195 | this.resetConnectionTimeout(); 196 | return; 197 | } 198 | if (this.cpuAverage > this.stopOnTimeoutIfCpuUsageBelow) { 199 | logger.debug({ 200 | domain: this.domain, 201 | container: this.containerName[0], 202 | cpuUsageAverage: this.cpuAverage 203 | }, 'Reached timeout but the container cpu usage is above the minimum configured'); 204 | this.resetConnectionTimeout(); 205 | return; 206 | } 207 | 208 | this.stopHost(); 209 | } 210 | 211 | private calculateCPUAverage(cpuUsage: number, systemCPUUsage: number, cpuCount: number): void { 212 | let cpuPercentage = 0; 213 | const cpuDelta = cpuUsage - this.lastContainerCPUUsage; 214 | const systemDelta = systemCPUUsage - this.lastSystemCPUUsage; 215 | 216 | if (cpuDelta > 0 && systemDelta > 0) { 217 | cpuPercentage = (cpuDelta / systemDelta) * cpuCount * 100; 218 | 219 | // using exponential weighted moving average 220 | const factor = 30; // if 1, then average = current value 221 | this.cpuAverageCounter += 1; 222 | this.cpuAverage += (cpuPercentage - this.cpuAverage) / Math.min(this.cpuAverageCounter, factor); 223 | } 224 | 225 | this.lastContainerCPUUsage = cpuUsage; 226 | this.lastSystemCPUUsage = systemCPUUsage; 227 | } 228 | 229 | private resetCPUAverage(): void { 230 | this.cpuAverage = 0; 231 | this.cpuAverageCounter = 0; 232 | } 233 | 234 | public getHeaders(): { [header: string]: string; } { 235 | const nameToUse = this.displayName[0] ?? this.containerName[0]; 236 | return { 237 | 'x-container-nursery-container-name': nameToUse 238 | }; 239 | } 240 | 241 | public getTarget(): ProxyTarget { 242 | return { 243 | protocol: this.proxyUseHttps && this.containerRunning ? 'https:' : 'http:', 244 | host: this.containerRunning ? this.proxyHost : '127.0.0.1', 245 | port: this.containerRunning ? this.proxyPort : 8080 246 | }; 247 | } 248 | 249 | public newConnection(): void { 250 | if (!this.containerRunning) { 251 | this.startHost(); 252 | } else { 253 | this.resetConnectionTimeout(); 254 | } 255 | } 256 | 257 | public newSocketConnection(socket: internal.Duplex): void { 258 | if (!this.containerRunning) { 259 | this.startHost(); 260 | } else { 261 | this.resetConnectionTimeout(); 262 | } 263 | 264 | this.activeSockets.add(socket); 265 | socket.once('close', () => { 266 | this.resetConnectionTimeout(); 267 | this.activeSockets.delete(socket); 268 | }); 269 | } 270 | 271 | public stopConnectionTimeout(): void { 272 | if (this.connectionTimeoutId) { 273 | clearTimeout(this.connectionTimeoutId); 274 | this.connectionTimeoutId = null; 275 | } 276 | } 277 | 278 | public stopContainerEventEmitter(): void { 279 | if (!this.containerEventEmitter) return; 280 | this.containerEventEmitter.emit('stop-stream'); 281 | this.containerEventEmitter.removeAllListeners(); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["es6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "src", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "build", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 74 | 75 | /* Type Checking */ 76 | "strict": true, /* Enable all strict type-checking options. */ 77 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | --------------------------------------------------------------------------------