diff --git a/Certs/StatusPageCerts/Readme.md b/Certs/StatusPageCerts/Readme.md new file mode 100644 index 0000000000..59fe4c65d4 --- /dev/null +++ b/Certs/StatusPageCerts/Readme.md @@ -0,0 +1 @@ +Certs for Status page with custom domains. \ No newline at end of file diff --git a/Certs/StatusPageCerts/status.genosyn.com.crt b/Certs/StatusPageCerts/status.genosyn.com.crt new file mode 100644 index 0000000000..c30fd46227 --- /dev/null +++ b/Certs/StatusPageCerts/status.genosyn.com.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFKTCCBBGgAwIBAgISBI+ZMOTLreLlsVt7PF9qX1u2MA0GCSqGSIb3DQEBCwUA +MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD +EwJSMzAeFw0yMjEyMTAxMjA2MTJaFw0yMzAzMTAxMjA2MTFaMB0xGzAZBgNVBAMT +EnN0YXR1cy5nZW5vc3luLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAMgb7+Qhe/f9YCIATTMF7kVGO4Zwd2nJ167vEczBOsHkNspiVD9UPdL0KpFV +L37sqiclcVbUds5QO0h81ckBMNwsRiqSeEj5V5FcRII1PdduROBB7mVfgtuwG2jQ +NtU2IEDAIuxQVTOep3ciZBK4iiwgYttOKbvw5EtjlYKMk2+ZJy2eUjvEstNGkm5b +iekNEvt5ZTInYfWoyCaD9gsjMEaB/ueL21M7jbgJ049n64CYv2robhU9JEQW5p8h +B849YvO+nzhGdCuWRU4FPXzI4aDPs6ONceodg0IIeEbEZMntc1AKXyL7TJp3+kLI +eBpmtwXqQvNuCeqV7QXMK7X7QEsCAwEAAaOCAkwwggJIMA4GA1UdDwEB/wQEAwIF +oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAd +BgNVHQ4EFgQUvnstsk3lPsOYn1nKiQGwdeXVGscwHwYDVR0jBBgwFoAUFC6zF7dY +VsuuUAlA5h+vnYsUwsYwVQYIKwYBBQUHAQEESTBHMCEGCCsGAQUFBzABhhVodHRw +Oi8vcjMuby5sZW5jci5vcmcwIgYIKwYBBQUHMAKGFmh0dHA6Ly9yMy5pLmxlbmNy +Lm9yZy8wHQYDVR0RBBYwFIISc3RhdHVzLmdlbm9zeW4uY29tMEwGA1UdIARFMEMw +CAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9j +cHMubGV0c2VuY3J5cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHYAtz77 +JN+cTbp18jnFulj0bF38Qs96nzXEnh0JgSXttJkAAAGE/CSqhwAABAMARzBFAiAm +PBXbjWWdkehEstdTQ5LoAS1omXJQNnuo1nt2YsoL3gIhAJSewh6nfHL+lXhckXtk +pSkI2qNdq2otCyq3Z9W/Z4YpAHUArfe++nz/EMiLnT2cHj4YarRnKV3PsQwkyoWG +NOvcgooAAAGE/CSqvQAABAMARjBEAiAwAB2istEPxzNrMuw04guD5NyOt/RKDX5l +0y71+H9ekQIgaBg24zPbRNw01G5Y7WUsS9/vBgAEpEtfqpMcedNVwXAwDQYJKoZI +hvcNAQELBQADggEBALU11dZ+HIWxGUz0Q4IqVLfN9dP4cTVdee5GOX8KNh2rmIBv +7YZtuGJVVbILW+D5w/r+3l3z3MFg1ocEHUtfDCtmZes4WyBHXntaJe9JuqY6/ArN +VUIR1r0S6iKmZ17S0/FjruyHOyOw5Zhxix1Q5dMCpuCIfOtOaG8cAf8Qi1re3br9 +6dwDpgW5mUQBLA9DGnnXYKvkd/EcEUnf39o2UaDCfFSeQgSu46/98GcOQQ0SpN6c +OqEJ4Dnc6NP/Trmb1nDQbIMx4c5y/q7miDb8URdK3H+EGdmmBie7M46se3i11oGC +u7agSvvW32dShImvoC8pJFAS0cEaAPdODHgg5U0= +-----END CERTIFICATE----- diff --git a/Certs/StatusPageCerts/status.genosyn.com.key b/Certs/StatusPageCerts/status.genosyn.com.key new file mode 100644 index 0000000000..d594a11bd5 --- /dev/null +++ b/Certs/StatusPageCerts/status.genosyn.com.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAyBvv5CF79/1gIgBNMwXuRUY7hnB3acnXru8RzME6weQ2ymJU +P1Q90vQqkVUvfuyqJyVxVtR2zlA7SHzVyQEw3CxGKpJ4SPlXkVxEgjU9125E4EHu +ZV+C27AbaNA21TYgQMAi7FBVM56ndyJkEriKLCBi204pu/DkS2OVgoyTb5knLZ5S +O8Sy00aSbluJ6Q0S+3llMidh9ajIJoP2CyMwRoH+54vbUzuNuAnTj2frgJi/auhu +FT0kRBbmnyEHzj1i876fOEZ0K5ZFTgU9fMjhoM+zo41x6h2DQgh4RsRkye1zUApf +IvtMmnf6Qsh4Gma3BepC824J6pXtBcwrtftASwIDAQABAoIBABmBohq4f+Y0uCet +VSm/REc1NAonVLk5vpGwLFsmeBhVv/wc+3MVCEpW0AQ1UPgDL48M0T0JmNkkVeIf +81oLGlC+HfV4NPfMPHKtSZg1NBw9FG9nR/1I5tOcx2mdPJgBravDMdBgTvPk8aCY +VBwkxIvqVt9wP5aSlm7bkyeQRoyvRYP+cWnLo1sR6sIEO4SyjtoFzKtp/CrP8MBs +67mVyLPurEqCB0wVDCAr/G2SkJhCUXGvqSzfDU0oGEVGWmb95PQGsOe1KjqegY7A +BTFKMNjbOyncYau4EkZcPAKpv/9Qgo+52bYDKekSxk/ZHitE0TmW4WlC73UMkIQK +yG+Si+kCgYEA8R7+roCHqaxT30bNlJe+FCkVQuXsBJp3TAPUxBSnZZCp0mifEhQc +WGvvG9AOSVmoCPDMGw1xUo+zsUNkxmi+FcLUeAM5hdiV68D1QfjhUlrPidEv74l9 +0WlW3+z1tJVbN2WYdXD1JHg5nyq/hEMg4Q/FLEAMj6daIK1PFc+INuMCgYEA1HUS +6q89LzFMSGrGxksxQIoT4cgg1pUA3QLWijEnnT2YDk3Z5iB7F+a1jYQFH1SXBYMx +pIwiHM+qXsumcg0upLPj+ulqGKMt0qPkJTb4w1rGr73BKygDdmFbym3jQxbAqpIY +gW1ZvuOBgR8hstBc0gLUOsgl5TeSel1h/swzpXkCgYEA4ZsmoRAR32gmcdtFr6rr +ZuGpyxZmZ0hAJxfOlEje9+ELhJvvenLmsrUK3PMm6urAltz3nLhPN/jNISb1u891 +S9coBcK+p8WnQRciY8AC05O0bDcWqwHyf2YYqxyEKZs15fdhV0GBncX/5DWTTKWi +tfKTgnvLRP5JDhoazUWJJhECgYEAwJaf3z2bKPx3Oe4Q4g+nRenku/a+TcYkUjQQ +ZpTIZDFBdTX9IC6xZqksSmwyeIQlokma5p5hDdzxg5z39MseTQ8Eyp5sHolNMHSA +i3uZZP0Uvpo0UPqkqNr4ajfSmy402Go27JxDjlaNPo8J7R4UBguqdt6X+4C0t1eP +TXmuF4ECgYEAzK8INorcqp4a+rHrtIrBqS6I6q/NtBydZ6QC03qOL/6/6ITdf894 +6+S5cksZZqr8HC86f/FOxjGvtLFuuuWoHLTgEra2Blm9lAwrSXIRIgbTDI19rQGq +WaNnUaRke0jQMkNXndToTlU2j7OBI1vGMA5UwrXAD8LWAMrkgzPeKUc= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/CommonServer/Utils/Express.ts b/CommonServer/Utils/Express.ts index 72491b4772..a19c7e05e3 100644 --- a/CommonServer/Utils/Express.ts +++ b/CommonServer/Utils/Express.ts @@ -11,8 +11,6 @@ import { import UserType from 'Common/Types/UserType'; import Dictionary from 'Common/Types/Dictionary'; import Port from 'Common/Types/Port'; -import https from 'https'; -import fs from 'fs'; export type RequestHandler = express.RequestHandler; export type NextFunction = express.NextFunction; @@ -68,33 +66,13 @@ class Express { public static async launchApplication( appName: string, port?: Port, - httpsOptions?: { - port?: Port, - sniCallback?: any - } ): Promise { if (!this.app) { this.setupExpress(); } - - - if (httpsOptions && httpsOptions.port) { - const serverOptions = { - SNICallback: httpsOptions?.sniCallback, - cert: fs.readFileSync( - "/usr/src/app/Certs/Cert.crt" - ), - key: fs.readFileSync( - "/usr/src/app/Certs/Key.key" - ), - } - - https.createServer(serverOptions, this.app).listen(httpsOptions?.port.toNumber(), () => { - logger.info(`${appName} HTTPS server started on port: ${httpsOptions?.port?.toNumber()}`); - }); - } + return new Promise((resolve: Function) => { diff --git a/CommonServer/Utils/StartServer.ts b/CommonServer/Utils/StartServer.ts index 7e386422a3..ebb9caef6a 100644 --- a/CommonServer/Utils/StartServer.ts +++ b/CommonServer/Utils/StartServer.ts @@ -94,12 +94,8 @@ app.use(logRequest); const init: Function = async ( appName: string, port?: Port, - httpsOptions?: { - port?: Port, - sniCallBack?: Function - } ): Promise => { - await Express.launchApplication(appName, port, httpsOptions); + await Express.launchApplication(appName, port); LocalCache.setString('app', 'name', appName); CommonAPI(appName); diff --git a/Nginx/default.tpl.conf b/Nginx/default.tpl.conf index 5bd6d55ed0..9b89f560e3 100644 --- a/Nginx/default.tpl.conf +++ b/Nginx/default.tpl.conf @@ -22,19 +22,10 @@ upstream status-page { server status-page:3105 weight=10 max_fails=3 fail_timeout=30s; } -upstream status-page-secure { - server status-page:3107 weight=10 max_fails=3 fail_timeout=30s; -} - - upstream status-page-api { server status-page:3106 weight=10 max_fails=3 fail_timeout=30s; } -upstream status-page-api-secure { - server status-page:3108 weight=10 max_fails=3 fail_timeout=30s; -} - upstream home { server home:1444 weight=10 max_fails=3 fail_timeout=30s; } @@ -43,78 +34,14 @@ upstream workers { server workers:3452 weight=10 max_fails=3 fail_timeout=30s; } -# Acme Verification on port 80 server { listen 80 default_server; - server_name _; # All domains. - - proxy_busy_buffers_size 512k; - proxy_buffers 4 512k; - proxy_buffer_size 256k; - - fastcgi_buffers 16 16k; - fastcgi_buffer_size 32k; - - location / { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://status-page; - } - - location /status-page { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://status-page; - } - - location /status-page-api { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://status-page-api; - } - - # Acme Verification. - location /.well-known { - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # enable WebSockets (for ws://sockjs not connected error in the accounts source: https://stackoverflow.com/questions/41381444/websocket-connection-failed-error-during-websocket-handshake-unexpected-respon) - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://status-page-api; - } -} - -server { listen 443 default_server ssl; # Port HTTPS - - ssl_certificate /etc/nginx/certs/Cert.crt; - ssl_certificate_key /etc/nginx/certs/Key.key; + + + ssl_certificate /etc/nginx/certs/StatusPageCerts/$ssl_server_name.crt; + ssl_certificate_key /etc/nginx/certs/StatusPageCerts/$ssl_server_name.key; server_name _; # All domains. @@ -139,12 +66,7 @@ server { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_ssl_server_name on; - proxy_ssl_verify off; - proxy_pass_request_headers on; - proxy_ssl_name $host; - - proxy_pass https://status-page-secure; + proxy_pass http://status-page; } @@ -160,12 +82,7 @@ server { proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_ssl_server_name on; - proxy_ssl_name $host; - proxy_ssl_verify off; - proxy_pass_request_headers on; - - proxy_pass https://status-page-secure; + proxy_pass http://status-page; } location /status-page-api { @@ -178,7 +95,8 @@ server { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_pass https://status-page-api-secure; + + proxy_pass http://status-page-api; } # Acme Verification. @@ -192,7 +110,8 @@ server { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_pass https://status-page-api-secure; + + proxy_pass http://status-page-api; } } diff --git a/StatusPage/Dockerfile.tpl b/StatusPage/Dockerfile.tpl index c680aa9430..39a23c3d0d 100755 --- a/StatusPage/Dockerfile.tpl +++ b/StatusPage/Dockerfile.tpl @@ -41,8 +41,6 @@ RUN npm install COPY ./CommonServer /usr/src/CommonServer RUN npm run compile - - # Install CommonUI RUN mkdir /usr/src/CommonUI WORKDIR /usr/src/CommonUI @@ -51,8 +49,6 @@ RUN npm install --force COPY ./CommonUI /usr/src/CommonUI RUN npm run compile - - #SET ENV Variables ENV PRODUCTION=true ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true @@ -69,11 +65,7 @@ RUN npm install # - 3105: StatusPage EXPOSE 3105 # API -EXPOSE 3106 -# HTTPS UI -EXPOSE 3107 -# HTTPS API -EXPOSE 3108 +EXPOSE 3106 {{ if eq .Env.ENVIRONMENT "development" }} #Run the app diff --git a/StatusPage/Index.ts b/StatusPage/Index.ts index 9da9da08fe..6551b7db93 100755 --- a/StatusPage/Index.ts +++ b/StatusPage/Index.ts @@ -14,9 +14,6 @@ import BadDataException from 'Common/Types/Exception/BadDataException'; import StatusPageDomain from 'Model/Models/StatusPageDomain'; import StatusPageDomainService from 'CommonServer/Services/StatusPageDomainService'; import Port from 'Common/Types/Port'; -import tls from 'tls'; -import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService'; -import GreenlockCertificate from 'Model/Models/GreenlockCertificate'; export const APP_NAME: string = 'status-page-api'; @@ -94,42 +91,7 @@ app.get( const init: Function = async (): Promise => { try { // init the app - await App(APP_NAME, new Port(3106), { - port: new Port(3108), - sniCallback: (serverName: string, callback: Function) => { - logger.info("SNI CALLBACK " + serverName); - - GreenlockCertificateService.findBy({ - query: { - key: serverName, - }, - select: { - blob: true, - isKeyPair: true - }, - skip: 0, - limit: 10, - props: { - isRoot: true, - }, - }).then((result: Array) => { - if (result.length === 0) { - return callback(null, null); - } - - const certBlob = JSON.parse(result.find((i) => !i.isKeyPair)?.blob || '{}'); - const keyBlob = JSON.parse(result.find((i) => i.isKeyPair)?.blob || '{}'); - - callback(null, tls.createSecureContext({ - cert: certBlob.cert, - key: keyBlob.privateKeyPem, - })); - }).catch((err: Error) => { - logger.error(err); - return callback("Server Error. Please try again later."); - }); - } - }); + await App(APP_NAME, new Port(3106)); // connect to the database. await PostgresAppInstance.connect( diff --git a/StatusPage/Serve.ts b/StatusPage/Serve.ts index fd77adb2c2..15fa07c678 100644 --- a/StatusPage/Serve.ts +++ b/StatusPage/Serve.ts @@ -8,10 +8,7 @@ import Express, { } from 'CommonServer/Utils/Express'; import logger from 'CommonServer/Utils/Logger'; import Port from 'Common/Types/Port'; -import tls from 'tls'; import { PostgresAppInstance } from 'CommonServer/Infrastructure/PostgresDatabase'; -import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService'; -import GreenlockCertificate from 'Model/Models/GreenlockCertificate'; export const APP_NAME: string = 'status-page'; const app: ExpressApplication = Express.getExpressApp(); @@ -27,42 +24,7 @@ app.get('/*', (_req: ExpressRequest, res: ExpressResponse) => { const init: Function = async (): Promise => { try { // init the app - await App(APP_NAME, new Port(3105), { - port: new Port(3107), - sniCallback: (serverName: string, callback: Function) => { - logger.info("SNI CALLBACK " + serverName); - - GreenlockCertificateService.findBy({ - query: { - key: serverName, - }, - select: { - blob: true, - isKeyPair: true - }, - skip: 0, - limit: 10, - props: { - isRoot: true, - }, - }).then((result: Array) => { - if (result.length === 0) { - return callback(null, null); - } - - const certBlob = JSON.parse(result.find((i) => !i.isKeyPair)?.blob || '{}'); - const keyBlob = JSON.parse(result.find((i) => i.isKeyPair)?.blob || '{}'); - - callback(null, tls.createSecureContext({ - cert: certBlob.cert, - key: keyBlob.privateKeyPem, - })); - }).catch((err: Error) => { - logger.error(err); - return callback("Server Error. Please try again later."); - }); - } - }); + await App(APP_NAME, new Port(3105)); // connect to the database. await PostgresAppInstance.connect( diff --git a/StatusPage/package.json b/StatusPage/package.json index 2aea857fad..424866d885 100644 --- a/StatusPage/package.json +++ b/StatusPage/package.json @@ -20,7 +20,7 @@ "use-async-effect": "^2.2.6" }, "scripts": { - "dev": "node --require ts-node/register Serve.ts & nodemon", + "dev": "webpack-dev-server --port=3105 --mode=development & nodemon", "build": "webpack build --mode=production", "test": "react-app-rewired test", "eject": "webpack eject", diff --git a/Workers/Jobs/StatusPageCerts/StausPageCerts.ts b/Workers/Jobs/StatusPageCerts/StausPageCerts.ts index 40db6227b4..5d59cfdb93 100644 --- a/Workers/Jobs/StatusPageCerts/StausPageCerts.ts +++ b/Workers/Jobs/StatusPageCerts/StausPageCerts.ts @@ -19,6 +19,9 @@ import { JSONObject } from 'Common/Types/JSON'; import Response from 'CommonServer/Utils/Response'; import LIMIT_MAX from 'Common/Types/Database/LimitMax'; import axios from 'axios'; +import GreenlockCertificate from 'Model/Models/GreenlockCertificate'; +import GreenlockCertificateService from 'CommonServer/Services/GreenlockCertificateService'; +import fs from 'fs'; const router: ExpressRouter = Express.getRouter(); @@ -323,6 +326,51 @@ RunCron( } ); + +RunCron( + 'StatusPageCerts:WriteCertsToDisk', + IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, + async () => { + // Fetch all domains where certs are added to greenlock. + + const certs: Array = + await GreenlockCertificateService.findBy({ + query: { + + }, + select: { + isKeyPair: true, + key: true, + blob: true, + }, + limit: LIMIT_MAX, + skip: 0, + props: { + isRoot: true, + }, + }); + + for (const cert of certs) { + if (!cert.isKeyPair) { + continue; + } + + const certBlob = certs.find((i) => i.key === cert.key && !i.isKeyPair); + + if (!certBlob) { + continue; + } + + const key = JSON.parse(cert.blob || '{}').privateKeyPem; + const crt = JSON.parse(certBlob.blob || '{}').cert; + + // Write to disk. + fs.writeFileSync(`/usr/src/Certs/StatusPageCerts/${cert.key}.crt`, crt, { flag: 'wx' }); + fs.writeFileSync(`/usr/src/Certs/StatusPageCerts/${cert.key}.key`, key, { flag: 'wx' }); + } + } +); + RunCron( 'StatusPageCerts:CheckSslProvisioningStatus', IsDevelopment ? EVERY_MINUTE : EVERY_HOUR, diff --git a/docker-compose.tpl.yml b/docker-compose.tpl.yml index 90ae84175c..04b9a12d6c 100644 --- a/docker-compose.tpl.yml +++ b/docker-compose.tpl.yml @@ -141,8 +141,6 @@ services: ports: - '3105:3105' # HTTP UI Port - '3106:3106' # HTTP API Port - - '3107:3107' # HTTPS UI Port - - '3108:3108' # HTTPS API Port {{ if eq .Env.ENVIRONMENT "development" }} - 9764:9229 # Debugging port. {{ end }} @@ -163,10 +161,9 @@ services: depends_on: - accounts - dashboard-api - - volumes: - - ./Certs:/usr/src/app/Certs + {{ if eq .Env.ENVIRONMENT "development" }} + volumes: - ./StatusPage:/usr/src/app - /usr/src/app/node_modules/ - ./Common:/usr/src/Common @@ -249,8 +246,9 @@ services: links: - postgres - mail - {{ if eq .Env.ENVIRONMENT "development" }} volumes: + - ./Certs:/usr/src/Certs + {{ if eq .Env.ENVIRONMENT "development" }} - ./Workers:/usr/src/app # Use node modules of the container and not host system. # https://stackoverflow.com/questions/29181032/add-a-volume-to-docker-but-exclude-a-sub-folder