use docker api client dockerode instead of `docker` child process (#61)

- improves performance slightly as no child process execution is required
- easier to use API than building cli arguments & parsing stdout
- red colored stderr output of container
- parse dockerArgs manually, support -e and -v for now
- use fast ping for docker availability check
- fix namespace display bug if action is inside a package
diff --git a/README.md b/README.md
index e9ad006..54399e8 100644
--- a/README.md
+++ b/README.md
@@ -685,12 +685,12 @@
 <a name="default-debug-ports-and-commands"></a>
 #### Default debug ports and commands
 
-To just add default debug ports and docker command for a kind, add a custom debug kind and export an object with  `description`, `port` and `command` fields. Optionally `dockerArgs` for extra docker arguments (such as passing in environment variables using `-e` if necessary).
+To just add default debug ports and docker command for a kind, add a custom debug kind and export an object with  `description`, `port` and `command` fields. Optionally implement `updateContainerConfig()` for extra container settings (such as passing in environment variables using `-e` or mounting volumes).
 
 <a name="support-code-reloading"></a>
 #### Support code reloading
 
-To support live code reloading/mounting, add a custom debug kind and export an object with a `mountAction` function. This has to return an action that dynamically loads the code at the start of each activation. A typical approach is to mount the `<source-path>` (folder) passed on the cli as `/code` inside the docker container, from where the mount action can reload it. The exact mechanism will depend on the language - in node.js for example, `eval()` is [used for plain actions](src/kinds/nodejs/mount-plain.js#L30). The docker mounting can be specified in `dockerArgs`.
+To support live code reloading/mounting, add a custom debug kind and export an object with a `mountAction` function. This has to return an action that dynamically loads the code at the start of each activation. A typical approach is to mount the `<source-path>` (folder) passed on the cli as `/code` inside the docker container, from where the mount action can reload it. The exact mechanism will depend on the language - in node.js for example, `eval()` is [used for plain actions](src/kinds/nodejs/mount-plain.js#L30). The docker mounting can be specified in `updateContainerConfig()`.
 
 The `mountAction(invoker)` must return an object that is an openwhisk action `/init` definition, which consists of:
 
diff --git a/package-lock.json b/package-lock.json
index ae04f84..2c5ed8e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -462,6 +462,14 @@
             "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=",
             "dev": true
         },
+        "asn1": {
+            "version": "0.2.4",
+            "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+            "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
+            "requires": {
+                "safer-buffer": "~2.1.0"
+            }
+        },
         "astral-regex": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -479,11 +487,34 @@
             "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
             "dev": true
         },
+        "base64-js": {
+            "version": "1.3.1",
+            "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+            "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+        },
+        "bcrypt-pbkdf": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+            "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+            "requires": {
+                "tweetnacl": "^0.14.3"
+            }
+        },
         "binary-extensions": {
             "version": "2.0.0",
             "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
             "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow=="
         },
+        "bl": {
+            "version": "4.0.2",
+            "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz",
+            "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==",
+            "requires": {
+                "buffer": "^5.5.0",
+                "inherits": "^2.0.4",
+                "readable-stream": "^3.4.0"
+            }
+        },
         "brace-expansion": {
             "version": "1.1.11",
             "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -508,6 +539,20 @@
             "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
             "dev": true
         },
+        "buffer": {
+            "version": "5.6.0",
+            "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
+            "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+            "requires": {
+                "base64-js": "^1.0.2",
+                "ieee754": "^1.1.4"
+            }
+        },
+        "buffer-from": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+            "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+        },
         "caching-transform": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
@@ -567,6 +612,11 @@
                 "readdirp": "~3.3.0"
             }
         },
+        "chownr": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+            "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
+        },
         "clean-stack": {
             "version": "2.2.0",
             "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
@@ -645,6 +695,17 @@
             "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
             "dev": true
         },
+        "concat-stream": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+            "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+            "requires": {
+                "buffer-from": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.0.2",
+                "typedarray": "^0.0.6"
+            }
+        },
         "convert-source-map": {
             "version": "1.7.0",
             "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -733,6 +794,27 @@
             "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
             "dev": true
         },
+        "docker-modem": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.1.2.tgz",
+            "integrity": "sha512-fwlfnsK9WV+m+qc/NZCiGt+oYAMjmCUeir0a/l3oHb0yc8FhRAucdwT4htKD3aLtV+1w2syQePH9pQFxsq1GFA==",
+            "requires": {
+                "debug": "^4.1.1",
+                "readable-stream": "^3.5.0",
+                "split-ca": "^1.0.1",
+                "ssh2": "^0.8.7"
+            }
+        },
+        "dockerode": {
+            "version": "3.2.0",
+            "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.2.0.tgz",
+            "integrity": "sha512-C+y/W4Kks7YLBsfUOTMkk1IVilb4cdj+rE+UZ5hnE+rpcn2frSs7kX+6H8GteTqHcv8sln+GyxuP1qdno3IzIw==",
+            "requires": {
+                "concat-stream": "~2.0.0",
+                "docker-modem": "^2.1.0",
+                "tar-fs": "~2.0.1"
+            }
+        },
         "doctrine": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -755,6 +837,14 @@
                 "iconv-lite": "~0.4.13"
             }
         },
+        "end-of-stream": {
+            "version": "1.4.4",
+            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+            "requires": {
+                "once": "^1.4.0"
+            }
+        },
         "es-abstract": {
             "version": "1.17.5",
             "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz",
@@ -1202,6 +1292,11 @@
             "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==",
             "dev": true
         },
+        "fs-constants": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+            "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
+        },
         "fs-extra": {
             "version": "8.1.0",
             "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
@@ -1250,8 +1345,7 @@
         "get-port": {
             "version": "5.1.1",
             "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
-            "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
-            "dev": true
+            "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="
         },
         "glob": {
             "version": "7.1.6",
@@ -1402,6 +1496,11 @@
                 "safer-buffer": ">= 2.1.2 < 3"
             }
         },
+        "ieee754": {
+            "version": "1.1.13",
+            "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+            "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+        },
         "ignore": {
             "version": "4.0.6",
             "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@@ -1452,8 +1551,7 @@
         "inherits": {
             "version": "2.0.4",
             "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-            "dev": true
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
         },
         "inquirer": {
             "version": "7.1.0",
@@ -1971,6 +2069,11 @@
                 "minimist": "^1.2.5"
             }
         },
+        "mkdirp-classic": {
+            "version": "0.5.2",
+            "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz",
+            "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g=="
+        },
         "mocha": {
             "version": "7.1.1",
             "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.1.tgz",
@@ -2472,7 +2575,6 @@
             "version": "1.4.0",
             "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
             "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "dev": true,
             "requires": {
                 "wrappy": "1"
             }
@@ -2690,6 +2792,15 @@
             "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
             "dev": true
         },
+        "pump": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+            "requires": {
+                "end-of-stream": "^1.1.0",
+                "once": "^1.3.1"
+            }
+        },
         "punycode": {
             "version": "2.1.1",
             "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -2702,6 +2813,16 @@
             "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==",
             "dev": true
         },
+        "readable-stream": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+            "requires": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            }
+        },
         "readdirp": {
             "version": "3.3.0",
             "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz",
@@ -2922,12 +3043,35 @@
                 }
             }
         },
+        "split-ca": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
+            "integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY="
+        },
         "sprintf-js": {
             "version": "1.0.3",
             "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
             "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
             "dev": true
         },
+        "ssh2": {
+            "version": "0.8.9",
+            "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz",
+            "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==",
+            "requires": {
+                "ssh2-streams": "~0.4.10"
+            }
+        },
+        "ssh2-streams": {
+            "version": "0.4.10",
+            "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz",
+            "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==",
+            "requires": {
+                "asn1": "~0.2.0",
+                "bcrypt-pbkdf": "^1.0.2",
+                "streamsearch": "~0.1.2"
+            }
+        },
         "stream-events": {
             "version": "1.0.5",
             "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
@@ -2937,6 +3081,11 @@
                 "stubs": "^3.0.0"
             }
         },
+        "streamsearch": {
+            "version": "0.1.2",
+            "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+            "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+        },
         "string-width": {
             "version": "4.2.0",
             "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -2989,6 +3138,21 @@
                 "es-abstract": "^1.17.5"
             }
         },
+        "string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "requires": {
+                "safe-buffer": "~5.2.0"
+            },
+            "dependencies": {
+                "safe-buffer": {
+                    "version": "5.2.0",
+                    "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
+                    "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
+                }
+            }
+        },
         "strip-ansi": {
             "version": "6.0.0",
             "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
@@ -3082,6 +3246,29 @@
                 }
             }
         },
+        "tar-fs": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
+            "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
+            "requires": {
+                "chownr": "^1.1.1",
+                "mkdirp-classic": "^0.5.2",
+                "pump": "^3.0.0",
+                "tar-stream": "^2.0.0"
+            }
+        },
+        "tar-stream": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz",
+            "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==",
+            "requires": {
+                "bl": "^4.0.1",
+                "end-of-stream": "^1.4.1",
+                "fs-constants": "^1.0.0",
+                "inherits": "^2.0.3",
+                "readable-stream": "^3.1.1"
+            }
+        },
         "teeny-request": {
             "version": "6.0.1",
             "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-6.0.1.tgz",
@@ -3155,6 +3342,11 @@
             "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
             "dev": true
         },
+        "tweetnacl": {
+            "version": "0.14.5",
+            "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+            "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q="
+        },
         "type-check": {
             "version": "0.3.2",
             "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
@@ -3170,6 +3362,11 @@
             "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
             "dev": true
         },
+        "typedarray": {
+            "version": "0.0.6",
+            "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+            "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+        },
         "typedarray-to-buffer": {
             "version": "3.1.5",
             "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -3199,6 +3396,11 @@
             "integrity": "sha1-iS/pWWCAXoVRnxzUOJ8stMu3ZS8=",
             "dev": true
         },
+        "util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+        },
         "uuid": {
             "version": "3.4.0",
             "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
@@ -3299,8 +3501,7 @@
         "wrappy": {
             "version": "1.0.2",
             "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-            "dev": true
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
         },
         "write": {
             "version": "1.0.3",
diff --git a/package.json b/package.json
index 3c90c97..61a4fa0 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,10 @@
         "chalk": "^4.0.0",
         "clone": "^2.1.2",
         "debug": "^4.1.1",
+        "dockerode": "^3.2.0",
         "fetch-retry": "^3.1.0",
         "fs-extra": "^8.1.0",
+        "get-port": "^5.1.1",
         "isomorphic-fetch": "^2.2.1",
         "livereload": "^0.9.1",
         "manakin": "^0.5.2",
@@ -65,7 +67,6 @@
         "eslint": "^6.8.0",
         "eslint-config-problems": "^4.0.0",
         "eslint-plugin-mocha": "^6.3.0",
-        "get-port": "^5.1.1",
         "mocha": "^7.1.0",
         "mocha-multi-reporters": "^1.1.7",
         "mock-require": "^3.0.3",
diff --git a/src/debugger.js b/src/debugger.js
index e7d250b..e0db32a 100644
--- a/src/debugger.js
+++ b/src/debugger.js
@@ -36,6 +36,12 @@
     }
 }
 
+function getNamespaceFromActionMetadata(actionMetadata) {
+    // if the action is inside a package, this returns <namespace>/<package>
+    // but we only want the namespace
+    return actionMetadata.namespace.split("/")[0];
+}
+
 /**
  * Central component of wskdebug.
  */
@@ -74,7 +80,7 @@
         // get the action metadata
         this.actionMetadata = await this.agentMgr.peekAction();
         log.debug("fetched action metadata from openwhisk");
-        this.wskProps.namespace = this.actionMetadata.namespace;
+        this.wskProps.namespace = getNamespaceFromActionMetadata(this.actionMetadata);
 
         const h = log.highlightColor;
         log.step("Debugging " + h(`/${this.wskProps.namespace}/${this.actionName}`) + " on " + h(this.wskProps.apihost));
@@ -82,6 +88,9 @@
         // local debug container
         this.invoker = new OpenWhiskInvoker(this.actionName, this.actionMetadata, this.argv, this.wskProps, this.wsk);
 
+        // quick fail for missing requirements such as docker not running
+        await this.invoker.checkIfDockerAvailable();
+
         try {
             // run build initially (would be required by starting container)
             if (this.argv.onBuild) {
@@ -95,7 +104,6 @@
             // task 1 - start local container
             const containerTask = (async () => {
                 const debug2 = log.newDebug();
-                log.spinner('Starting local container');
 
                 // start container - get it up fast for VSCode to connect within its 10 seconds timeout
                 await this.invoker.startContainer(debug2);
@@ -277,21 +285,29 @@
             log.log();
             log.log();
             log.debug("shutting down...");
-            log.spinner("Shutting down");
         } else {
             log.debug("aborting start - shutting down ...");
         }
+        log.spinner("Shutting down");
 
         // need to shutdown everything even if some fail, hence tryCatch() for each
 
         if (this.agentMgr) {
             await this.tryCatch(this.agentMgr.shutdown());
         }
+
+        // ------------< critical removal must happen above this line >---------------
+
+        // in VS Code, we will not run beyond this line upon debug stop.
+        // this is because invoker.stop() will kill the container & thus close the
+        // debug port, upon which VS Code kills the debug process (us)
         if (this.invoker) {
             await this.tryCatch(this.invoker.stop());
-            log.debug(`stopped container: ${this.invoker.name()}`);
         }
+
         if (this.watcher) {
+            // this is not critical on a process exit, only if Debugger is used programmatically
+            // and might be reused for a new run()
             await this.tryCatch(this.watcher.stop());
             log.debug("stopped source file watching");
         }
diff --git a/src/dockerutils.js b/src/dockerutils.js
new file mode 100644
index 0000000..0298c63
--- /dev/null
+++ b/src/dockerutils.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+const yargsParser = require('yargs-parser');
+
+function safeContainerName(name) {
+    // docker container names are restricted to [a-zA-Z0-9][a-zA-Z0-9_.-]*
+
+    // 1. replace special characters with dash
+    name = name.replace(/[^a-zA-Z0-9_.-]+/g, '-');
+    // 2. leading character is more limited
+    name = name.replace(/^[^a-zA-Z0-9]+/g, '');
+    // 3. (nice to have) remove trailing special chars
+    name = name.replace(/[^a-zA-Z0-9]+$/g, '');
+
+    return name;
+}
+
+// convert docker run cli args to docker create container config
+// https://docs.docker.com/engine/reference/commandline/run/
+// https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
+function dockerRunArgs2CreateContainerConfig(args, containerConfig) {
+    if (!args) {
+        return containerConfig;
+    }
+
+    containerConfig = containerConfig || {};
+
+    const argv = yargsParser(args.split(" "));
+
+    for (const [key, value] of Object.entries(argv)) {
+        // treat all as array, makes it simpler below
+        const values = Array.isArray(value) ? value : [ value ];
+
+        switch (key) {
+        case "e": // environment variables
+            values.forEach(e => containerConfig.Env.push(e));
+            break;
+        case "v": // volume mounts (binds)
+            values.forEach(v => containerConfig.HostConfig.Binds.push(v));
+            break;
+        case "_": // ignore yargs specials
+        case "$0":
+            break;
+        default:
+            throw new Error(`Unsupported argument in --dockerArgs: '-${key}'. Please report at https://github.com/apache/openwhisk-wskdebug/issues`)
+        }
+    }
+
+    return containerConfig;
+}
+
+module.exports = {
+    safeContainerName,
+    dockerRunArgs2CreateContainerConfig
+};
diff --git a/src/invoker.js b/src/invoker.js
index 32b9d65..0fcd536 100644
--- a/src/invoker.js
+++ b/src/invoker.js
@@ -17,14 +17,18 @@
 
 'use strict';
 
-const { spawn, execSync } = require('child_process');
 const fetch = require('fetch-retry')(require('isomorphic-fetch'));
 const kinds = require('./kinds/kinds');
 const path = require('path');
 const log = require("./log");
+const Docker = require('dockerode');
+const getPort = require('get-port');
+const dockerUtils = require('./dockerutils');
+const prettyBytes = require('pretty-bytes');
 
 const RUNTIME_PORT = 8080;
-const INIT_RETRY_DELAY_MS = 100;
+const MAX_INIT_RETRY_MS = 20000; // 20 sec
+const INIT_RETRY_DELAY_MS = 200;
 
 // https://github.com/apache/incubator-openwhisk/blob/master/docs/reference.md#system-limits
 const OPENWHISK_DEFAULTS = {
@@ -32,18 +36,6 @@
     memory: 256
 };
 
-function execute(cmd, options, debug2) {
-    cmd = cmd.replace(/\s+/g, ' ');
-    const result = execSync(cmd, options);
-
-    (debug2 || log.debug)(`executed: ${cmd}`);
-    if (result) {
-        return result.toString().trim();
-    } else {
-        return '';
-    }
-}
-
 // if value is a function, invoke it with args, otherwise return it as object
 // if value is undefined, will return undefined
 function resolveValue(value, ...args) {
@@ -79,12 +71,14 @@
         this.wskProps = wskProps;
         this.wsk = wsk;
 
-        this.containerName = this.asContainerName(`wskdebug-${this.action.name}-${Date.now()}`);
+        this.containerName = dockerUtils.safeContainerName(`wskdebug-${this.action.name}-${Date.now()}`);
+        this.docker = new Docker();
     }
 
     async checkIfDockerAvailable() {
         try {
-            execute("docker info", {stdio: 'ignore'});
+            await this.docker.ping();
+            log.debug("docker - availability check")
         } catch (e) {
             throw new Error("Docker not running on local system. A local docker environment is required for the debugger.")
         }
@@ -183,20 +177,72 @@
         }
     }
 
-    async startContainer(debug2) {
-        let showDockerRunOutput = log.isVerbose;
-
-        // quick fail for missing requirements such as docker not running
-        await this.checkIfDockerAvailable();
-
+    async isImagePresent(image, debug) {
         try {
-            execute(`docker inspect --type=image ${this.image} 2> /dev/null`);
+            await this.docker.getImage(image).inspect();
+            debug(`docker - image inspected, is present: ${image}`);
+            return true;
         } catch (e) {
-            // make sure the user can see the image download process as part of docker run
-            showDockerRunOutput = true;
-            log.warn(`
+            debug(`docker - image inspected, not found: ${image}`);
+            return false;
+        }
+    }
+
+    async pull(image) {
+        await new Promise((resolve, reject) => {
+            this.docker.pull(image, (err, stream) => {
+                // streaming output from pull...
+                if (err) {
+                    return reject(err);
+                }
+
+                function onFinished(err, output) {
+                    if (err) {
+                        return reject(err);
+                    }
+                    return resolve(output);
+                }
+
+                const events = {};
+                function onProgress(event) {
+                    if (!event.progress) {
+                        return;
+                    }
+
+                    if (event.status) {
+                        events[event.status] = events[event.status] || {};
+                        if (event.id) {
+                            events[event.status][event.id] = event;
+                        }
+                    }
+                    const progressMsg = Object.entries(events).reduce((result, [status, events], idx) => {
+                        const progress = Object.values(events).reduce((sum, e) => {
+                            if (e.progressDetail && e.progressDetail.current && e.progressDetail.total) {
+                                sum.current += e.progressDetail.current;
+                                sum.total   += e.progressDetail.total;
+                            }
+                            return sum;
+                        }, { current: 0, total: 0 });
+
+                        return result + `${idx > 0 ? ", " : ""}${status}: ${prettyBytes(progress.current)} of ${prettyBytes(progress.total)}`;
+                    }, "");
+
+                    log.spinner(`Pulling docker image ${image} (${progressMsg})`);
+                }
+
+                this.docker.modem.followProgress(stream, onFinished, onProgress);
+            });
+        });
+    }
+
+    async startContainer(debug) {
+        if (!await this.isImagePresent(this.image, debug)) {
+            // show after 8 seconds, as VS code will timeout after 10 secs by default,
+            // so that the user can see it after all the "docker pull" progress output
+            setTimeout(() => {
+                log.warn(`
 +------------------------------------------------------------------------------------------+
-| Docker image must be downloaded: ${this.image}
+| Docker image being downloaded: ${this.image}
 |                                                                                          |
 | Note: If you debug in VS Code and it fails with "Cannot connect to runtime process"      |
 | due to a timeout, run this command once:                                                 |
@@ -206,36 +252,84 @@
 | Alternatively set a higher 'timeout' in the launch configuration, such as 60000 (1 min). |
 +------------------------------------------------------------------------------------------+
 `);
+            }, 8000);
+
+            debug(`Pulling ${this.image}`)
+            log.spinner(`Pulling ${this.image}...`);
+
+            await this.pull(this.image);
+
+            debug("Pull complete");
         }
 
-        execute(
-            `docker run
-                -d
-                --name ${this.name()}
-                --rm
-                -m ${this.memory}
-                -p ${RUNTIME_PORT}
-                -p ${this.debug.port}:${this.debug.internalPort}
-                ${this.dockerArgsFromKind}
-                ${this.dockerArgsFromUser}
-                ${this.image}
-                ${this.debug.command}
-            `,
-            // live stream view for docker image download output
-            { stdio: showDockerRunOutput ? "inherit" : null },
-            debug2
+        log.spinner('Starting container');
+
+        // links for docker create container config:
+        //   docker api: https://docs.docker.com/engine/api/v1.37/#operation/ContainerCreate
+        //   docker run impl: https://github.com/docker/cli/blob/2c3797015f5e7ef4502235b638d161279c471a8d/cli/command/container/run.go#L33
+        //   https://github.com/apocas/dockerode/issues/257
+        //   https://github.com/apocas/dockerode/blob/master/lib/docker.js#L1442
+        //   https://medium.com/@johnnyeric/how-to-reproduce-command-docker-run-via-docker-remote-api-with-node-js-5918d7b221ea
+
+        const containerRuntimePort = `${RUNTIME_PORT}/tcp`;
+        const hostRuntimePort = await getPort();
+        this.containerURL = `http://0.0.0.0:${hostRuntimePort}`;
+        const containerDebugPort = `${this.debug.internalPort}/tcp`;
+
+        const createContainerConfig = {
+            name: this.containerName,
+            Image: this.image,
+            Cmd: [ 'sh', '-c', this.debug.command ],
+            Env: [],
+            Volumes: {},
+            ExposedPorts: {
+                [containerRuntimePort]: {},
+                [containerDebugPort]: {}
+            },
+            HostConfig: {
+                AutoRemove: true,
+                PortBindings: {
+                    [containerRuntimePort]: [{ HostPort: `${hostRuntimePort}` }],
+                    [containerDebugPort]: [{ HostPort: `${this.debug.port}` }]
+                },
+                Memory: this.memory,
+                Binds: []
+            }
+        };
+
+        if (this.debug.updateContainerConfig) {
+            this.debug.updateContainerConfig(this, createContainerConfig);
+        }
+
+        dockerUtils.dockerRunArgs2CreateContainerConfig(this.dockerArgsFromUser, createContainerConfig);
+
+        debug("docker - creating container:", createContainerConfig);
+
+        this.container = await this.docker.createContainer(createContainerConfig);
+
+        const stream = await this.container.attach({
+            stream: true,
+            stdout: true,
+            stderr: true
+        });
+
+        const spinnerSafeStream = (stream) => ({
+            write: (data) => {
+                log.stopSpinner();
+                stream(data.toString().replace(/\n$/, ""));
+                log.resumeSpinner();
+            }
+        });
+
+        this.container.modem.demuxStream(
+            stream,
+            spinnerSafeStream(console.log),
+            spinnerSafeStream(console.error)
         );
 
-        this.containerRunning = true;
+        await this.container.start();
 
-        log.stopSpinner();
-        spawn("docker", ["logs", "-t", "-f", this.name()], {
-            stdio: [
-                "inherit", // stdin
-                global.mochaLogFile || "inherit", // stdout
-                global.mochaLogFile || "inherit"  // stderr
-            ]
-        });
+        debug(`docker - started container ${this.container.id}`);
     }
 
     getSourcePath() {
@@ -254,6 +348,18 @@
         return this.debug.port;
     }
 
+    name() {
+        return this.containerName;
+    }
+
+    url() {
+        return this.containerURL || "";
+    }
+
+    timeout() {
+        return this.action.limits.timeout || OPENWHISK_DEFAULTS.timeout;
+    }
+
     async init(actionWithCode) {
         let action;
         if (this.sourceMountAction) {
@@ -267,6 +373,8 @@
             };
         }
 
+        const RETRIES = MAX_INIT_RETRY_MS / INIT_RETRY_DELAY_MS;
+
         const response = await fetch(`${this.url()}/init`, {
             method: "POST",
             headers: {
@@ -275,8 +383,14 @@
             body: JSON.stringify({
                 value: action
             }),
-            retries: this.timeout() / INIT_RETRY_DELAY_MS,
-            retryDelay: INIT_RETRY_DELAY_MS
+            retryDelay: INIT_RETRY_DELAY_MS,
+            retryOn: function(attempt, error) {
+                // after 1.5 seconds, show retry to user via spinner
+                if (attempt >= 1500 / INIT_RETRY_DELAY_MS) {
+                    log.spinner(`Installing action (retry ${attempt}/${RETRIES})`)
+                }
+                return error !== null && attempt < RETRIES;
+            }
         });
 
         if (response.status === 502) {
@@ -308,40 +422,15 @@
     }
 
     async stop() {
-        if (this.containerRunning) {
-            execute(`docker kill ${this.name()}`);
+        if (this.container) {
+            // log this here for VS Code, will be the last visible log message since
+            // we will be killed by VS code after the container is gone after the kill()
+            log.log(`Stopping container ${this.name()}.`);
+            await this.container.kill();
+            delete this.container;
+            log.debug(`docker - stopped container ${this.name()}`);
         }
     }
-
-    name() {
-        return this.containerName;
-    }
-
-    url() {
-        if (!this.containerURL) {
-            // ask docker for the exposed IP and port of the RUNTIME_PORT on the container
-            const host = execute(`docker port ${this.name()} ${RUNTIME_PORT}`);
-            this.containerURL = `http://${host}`;
-        }
-        return this.containerURL;
-    }
-
-    timeout() {
-        return this.action.limits.timeout || OPENWHISK_DEFAULTS.timeout;
-    }
-
-    asContainerName(name) {
-        // docker container names are restricted to [a-zA-Z0-9][a-zA-Z0-9_.-]*
-
-        // 1. replace special characters with dash
-        name = name.replace(/[^a-zA-Z0-9_.-]+/g, '-');
-        // 2. leading character is more limited
-        name = name.replace(/^[^a-zA-Z0-9]+/g, '');
-        // 3. (nice to have) remove trailing special chars
-        name = name.replace(/[^a-zA-Z0-9]+$/g, '');
-
-        return name;
-    }
 }
 
 module.exports = OpenWhiskInvoker;
diff --git a/src/kinds/nodejs/nodejs.js b/src/kinds/nodejs/nodejs.js
index 1791def..5f1e32b 100644
--- a/src/kinds/nodejs/nodejs.js
+++ b/src/kinds/nodejs/nodejs.js
@@ -34,25 +34,22 @@
         return `node --expose-gc --inspect=0.0.0.0:${invoker.debug.internalPort} app.js`
     },
 
-    // return extra docker arguments such as mounting the source path
-    dockerArgs: function(invoker) {
-        let args = "";
+    // set extra docker container settings such as mounting the source path
+    updateContainerConfig: function(invoker, containerConfig) {
         if (invoker.sourceDir) {
             if (!invoker.sourceFile) {
                 throw new Error(`[source-path] or --build-path must point to a source file, it cannot be a folder: '${invoker.sourcePath}'`);
             }
 
-            args += ` -v "${invoker.sourceDir}:${CODE_MOUNT}"`;
+            containerConfig.HostConfig.Binds.push(`${invoker.sourceDir}:${CODE_MOUNT}`);
         }
 
         if (process.env.WSK_NODE_DEBUG) {
-            args += ` -e NODE_DEBUG='${process.env.WSK_NODE_DEBUG}'`;
+            containerConfig.Env.push(`NODE_DEBUG=${process.env.WSK_NODE_DEBUG}`);
         }
         if (process.env.DEBUG) {
-            args += ` -e DEBUG='${process.env.DEBUG}'`;
+            containerConfig.Env.push(`DEBUG=${process.env.DEBUG}`);
         }
-
-        return args;
     },
 
     // return action for /init that mounts the sources specified by invoker.sourcePath
diff --git a/src/log.js b/src/log.js
index fc39e25..0b5f555 100644
--- a/src/log.js
+++ b/src/log.js
@@ -117,6 +117,12 @@
         }
     },
 
+    verboseStep: function(...args) {
+        if (this.isVerbose) {
+            this.step(...args);
+        }
+    },
+
     verboseWrite: function(text) {
         if (this.isVerbose) {
             process.stdout.write(text);
@@ -161,6 +167,12 @@
         spinner.stop();
     },
 
+    resumeSpinner: function() {
+        if (spinner.text) {
+            this.spinner(spinner.text);
+        }
+    },
+
     /** Finish any running spinner and show a log message with a success symbol in front. */
     succeed: function(text) {
         spinner.stopAndPersist({
diff --git a/test/wskdebug.test.js b/test/wskdebug.test.js
index e988ad3..e771336 100644
--- a/test/wskdebug.test.js
+++ b/test/wskdebug.test.js
@@ -23,6 +23,7 @@
 // tests basic cli
 
 let wskdebug = require('../index');
+const dockerUtils = require('../src/dockerutils');
 
 const test = require('./test');
 const assert = require('assert');
@@ -111,4 +112,26 @@
         await wskdebug(`package/action`);
         assert.strictEqual(argv.action, "package/action");
     });
+
+    it("should parse docker args", function() {
+        const args = " -e foo=bar -v /some/path:/mount/path -v /another:/path";
+
+        const containerConfig = {
+            Cmd: [],
+            Env: [],
+            Volumes: {},
+            HostConfig: {
+                Binds: [],
+                ExposedPorts: {},
+                PortBindings: {}
+            }
+        };
+        dockerUtils.dockerRunArgs2CreateContainerConfig(args, containerConfig);
+
+        console.log(containerConfig);
+
+        assert.strictEqual(containerConfig.Env[0], "foo=bar");
+        assert.strictEqual(containerConfig.HostConfig.Binds[0], "/some/path:/mount/path");
+        assert.strictEqual(containerConfig.HostConfig.Binds[1], "/another:/path");
+    })
 });