Repository: exasol/integration-test-docker-environment (language: python)

Issue:
Create more integration tests for API calls
## Background

In #235 we spitted up the commands and API calls. It would be good to create integration tests for the three important API calls:
* `build_test_container()`
* `push_test_container()`
* `spawn_test_environment()`



Discussion / hints:

Fix patch:
diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md
index f9f58f601..4d3eb820c 100644
--- a/doc/changes/changelog.md
+++ b/doc/changes/changelog.md
@@ -1,5 +1,6 @@
 # Changes
 
+* [0.12.0](changes_0.12.0.md)
 * [0.11.0](changes_0.11.0.md)
 * [0.10.0](changes_0.10.0.md)
 * [0.9.0](changes_0.9.0.md)
diff --git a/doc/changes/changes_0.12.0.md b/doc/changes/changes_0.12.0.md
index 2660ca274..0f2573a2d 100644
--- a/doc/changes/changes_0.12.0.md
+++ b/doc/changes/changes_0.12.0.md
@@ -31,6 +31,7 @@ TBD
 ## Refactoring:
 
 - #235: Moved implementations of all click commands in separate methods
+- #240: Created more integation tests for api calls and implemented return values
 
 ## Documentation:
 - TBD
diff --git a/poetry.lock b/poetry.lock
index 08258bbe6..80a1f5a9f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,11 +8,11 @@ python-versions = ">=3.6"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.0.12"
+version = "2.1.0"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "main"
 optional = false
-python-versions = ">=3.5.0"
+python-versions = ">=3.6.0"
 
 [package.extras]
 unicode_backport = ["unicodedata2"]
@@ -54,11 +54,11 @@ ssh = ["paramiko (>=2.4.2)"]
 
 [[package]]
 name = "docutils"
-version = "0.18.1"
+version = "0.19"
 description = "Docutils -- Python Documentation Utilities"
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.7"
 
 [[package]]
 name = "gitdb"
@@ -172,7 +172,7 @@ python-versions = "*"
 
 [[package]]
 name = "networkx"
-version = "2.8.4"
+version = "2.8.5"
 description = "Python package for creating and manipulating graphs and networks"
 category = "main"
 optional = false
@@ -217,7 +217,7 @@ python-versions = "*"
 
 [[package]]
 name = "python-daemon"
-version = "2.3.0"
+version = "2.3.1"
 description = "Library to implement a well-behaved Unix daemon process."
 category = "main"
 optional = false
@@ -243,7 +243,7 @@ six = ">=1.5"
 
 [[package]]
 name = "requests"
-version = "2.28.0"
+version = "2.28.1"
 description = "Python HTTP for Humans."
 category = "main"
 optional = false
@@ -251,12 +251,12 @@ python-versions = ">=3.7, <4"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2.0.0,<2.1.0"
+charset-normalizer = ">=2,<3"
 idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
-use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 
 [[package]]
@@ -312,19 +312,19 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "tornado"
-version = "6.1"
+version = "6.2"
 description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
 category = "main"
 optional = false
-python-versions = ">= 3.5"
+python-versions = ">= 3.7"
 
 [[package]]
 name = "urllib3"
-version = "1.26.9"
+version = "1.26.11"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "main"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
 
 [package.extras]
 brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
@@ -355,8 +355,8 @@ certifi = [
     {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"},
 ]
 charset-normalizer = [
-    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
-    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+    {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"},
+    {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"},
 ]
 click = [
     {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
@@ -371,8 +371,8 @@ docker = [
     {file = "docker-5.0.3.tar.gz", hash = "sha256:d916a26b62970e7c2f554110ed6af04c7ccff8e9f81ad17d0d40c75637e227fb"},
 ]
 docutils = [
-    {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"},
-    {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
+    {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"},
+    {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"},
 ]
 gitdb = [
     {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"},
@@ -452,8 +452,8 @@ netaddr = [
     {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"},
 ]
 networkx = [
-    {file = "networkx-2.8.4-py3-none-any.whl", hash = "sha256:6933b9b3174a0bdf03c911bb4a1ee43a86ce3edeb813e37e1d4c553b3f4a2c4f"},
-    {file = "networkx-2.8.4.tar.gz", hash = "sha256:5e53f027c0d567cf1f884dbb283224df525644e43afd1145d64c9d88a3584762"},
+    {file = "networkx-2.8.5-py3-none-any.whl", hash = "sha256:a762f4b385692d9c3a6f2912d058d76d29a827deaedf9e63ed14d397b8030687"},
+    {file = "networkx-2.8.5.tar.gz", hash = "sha256:15a7b81a360791c458c55a417418ea136c13378cfdc06a2dcdc12bd2f9cf09c1"},
 ]
 pydot = [
     {file = "pydot-1.4.2-py2.py3-none-any.whl", hash = "sha256:66c98190c65b8d2e2382a441b4c0edfdb4f4c025ef9cb9874de478fb0793a451"},
@@ -468,16 +468,16 @@ pyreadline3 = [
     {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"},
 ]
 python-daemon = [
-    {file = "python-daemon-2.3.0.tar.gz", hash = "sha256:bda993f1623b1197699716d68d983bb580043cf2b8a66a01274d9b8297b0aeaf"},
-    {file = "python_daemon-2.3.0-py2.py3-none-any.whl", hash = "sha256:191c7b67b8f7aac58849abf54e19fe1957ef7290c914210455673028ad454989"},
+    {file = "python-daemon-2.3.1.tar.gz", hash = "sha256:15c2c5e2cef563e0a5f98d542b77ba59337380b472975d2b2fd6b8c4d5cf46ca"},
+    {file = "python_daemon-2.3.1-py2.py3-none-any.whl", hash = "sha256:4e3bf67784c78aaa55ec001a2f832b464a54c5f9c89c11b311e2416a8c247431"},
 ]
 python-dateutil = [
     {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
     {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
 ]
 requests = [
-    {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"},
-    {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"},
+    {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
+    {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
 ]
 simplejson = [
     {file = "simplejson-3.17.6-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a89acae02b2975b1f8e4974cb8cdf9bf9f6c91162fb8dec50c259ce700f2770a"},
@@ -563,51 +563,21 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 tornado = [
-    {file = "tornado-6.1-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32"},
-    {file = "tornado-6.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c"},
-    {file = "tornado-6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05"},
-    {file = "tornado-6.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910"},
-    {file = "tornado-6.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b"},
-    {file = "tornado-6.1-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675"},
-    {file = "tornado-6.1-cp35-cp35m-win32.whl", hash = "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5"},
-    {file = "tornado-6.1-cp35-cp35m-win_amd64.whl", hash = "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68"},
-    {file = "tornado-6.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb"},
-    {file = "tornado-6.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c"},
-    {file = "tornado-6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921"},
-    {file = "tornado-6.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558"},
-    {file = "tornado-6.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c"},
-    {file = "tornado-6.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085"},
-    {file = "tornado-6.1-cp36-cp36m-win32.whl", hash = "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575"},
-    {file = "tornado-6.1-cp36-cp36m-win_amd64.whl", hash = "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795"},
-    {file = "tornado-6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f"},
-    {file = "tornado-6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102"},
-    {file = "tornado-6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4"},
-    {file = "tornado-6.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd"},
-    {file = "tornado-6.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01"},
-    {file = "tornado-6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d"},
-    {file = "tornado-6.1-cp37-cp37m-win32.whl", hash = "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df"},
-    {file = "tornado-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37"},
-    {file = "tornado-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95"},
-    {file = "tornado-6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a"},
-    {file = "tornado-6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5"},
-    {file = "tornado-6.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288"},
-    {file = "tornado-6.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f"},
-    {file = "tornado-6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6"},
-    {file = "tornado-6.1-cp38-cp38-win32.whl", hash = "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326"},
-    {file = "tornado-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c"},
-    {file = "tornado-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5"},
-    {file = "tornado-6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe"},
-    {file = "tornado-6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea"},
-    {file = "tornado-6.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2"},
-    {file = "tornado-6.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0"},
-    {file = "tornado-6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd"},
-    {file = "tornado-6.1-cp39-cp39-win32.whl", hash = "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c"},
-    {file = "tornado-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4"},
-    {file = "tornado-6.1.tar.gz", hash = "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791"},
+    {file = "tornado-6.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:20f638fd8cc85f3cbae3c732326e96addff0a15e22d80f049e00121651e82e72"},
+    {file = "tornado-6.2-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:87dcafae3e884462f90c90ecc200defe5e580a7fbbb4365eda7c7c1eb809ebc9"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba09ef14ca9893954244fd872798b4ccb2367c165946ce2dd7376aebdde8e3ac"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8150f721c101abdef99073bf66d3903e292d851bee51910839831caba341a75"},
+    {file = "tornado-6.2-cp37-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3a2f5999215a3a06a4fc218026cd84c61b8b2b40ac5296a6db1f1451ef04c1e"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5f8c52d219d4995388119af7ccaa0bcec289535747620116a58d830e7c25d8a8"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_i686.whl", hash = "sha256:6fdfabffd8dfcb6cf887428849d30cf19a3ea34c2c248461e1f7d718ad30b66b"},
+    {file = "tornado-6.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:1d54d13ab8414ed44de07efecb97d4ef7c39f7438cf5e976ccd356bebb1b5fca"},
+    {file = "tornado-6.2-cp37-abi3-win32.whl", hash = "sha256:5c87076709343557ef8032934ce5f637dbb552efa7b21d08e89ae7619ed0eb23"},
+    {file = "tornado-6.2-cp37-abi3-win_amd64.whl", hash = "sha256:e5f923aa6a47e133d1cf87d60700889d7eae68988704e20c75fb2d65677a8e4b"},
+    {file = "tornado-6.2.tar.gz", hash = "sha256:9b630419bde84ec666bfd7ea0a4cb2a8a651c2d5cccdbdd1972a0c859dfc3c13"},
 ]
 urllib3 = [
-    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
-    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
+    {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"},
+    {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"},
 ]
 websocket-client = [
     {file = "websocket-client-1.3.3.tar.gz", hash = "sha256:d58c5f284d6a9bf8379dab423259fe8f85b70d5fa5d2916d5791a84594b122b1"},
diff --git a/pyproject.toml b/pyproject.toml
index 1907a6f03..e7dbcc47a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "exasol-integration-test-docker-environment"
-version = "0.11.0"
+version = "0.12.0"
 description = "Integration Test Docker Environment for Exasol"
 
 license = "MIT"
diff --git a/setup.py b/setup.py
index 5ede934a7..a976fb0c7 100644
--- a/setup.py
+++ b/setup.py
@@ -12,12 +12,15 @@
  'exasol_integration_test_docker_environment.lib.config',
  'exasol_integration_test_docker_environment.lib.data',
  'exasol_integration_test_docker_environment.lib.docker',
+ 'exasol_integration_test_docker_environment.lib.docker.container',
  'exasol_integration_test_docker_environment.lib.docker.images',
  'exasol_integration_test_docker_environment.lib.docker.images.clean',
  'exasol_integration_test_docker_environment.lib.docker.images.create',
  'exasol_integration_test_docker_environment.lib.docker.images.create.utils',
  'exasol_integration_test_docker_environment.lib.docker.images.push',
  'exasol_integration_test_docker_environment.lib.docker.images.save',
+ 'exasol_integration_test_docker_environment.lib.docker.networks',
+ 'exasol_integration_test_docker_environment.lib.docker.volumes',
  'exasol_integration_test_docker_environment.lib.logging',
  'exasol_integration_test_docker_environment.lib.test_environment',
  'exasol_integration_test_docker_environment.lib.test_environment.database_setup',
@@ -81,7 +84,7 @@
 
 setup_kwargs = {
     'name': 'exasol-integration-test-docker-environment',
-    'version': '0.11.0',
+    'version': '0.12.0',
     'description': 'Integration Test Docker Environment for Exasol',
     'long_description': '# Integration Test Docker Environment\n\n## About\n\nThis project provides a command line interface to start a test environment with an [Exasol Docker-DB](https://hub.docker.com/r/exasol/docker-db). It starts an Exasol Docker-DB container and an associated test container where [EXAPlus CLI](https://docs.exasol.com/connect_exasol/sql_clients/exaplus_cli/exaplus_cli.htm) and  the [Exasol ODBC driver](https://docs.exasol.com/connect_exasol/drivers/odbc.htm) are already installed.\n\nBoth containers exist in the same Docker network. This allows you to connect from the test container to the Docker-DB container. Furthermore, the database gets populated with some test data. You can find the test data under `tests/test/enginedb_small/import.sql`. Besides the test container, you can also access the Exasol database and the Bucket-FS from the host via forwarded ports.\n\n### Comparison to Docker-DB and the Exasol Testcontainers\n\n* This project uses the [Docker-DB](https://hub.docker.com/r/exasol/docker-db), but it does the configuration, setup and startup for you. For example, it waits until the Database and the Bucket-FS accept connections. It also populates the database with test data and provides with the test container as an environment to access the database.\n* The [Exasol Testcontainers](https://github.com/exasol/exasol-testcontainers/) provide similar features for Java integration tests, so if you have an Java project use the Exasol Testcontainer, because they are more integrated in the Java ecosystem then this project. Testcontainers are designed to integrate with popular unit testing frameworks like [JUnit4](https://www.testcontainers.org/test_framework_integration/junit_4/), [JUnit5](https://www.testcontainers.org/test_framework_integration/junit_5/) and [Spock](https://www.testcontainers.org/test_framework_integration/spock/). Most notably, lifecycle management of containers controlled by the tests lifecycle.\n\nHowever, if you have a project in any other language you can use this project to start a test Exasol database via a command line interface.\n\n## Prerequisites\n\nIn order to start a Docker-DB Test Environment, you need:\n\n* Tested Operating System:\n  * Linux\n  * Mac OS X with [Docker Desktop on Mac](https://docs.docker.com/docker-for-mac/install/) and Intel processor\n  * Windows is currently **not supported**\n* Docker with privileged mode\n* At least 2 GiB RAM\n* We recommend at least 15 GB free disk space on the partition \n  where Docker stores its images and containers. On Linux Docker typically stores \n  the images under `/var/lib/docker`.\n\n## Getting started\n\nClone the repository\n\n```\ngit clone https://github.com/exasol/integration-test-docker-environment\n```\n\nStarting the test environment:\n\n```\n./start-test-env spawn-test-environment --environment-name <NAME>\n```\nor if you work on the code of the Test Environment (requires Python >=3.8 with [poetry](https://python-poetry.org/)):\n\n```\n./start-test-env-with-poetry spawn-test-environment --environment-name <NAME>\n```\n\nShutdown of the test environment is currently done manual.\n\n### Options\n\nThe following options are available to customize the test environment. \n\n```\nUsage: main.py spawn-test-environment [OPTIONS]\n\n  This command spawn a test environment with a docker-db container and a\n  connected test-container. The test-container is reachable by the database\n  for output redirects of UDFs.\n\nOptions:\n  --environment-name TEXT         Name of the docker environment. This name\n                                  gets used as suffix for the container\n                                  db_container_<name> and\n                                  test_container_<name>  [required]\n\n  --database-port-forward INTEGER\n                                  Host port to which the database port gets\n                                  forwarded\n\n  --bucketfs-port-forward INTEGER\n                                  Host port to which the BucketFS port gets\n                                  forwarded\n\n  --db-mem-size TEXT              The main memory used by the database. Format\n                                  <number> <unit>, e.g. 1 GiB. The minimum\n                                  size is 1 GB, below that the database will\n                                  not start.  [default: 2 GiB]\n\n  --db-disk-size TEXT             The disk size available for the database.\n                                  Format <number> <unit>, e.g. 1 GiB. The\n                                  minimum size is 100 MiB. However, the setup\n                                  creates volume files with at least 2 GB\n                                  larger size, because the database needs at\n                                  least so much more disk.  [default: 2 GiB]\n\n  --nameserver TEXT               Add a nameserver to the list of DNS\n                                  nameservers which the docker-db should use\n                                  for resolving domain names. You can repeat\n                                  this option to add further nameservers.\n\n  --deactivate-database-setup / --no-deactivate-database-setup\n                                  Deactivates the setup of the spawned\n                                  database, this means no data get populated\n                                  and no JDBC drivers get uploaded. This can\n                                  be used either to save time or as a\n                                  workaround for MacOSX where the\n                                  test_container seems not to be able to\n                                  access the tests directory  [default: False]\n\n  --docker-runtime TEXT           The docker runtime used to start all\n                                  containers\n\n  --docker-db-image-version TEXT  Docker DB Image Version against which the\n                                  tests should run.  [default: 7.1.12]\n\n  --docker-db-image-name TEXT     Docker DB Image Name against which the tests\n                                  should run.  [default: exasol/docker-db]\n\n  --output-directory DIRECTORY    Output directory where the system stores all\n                                  output and log files.  [default:\n                                  .build_output]\n\n  --temporary-base-directory DIRECTORY\n                                  Directory where the system creates temporary\n                                  directories.  [default: /tmp]\n```\n\nYou can look at them on the commandline with:\n\n```\n./start-test-env spawn-test-environment --help \n```\n\n### The integration-test-docker-environment command line tool\nBesides, the already mentioned command `spawn-test-environment`, the integration-test-docker-environemnt\ncommand line tool provides a couple of other helpful tools.\n\nRun `main.py --help`, to get a list of the available commands:\n```console\nUsage: main.py [OPTIONS] COMMAND [ARGS]...\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  build-test-container    This command builds all stages of the test...\n  health                  Check the health of the execution environment.\n  push-test-container     This command pushs all stages of the test...\n  spawn-test-environment  This command spawn a test environment with a...\n```\n\nTo get more details on a specific command run `main.py <command> --help`,\ne.g. `main.py health --help`:\n\n```console\nUsage: main.py health [OPTIONS]\n\n  Check the health of the execution environment.\n\n  If no issues have been found, using the library or executing the test should\n  work just fine. For all found issues there will be a proposed fix/solution.\n\n  If the environment was found to be healthy the exit code will be 0.\n\nOptions:\n  --help  Show this message and exit.\n```\n\n### Default Credentials\n\nThe default credentials for the database are\n\n  * User: `sys`\n  * Password: `exasol`\n  \nand for the Bucket-FS:\n\n  * User: `w`\n  * Password: `write`\n  \nor\n\n  * User: `r`\n  * Password: `read`\n\n### Accessing the Environment Information\n\nThe python setup script creates configuration files on the host and in the test container.\n\nOn the host the container information get stored in the build output directory usually under `.build_output/cache/<ENVIRONMENT_NAME>/*`. In the test container the config file is stored at the root directory `/`.\n\nThe following config files are available:\n\n- environment_info.sh: This file is meant to be source by the bash and then provides the information as environment variables. Here an example for the content:\n\n  ```\n  export ENVIRONMENT_NAME=test\n  export ENVIRONMENT_TYPE=EnvironmentType.docker_db\n\n  # Database IP in environment docker network\n  export ENVIRONMENT_DATABASE_HOST=172.21.0.2\n  export ENVIRONMENT_DATABASE_DB_PORT=8888\n  export ENVIRONMENT_DATABASE_BUCKETFS_PORT=6583\n  export ENVIRONMENT_DATABASE_CONTAINER_NAME=db_container_test\n  export ENVIRONMENT_DATABASE_CONTAINER_NETWORK_ALIASES="exasol_test_database db_container_test"\n  # Database IP in the environment docker network\n  export ENVIRONMENT_DATABASE_CONTAINER_IP_ADDRESS=172.21.0.2\n  export ENVIRONMENT_DATABASE_CONTAINER_VOLUMNE_NAME=db_container_test_volume\n  # Database IP on the docker default bridge which under Linux available from the host\n  export ENVIRONMENT_DATABASE_CONTAINER_DEFAULT_BRIDGE_IP_ADDRESS=172.17.0.3\n\n  export ENVIRONMENT_TEST_CONTAINER_NAME=test_container_test\n  export ENVIRONMENT_TEST_CONTAINER_NETWORK_ALIASES="test_container test_container_test"\n  # Test Container IP in the environment docker network\n  export ENVIRONMENT_TEST_CONTAINER_IP_ADDRESS=172.21.0.3\n  ```\n- environment_info.json: Contains the EnvironmentInfo objects pickled with JsonPickle\n  \n### Currently supported Exasol Versions\n\n* **7.0**: up to 7.0.19 **except 7.0.5**\n* **7.1**: up to 7.1.12\n\n\nIf you need further versions, please open an issue.\n\n### Tested Docker Runtimes\n\n- Docker Default Runtime\n- [NVIDIA Container Runtime](https://github.com/NVIDIA/nvidia-container-runtime) for GPU accelerated UDFs\n\n## Mac OS X Support\n\n### What do I need to do to start the Test Environment with Mac OS X\n\nThe Exasol Docker-DB needs per default a bit more than 2 GB of RAM, however the Docker VM on Mac OS X provides often not enough RAM to accommodate this. You should increase the RAM of the Docker VM to at least 4.25 GB or reduce the DB Mem Size for the Exasol Docker-DB to less than 2 GB with `--db-mem-size 1 GiB`.\n\n### What happens under the hood\n\nMac OS X with Docker Desktop for Mac uses a lightweight virtual machine with linux in which the docker daemon runs and the containers get started. This makes [networking](https://docs.docker.com/docker-for-mac/networking/) and [shared directories](https://docs.docker.com/docker-for-mac/osxfs/) more complicated then on Linux.\n\nWe start the python setup script for the test environment in its own Docker container, lets call it `docker runner`, because the library [Luigi](https://luigi.readthedocs.io/en/stable/) can have problems with Mac OS X and to avoid the installation of further dependencies. To support Mac OS X, the `start-test-env` script starts the `docker runner` container and mounts the docker socket at `/var/run/docker.sock` and the directory of the test environment from the Mac OS X host to the container. Then, it starts `start-test-env-without-docker` which then starts the python script. It is important, that the repository gets cloned to the Mac OS X host and not to a docker container, because the python scripts tries to start further docker container which use host mounts to share the tests directory of the test environment with the docker container.\n',
     'author': 'Torsten Kilias',


Test patch:
diff --git a/.github/workflows/test_db_versions_minimal.yml b/.github/workflows/test_db_versions_minimal.yml
index 7dc3d72a6..ff060c082 100644
--- a/.github/workflows/test_db_versions_minimal.yml
+++ b/.github/workflows/test_db_versions_minimal.yml
@@ -46,5 +46,6 @@ jobs:
       run: |
           ./scripts/test/run_test.sh exasol_integration_test_docker_environment/test/test_test_environment.py
           ./scripts/test/run_test.sh exasol_integration_test_docker_environment/test/test_doctor.py
+          ./scripts/test/run_test.sh exasol_integration_test_docker_environment/test/test_termination_handler.py
       env:
         EXASOL_VERSION: "${{ matrix.exasol_version }}"
diff --git a/exasol_integration_test_docker_environment/cli/commands/build_test_container.py b/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
index 75c0af289..9a87869dc 100644
--- a/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
+++ b/exasol_integration_test_docker_environment/cli/commands/build_test_container.py
@@ -1,7 +1,8 @@
 from typing import Tuple, Optional
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options
+from exasol_integration_test_docker_environment.cli.termination_handler import TerminationHandler
+from exasol_integration_test_docker_environment.lib.api.common import add_options
 from exasol_integration_test_docker_environment.cli.options.build_options import build_options
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
 from exasol_integration_test_docker_environment.cli.options.system_options import system_options
@@ -37,23 +38,23 @@ def build_test_container(
     instead of building them.
 
     """
-    success = api.build_test_container(force_rebuild,
-                                       force_rebuild_from,
-                                       force_pull,
-                                       output_directory,
-                                       temporary_base_directory,
-                                       log_build_context_content,
-                                       cache_directory,
-                                       build_name,
-                                       source_docker_repository_name,
-                                       source_docker_tag_prefix,
-                                       source_docker_username,
-                                       source_docker_password,
-                                       target_docker_repository_name,
-                                       target_docker_tag_prefix,
-                                       target_docker_username,
-                                       target_docker_password,
-                                       workers,
-                                       task_dependencies_dot_file)
-    if not success:
-        exit(1)
+
+    with TerminationHandler():
+        api.build_test_container(force_rebuild,
+                                 force_rebuild_from,
+                                 force_pull,
+                                 output_directory,
+                                 temporary_base_directory,
+                                 log_build_context_content,
+                                 cache_directory,
+                                 build_name,
+                                 source_docker_repository_name,
+                                 source_docker_tag_prefix,
+                                 source_docker_username,
+                                 source_docker_password,
+                                 target_docker_repository_name,
+                                 target_docker_tag_prefix,
+                                 target_docker_username,
+                                 target_docker_password,
+                                 workers,
+                                 task_dependencies_dot_file)
diff --git a/exasol_integration_test_docker_environment/cli/commands/push_test_container.py b/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
index d98056943..1f7720279 100644
--- a/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
+++ b/exasol_integration_test_docker_environment/cli/commands/push_test_container.py
@@ -1,7 +1,8 @@
 from typing import Tuple, Optional
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options
+from exasol_integration_test_docker_environment.cli.termination_handler import TerminationHandler
+from exasol_integration_test_docker_environment.lib.api.common import add_options
 from exasol_integration_test_docker_environment.cli.options.build_options import build_options
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import docker_repository_options
 from exasol_integration_test_docker_environment.cli.options.push_options import push_options
@@ -39,25 +40,24 @@ def push_test_container(
     This command pushs all stages of the test container for the test environment.
     If the stages do not exists locally, the system will build or pull them before the push.
     """
-    success = api.push_test_container(force_push,
-                                      push_all,
-                                      force_rebuild,
-                                      force_rebuild_from,
-                                      force_pull,
-                                      output_directory,
-                                      temporary_base_directory,
-                                      log_build_context_content,
-                                      cache_directory,
-                                      build_name,
-                                      source_docker_repository_name,
-                                      source_docker_tag_prefix,
-                                      source_docker_username,
-                                      source_docker_password,
-                                      target_docker_repository_name,
-                                      target_docker_tag_prefix,
-                                      target_docker_username,
-                                      target_docker_password,
-                                      workers,
-                                      task_dependencies_dot_file)
-    if not success:
-        exit(1)
+    with TerminationHandler():
+        api.push_test_container(force_push,
+                                push_all,
+                                force_rebuild,
+                                force_rebuild_from,
+                                force_pull,
+                                output_directory,
+                                temporary_base_directory,
+                                log_build_context_content,
+                                cache_directory,
+                                build_name,
+                                source_docker_repository_name,
+                                source_docker_tag_prefix,
+                                source_docker_username,
+                                source_docker_password,
+                                target_docker_repository_name,
+                                target_docker_tag_prefix,
+                                target_docker_username,
+                                target_docker_password,
+                                workers,
+                                task_dependencies_dot_file)
diff --git a/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py b/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
index 03e9ddcfa..9371b18f4 100644
--- a/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
+++ b/exasol_integration_test_docker_environment/cli/commands/spawn_test_environment.py
@@ -2,7 +2,8 @@
 import click
 
 from exasol_integration_test_docker_environment.cli.cli import cli
-from exasol_integration_test_docker_environment.cli.common import add_options
+from exasol_integration_test_docker_environment.cli.termination_handler import TerminationHandler
+from exasol_integration_test_docker_environment.lib.api.common import add_options
 from exasol_integration_test_docker_environment.cli.options.system_options import system_options, \
     output_directory_option, tempory_base_directory_option
 from exasol_integration_test_docker_environment.cli.options.test_environment_options import docker_db_options
@@ -39,7 +40,7 @@ def spawn_test_environment(
         bucketfs_port_forward: Optional[int],
         db_mem_size: str,
         db_disk_size: str,
-        nameserver: Tuple[str,...],
+        nameserver: Tuple[str, ...],
         deactivate_database_setup: bool,
         docker_runtime: Optional[str],
         docker_db_image_version: str,
@@ -61,35 +62,33 @@ def spawn_test_environment(
     This command spawn a test environment with a docker-db container and a connected test-container.
     The test-container is reachable by the database for output redirects of UDFs.
     """
-    success = False
-    try:
-        success = api.spawn_test_environment(environment_name,
-                                             database_port_forward,
-                                             bucketfs_port_forward,
-                                             db_mem_size,
-                                             db_disk_size,
-                                             nameserver,
-                                             deactivate_database_setup,
-                                             docker_runtime,
-                                             docker_db_image_version,
-                                             docker_db_image_name,
-                                             create_certificates,
-                                             source_docker_repository_name,
-                                             source_docker_tag_prefix,
-                                             source_docker_username,
-                                             source_docker_password,
-                                             target_docker_repository_name,
-                                             target_docker_tag_prefix,
-                                             target_docker_username,
-                                             target_docker_password,
-                                             output_directory,
-                                             temporary_base_directory,
-                                             workers,
-                                             task_dependencies_dot_file)
-    except ArgumentConstraintError as e:
-        handle_wrong_argument_error(*e.args)
-    if not success:
-        exit(1)
+    with TerminationHandler():
+        try:
+            api.spawn_test_environment(environment_name,
+                                       database_port_forward,
+                                       bucketfs_port_forward,
+                                       db_mem_size,
+                                       db_disk_size,
+                                       nameserver,
+                                       deactivate_database_setup,
+                                       docker_runtime,
+                                       docker_db_image_version,
+                                       docker_db_image_name,
+                                       create_certificates,
+                                       source_docker_repository_name,
+                                       source_docker_tag_prefix,
+                                       source_docker_username,
+                                       source_docker_password,
+                                       target_docker_repository_name,
+                                       target_docker_tag_prefix,
+                                       target_docker_username,
+                                       target_docker_password,
+                                       output_directory,
+                                       temporary_base_directory,
+                                       workers,
+                                       task_dependencies_dot_file)
+        except ArgumentConstraintError as e:
+            handle_wrong_argument_error(*e.args)
 
 
 def handle_wrong_argument_error(argument_name, message):
diff --git a/exasol_integration_test_docker_environment/cli/termination_handler.py b/exasol_integration_test_docker_environment/cli/termination_handler.py
new file mode 100644
index 000000000..d58faadc2
--- /dev/null
+++ b/exasol_integration_test_docker_environment/cli/termination_handler.py
@@ -0,0 +1,55 @@
+import sys
+from datetime import datetime
+from sys import stderr
+from traceback import print_tb
+
+from exasol_integration_test_docker_environment.lib.api.api_errors import TaskRuntimeError
+
+
+class TerminationHandler:
+    """
+    This helper class measures and logs time duration of the job and also logs the error message.
+    """
+    def __init__(self):
+        self._start_time = None
+
+    def __enter__(self):
+        self._start_time = datetime.now()
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        exit_value = 0
+        if exc_type == TaskRuntimeError:
+            self._handle_failure(exc_val)
+            exit_value = 1
+        elif exc_type is None and exc_val is None and exc_tb is None:
+            self._handle_success()
+        else:
+            self._handle_unexpected_failure(exc_val, exc_tb)
+            exit_value = 1
+        sys.exit(exit_value)
+
+    def _handle_unexpected_failure(self, exc_val, exc_tb):
+        timedelta = datetime.now() - self._start_time
+        print("The command failed after %s s with:" % timedelta.total_seconds(), file=stderr)
+        print("Caught exception:%s" % exc_val, file=stderr)
+        print_tb(exc_tb)
+
+    def _handle_failure(self, task_error: TaskRuntimeError):
+        timedelta = datetime.now() - self._start_time
+        print("The command failed after %s s with:" % timedelta.total_seconds(), file=stderr)
+        self._print_task_failures(task_error)
+
+    @staticmethod
+    def _print_task_failures(task_error: TaskRuntimeError):
+        print(file=stderr)
+        print("Task failure message: %s" % task_error.msg, file=stderr)
+        print("Task Failures:", file=stderr)
+        if task_error.inner is not None:
+            for failure in task_error.inner:
+                print(failure, file=stderr)
+        print(file=stderr)
+
+    def _handle_success(self):
+        timedelta = datetime.now() - self._start_time
+        print("The command took %s s" % timedelta.total_seconds(), file=stderr)
+
diff --git a/exasol_integration_test_docker_environment/lib/api/api_errors.py b/exasol_integration_test_docker_environment/lib/api/api_errors.py
index 79d4469ec..985666474 100644
--- a/exasol_integration_test_docker_environment/lib/api/api_errors.py
+++ b/exasol_integration_test_docker_environment/lib/api/api_errors.py
@@ -1,7 +1,16 @@
+from typing import Optional, List
+
 
 class ArgumentConstraintError(ValueError):
-    pass
+    """Represents an error for an argument not fulfilling a specific constraint"""
 
 
 class HealthProblem(RuntimeError):
-    pass
+    """Represents a problem found by analyzing the project health"""
+
+
+class TaskRuntimeError(RuntimeError):
+    """Represents an error which occurred during execution of a luigi task"""
+    def __init__(self, msg, inner: Optional[List[str]] = None):
+        self.msg = msg
+        self.inner = inner
diff --git a/exasol_integration_test_docker_environment/lib/api/build_test_container.py b/exasol_integration_test_docker_environment/lib/api/build_test_container.py
index 7dfafef71..eebbaa248 100644
--- a/exasol_integration_test_docker_environment/lib/api/build_test_container.py
+++ b/exasol_integration_test_docker_environment/lib/api/build_test_container.py
@@ -1,9 +1,10 @@
 from typing import Tuple, Optional
 
-from exasol_integration_test_docker_environment.cli.common import set_build_config, \
+from exasol_integration_test_docker_environment.lib.api.common import set_build_config, \
     set_docker_repository_config, run_task, generate_root_task
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
 from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+from exasol_integration_test_docker_environment.lib.docker.images.image_info import ImageInfo
 from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import AnalyzeTestContainer, \
     DockerTestContainerBuild
 
@@ -26,11 +27,12 @@ def build_test_container(
         target_docker_username: Optional[str] = None,
         target_docker_password: Optional[str] = None,
         workers: int = 5,
-        task_dependencies_dot_file: Optional[str] = None) -> bool:
+        task_dependencies_dot_file: Optional[str] = None) -> ImageInfo:
     """
-    This command builds all stages of the test container for the test environment.
-    If stages are cached in a docker registry, they command is going to pull them,
+    This function builds all stages of the test container for the test environment.
+    If stages are cached in a docker registry, the function is going to pull them,
     instead of building them.
+    It returns the image info of the test-container.
     """
     set_build_config(force_rebuild,
                      force_rebuild_from,
@@ -48,5 +50,5 @@ def build_test_container(
     set_docker_repository_config(target_docker_password, target_docker_repository_name, target_docker_username,
                                  target_docker_tag_prefix, "target")
     task_creator = lambda: generate_root_task(task_class=DockerTestContainerBuild)
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
-    return success
+    image_infos = run_task(task_creator, workers, task_dependencies_dot_file)
+    return image_infos["test-container"]
diff --git a/exasol_integration_test_docker_environment/cli/common.py b/exasol_integration_test_docker_environment/lib/api/common.py
similarity index 84%
rename from exasol_integration_test_docker_environment/cli/common.py
rename to exasol_integration_test_docker_environment/lib/api/common.py
index 74f6894dc..ff8762129 100644
--- a/exasol_integration_test_docker_environment/cli/common.py
+++ b/exasol_integration_test_docker_environment/lib/api/common.py
@@ -1,14 +1,14 @@
 import getpass
 import json
+import logging
 import os
-import traceback
 from datetime import datetime
 from pathlib import Path
 from typing import (
     Callable,
     Tuple,
     Set,
-    Optional
+    Optional, Any
 )
 
 import luigi
@@ -16,6 +16,7 @@
 from networkx import DiGraph
 
 from exasol_integration_test_docker_environment.lib import extract_modulename_for_build_steps
+from exasol_integration_test_docker_environment.lib.api.api_errors import TaskRuntimeError
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 from exasol_integration_test_docker_environment.lib.base.luigi_log_config import get_luigi_log_config, get_log_path
 from exasol_integration_test_docker_environment.lib.base.task_dependency import TaskDependency, DependencyState
@@ -93,9 +94,8 @@ def generate_root_task(task_class, *args, **kwargs) -> DependencyLoggerBaseTask:
 
 def run_task(task_creator: Callable[[], DependencyLoggerBaseTask], workers: int,
              task_dependencies_dot_file: Optional[str]) \
-        -> Tuple[bool, DependencyLoggerBaseTask]:
+        -> Any:
     setup_worker()
-    start_time = datetime.now()
     task = task_creator()
     success = False
     try:
@@ -104,39 +104,24 @@ def run_task(task_creator: Callable[[], DependencyLoggerBaseTask], workers: int,
                                                local_scheduler=True,
                                                logging_conf_file=f'{luigi_config}')
         success = not task.failed_target.exists() and no_scheduling_errors
+        generate_graph_from_task_dependencies(task, task_dependencies_dot_file)
+
         if success:
-            handle_success(task, task_dependencies_dot_file, start_time)
-            return True, task
-        else:
-            handle_failure(task, task_dependencies_dot_file, start_time)
-            return False, task
+            return task.get_result()
+        elif task.failed_target.exists():
+            logging.error(f"Task {task} failed. failed target exists.")
+            raise TaskRuntimeError(msg=f"Task {task} (or any of it's subtasks) failed.",
+                                   inner=list(task.collect_failures().keys()))
+        elif not no_scheduling_errors:
+            logging.error(f"Task {task} failed. : luigi reported a scheduling error.")
+            raise TaskRuntimeError(msg=f"Task {task} failed. reason: luigi reported a scheduling error.")
     except BaseException as e:
-        traceback.print_exc()
-        print("Going to abort the task %s" % task)
-        return False, task  # TODO return exception
+        logging.error("Going to abort the task %s" % task)
+        raise e
     finally:
         task.cleanup(success)
 
 
-def handle_failure(task: DependencyLoggerBaseTask, task_dependencies_dot_file: Optional[str], start_time: datetime):
-    generate_graph_from_task_dependencies(task, task_dependencies_dot_file)
-    timedelta = datetime.now() - start_time
-    print("The command failed after %s s with:" % timedelta.total_seconds())
-    print_task_failures(task)
-
-def print_task_failures(task: DependencyLoggerBaseTask):
-    print()
-    print("Task Failures:")
-    for failure in task.collect_failures().keys():
-        print(failure)
-    print()
-
-def handle_success(task: DependencyLoggerBaseTask, task_dependencies_dot_file: Optional[str], start_time: datetime):
-    generate_graph_from_task_dependencies(task, task_dependencies_dot_file)
-    timedelta = datetime.now() - start_time
-    print("The command took %s s" % timedelta.total_seconds())
-
-
 def generate_graph_from_task_dependencies(task: DependencyLoggerBaseTask, task_dependencies_dot_file: Optional[str]):
     if task_dependencies_dot_file is not None:
         print(f"Generate Task Dependency Graph to {task_dependencies_dot_file}")
diff --git a/exasol_integration_test_docker_environment/lib/api/push_test_container.py b/exasol_integration_test_docker_environment/lib/api/push_test_container.py
index 394f4da5f..68746a6cd 100644
--- a/exasol_integration_test_docker_environment/lib/api/push_test_container.py
+++ b/exasol_integration_test_docker_environment/lib/api/push_test_container.py
@@ -1,9 +1,10 @@
 from typing import Tuple, Optional
 
-from exasol_integration_test_docker_environment.cli.common import set_docker_repository_config, \
+from exasol_integration_test_docker_environment.lib.api.common import set_docker_repository_config, \
     run_task, set_build_config, generate_root_task
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
 from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
+from exasol_integration_test_docker_environment.lib.docker.images.image_info import ImageInfo
 from exasol_integration_test_docker_environment.lib.test_environment.analyze_test_container import \
     AnalyzeTestContainer, DockerTestContainerPush
 
@@ -28,10 +29,10 @@ def push_test_container(
         target_docker_username: Optional[str] = None,
         target_docker_password: Optional[str] = None,
         workers: int = 5,
-        task_dependencies_dot_file: Optional[str] = None) -> bool:
+        task_dependencies_dot_file: Optional[str] = None) -> ImageInfo:
     """
-    This command pushs all stages of the test container for the test environment.
-    If the stages do not exists locally, the system will build or pull them before the push.
+    This function pushes all stages of the test container for the test environment.
+    If the stages do not exist locally, the system will build or pull them before the push.
     """
     set_build_config(force_rebuild,
                      force_rebuild_from,
@@ -51,5 +52,5 @@ def push_test_container(
     task_creator = lambda: generate_root_task(task_class=DockerTestContainerPush,
                                               force_push=force_push,
                                               push_all=push_all)
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
-    return success
+    image_infos = run_task(task_creator, workers, task_dependencies_dot_file)
+    return image_infos[0]
diff --git a/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py b/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py
index e1104d4fe..82573eff7 100644
--- a/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py
+++ b/exasol_integration_test_docker_environment/lib/api/spawn_test_environment.py
@@ -1,16 +1,28 @@
-from typing import Tuple, Optional
+import functools
+from typing import Tuple, Optional, Callable
 import humanfriendly
 
-from exasol_integration_test_docker_environment.cli.common import set_build_config, set_docker_repository_config, \
+from exasol_integration_test_docker_environment.lib.api.common import set_build_config, set_docker_repository_config, \
     run_task, generate_root_task
 from exasol_integration_test_docker_environment.cli.options.docker_repository_options import DEFAULT_DOCKER_REPOSITORY_NAME
 from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
 from exasol_integration_test_docker_environment.cli.options.test_environment_options import LATEST_DB_VERSION
 from exasol_integration_test_docker_environment.lib.api.api_errors import ArgumentConstraintError
+from exasol_integration_test_docker_environment.lib.data.environment_info import EnvironmentInfo
+from exasol_integration_test_docker_environment.lib.docker.container.utils import remove_docker_container
+from exasol_integration_test_docker_environment.lib.docker.volumes.utils import remove_docker_volumes
+from exasol_integration_test_docker_environment.lib.docker.networks.utils import remove_docker_networks
 from exasol_integration_test_docker_environment.lib.test_environment.spawn_test_environment_with_docker_db import \
     SpawnTestEnvironmentWithDockerDB
 
 
+def _cleanup(environment_info: EnvironmentInfo) -> None:
+    remove_docker_container([environment_info.test_container_info.container_name,
+                             environment_info.database_info.container_info.container_name])
+    remove_docker_volumes([environment_info.database_info.container_info.volume_name])
+    remove_docker_networks([environment_info.network_info.network_name])
+
+
 def spawn_test_environment(
         environment_name: str,
         database_port_forward: Optional[int] = None,
@@ -34,10 +46,14 @@ def spawn_test_environment(
         output_directory: str = DEFAULT_OUTPUT_DIRECTORY,
         temporary_base_directory: str = "/tmp",
         workers: int = 5,
-        task_dependencies_dot_file: Optional[str] = None) -> bool:
+        task_dependencies_dot_file: Optional[str] = None) -> Tuple[EnvironmentInfo, Callable[[], None]]:
     """
-    This command spawn a test environment with a docker-db container and a connected test-container.
+    This function spawns a test environment with a docker-db container and a connected test-container.
     The test-container is reachable by the database for output redirects of UDFs.
+    The function returns an environment_info object, describing the environment, and a cleanup-method, which
+    can be used to stop the environment.
+    raises: TaskRuntimeError if spawning the test environment fails
+
     """
     parsed_db_mem_size = humanfriendly.parse_size(db_mem_size)
     if parsed_db_mem_size < humanfriendly.parse_size("1 GiB"):
@@ -79,5 +95,5 @@ def spawn_test_environment(
                                               is_setup_database_activated=not deactivate_database_setup,
                                               create_certificates=create_certificates
                                               )
-    success, task = run_task(task_creator, workers, task_dependencies_dot_file)
-    return success
+    environment_info = run_task(task_creator, workers, task_dependencies_dot_file)
+    return environment_info, functools.partial(_cleanup, environment_info)
diff --git a/exasol_integration_test_docker_environment/lib/base/abstract_task_future.py b/exasol_integration_test_docker_environment/lib/base/abstract_task_future.py
index 779b9028a..ce0769bc8 100644
--- a/exasol_integration_test_docker_environment/lib/base/abstract_task_future.py
+++ b/exasol_integration_test_docker_environment/lib/base/abstract_task_future.py
@@ -1,11 +1,7 @@
-from exasol_integration_test_docker_environment.abstract_method_exception import AbstractMethodException
+import abc
 
-DEFAULT_RETURN_OBJECT_NAME = "default"
 
-
-class AbstractTaskFuture:
-    def get_output(self, name: str = DEFAULT_RETURN_OBJECT_NAME):
-        raise AbstractMethodException()
-
-    def list_outputs(self):
-        raise AbstractMethodException()
+class AbstractTaskFuture(abc.ABC):
+    @abc.abstractmethod
+    def get_output(self):
+        ...
diff --git a/exasol_integration_test_docker_environment/lib/base/base_task.py b/exasol_integration_test_docker_environment/lib/base/base_task.py
index 4bc4fc27f..f559b8dfd 100644
--- a/exasol_integration_test_docker_environment/lib/base/base_task.py
+++ b/exasol_integration_test_docker_environment/lib/base/base_task.py
@@ -3,7 +3,7 @@
 import logging
 import shutil
 from pathlib import Path
-from typing import Dict, List, Generator, Any, Union, Set
+from typing import Dict, List, Generator, Any, Union, Set, Optional
 
 import luigi
 import six
@@ -12,8 +12,7 @@
 from luigi.task import TASK_ID_TRUNCATE_HASH
 
 from exasol_integration_test_docker_environment.abstract_method_exception import AbstractMethodException
-from exasol_integration_test_docker_environment.lib.base.abstract_task_future import AbstractTaskFuture, \
-    DEFAULT_RETURN_OBJECT_NAME
+from exasol_integration_test_docker_environment.lib.base.abstract_task_future import AbstractTaskFuture
 from exasol_integration_test_docker_environment.lib.base.pickle_target import PickleTarget
 from exasol_integration_test_docker_environment.lib.base.task_logger_wrapper import TaskLoggerWrapper
 from exasol_integration_test_docker_environment.lib.base.task_state import TaskState
@@ -28,44 +27,61 @@
 
 
 class RequiresTaskFuture(AbstractTaskFuture):
-
-    def __init__(self, task: "BaseTask", index: int):
-        self._index = index
-        self._task = task
-        self._outputs_dict = None
-
-    def get_output(self, name: str = DEFAULT_RETURN_OBJECT_NAME):
-        return self._get_outputs_dict()[name].read()
-
-    def list_outputs(self) -> List[str]:
-        return list(self._get_outputs_dict().keys())
-
-    def _get_outputs_dict(self) -> Dict[str, PickleTarget]:
-        if self._task._task_state == TaskState.RUN:
-            if self._outputs_dict is None:
-                completion_target = self._task.input()[self._index]
-                self._outputs_dict = completion_target.read()
-            return self._outputs_dict
+    """
+    Represents a future for a statically ("requires") registered task.
+    Details:
+    - The RegisterTaskFuture is used with dependencies returned in luigis required method
+    - In the required method you return tasks and usually you would to get the output target of your child tasks
+      via luigis input() method (check https://luigi.readthedocs.io/en/stable/tasks.html#task-input)
+    - However, if you have multiple child tasks which return in requires, for example in a dict or list,
+      you need address each task in the input first and then you address the output
+    - output can also return a nested dict or list of targets
+    - requires can return a nested dict or list of tasks
+    - we reduced the output to a single completion target with the list of return value targets
+      and the child tasks are collected in the registered_tasks list
+    - The RegisterTaskFuture gets the position of the task in the registered_tasks as input, such as current task
+    - If the user call get_output the RegisterTaskFuture call the input() method of the current task and
+       uses the position of child task to retrieve the completion target of the child task
+    - It then reads the completion target to get the list of return value targets and reads the requested one.
+    """
+
+    def __init__(self, current_task: "BaseTask", child_task_index: int):
+        self._child_task_index = child_task_index
+        self._current_task = current_task
+        self._outputs_cache = None
+
+    def get_output(self) -> Any:
+        if self._current_task._task_state == TaskState.RUN:
+            if self._outputs_cache is None:
+                completion_target = self._current_task.input()[self._child_task_index]
+                self._outputs_cache = completion_target.read()
+            return self._outputs_cache
         else:
-            raise WrongTaskStateException(self._task._task_state, "RequiresTaskFuture.read_outputs_dict")
+            raise WrongTaskStateException(self._current_task._task_state, "RequiresTaskFuture.read_outputs_dict")
 
 
 class RunTaskFuture(AbstractTaskFuture):
+    """
+    Represents a future for a dynamically registered task.
+    Details:
+    - run_dependencies uses luigi dynamic task by yielding the tasks
+    - luigi returns for each yielded task the return value of that tasks output method
+      (see https://luigi.readthedocs.io/en/stable/tasks.html#dynamic-dependencies)
+    - the output method returns targets and in case of the BaseTask this is always the completion target
+      which contains the list of return value targets
+    - the RunTaskFuture encapsulate the target returned by the yield and handles reading the target for the user,
+      such that the user get directly the return values of the child task
+
+    """
 
     def __init__(self, completion_target: PickleTarget):
-        self._outputs_dict = None
+        self._outputs_cache = None
         self.completion_target = completion_target
 
-    def get_output(self, name: str = DEFAULT_RETURN_OBJECT_NAME):
-        return self._get_outputs_dict()[name].read()
-
-    def list_outputs(self) -> List[str]:
-        return list(self._get_outputs_dict().keys())
-
-    def _get_outputs_dict(self) -> Dict[str, PickleTarget]:
-        if self._outputs_dict is None:
-            self._outputs_dict = self.completion_target.read()
-        return self._outputs_dict
+    def get_output(self) -> Any:
+        if self._outputs_cache is None:
+            self._outputs_cache = self.completion_target.read()
+        return self._outputs_cache
 
 
 class BaseTask(Task):
@@ -75,7 +91,6 @@ class BaseTask(Task):
     def __init__(self, *args, **kwargs):
         self._registered_tasks = []
         self._run_dependencies_tasks = []
-        self._registered_return_targets = {}
         self._task_state = TaskState.INIT
         super().__init__(*args, **kwargs)
         self.task_id = self.task_id_str(self.get_task_family(),
@@ -88,8 +103,9 @@ def __init__(self, *args, **kwargs):
     def _init_non_pickle_attributes(self):
         logger = logging.getLogger(f'luigi-interface.{self.__class__.__name__}')
         self.logger = TaskLoggerWrapper(logger, self.__repr__())
-        self._complete_target = PickleTarget(path=self._get_tmp_path_for_completion_target())
         self._run_dependencies_target = PickleTarget(path=self._get_tmp_path_for_run_dependencies())
+        self._complete_target = PickleTarget(path=self._get_tmp_path_for_completion_target())
+        self._registered_return_target = None
 
     def __getstate__(self):
         new_dict = dict(self.__dict__)
@@ -156,9 +172,6 @@ def _get_tmp_path_for_task(self) -> Path:
         return Path(self._get_tmp_path_for_job(),
                     self.task_id)
 
-    def _get_tmp_path_for_returns(self, name: str) -> Path:
-        return Path(self._get_tmp_path_for_task(), RETURN_TARGETS, name)
-
     def _get_tmp_path_for_completion_target(self) -> Path:
         return Path(self._get_tmp_path_for_task(), COMPLETION_TARGET)
 
@@ -178,7 +191,12 @@ def get_cache_path(self) -> Path:
     def register_required(self):
         pass
 
-    def register_dependency(self, task: "BaseTask"):
+    def register_dependency(self, task: "BaseTask") -> RequiresTaskFuture:
+        """
+        Registers a 'requires' (a static) dependency of the task. It returns a future which can be used in the
+        run_task() method via BaseTask.get_values_from_future() to get access to the result of the child task.
+        See class RequiresTaskFuture for more details.
+        """
         if self._task_state == TaskState.INIT:
             index = len(self._registered_tasks)
             self._registered_tasks.append(task)
@@ -206,11 +224,8 @@ def get_values_from_futures(self, futures):
         else:
             return futures
 
-    def get_values_from_future(self, future: AbstractTaskFuture) -> Union[Any, Dict[str, Any]]:
-        if len(future.list_outputs()) == 1 and DEFAULT_RETURN_OBJECT_NAME in future.list_outputs():
-            return future.get_output()
-        else:
-            return {future.get_output(key) for key in future.list_outputs()}
+    def get_values_from_future(self, future: AbstractTaskFuture) -> Union[Any, Set[str]]:
+        return future.get_output()
 
     def requires(self):
         return self._registered_tasks
@@ -226,7 +241,7 @@ def run(self):
                 yield from task_generator
             self._task_state = TaskState.FINISHED
             self.logger.info("Write complete_target")
-            self._complete_target.write(self._registered_return_targets)
+            self._complete_target.write(self._registered_return_target)
         except Exception as e:
             self._task_state = TaskState.ERROR
             self.logger.exception("Exception in run: %s", e)
@@ -236,6 +251,12 @@ def run_task(self):
         raise AbstractMethodException()
 
     def run_dependencies(self, tasks) -> Generator["BaseTask", PickleTarget, Any]:
+        """
+        Runs a 'run' (a dynamic) dependency
+        (that means a dependencies which was evaluated during the runtime of the task), and returns a RunTaskFuture.
+        The returned future can then be used with BaseTask.get_values_from_futures() to get access to the result
+        of the child task.
+        """
         if self._task_state == TaskState.RUN:
             self._register_run_dependencies(tasks)
             self._run_dependencies_target.write(self._run_dependencies_tasks)
@@ -266,23 +287,27 @@ def _generate_run_task_futures(self, completion_targets: Union[Any]) -> Union[
         else:
             return completion_targets
 
-    def return_object(self, object: Any, name: str = DEFAULT_RETURN_OBJECT_NAME):
+    def return_object(self, object: Any):
         """Returns the object to the calling task. The object needs to be pickleable"""
         if self._task_state == TaskState.RUN:
-            if name not in self._registered_return_targets:
-                target = PickleTarget(self._get_tmp_path_for_returns(name))
-                self._registered_return_targets[name] = target
-                target.write(object)
+            if self._registered_return_target is None:
+                self._registered_return_target = object
             else:
-                raise Exception(f"return target {name} already used")
+                raise Exception(f"return target already used")
         else:
             raise WrongTaskStateException(self._task_state, "return_target")
 
-    def get_return_object(self, name: str = DEFAULT_RETURN_OBJECT_NAME) -> Any:
-        if self._task_state == TaskState.FINISHED:
-            return self._registered_return_targets.get(name).read()
-        else:
-            raise WrongTaskStateException(self._task_state, "get_return_object")
+    def get_result(self) -> Any:
+        """
+        Returns the return value of the task,
+        this means the single value which was passed with return_object() during run_task().
+        Note that it's safe to call this method from the client side, ignoring luigi's scheduler, as it uses
+        persistent data to get the result.
+        """
+        if not self.output().exists():
+            # Actual state might be unknown, because we might be called from the client side.
+            raise WrongTaskStateException(TaskState.NONE, "get_result")
+        return self.output().read()
 
     def __repr__(self):
         """
@@ -359,4 +384,3 @@ def cleanup_child_task(self, success: bool, cleanup_checklist: Set[str]):
 
     def cleanup_task(self, success: bool):
         pass
-
diff --git a/exasol_integration_test_docker_environment/lib/base/stoppable_base_task.py b/exasol_integration_test_docker_environment/lib/base/stoppable_base_task.py
index 9790e7b9a..aa35a4513 100644
--- a/exasol_integration_test_docker_environment/lib/base/stoppable_base_task.py
+++ b/exasol_integration_test_docker_environment/lib/base/stoppable_base_task.py
@@ -46,7 +46,7 @@ def handle_failure(self, exception, exception_tb):
                 with self.failed_target.open("w") as f:
                     f.write("%s" % self.task_id)
 
-    def collect_failures(self)->Dict[str,None]:
+    def collect_failures(self) -> Dict[str,None]:
         failures = OrderedDict()
         failures.update(self.collect_failures_of_child_tasks())
         if self.get_failure_log_path().exists():
diff --git a/exasol_integration_test_docker_environment/lib/docker/container/__init__.py b/exasol_integration_test_docker_environment/lib/docker/container/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/exasol_integration_test_docker_environment/lib/docker/container/utils.py b/exasol_integration_test_docker_environment/lib/docker/container/utils.py
new file mode 100644
index 000000000..951c0f517
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/docker/container/utils.py
@@ -0,0 +1,16 @@
+import logging
+from typing import List
+
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+
+
+def remove_docker_container(containers: List[str]):
+    """
+    Removes the given container using docker API.
+    """
+    with ContextDockerClient() as docker_client:
+        for container in containers:
+            try:
+                docker_client.containers.get(container).remove(force=True)
+            except Exception as e:
+                logging.error(e)
diff --git a/exasol_integration_test_docker_environment/lib/docker/networks/__init__.py b/exasol_integration_test_docker_environment/lib/docker/networks/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/exasol_integration_test_docker_environment/lib/docker/networks/utils.py b/exasol_integration_test_docker_environment/lib/docker/networks/utils.py
new file mode 100644
index 000000000..b1822570b
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/docker/networks/utils.py
@@ -0,0 +1,16 @@
+import logging
+from typing import Iterator
+
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+
+
+def remove_docker_networks(networks: Iterator[str]):
+    """
+    Removes the given networks using docker API.
+    """
+    with ContextDockerClient() as docker_client:
+        for network in networks:
+            try:
+                docker_client.networks.get(network).remove()
+            except Exception as e:
+                logging.error(e)
diff --git a/exasol_integration_test_docker_environment/lib/docker/volumes/__init__.py b/exasol_integration_test_docker_environment/lib/docker/volumes/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/exasol_integration_test_docker_environment/lib/docker/volumes/utils.py b/exasol_integration_test_docker_environment/lib/docker/volumes/utils.py
new file mode 100644
index 000000000..f89dbe1aa
--- /dev/null
+++ b/exasol_integration_test_docker_environment/lib/docker/volumes/utils.py
@@ -0,0 +1,16 @@
+import logging
+from typing import List
+
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+
+
+def remove_docker_volumes(volumes: List[str]):
+    """
+    Removes the given volumes using docker API.
+    """
+    with ContextDockerClient() as docker_client:
+        for volume in volumes:
+            try:
+                docker_client.volumes.get(volume).remove(force=True)
+            except Exception as e:
+                logging.error(e)
diff --git a/exasol_integration_test_docker_environment/lib/test_environment/analyze_test_container.py b/exasol_integration_test_docker_environment/lib/test_environment/analyze_test_container.py
index 43f3ee9e5..bf32aa633 100644
--- a/exasol_integration_test_docker_environment/lib/test_environment/analyze_test_container.py
+++ b/exasol_integration_test_docker_environment/lib/test_environment/analyze_test_container.py
@@ -70,5 +70,6 @@ def run_task(self):
         build_tasks = self.create_build_tasks(shortcut_build=not self.push_all)
         push_task_creator = PushTaskCreatorFromBuildTasks(self)
         push_tasks = push_task_creator.create_tasks_for_build_tasks(build_tasks)
-        image_infos = yield from self.run_dependencies(push_tasks)
-        self.return_object(image_infos)
+        image_info_future = yield from self.run_dependencies(push_tasks)
+        image_info = self.get_values_from_futures(image_info_future)
+        self.return_object(image_info)
diff --git a/exasol_integration_test_docker_environment/test/test_api_build_test_container.py b/exasol_integration_test_docker_environment/test/test_api_build_test_container.py
new file mode 100644
index 000000000..acd4688b7
--- /dev/null
+++ b/exasol_integration_test_docker_environment/test/test_api_build_test_container.py
@@ -0,0 +1,18 @@
+import unittest
+
+from exasol_integration_test_docker_environment.lib import api
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+
+
+class APIBuildTestContainerTest(unittest.TestCase):
+
+    def test_build_test_container(self):
+        image_info = api.build_test_container()
+        with ContextDockerClient() as docker_client:
+            image = docker_client.images.get(image_info.get_target_complete_name())
+            assert len(image.tags) == 1
+            assert image.tags[0] == image_info.get_target_complete_name()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_api_push_test_container.py b/exasol_integration_test_docker_environment/test/test_api_push_test_container.py
new file mode 100644
index 000000000..49987e3f2
--- /dev/null
+++ b/exasol_integration_test_docker_environment/test/test_api_push_test_container.py
@@ -0,0 +1,33 @@
+import unittest
+from sys import stderr
+
+from exasol_integration_test_docker_environment.lib import api
+from exasol_integration_test_docker_environment.testing.api_test_environment import ApiTestEnvironment
+from exasol_integration_test_docker_environment.testing.docker_registry import LocalDockerRegistryContextManager
+
+
+class APIPushTestContainerTest(unittest.TestCase):
+
+    def setUp(self):
+        print(f"SetUp {self.__class__.__name__}", file=stderr)
+        self.test_environment = ApiTestEnvironment(self)
+
+    def tearDown(self):
+        self.test_environment.close()
+
+    def test_docker_push(self):
+        with LocalDockerRegistryContextManager(self.test_environment.name) as docker_registry:
+            print("registry:", docker_registry.repositories, file=stderr)
+            docker_repository_name = docker_registry.name
+            image_info = api.push_test_container(source_docker_repository_name=docker_repository_name,
+                                                 target_docker_repository_name=docker_repository_name)
+            print("repos:", docker_registry.repositories, file=stderr)
+            images = docker_registry.images
+            print("images", images, file=stderr)
+            self.assertEqual(len(images["tags"]), 1,
+                             f"{images} doesn't have the expected 1 tags, it has {len(images['tags'])}")
+            self.assertIn(image_info.get_target_complete_tag(), images["tags"][0])
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_api_spawn_test_environment.py b/exasol_integration_test_docker_environment/test/test_api_spawn_test_environment.py
new file mode 100644
index 000000000..ef63e4206
--- /dev/null
+++ b/exasol_integration_test_docker_environment/test/test_api_spawn_test_environment.py
@@ -0,0 +1,78 @@
+import unittest
+from sys import stderr
+
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+from exasol_integration_test_docker_environment.testing import utils
+from exasol_integration_test_docker_environment.testing.api_test_environment import ApiTestEnvironment
+
+
+class APISpawnTestEnvironmentTest(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        print(f"SetUp {cls.__name__}", file=stderr)
+        cls.test_environment = ApiTestEnvironment(cls)
+        cls.docker_environment_name = cls.__name__
+        cls.spawned_docker_test_environments = \
+            cls.test_environment.spawn_docker_test_environment(name=cls.docker_environment_name)
+
+    @classmethod
+    def tearDownClass(cls):
+        utils.close_environments(cls.spawned_docker_test_environments, cls.test_environment)
+
+    def test_all_containers_started(self):
+        with ContextDockerClient() as docker_client:
+            containers = [c.name for c in docker_client.containers.list() if self.docker_environment_name in c.name]
+            self.assertEqual(len(containers), 2,
+                             f"Not exactly 2 containers in {containers}.")
+            db_container = [c for c in containers if "db_container" in c]
+            self.assertEqual(len(db_container), 1,
+                             f"Found no db container in {containers}.")
+            test_container = [c for c in containers if "test_container" in c]
+            self.assertEqual(len(test_container), 1,
+                             f"Found no test container in {containers}.")
+
+    def test_docker_available_in_test_container(self):
+        environment_info = self.spawned_docker_test_environments.environment_info
+        with ContextDockerClient() as docker_client:
+            test_container = docker_client.containers.get(environment_info.test_container_info.container_name)
+            exit_result = test_container.exec_run("docker ps")
+            exit_code = exit_result[0]
+            output = exit_result[1]
+            self.assertEqual(exit_code, 0,
+                             f"Error while executing 'docker ps' in test container got output\n {output}.")
+
+    def test_db_container_available(self):
+        environment_info = self.spawned_docker_test_environments.environment_info
+        with ContextDockerClient() as docker_client:
+            db_container = docker_client.containers.get(environment_info.database_info.container_info.container_name)
+            exit_result = db_container.exec_run("ls /exa")
+            exit_code = exit_result[0]
+            output = exit_result[1]
+            self.assertEqual(exit_code, 0,
+                             f"Error while executing 'ls /exa' in db container got output\n {output}.")
+
+    def test_db_available(self):
+        environment_info = self.spawned_docker_test_environments.environment_info
+        with ContextDockerClient() as docker_client:
+            test_container = docker_client.containers.get(environment_info.test_container_info.container_name)
+            exit_result = test_container.exec_run(self.create_db_connection_command())
+            exit_code = exit_result[0]
+            output = exit_result[1]
+            self.assertEqual(exit_code, 0,
+                             f"Error while executing 'exaplus' in test container got output\n {output}.")
+
+    def create_db_connection_command(self):
+        spawned_docker_test_environments = self.spawned_docker_test_environments
+        username = spawned_docker_test_environments.db_username
+        password = spawned_docker_test_environments.db_password
+        db_host = spawned_docker_test_environments.environment_info.database_info.host
+        db_port = spawned_docker_test_environments.environment_info.database_info.db_port
+        connection_options = f"-c '{db_host}:{db_port}' -u '{username}' -p '{password}'"
+        cmd = f"""$EXAPLUS {connection_options}  -sql 'select 1;' -jdbcparam 'validateservercertificate=0'"""
+        bash_cmd = f"""bash -c "{cmd}" """
+        return bash_cmd
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_base_task.py b/exasol_integration_test_docker_environment/test/test_base_task.py
index 58f932905..1f53706e7 100644
--- a/exasol_integration_test_docker_environment/test/test_base_task.py
+++ b/exasol_integration_test_docker_environment/test/test_base_task.py
@@ -5,7 +5,7 @@
 import luigi
 from luigi import Parameter, Config
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 from exasol_integration_test_docker_environment.lib.base.json_pickle_parameter import JsonPickleParameter
 
@@ -18,7 +18,6 @@ def register_required(self):
 
     def run_task(self):
         self.logger.info("RUN")
-        self.logger.info(f"task2 list_outputs {self.task2.list_outputs()}")
         self.logger.info(f"task2 {self.task2.get_output()}")
         tasks_3 = yield from self.run_dependencies({
             "1": TestTask3(input_param="e"),
diff --git a/exasol_integration_test_docker_environment/test/test_base_task_cleanup.py b/exasol_integration_test_docker_environment/test/test_base_task_cleanup.py
index 77a1a5ffd..bba3445c2 100644
--- a/exasol_integration_test_docker_environment/test/test_base_task_cleanup.py
+++ b/exasol_integration_test_docker_environment/test/test_base_task_cleanup.py
@@ -4,7 +4,7 @@
 import luigi
 from luigi import BoolParameter, IntParameter
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 
 TestBaseTask = DependencyLoggerBaseTask
diff --git a/exasol_integration_test_docker_environment/test/test_common_run_task.py b/exasol_integration_test_docker_environment/test/test_common_run_task.py
index 427abe138..3d0c24c8d 100644
--- a/exasol_integration_test_docker_environment/test/test_common_run_task.py
+++ b/exasol_integration_test_docker_environment/test/test_common_run_task.py
@@ -2,6 +2,11 @@
 import unittest
 from pathlib import Path
 
+import luigi
+
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task, run_task
+from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
+
 
 class CommonRunTaskTest(unittest.TestCase):
     """
@@ -27,5 +32,25 @@ def test_different_logging_file_raises_error(self):
         self._execute_in_new_process(target="run_test_different_logging_file_raises_error")
 
 
+class TestTaskWithReturn(DependencyLoggerBaseTask):
+    x = luigi.Parameter()
+
+    def run_task(self):
+        self.return_object(f"{self.x}-123")
+
+
+class ReturnValueRunTaskTest(unittest.TestCase):
+
+    def test_return_value(self) -> None:
+        """
+        Integration test which verifies that the return value processing in run_task works as expected.
+        """
+
+        task_creator = lambda: generate_root_task(task_class=TestTaskWithReturn, x="Test")
+
+        return_value = run_task(task_creator, workers=5, task_dependencies_dot_file=None)
+        assert return_value == "Test-123"
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py b/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
index 34492cb91..4780b20f9 100644
--- a/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
+++ b/exasol_integration_test_docker_environment/test/test_common_run_task_subprocess.py
@@ -5,7 +5,7 @@
 
 import luigi
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task, run_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task, run_task
 from exasol_integration_test_docker_environment.cli.options.system_options import DEFAULT_OUTPUT_DIRECTORY
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 
@@ -15,7 +15,7 @@
 class TestTask(DependencyLoggerBaseTask):
     x = luigi.Parameter()
 
-    def run(self):
+    def run_task(self):
         self.logger.info(f"Logging: {self.x}")
 
 
@@ -27,8 +27,7 @@ def create_task():
         return generate_root_task(task_class=TestTask, x=f"{next(task_id_generator)}")
 
     for i in range(NUMBER_TASK):
-        success, task = run_task(create_task, workers=5, task_dependencies_dot_file=None)
-        assert success
+        run_task(create_task, workers=5, task_dependencies_dot_file=None)
 
     assert log_path.exists()
     with open(log_path, "r") as f:
@@ -69,12 +68,15 @@ def run_test_different_logging_file_raises_error() -> None:
     with tempfile.TemporaryDirectory() as temp_dir:
         task_creator = lambda: generate_root_task(task_class=TestTask, x="Test")
 
-        success, task = run_task(task_creator, workers=5, task_dependencies_dot_file=None)
-        assert success
+        run_task(task_creator, workers=5, task_dependencies_dot_file=None)
         log_path = Path(temp_dir) / "main.log"
         os.environ[LOG_ENV_VARIABLE_NAME] = str(log_path)
-        success, task = run_task(task_creator, workers=5, task_dependencies_dot_file=None)
-        assert success is False
+        exception_thrown = False
+        try:
+            run_task(task_creator, workers=5, task_dependencies_dot_file=None)
+        except ValueError as e:
+            exception_thrown = True
+        assert exception_thrown
 
 
 if __name__ == '__main__':
@@ -88,5 +90,5 @@ def run_test_different_logging_file_raises_error() -> None:
     try:
         dispatcher[test_type]()
     except KeyError as e:
-        raise ValueError("Unknow Test argument") from e
+        raise ValueError("Unknown Test argument") from e
 
diff --git a/exasol_integration_test_docker_environment/test/test_docker_build_base.py b/exasol_integration_test_docker_environment/test/test_docker_build_base.py
index 5d9536c61..9fe45918b 100644
--- a/exasol_integration_test_docker_environment/test/test_docker_build_base.py
+++ b/exasol_integration_test_docker_environment/test/test_docker_build_base.py
@@ -7,7 +7,7 @@
 
 from typing import Set, Dict
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task
 from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
 from exasol_integration_test_docker_environment.lib.docker.images.create.docker_build_base import DockerBuildBase
 from exasol_integration_test_docker_environment.lib.docker.images.create.docker_image_analyze_task import \
diff --git a/exasol_integration_test_docker_environment/test/test_generate_graph_plot.py b/exasol_integration_test_docker_environment/test/test_generate_graph_plot.py
index 3912dc950..a1fdb43de 100644
--- a/exasol_integration_test_docker_environment/test/test_generate_graph_plot.py
+++ b/exasol_integration_test_docker_environment/test/test_generate_graph_plot.py
@@ -4,7 +4,7 @@
 
 import luigi
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task, run_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task, run_task, set_build_config
 from exasol_integration_test_docker_environment.lib.base.dependency_logger_base_task import DependencyLoggerBaseTask
 
 
@@ -14,7 +14,7 @@ class TestTask(DependencyLoggerBaseTask):
     def register_required(self):
         self.register_dependency(self.create_child_task(task_class=TestChildTask, y=["1", "2", "3"]))
 
-    def run(self):
+    def run_task(self):
         pass
 
 
@@ -36,8 +36,7 @@ def create_task():
         with tempfile.TemporaryDirectory() as d:
             for i in range(NUMBER_TASK):
                 dot_file = Path(d) / f"dot_file_{i}.dot"
-                success, task = run_task(create_task, workers=5, task_dependencies_dot_file=str(dot_file))
-                assert success
+                run_task(create_task, workers=5, task_dependencies_dot_file=str(dot_file))
                 assert dot_file.exists()
 
 
diff --git a/exasol_integration_test_docker_environment/test/test_termination_handler.py b/exasol_integration_test_docker_environment/test/test_termination_handler.py
new file mode 100644
index 000000000..5663c877a
--- /dev/null
+++ b/exasol_integration_test_docker_environment/test/test_termination_handler.py
@@ -0,0 +1,98 @@
+import sys
+import unittest
+from multiprocessing import Queue
+from typing import List
+
+import multiprocessing as mp
+import re
+from importlib import reload
+
+from exasol_integration_test_docker_environment.lib.api.api_errors import TaskRuntimeError
+import exasol_integration_test_docker_environment.cli.termination_handler
+
+
+class StdoutQueue:
+    def __init__(self, wrapped_queue: Queue):
+        self.queue = wrapped_queue
+        self.old_stdout = sys.stdout
+
+    def write(self, msg):
+        self.queue.put(msg)
+
+    def flush(self):
+        self.old_stdout.flush()
+
+
+def run_positive(queue: Queue) -> None:
+    stdout_queue = StdoutQueue(queue)
+    sys.stdout = stdout_queue
+    sys.stderr = stdout_queue
+    # we need to release the termination module here, otherwise the new sys.stdout/sys.stderr might not be set correctly
+    m = reload(exasol_integration_test_docker_environment.cli.termination_handler)
+    with m.TerminationHandler():
+        pass
+
+
+def run_with_task_error(queue: Queue) -> None:
+    stdout_queue = StdoutQueue(queue)
+    sys.stdout = stdout_queue
+    sys.stderr = stdout_queue
+    # we need to release the termination module here, otherwise the new sys.stdout/sys.stderr might not be set correctly
+    m = reload(exasol_integration_test_docker_environment.cli.termination_handler)
+    with m.TerminationHandler():
+        raise TaskRuntimeError(msg="test", inner=["task runtime error test"])
+
+
+def run_with_unknown_error(queue: Queue) -> None:
+    stdout_queue = StdoutQueue(queue)
+    sys.stdout = stdout_queue
+    sys.stderr = stdout_queue
+    # we need to release the termination module here, otherwise the new sys.stdout/sys.stderr might not be set correctly
+    m = reload(exasol_integration_test_docker_environment.cli.termination_handler)
+    with m.TerminationHandler():
+        raise RuntimeError("unknown error")
+
+
+def get_queue_content(q: Queue) -> List[str]:
+    result = list()
+    while not q.empty():
+        result.append(q.get(block=False))
+    return result
+
+
+class TestTerminationHandler(unittest.TestCase):
+
+    def test_success(self):
+        q = Queue()
+        p = mp.Process(target=run_positive, args=(q,))
+        p.start()
+        p.join()
+        res = get_queue_content(q)
+        self.assertTrue(any(re.match(r"^The command took .+ s$", line) for line in res))
+        self.assertEqual(p.exitcode, 0)
+
+    def test_task_runtime_error(self):
+        q = Queue()
+        p = mp.Process(target=run_with_task_error, args=(q,))
+        p.start()
+        p.join()
+        res = get_queue_content(q)
+        self.assertTrue(any(re.match(r"^The command failed after .+ s with:$", line) for line in res))
+        self.assertTrue(any("task runtime error test" == line for line in res))
+        self.assertFalse(any(line.startswith("Caught exception:") for line in res))
+        self.assertEqual(p.exitcode, 1)
+
+    def test_unknown_error(self):
+        q = Queue()
+        p = mp.Process(target=run_with_unknown_error, args=(q,))
+        p.start()
+        p.join()
+        res = get_queue_content(q)
+        self.assertTrue(any(re.match(r"^The command failed after .+ s with:$", line) for line in res))
+        self.assertTrue(any("Caught exception:unknown error" == line for line in res))
+        self.assertTrue(any('raise RuntimeError("unknown error")' in line for line in res))
+        self.assertEqual(p.exitcode, 1)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/exasol_integration_test_docker_environment/test/test_test_container_reuse.py b/exasol_integration_test_docker_environment/test/test_test_container_reuse.py
index 34b099848..952a832b1 100644
--- a/exasol_integration_test_docker_environment/test/test_test_container_reuse.py
+++ b/exasol_integration_test_docker_environment/test/test_test_container_reuse.py
@@ -6,7 +6,7 @@
 
 import luigi
 
-from exasol_integration_test_docker_environment.cli.common import set_build_config, set_docker_repository_config, \
+from exasol_integration_test_docker_environment.lib.api.common import set_build_config, set_docker_repository_config, \
     generate_root_task
 from exasol_integration_test_docker_environment.lib.base.docker_base_task import DockerBaseTask
 from exasol_integration_test_docker_environment.lib.data.container_info import ContainerInfo
@@ -97,7 +97,7 @@ def run1(self):
         try:
             success = luigi.build([task], workers=1, local_scheduler=True, log_level="INFO")
             if success:
-                result = task.get_return_object()
+                result = task.get_result()
                 task.cleanup(True)
                 return result
             else:
@@ -112,8 +112,7 @@ def run2(self):
             success = luigi.build([task], workers=1, local_scheduler=True, log_level="INFO")
 
             if success:
-                result = task.get_return_object()
-                return result
+                return task.get_result()
             else:
                 raise Exception("Task failed")
         except Exception as e:
@@ -126,6 +125,10 @@ def test_test_container_no_reuse_after_change(self):
         with dockerfile.open("a") as f:
             f.write("\n#Test\n")
         p2 = self.run2()
+        assert "container_id" in p1
+        assert "image_id" in p1
+        assert "container_id" in p2
+        assert "image_id" in p2
         print(p1)
         print(p2)
         assert p1 != p2
@@ -133,6 +136,8 @@ def test_test_container_no_reuse_after_change(self):
     def test_test_container_reuse(self):
         p1 = self.run1()
         p2 = self.run2()
+        assert "container_id" in p1
+        assert "image_id" in p1
         print(p1)
         print(p2)
         assert p1 == p2
diff --git a/exasol_integration_test_docker_environment/test/test_test_env_reuse.py b/exasol_integration_test_docker_environment/test/test_test_env_reuse.py
index af9908838..54f74e58b 100644
--- a/exasol_integration_test_docker_environment/test/test_test_env_reuse.py
+++ b/exasol_integration_test_docker_environment/test/test_test_env_reuse.py
@@ -5,7 +5,7 @@
 from exasol_integration_test_docker_environment.lib.data.environment_type import EnvironmentType
 from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
 from exasol_integration_test_docker_environment.lib.test_environment.spawn_test_environment import SpawnTestEnvironment
-from exasol_integration_test_docker_environment.cli.common import set_docker_repository_config, generate_root_task
+from exasol_integration_test_docker_environment.lib.api.common import set_docker_repository_config, generate_root_task
 from exasol_integration_test_docker_environment.testing import luigi_utils
 from exasol_integration_test_docker_environment.cli.options import test_environment_options
 from exasol_integration_test_docker_environment.testing.utils import check_db_version_from_env
@@ -108,7 +108,7 @@ def _verify_test_data(self, test_environment_info):
 
     def test_initial_reuse_database_setup_populates_data(self):
         task = self.run_spawn_test_env(cleanup=True)
-        self._verify_test_data(task.get_return_object())
+        self._verify_test_data(task.get_result())
         task.cleanup(True)
 
     def get_instance_ids(self, test_environment_info):
@@ -121,13 +121,13 @@ def get_instance_ids(self, test_environment_info):
 
     def test_reuse_env_same_instances(self):
         task = self.run_spawn_test_env(cleanup=False)
-        test_environment_info = task.get_return_object()
+        test_environment_info = task.get_result()
         old_instance_ids = self.get_instance_ids(test_environment_info)
         # This clean is supposed to not remove docker instances
         task.cleanup(True)
 
         task = self.run_spawn_test_env(cleanup=True)
-        test_environment_info = task.get_return_object()
+        test_environment_info = task.get_result()
         new_instance_ids = self.get_instance_ids(test_environment_info)
         self.assertEquals(old_instance_ids, new_instance_ids)
         self._verify_test_data(test_environment_info)
diff --git a/exasol_integration_test_docker_environment/testing/api_test_environment.py b/exasol_integration_test_docker_environment/testing/api_test_environment.py
new file mode 100644
index 000000000..fb80c47ae
--- /dev/null
+++ b/exasol_integration_test_docker_environment/testing/api_test_environment.py
@@ -0,0 +1,76 @@
+import inspect
+import os
+import shutil
+import tempfile
+from pathlib import Path
+from sys import stderr
+from typing import Dict, Any
+
+from exasol_integration_test_docker_environment.cli.options.test_environment_options import LATEST_DB_VERSION
+from exasol_integration_test_docker_environment.lib.api import spawn_test_environment
+from exasol_integration_test_docker_environment.testing.docker_registry import default_docker_repository_name
+from exasol_integration_test_docker_environment.testing.exaslct_docker_test_environment import \
+    ExaslctDockerTestEnvironment
+from exasol_integration_test_docker_environment.testing.utils import find_free_ports, check_db_version_from_env
+
+
+class ApiTestEnvironment:
+
+    def __init__(self, test_object):
+        self.test_object = test_object
+        if not inspect.isclass(self.test_object):
+            self.test_class = self.test_object.__class__
+        else:
+            self.test_class = self.test_object
+        self.flavor_path = self.get_test_flavor()
+        self.name = self.test_class.__name__
+        self.docker_repository_name = default_docker_repository_name(self.name)
+        self.temp_dir = tempfile.mkdtemp()
+
+    def get_test_flavor(self):
+        source_file_of_test_object = inspect.getsourcefile(self.test_class)
+        flavor_path = Path(os.path.realpath(source_file_of_test_object)).parent.joinpath(
+            "resources/test-flavor")
+        return flavor_path
+
+    @property
+    def output_dir(self):
+        return self.temp_dir
+
+    @property
+    def task_dependency_dot_file(self):
+        return f"{self.name}.dot"
+
+    def close(self):
+        try:
+            shutil.rmtree(self.temp_dir)
+        except Exception as e:
+            print(e, file=stderr)
+
+    def spawn_docker_test_environment(self, name: str, additional_parameter: Dict[str, Any] = None) \
+            -> ExaslctDockerTestEnvironment:
+        database_port, bucketfs_port = find_free_ports(2)
+        on_host_parameter = ExaslctDockerTestEnvironment(
+            name=self.name + "_" + name,
+            database_host="localhost",
+            db_username="sys",
+            db_password="exasol",
+            bucketfs_username="w",
+            bucketfs_password="write",
+            database_port=database_port,
+            bucketfs_port=bucketfs_port)
+        db_version_from_env = check_db_version_from_env()
+        if additional_parameter is None:
+            on_host_parameter.environment_info, on_host_parameter.clean_up = \
+                spawn_test_environment(environment_name=on_host_parameter.name,
+                                       database_port_forward=on_host_parameter.database_port,
+                                       bucketfs_port_forward=on_host_parameter.bucketfs_port,
+                                       docker_db_image_version=db_version_from_env or LATEST_DB_VERSION)
+        else:
+            on_host_parameter.environment_info, on_host_parameter.clean_up = \
+                spawn_test_environment(environment_name=on_host_parameter.name,
+                                       database_port_forward=on_host_parameter.database_port,
+                                       bucketfs_port_forward=on_host_parameter.bucketfs_port,
+                                       docker_db_image_version=db_version_from_env or LATEST_DB_VERSION,
+                                       **additional_parameter)
+        return on_host_parameter
diff --git a/exasol_integration_test_docker_environment/testing/docker_registry.py b/exasol_integration_test_docker_environment/testing/docker_registry.py
new file mode 100644
index 000000000..657b8a2ed
--- /dev/null
+++ b/exasol_integration_test_docker_environment/testing/docker_registry.py
@@ -0,0 +1,71 @@
+import json
+import logging
+import time
+
+import requests
+
+from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+from exasol_integration_test_docker_environment.lib.docker.container.utils import remove_docker_container
+from exasol_integration_test_docker_environment.testing.utils import find_free_ports
+
+
+def default_docker_repository_name(env_name: str) -> str:
+    return f"exaslct_test/{env_name.lower()}"
+
+
+class LocalDockerRegistry:
+    def __init__(self, name: str, registry_container, registry_port):
+        self._name = name
+        self._registry_container = registry_container
+        self._registry_port = registry_port
+
+    @property
+    def name(self):
+        return f"localhost:{self._registry_port}/{self._name.lower()}"
+
+    @property
+    def images(self):
+        url = f"http://localhost:{self._registry_port}/v2/{self._name.lower()}/tags/list"
+        result = requests.request("GET", url)
+        images = json.loads(result.content.decode("UTF-8"))
+        return images
+
+    @property
+    def repositories(self):
+        result = requests.request("GET", f"http://localhost:{self._registry_port}/v2/_catalog/")
+        repositories_ = json.loads(result.content.decode("UTF-8"))["repositories"]
+        return repositories_
+
+
+class LocalDockerRegistryContextManager:
+    def __init__(self, name: str):
+        self._name = name
+        self._local_docker_registry = None
+
+    def __enter__(self):
+        registry_port = find_free_ports(1)[0]
+        registry_container_name = self._name.replace("/", "_") + "_registry"
+        with ContextDockerClient() as docker_client:
+            logging.debug("Start pull of registry:2")
+            docker_client.images.pull(repository="registry", tag="2")
+            logging.debug(f"Start container of {registry_container_name}")
+            try:
+                docker_client.containers.get(registry_container_name).remove(force=True)
+            except:
+                pass
+            registry_container = docker_client.containers.run(
+                image="registry:2", name=registry_container_name,
+                ports={5000: registry_port},
+                detach=True
+            )
+            time.sleep(10)
+            logging.debug(f"Finished start container of {registry_container_name}")
+            self._local_docker_registry = LocalDockerRegistry(self._name, registry_container, registry_port)
+            return self._local_docker_registry
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.close()
+
+    def close(self):
+        if self._local_docker_registry is not None:
+            remove_docker_container([self._local_docker_registry._registry_container.id])
diff --git a/exasol_integration_test_docker_environment/testing/exaslct_docker_test_environment.py b/exasol_integration_test_docker_environment/testing/exaslct_docker_test_environment.py
index 011b0ade5..bf4244911 100644
--- a/exasol_integration_test_docker_environment/testing/exaslct_docker_test_environment.py
+++ b/exasol_integration_test_docker_environment/testing/exaslct_docker_test_environment.py
@@ -2,7 +2,6 @@
 from typing import Optional
 
 from exasol_integration_test_docker_environment.lib.data.environment_info import EnvironmentInfo
-from exasol_integration_test_docker_environment.testing.utils import remove_docker_container, remove_docker_volumes
 
 
 class ExaslctDockerTestEnvironment:
@@ -22,8 +21,8 @@ def __init__(self, name: str, database_host: str,
         self.name = name
         self.environment_info = environment_info
         self.completed_process = completed_process
+        self.clean_up = None
 
     def close(self):
-        remove_docker_container([f"test_container_{self.name}",
-                                 f"db_container_{self.name}"])
-        remove_docker_volumes([f"db_container_{self.name}_volume"])
+        if self.clean_up is not None:
+            self.clean_up()
diff --git a/exasol_integration_test_docker_environment/testing/exaslct_test_environment.py b/exasol_integration_test_docker_environment/testing/exaslct_test_environment.py
index ddcbb3b38..8974de901 100644
--- a/exasol_integration_test_docker_environment/testing/exaslct_test_environment.py
+++ b/exasol_integration_test_docker_environment/testing/exaslct_test_environment.py
@@ -1,21 +1,31 @@
+import functools
 import inspect
 import os
 import shutil
 import subprocess
 import tempfile
-import time
 from pathlib import Path
 import shlex
+from sys import stderr
 from typing import List
 
 from exasol_integration_test_docker_environment.lib.data.environment_info import EnvironmentInfo
 from exasol_integration_test_docker_environment.lib.docker import ContextDockerClient
+from exasol_integration_test_docker_environment.lib.docker.container.utils import remove_docker_container
+from exasol_integration_test_docker_environment.lib.docker.volumes.utils import remove_docker_volumes
+from exasol_integration_test_docker_environment.testing.docker_registry import default_docker_repository_name
 from exasol_integration_test_docker_environment.testing.exaslct_docker_test_environment import \
     ExaslctDockerTestEnvironment
 from exasol_integration_test_docker_environment.testing.spawned_test_environments import SpawnedTestEnvironments
 from exasol_integration_test_docker_environment.testing.utils import find_free_ports, check_db_version_from_env
 
 
+def _cleanup(env_name: str):
+    remove_docker_container([f"test_container_{env_name}",
+                             f"db_container_{env_name}"])
+    remove_docker_volumes([f"db_container_{env_name}_volume"])
+
+
 class ExaslctTestEnvironment:
 
     def __init__(self, test_object, executable="./exaslct", clean_images_at_close=True):
@@ -28,7 +38,7 @@ def __init__(self, test_object, executable="./exaslct", clean_images_at_close=Tr
             self.test_class = self.test_object
         self.flavor_path = self.get_test_flavor()
         self.name = self.test_class.__name__
-        self._repository_prefix = "exaslct_test"
+        self._docker_repository_name = default_docker_repository_name(self.name)
         if "GOOGLE_CLOUD_BUILD" in os.environ:
             # We need to put the output directories into the workdir,
             # because only this is shared between the current container and
@@ -48,19 +58,20 @@ def get_test_flavor(self):
         return flavor_path
 
     @property
-    def repository_prefix(self):
-        return self._repository_prefix
+    def repository_name(self):
+        return self._docker_repository_name
 
-    @repository_prefix.setter
-    def repository_prefix(self, value):
-        self._repository_prefix = value
+    @repository_name.setter
+    def repository_name(self, value):
+        self._docker_repository_name = value
         self._update_attributes()
 
     def _update_attributes(self):
-        self.repository_name = f"{self._repository_prefix.lower()}/{self.name.lower()}"  # docker repository names must be lowercase
         self.flavor_path_argument = f"--flavor-path {self.get_test_flavor()}"
-        self.docker_repository_arguments = f"--source-docker-repository-name {self.repository_name} --target-docker-repository-name {self.repository_name}"
-        self.clean_docker_repository_arguments = f"--docker-repository-name {self.repository_name}"
+        repository_name = self.repository_name
+        self.docker_repository_arguments = f"--source-docker-repository-name {repository_name} " \
+                                           f"--target-docker-repository-name {repository_name}"
+        self.clean_docker_repository_arguments = f"--docker-repository-name {repository_name}"
         self.output_directory_arguments = f"--output-directory {self.temp_dir}"
         self.task_dependencies_argument = " ".join([f"--task-dependencies-dot-file {self.name}.dot", ])
 
@@ -84,8 +95,8 @@ def run_command(self, command: str,
             command = f"{command} {self.docker_repository_arguments}"
         if use_docker_repository and clean:
             command = f"{command} {self.clean_docker_repository_arguments}"
-        print()
-        print(f"command: {command}")
+        print(file=stderr)
+        print(f"command: {command}", file=stderr)
         if capture_output:
             completed_process = subprocess.run(shlex.split(command), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
         else:
@@ -93,7 +104,8 @@ def run_command(self, command: str,
         try:
             completed_process.check_returncode()
         except subprocess.CalledProcessError as e:
-            print(e.stdout.decode("UTF-8"))
+            if capture_output:
+                print(e.stdout.decode("UTF-8"), file=stderr)
             raise e
         return completed_process
 
@@ -102,11 +114,11 @@ def close(self):
             if self.clean_images_at_close:
                 self.clean_images()
         except Exception as e:
-            print(e)
+            print(e, file=stderr)
         try:
             shutil.rmtree(self.temp_dir)
         except Exception as e:
-            print(e)
+            print(e, file=stderr)
 
     def spawn_docker_test_environments(self, name: str, additional_parameter: List[str] = None) \
             -> SpawnedTestEnvironments:
@@ -141,6 +153,7 @@ def spawn_docker_test_environments(self, name: str, additional_parameter: List[s
             with environment_info_json_path.open() as f:
                 environment_info = EnvironmentInfo.from_json(f.read())
                 on_host_parameter.environment_info = environment_info
+        on_host_parameter.clean_up = functools.partial(_cleanup, on_host_parameter.name)
         if "GOOGLE_CLOUD_BUILD" in os.environ:
             google_cloud_parameter = ExaslctDockerTestEnvironment(
                 name=on_host_parameter.name,
@@ -154,6 +167,7 @@ def spawn_docker_test_environments(self, name: str, additional_parameter: List[s
                 environment_info=on_host_parameter.completed_process,
                 completed_process=on_host_parameter.completed_process
             )
+
             with ContextDockerClient() as docker_client:
                 db_container = docker_client.containers.get(f"db_container_{google_cloud_parameter.name}")
                 cloudbuild_network = docker_client.networks.get("cloudbuild")
@@ -165,33 +179,3 @@ def spawn_docker_test_environments(self, name: str, additional_parameter: List[s
         else:
             return SpawnedTestEnvironments(on_host_parameter, None)
 
-    def create_registry(self):
-        registry_port = find_free_ports(1)[0]
-        registry_container_name = self.name.replace("/", "_") + "_registry"
-        with ContextDockerClient() as docker_client:
-            print("Start pull of registry:2")
-            docker_client.images.pull(repository="registry", tag="2")
-            print(f"Start container of {registry_container_name}")
-            try:
-                docker_client.containers.get(registry_container_name).remove(force=True)
-            except:
-                pass
-            registry_container = docker_client.containers.run(
-                image="registry:2", name=registry_container_name,
-                ports={5000: registry_port},
-                detach=True
-            )
-            time.sleep(10)
-            print(f"Finished start container of {registry_container_name}")
-            if "GOOGLE_CLOUD_BUILD" in os.environ:
-                cloudbuild_network = docker_client.networks.get("cloudbuild")
-                cloudbuild_network.connect(registry_container)
-                registry_container.reload()
-                registry_host = registry_container.attrs["NetworkSettings"]["Networks"][cloudbuild_network.name][
-                    "IPAddress"]
-                # self.repository_prefix = f"{registry_host}:5000"
-                self.repository_prefix = f"localhost:{registry_port}"
-                return registry_container, registry_host, "5000"
-            else:
-                self.repository_prefix = f"localhost:{registry_port}"
-                return registry_container, "localhost", registry_port
diff --git a/exasol_integration_test_docker_environment/testing/luigi_utils.py b/exasol_integration_test_docker_environment/testing/luigi_utils.py
index 7963bcc51..aaa3d0293 100644
--- a/exasol_integration_test_docker_environment/testing/luigi_utils.py
+++ b/exasol_integration_test_docker_environment/testing/luigi_utils.py
@@ -2,7 +2,7 @@
 
 import luigi
 
-from exasol_integration_test_docker_environment.cli.common import generate_root_task
+from exasol_integration_test_docker_environment.lib.api.common import generate_root_task
 from exasol_integration_test_docker_environment.lib.docker.images.clean.clean_images import CleanImagesStartingWith
 
 
diff --git a/exasol_integration_test_docker_environment/testing/utils.py b/exasol_integration_test_docker_environment/testing/utils.py
index a69d8dd02..f56eddfaa 100644
--- a/exasol_integration_test_docker_environment/testing/utils.py
+++ b/exasol_integration_test_docker_environment/testing/utils.py
@@ -32,37 +32,6 @@ def find_free_ports(num_ports: int) -> List[int]:
     return ret_val
 
 
-def remove_docker_container(containers):
-    with ContextDockerClient() as docker_client:
-        for container in containers:
-            try:
-                docker_client.containers.get(container).remove(force=True)
-            except Exception as e:
-                print(e)
-
-
-def remove_docker_volumes(volumes):
-    with ContextDockerClient() as docker_client:
-        for volume in volumes:
-            try:
-                docker_client.volumes.get(volume).remove(force=True)
-            except Exception as e:
-                print(e)
-
-
-def request_registry_images(registry_host, registry_port, repo_name):
-    url = f"http://{registry_host}:{registry_port}/v2/{repo_name}/tags/list"
-    result = requests.request("GET", url)
-    images = json.loads(result.content.decode("UTF-8"))
-    return images
-
-
-def request_registry_repositories(registry_host, registry_port):
-    result = requests.request("GET", f"http://{registry_host}:{registry_port}/v2/_catalog/")
-    repositories_ = json.loads(result.content.decode("UTF-8"))["repositories"]
-    return repositories_
-
-
 def close_environments(*args):
     for env in args:
         try: