diff --git a/iem-api/.python-version b/iem-api/.python-version
index f69abe410a3c3fe2bf44a95f192f3c915ef19e1a..0a590336d5996461bb614a2b45ad675376e2d4d6 100644
--- a/iem-api/.python-version
+++ b/iem-api/.python-version
@@ -1 +1 @@
-3.9.7
+3.9.10
diff --git a/iem-api/Dockerfile b/iem-api/Dockerfile
index 635043b24d1dd1182614380f044d470f0af21803..eb07ab2e9dc86885354370d98f927176d6701405 100644
--- a/iem-api/Dockerfile
+++ b/iem-api/Dockerfile
@@ -1,17 +1,16 @@
 FROM hashicorp/terraform:1.1.4
 
-ARG API_KEY
+COPY requirements.txt /tmp/requirements.txt
+RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3=3.9.16-r0 gnupg
+RUN pip3 install -r /tmp/requirements.txt
+# install docker stack
+RUN apk add docker docker-compose
 
-ENV API_KEY=$API_KEY
+ENV API_KEY=changeme
 ENV IEM_HOME=/opt/iem/
+ENV DOCKERIZED=true
 
 COPY src/resources/ansible.cfg /etc/ansible/ansible.cfg
-COPY requirements.txt /tmp/requirements.txt
-COPY src ${IEM_HOME}src
-COPY main.py ${IEM_HOME}main.py
-
-RUN apk add py3-pip cargo g++ python3-dev file libffi-dev openssl-dev bash python3=3.9.13-r1 gnupg
-RUN pip3 install -r /tmp/requirements.txt
 
 # RUN adduser -h ${IEM_HOME} -S -D iem
 COPY certs/config ${IEM_HOME}.ssh/config
@@ -28,7 +27,14 @@ RUN ansible-galaxy collection install community.general
 COPY roles.yml /tmp/roles.yml
 RUN ansible-galaxy install -r /tmp/roles.yml
 
+RUN mkdir -p ${IEM_HOME}db && \
+    mkdir -p ${IEM_HOME}deployments
+
+COPY src ${IEM_HOME}src
+COPY main.py ${IEM_HOME}main.py
+COPY logging.ini ${IEM_HOME}logging.ini
+
 ENTRYPOINT ["/usr/bin/env"]
 WORKDIR ${IEM_HOME}
-CMD /usr/bin/uvicorn main:app --host 0.0.0.0
+CMD /usr/bin/uvicorn main:app --host 0.0.0.0 --log-level info
 EXPOSE 8000
diff --git a/iem-api/certs/config b/iem-api/certs/config
new file mode 100644
index 0000000000000000000000000000000000000000..a3dd79d71dd647b0bf2b82cc61266647e7d979bc
--- /dev/null
+++ b/iem-api/certs/config
@@ -0,0 +1,3 @@
+Host *
+    StrictHostKeyChecking no
+    UserKnownHostsFile=/dev/null
\ No newline at end of file
diff --git a/iem-api/certs/id_rsa b/iem-api/certs/id_rsa
new file mode 100644
index 0000000000000000000000000000000000000000..86a197bf1b6772c1eda0f7ac9d77918997b54a4a
--- /dev/null
+++ b/iem-api/certs/id_rsa
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA1FrTNE42EgZr9WJNMtvpKFHYhPUJ4lzEp83EM0jYY3TyjmIe
+ThMuqMLAHCk22fl4a8PttucggJ5ZWKhcJh623/y8AybJcmqZgq9a41Q609dmirf0
+7frCl+6zL8Mqy2Le2BD4eRADcq11s8r8Ys6J+EBPHQgEnK9CeZLSc/WFRlVr4bOD
+s0bEouDxjTAMYjYcpsCwqYgGdIXI9WWsnt3RvcEe8CaiTqoyDN8ZtgkG6MweSrTQ
+js8ySHO6o25cOoF7aT9Ihhf32I+KUanNIOvk3RAw2z1FK5xkFbbqMggZqz7rJn3M
+sn2dDiCQi2CWox2OYXV/jJKLC3UFuOX64fS9cwIDAQABAoIBAQCs69Tm1/Vx0ibh
+aA4DJ06C1bsh8cP9v5soJgfp1xzWSGooBcA1xasOI6B6jhkrgNlNr/uIIEe4VLne
+1yJKrGIwnUagrloGQMYGxDKXwYQx80p+FXRuwe7p96eUcjIL8tQSUCd1tdOI87VQ
+FjBVaWiybfO+aUQQLytLgoK7iKfhb7vO+9F+ZK1iDjBDNxFuiOM5zoeWOI7boYkD
+2yXIkwoBePS2rosbPLa649sVakKex2WhQdUFst4Zba2RhnWQBXUY44LvEK5TzScF
+FyYphPOUSplbzzM2+fuOna91NIWmJyHmf15lj7X9kC66XFIZMlvapksB8stEpDiA
+4al3IdBJAoGBAPPuM3xkr/kQYYn7E42fgpmINZ78V48gMLhpyUOabZtF8inMyMPB
+q7kfHns8Vx0ET8grSNr3qwDDV82lwvGqRCFChASMdQRR9LanydkDSeqpuZyQtVlt
+A/65YUdcNY7Vy+M+fRh5Srh/6qcO3beLeLWXbJ4RHBP/OEmHuF4mLfgVAoGBAN7c
+qdxTOhXPvOU69Bs5rJdfo6qBI1Yg8MCGctsUwPFpw1kW773ROOPa6XI6D74Dsdg8
+ypZ+IC3pRVtx61Xi3NOwxWNTTG+dyUgTSFz+WKjywXZXeHIbWngiFqk8JFYQWPzk
+6YaJk4tZhk2YuNNaCCYRgQqyWv8coEurRlMXZHlnAoGBALcJwdaQ0z8oXJimL4jw
+7ZX5kIrpPWanuAdZUe4Jfj+qX8mf4fKKbCowQLYmlBOw/ZDtcfDlMYsUCdnFjZ+7
+rP3sJJYpM1F3khJRm3PdNOUCUMY8C+i7lejZADcE6SdyJFkztbjcowYI7nJHBHZL
+ENvqcVW27wPOWlVKozz6lzn1AoGALVwmaoS6DtRwcwuzwZLUkR7TNhIAujgMKHN1
+DyhDOR+4tfpYI39hH+dfmnM83wTrfsKozUawkAepqToflySMo72X/2Zl6VXpMPVT
+xjGyo/h87fRRvI/asxblG9702luLcTW6XjrEQBmhn0uVWtc5T15CsIWqxb/y1FPx
+BVp+hcMCgYAlJXbjzjbbDoIOCsXPSPe9voBL8zVwp0aNuvQcuB/vCt1n1c1DWuPr
+AGMy/fRwY0Znag+ODMuulm7RgXUQy6ifJHiz9cKVGg/mGifaJSjgC+1AI9HFlij3
+asM5CueU0gK974rDxQkwmIWpRH57+kf6s8tGDrPPvqX9S4p3oxFlTw==
+-----END RSA PRIVATE KEY-----
diff --git a/iem-api/certs/id_rsa.pub b/iem-api/certs/id_rsa.pub
new file mode 100644
index 0000000000000000000000000000000000000000..245caaf060f0a8a06f315a8ba009f65b1e429d18
--- /dev/null
+++ b/iem-api/certs/id_rsa.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUWtM0TjYSBmv1Yk0y2+koUdiE9QniXMSnzcQzSNhjdPKOYh5OEy6owsAcKTbZ+Xhrw+225yCAnllYqFwmHrbf/LwDJslyapmCr1rjVDrT12aKt/Tt+sKX7rMvwyrLYt7YEPh5EANyrXWzyvxizon4QE8dCAScr0J5ktJz9YVGVWvhs4OzRsSi4PGNMAxiNhymwLCpiAZ0hcj1Zaye3dG9wR7wJqJOqjIM3xm2CQbozB5KtNCOzzJIc7qjblw6gXtpP0iGF/fYj4pRqc0g6+TdEDDbPUUrnGQVtuoyCBmrPusmfcyyfZ0OIJCLYJajHY5hdX+MkosLdQW45frh9L1z josu@WKM0092A
diff --git a/iem-api/db/.gitignore b/iem-api/db/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..8d96d78ffb43f7e6e74d5dab1726cd346f6d23ba
--- /dev/null
+++ b/iem-api/db/.gitignore
@@ -0,0 +1 @@
+iem.db
diff --git a/iem-api/logging.ini b/iem-api/logging.ini
new file mode 100644
index 0000000000000000000000000000000000000000..7a1ca118cbc13a3b9c05bb00e579c9a043a49a3c
--- /dev/null
+++ b/iem-api/logging.ini
@@ -0,0 +1,31 @@
+[loggers]
+keys=root,src,uvicorn
+
+[handlers]
+keys=stream_handler
+
+[formatters]
+keys=formatter
+
+[logger_root]
+level=INFO
+handlers=stream_handler
+
+[logger_src]
+level=INFO
+handlers=stream_handler
+qualname=src
+propagate=0
+
+[logger_uvicorn]
+level=INFO
+handlers=stream_handler
+qualname=uvicorn
+propagate=0
+
+[handler_stream_handler]
+class=StreamHandler
+formatter=formatter
+
+[formatter_formatter]
+format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s
diff --git a/iem-api/main.py b/iem-api/main.py
index 3fb84f4b479de3ea5635bd2ca4b3138d7bed5a1a..01bf7c09da67b33a9c27f6594a0ebec5562b0d86 100755
--- a/iem-api/main.py
+++ b/iem-api/main.py
@@ -1,33 +1,36 @@
+#!/usr/bin/env python3
+
 import json
 import logging
-import os
+from typing import List
 
-from fastapi import FastAPI, BackgroundTasks, status, Security, Depends, HTTPException
+import uvicorn
+from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Security, status
 from fastapi.openapi.utils import get_openapi
-from fastapi.security.api_key import APIKeyHeader, APIKey
-from typing import List
+from fastapi.security.api_key import APIKey, APIKeyHeader
 
 from src.core.iem import Iem
-from src.core.persistence import Sqlite
+from src.core.persistence import Persistence
 from src.core.utils import (
     BaseResponse,
-    DeploymentResponse,
-    DeploymentRequest,
     DeleteDeploymentRequest,
+    DeploymentRequest,
+    DeploymentResponse,
+    SelfHealingRequest,
 )
 
-LOGGER = logging.getLogger("iem")
-
 api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
 
 app = FastAPI(
-    title="IaC Execution Manager", version="0.1.15", description="IaC Execution Manager"
+    title="IaC Execution Manager", version="1.0.0", description="IaC Execution Manager"
 )
 
+logging.config.fileConfig("logging.ini")
+
 
 async def get_api_key(api_key_query: str = Security(api_key_header)):
 
-    if Sqlite().valid_api_key(api_key_query=api_key_query):
+    if Persistence().valid_api_key(api_key_query=api_key_query):
         return api_key_query
     else:
         raise HTTPException(
@@ -37,7 +40,7 @@ async def get_api_key(api_key_query: str = Security(api_key_header)):
 
 
 @app.get("/", tags=["greeting"])
-async def read_root(api_key: APIKey = Depends(get_api_key)):
+async def read_root(_: APIKey = Depends(get_api_key)):
     return {
         "message": "Hello from the IaC Execution Manager!",
         "version": app.version,
@@ -48,11 +51,7 @@ async def read_root(api_key: APIKey = Depends(get_api_key)):
 
 @app.get("/deployments/", response_model=List[DeploymentResponse], tags=["deployments"])
 async def read_status(
-    start: int = 0,
-    count: int = 25,
-    start_date: str = "1970-01-01",
-    end_date: str = "2100-01-01",
-    api_key: APIKey = Depends(get_api_key),
+    _: APIKey = Depends(get_api_key),
 ):
     all_deployments = Iem(credentials=None).get_all_deployments()
     return list(all_deployments)
@@ -65,9 +64,7 @@ async def read_status(
 )
 async def read_status_deployment(
     deployment_id: str,
-    start: int = 0,
-    count: int = 1,
-    api_key: APIKey = Depends(get_api_key),
+    _: APIKey = Depends(get_api_key),
 ):
     deployment = Iem().get_deployment(deployment_id=deployment_id)
     return list(deployment)
@@ -82,11 +79,10 @@ async def read_status_deployment(
 async def deploy(
     d: DeploymentRequest,
     background_tasks: BackgroundTasks,
-    api_key: APIKey = Depends(get_api_key),
+    _: APIKey = Depends(get_api_key),
 ):
-    logging.warning(d)
     i = Iem(credentials=d.credentials)
-    background_tasks.add_task(i.deploy, d.deployment_id, d.repository, d.commit)
+    background_tasks.add_task(i.deploy, d.deployment_id, d.bundle.base64)
     return BaseResponse(message="Deployment Request Created")
 
 
@@ -99,15 +95,29 @@ async def deploy(
 async def undeploy(
     d: DeleteDeploymentRequest,
     background_tasks: BackgroundTasks,
-    api_key: APIKey = Depends(get_api_key),
+    _: APIKey = Depends(get_api_key),
 ):
-    logging.warning(d)
     i = Iem(credentials=d.credentials)
     background_tasks.add_task(i.destroy, d.deployment_id)
     return BaseResponse(message="Undeployment Request Created")
 
 
-if os.getenv("STAGE") == "dev":
+@app.post(
+    "/self-healing/{strategy}",
+    status_code=status.HTTP_201_CREATED,
+    response_model=BaseResponse,
+    tags=["deployments"],
+)
+async def deploy(
+    d: SelfHealingRequest,
+    background_tasks: BackgroundTasks,
+    _: APIKey = Depends(get_api_key),
+):
+    return BaseResponse(message="Deployment Request Created")
+
+
+if __name__ == "__main__":
+    uvicorn.run("main:app", host="127.0.0.1", port=8000, log_level="info")
     with open("../openapi.json", "w") as f:
         json.dump(
             get_openapi(
diff --git a/iem-api/src/core/engine.py b/iem-api/src/core/engine.py
index 08f25cb557bd109225b33d981be86000d83c3214..6eac90bca0006b3105982302190202fec96974b7 100644
--- a/iem-api/src/core/engine.py
+++ b/iem-api/src/core/engine.py
@@ -1,12 +1,13 @@
 import logging
 import os
 import subprocess
-
+import time
 from abc import ABC, abstractmethod
-from jinja2 import Template
 from subprocess import CalledProcessError
 
-LOGGER = logging.getLogger("iem")
+from jinja2 import Template
+
+LOGGER = logging.getLogger(__name__)
 
 
 class Factory:
@@ -58,15 +59,16 @@ class Terraform(Engine):
             )
             output = subprocess.run(
                 ["terraform", "apply", "-auto-approve"],
-                check=True,
                 cwd=self._repo_path,
                 env=self._env,
-                # capture_output=True,
+                capture_output=True,
             )
+            LOGGER.info(output.stdout.decode("utf-8"))
+            output.check_returncode()
             return "CREATED", output.stdout, output.stderr
         except CalledProcessError as e:
             LOGGER.exception(e)
-            return "ERROR", None, None
+            raise e
 
     def destroy(self):
         try:
@@ -75,12 +77,14 @@ class Terraform(Engine):
                 check=True,
                 cwd=self._repo_path,
                 env=self._env,
-                # capture_output=True,
+                capture_output=True,
             )
+            LOGGER.info(output.stdout.decode("utf-8"))
+            output.check_returncode()
             return "DESTROYED", output.stdout, output.stderr
         except CalledProcessError as e:
             LOGGER.exception(e)
-            return "ERROR", None, None
+            raise e
 
     def output(self):
         try:
@@ -120,26 +124,30 @@ class Ansible(Engine):
 
     def apply(self):
         LOGGER.info("About to apply ansible")
-        try:
-            output = subprocess.run(
-                ["ansible", "all", "-i", "inventory", "-m", "wait_for_connection"],
-                check=True,
-                cwd=self._repo_path,
-                env=self._env,
-                capture_output=True,
-            )
-            LOGGER.info("All hosts in the inventory are reachable.")
-            output = subprocess.run(
-                ["ansible-playbook", "-i", "inventory", "main.yml"],
-                check=True,
-                cwd=self._repo_path,
-                env=self._env,
-                capture_output=True,
-            )
-            return "CREATED", output.stdout, output.stderr
-        except CalledProcessError as e:
-            LOGGER.exception(e.output)
-            raise e
+        for i in range(2):
+            try:
+                output = subprocess.run(
+                    ["ansible", "all", "-i", "inventory", "-m", "wait_for_connection"],
+                    check=True,
+                    cwd=self._repo_path,
+                    env=self._env,
+                    capture_output=True,
+                )
+                LOGGER.info("All hosts in the inventory are reachable.")
+                output = subprocess.run(
+                    ["ansible-playbook", "-i", "inventory", "main.yml"],
+                    check=True,
+                    cwd=self._repo_path,
+                    env=self._env,
+                    capture_output=True,
+                )
+                LOGGER.info(output.stdout.decode("utf-8"))
+                return "CREATED", output.stdout, output.stderr
+            except CalledProcessError as e:
+                LOGGER.exception(e.output)
+                if i == 1:
+                    raise e
+                time.sleep(10)
 
     def destroy(self):
         LOGGER.info("Nothing to be seen here.")
diff --git a/iem-api/src/core/iem.py b/iem-api/src/core/iem.py
index e4ad215022efdf497b2358ff720cde87a7f155f2..d5a40dc2c717d248a082dc76b47e7d85a323ade9 100644
--- a/iem-api/src/core/iem.py
+++ b/iem-api/src/core/iem.py
@@ -1,54 +1,39 @@
-import git
+import base64
+import binascii
 import json
 import logging
 import os
-import subprocess
+import shutil
+from io import BytesIO
+from subprocess import CalledProcessError
+from zipfile import BadZipFile, ZipFile
 
-from git import GitCommandError, InvalidGitRepositoryError
 from omegaconf import OmegaConf
-from subprocess import CalledProcessError
 
 from src.core.engine import Factory
-from src.core.persistence import Sqlite
-from src.core.utils import DeploymentResponse, Credentials
+from src.core.persistence import Persistence
+from src.core.utils import Credentials, DeploymentResponse
 
-LOGGER = logging.getLogger("iem")
+LOGGER = logging.getLogger(__name__)
 
 
 class Iem:
     def __init__(self, credentials: Credentials = None):
-        logging.basicConfig(
-            level=logging.INFO, format="%(asctime)s %(levelname)-8s %(message)s"
-        )
-
         self._credentials = credentials
-        # if credentials.aws:
-        #    self._aws_access_key_id = credentials.aws.access_key_id
-        #    self._aws_secret_access_key = credentials.aws.secret_access_key
 
-        # if credentials.openstack:
-        #    self._os_username = credentials.openstack.user_name
-        #    self._os_password = credentials.openstack.password
-        #    self._os_auth_url = credentials.openstack.auth_url
-        #    self._os_project_name = credentials.openstack.project_name
+        self._path_deployments = f"{os.environ['IEM_HOME']}deployments/"
 
-        # Check IEM_HOME variable
-        if os.getenv("IEM_HOME") is None:
-            LOGGER.error("Please define IEM_HOME environment variable.")
-            exit(-1)
-        self._iem_home = os.getenv("IEM_HOME")
-
-        self._persistence = Sqlite()
+        self._persistence = Persistence()
 
     def get_all_deployments(self):
         rows = self._persistence.get_all_deployments()
         for r in rows:
             d = DeploymentResponse(
-                status_time=r[0],
-                deployment_id=r[1],
-                status=r[2],
-                stdout=r[3],
-                stderr=r[4],
+                status_time=r.status_time,
+                deployment_id=r.deployment_id,
+                status=r.status,
+                stdout=r.stdout,
+                stderr=r.stderr,
             )
             yield d
 
@@ -56,54 +41,87 @@ class Iem:
         row = self._persistence.get_deployment(deployment_id=deployment_id)
         if row:
             d = DeploymentResponse(
-                status_time=row[0],
-                deployment_id=row[1],
-                status=row[2],
-                stdout=row[3],
-                stderr=row[4],
+                status_time=row.status_time,
+                deployment_id=row.deployment_id,
+                status=row.status,
+                stdout=row.stdout,
+                stderr=row.stderr,
             )
             yield d
 
-    def deploy(self, deployment_id: str, repository: str, commit: str):
+    def _get_env(self, deployment_id: str, credentials: Credentials) -> dict:
+        my_env = os.environ.copy()
+        if credentials.aws:
+            my_env["AWS_ACCESS_KEY_ID"] = credentials.aws.access_key_id
+            my_env["AWS_SECRET_ACCESS_KEY"] = credentials.aws.secret_access_key
+        if credentials.openstack:
+            my_env["OS_USERNAME"] = credentials.openstack.user_name
+            my_env["OS_PASSWORD"] = credentials.openstack.password
+            my_env["OS_AUTH_URL"] = credentials.openstack.auth_url
+            my_env["OS_PROJECT_NAME"] = credentials.openstack.project_name
+        if credentials.azure:
+            my_env["ARM_CLIENT_ID"] = credentials.azure.arm_client_id
+            my_env["ARM_CLIENT_SECRET"] = credentials.azure.arm_client_secret
+            my_env["ARM_SUBSCRIPTION_ID"] = credentials.azure.arm_subscription_id
+            my_env["ARM_TENANT_ID"] = credentials.azure.arm_tenant_id
+        if credentials.vmware:
+            my_env["VSPHERE_USER"] = credentials.vmware.user_name
+            my_env["VSPHERE_PASSWORD"] = credentials.vmware.password
+            my_env["VSPHERE_SERVER"] = credentials.vmware.server
+            my_env[
+                "VSPHERE_ALLOW_UNVERIFIED_SSL"
+            ] = credentials.vmware.allow_unverified_ssl
+
+        my_env["DEPLOYMENT_ID"] = deployment_id
+
+        return my_env
+
+    def _apply_stage(self, my_env: dict, repo_path: str, stage: str):
+        conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml")
+
+        self.validate(env=my_env, io=conf.input)
+
+        LOGGER.info(f"About to run stage: {stage}")
+        my_eng = Factory().get_engine(conf.engine)(
+            repo_path=f"{repo_path}/{stage}", my_env=my_env
+        )
+        status, stdout, stderr = my_eng.apply()
+
+        if conf.output:
+            output = my_eng.output()
+            self.update_env(output=output, env=my_env, io=conf.output)
+            self.validate(env=my_env, io=conf.output)
+
+        return status, stdout, stderr
+
+    def deploy(self, deployment_id: str, bundle: str):
         function_name = "deploy"
-        LOGGER.info(f"Running {function_name} method")
+        LOGGER.info(f"Running {function_name} method.")
 
         self._persistence.insert_deployment(
             deployment_id=deployment_id, status="STARTED", stdout=None, stderr=None
         )
 
+        repo_path = f"{self._path_deployments}{deployment_id}"
+        if not os.path.exists(repo_path):
+            os.makedirs(repo_path)
+
         try:
-            repo_path = f"{self._iem_home}{deployment_id}"
-            self.get_repo(repo_path=repo_path, repository=repository, commit=commit)
-
-            LOGGER.info(f"About to deploy project {repo_path}")
-            my_env = os.environ.copy()
-            if self._credentials.aws:
-                my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id
-                my_env[
-                    "AWS_SECRET_ACCESS_KEY"
-                ] = self._credentials.aws.secret_access_key
-            if self._credentials.openstack:
-                my_env["OS_USERNAME"] = self._credentials.openstack.user_name
-                my_env["OS_PASSWORD"] = self._credentials.openstack.password
-                my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url
-                my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name
+            LOGGER.info(f"Decompressing base64 bundle.")
+            bundle_decode = base64.b64decode(bundle)
+            ZipFile(BytesIO(bundle_decode)).extractall(repo_path)
 
+            LOGGER.info(f"Reading credentials.")
+            my_env = self._get_env(
+                deployment_id=deployment_id, credentials=self._credentials
+            )
+
+            LOGGER.info(f"About to deploy project {repo_path}.")
             project_conf = OmegaConf.load(f"{repo_path}/config.yaml")
             for stage in project_conf.iac:
-                conf = OmegaConf.load(f"{repo_path}/{stage}/config.yaml")
-
-                self.validate(env=my_env, io=conf.input)
-
-                my_eng = Factory().get_engine(conf.engine)(
-                    repo_path=f"{repo_path}/{stage}", my_env=my_env
+                status, stdout, stderr = self._apply_stage(
+                    my_env=my_env, repo_path=repo_path, stage=stage
                 )
-                status, stdout, stderr = my_eng.apply()
-
-                if conf.output:
-                    output = my_eng.output()
-                    self.update_env(output=output, env=my_env, io=conf.output)
-                    self.validate(env=my_env, io=conf.output)
 
             self._persistence.insert_deployment(
                 deployment_id=deployment_id,
@@ -111,12 +129,14 @@ class Iem:
                 stdout=stdout,
                 stderr=stderr,
             )
+            LOGGER.info(f"Deployment completed for project {repo_path}")
         except (
+            binascii.Error,
+            BadZipFile,
             CalledProcessError,
-            GitCommandError,
             FileNotFoundError,
-            InvalidGitRepositoryError,
             NameError,
+            KeyError,
         ) as e:
             LOGGER.exception(e)
             self._persistence.insert_deployment(
@@ -130,17 +150,11 @@ class Iem:
         function_name = "destroy"
         LOGGER.info(f"Running {function_name} method")
 
-        repo_path = f"{self._iem_home}{deployment_id}"
+        repo_path = f"{self._path_deployments}{deployment_id}"
 
-        my_env = os.environ.copy()
-        if self._credentials.aws:
-            my_env["AWS_ACCESS_KEY_ID"] = self._credentials.aws.access_key_id
-            my_env["AWS_SECRET_ACCESS_KEY"] = self._credentials.aws.secret_access_key
-        if self._credentials.openstack:
-            my_env["OS_USERNAME"] = self._credentials.openstack.user_name
-            my_env["OS_PASSWORD"] = self._credentials.openstack.password
-            my_env["OS_AUTH_URL"] = self._credentials.openstack.auth_url
-            my_env["OS_PROJECT_NAME"] = self._credentials.openstack.project_name
+        my_env = self._get_env(
+            deployment_id=deployment_id, credentials=self._credentials
+        )
 
         LOGGER.info(f"About to destroy project {repo_path}")
         project_conf = OmegaConf.load(f"{repo_path}/config.yaml")
@@ -153,6 +167,11 @@ class Iem:
             )
             status, stdout, stderr = my_eng.destroy()
 
+        if "/home" in repo_path:
+            LOGGER.exception(f"Cannot delete {repo_path}")
+        elif os.getenv("DOCKERIZED") == "true":
+            shutil.rmtree(repo_path)
+
         self._persistence.insert_deployment(
             deployment_id=deployment_id,
             status=status,
@@ -173,21 +192,3 @@ class Iem:
             if variable not in env:
                 LOGGER.error(f"Variable {variable} not found in environment.")
                 raise NameError
-
-    def get_repo(self, repo_path: str, repository: str, commit: str):
-        LOGGER.info(
-            f"About to download the project {repo_path} with repository {repository}"
-        )
-        if not os.path.isdir(repo_path):
-            os.makedirs(repo_path)
-            repo = git.Repo.clone_from(
-                url=repository, to_path=repo_path, no_checkout=True
-            )
-        else:
-            repo = git.Repo(repo_path)
-
-        repo.git.checkout(commit)
-
-        for submodule in repo.submodules:
-            submodule.update(init=True)
-            submodule.module().git.checkout()
diff --git a/iem-api/src/core/persistence.py b/iem-api/src/core/persistence.py
index 2f4979e53ff3085ed75c5d8bf5e6ab17a120314b..80ada9c155c49e20b31ffcf719d132dc66973758 100644
--- a/iem-api/src/core/persistence.py
+++ b/iem-api/src/core/persistence.py
@@ -1,86 +1,16 @@
+import logging
 import os
-import sqlite3
-
-# from dataclasses import dataclass
 from datetime import datetime
+
 from ratelimiter import RateLimiter
+from sqlalchemy import Column, DateTime, Integer, String, create_engine, desc, select
+from sqlalchemy.orm import Session, declarative_base
 
-from sqlalchemy import create_engine
-from sqlalchemy import Column
-from sqlalchemy import DateTime, Integer, String
-from sqlalchemy import select, desc
-from sqlalchemy.orm import declarative_base, Session
+LOGGER = logging.getLogger(__name__)
 
 Base = declarative_base()
 
 
-class Sqlite:
-    def __init__(self):
-        self._db_name = "iem.db"
-
-        # create database if it does not exist
-        if not os.path.isfile(self._db_name):
-            self.__create_database()
-
-    def __create_database(self):
-        conn = sqlite3.connect(self._db_name)
-        with conn:
-            sql = """CREATE TABLE deployments (id INTEGER PRIMARY KEY AUTOINCREMENT,
-            status_time DATETIME DEFAULT CURRENT_TIMESTAMP,
-            deployment_id TEXT NOT NULL, status TEXT NOT NULL, stdout TEXT, stderr TEXT);"""
-            conn.execute(sql)
-
-    def insert_deployment(
-        self, deployment_id: str, status: str, stdout: str, stderr: str
-    ):
-        conn = sqlite3.connect(self._db_name)
-        with conn:
-            sql = """INSERT INTO deployments (deployment_id, status, stdout, stderr) VALUES (?,?,?,?)"""
-
-            cursor = conn.cursor()
-            cursor.execute(
-                sql,
-                (
-                    deployment_id,
-                    status,
-                    stdout,
-                    stderr,
-                ),
-            )
-            conn.commit()
-
-    def get_deployment(self, deployment_id: str):
-        conn = sqlite3.connect(self._db_name)
-        with conn:
-            sql = """SELECT status_time, deployment_id, status, stdout, stderr FROM deployments
-                     WHERE deployment_id=? ORDER BY id DESC LIMIT 1"""
-
-            cursor = conn.cursor()
-            cursor.execute(sql, (deployment_id,))
-            row = cursor.fetchone()
-
-            return row
-
-    def get_all_deployments(self):
-        conn = sqlite3.connect(self._db_name)
-        with conn:
-            sql = """SELECT status_time, deployment_id, status, stdout, stderr
-                     FROM deployments ORDER BY id DESC LIMIT 25 OFFSET 0"""
-
-            cursor = conn.cursor()
-            cursor.execute(sql)
-            rows = cursor.fetchall()
-
-            return rows
-
-    @RateLimiter(max_calls=10, period=1)
-    def valid_api_key(self, api_key_query: str):
-        if api_key_query == os.getenv("API_KEY"):
-            return True
-        else:
-            return False
-
-
 class Deployment(Base):
     __tablename__ = "deployments"
 
@@ -96,8 +26,8 @@ class Deployment(Base):
 
 
 class Persistence:
-    def __init__(self):
-        self._engine = create_engine("sqlite:///:memory:", future=True)
+    def __init__(self, engine_url="sqlite:///db/iem.db"):
+        self._engine = create_engine(url=engine_url, future=True)
         Base.metadata.create_all(self._engine)
 
     def insert_deployment(
diff --git a/iem-api/src/core/utils.py b/iem-api/src/core/utils.py
index 5ec5df2b1914207333f99027ccb25371cea6b9ea..683afb2738b6b896dcf2b49840d983047148bfba 100644
--- a/iem-api/src/core/utils.py
+++ b/iem-api/src/core/utils.py
@@ -1,10 +1,7 @@
-import logging
-
 from datetime import datetime
-from pydantic import BaseModel
 from typing import Dict, Optional
 
-LOGGER = logging.getLogger("iem")
+from pydantic import BaseModel
 
 
 class BaseResponse(BaseModel):
@@ -42,16 +39,39 @@ class Openstack(BaseModel):
     user_domain_name: Optional[str]
 
 
+class Docker(BaseModel):
+    server: str
+    user_name: str
+    password: str
+
+
+class Vmware(BaseModel):
+    user_name: str
+    password: str
+    server: str
+    allow_unverified_ssl: Optional[str]
+
+
 class Credentials(BaseModel):
     aws: Optional[Aws] = None
     azure: Optional[Azure] = None
     openstack: Optional[Openstack] = None
+    vmware: Optional[Vmware] = None
+    docker: Optional[Docker] = None
+
+
+class Bundle(BaseModel):
+    base64: str
 
 
 class DeploymentRequest(BaseModel):
     deployment_id: str
-    repository: str
-    commit: str
+    credentials: Credentials
+    bundle: Bundle
+
+
+class SelfHealingRequest(BaseModel):
+    deployment_id: str
     credentials: Credentials
 
 
diff --git a/iem-api/src/resources/id_iem b/iem-api/src/resources/id_iem
new file mode 100644
index 0000000000000000000000000000000000000000..2f5c3078d02d507c7dd8a41c16ce3a24b897dac3
--- /dev/null
+++ b/iem-api/src/resources/id_iem
@@ -0,0 +1,38 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+-----END OPENSSH PRIVATE KEY-----
diff --git a/iem-api/src/resources/id_iem.pub b/iem-api/src/resources/id_iem.pub
new file mode 100644
index 0000000000000000000000000000000000000000..c4368a5642c47e33f57f5df4e56bd874aebcccad
--- /dev/null
+++ b/iem-api/src/resources/id_iem.pub
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDV3VeYPD1WKQpq2PztNUQ0IAnm4JCmCPJpCLcem9kO3DvNbVBN5Q0JF0nfqgc8b1wP7uBH1b3G8jaX7Zsq6CpNu4OvXKw/EBxO3pdiAx6v0mRZMLBx2qcBd1TB90sxQ+juY6//06c52OgSjQ3Cey8a8/GKdtTMxRKh+rAuAch2WjcZBmTZPe+9E3fOhibHg5UA3sLhFcAPuEuEQRbTDBSsCqdCqHXfIe4NiLqJYVLOHkHuHfvTRaLROh7zmf20FmIP7s1gYU3GpDKRgZeuo2ZM9791N98B0JgSnepbx2/MDuG2WvAFwHIK7WSNVxGuxdvXspUWfDGam/G4ZGuxzaxVHtwYgSyxNoAUXcv82GS/MNLXqpfapvPR/LGcWAEcg1FgzjkC8y4MBZ6tQDowAYr/BEnUI7xmtIm0ixBWdd2zGtyMoSbvqHN0DSsRM+YDkD4XMcfd9Kp9kiIWHl1N9RSeV3D1JNfDGNGAE1qym25Ni0zWNbSyEUxpLkXj4T2wEDM=
diff --git a/iem-api/tests/__init__.py b/iem-api/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/iem-api/tests/it/__init__.py b/iem-api/tests/it/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/iem-api/tests/it/test_it_iem.py b/iem-api/tests/it/test_it_iem.py
new file mode 100644
index 0000000000000000000000000000000000000000..eb545cb33c427accf7ed2829ea71529ae9c50629
--- /dev/null
+++ b/iem-api/tests/it/test_it_iem.py
@@ -0,0 +1,83 @@
+import base64
+import logging
+import os
+import unittest
+import uuid
+
+from src.core.iem import Iem
+from src.core.utils import Aws, Credentials, Openstack
+
+LOGGER = logging.getLogger(__name__)
+
+
+class TestIem(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        super(TestIem, self).__init__(*args, **kwargs)
+
+        # Check IEM_HOME variable
+        self._iem_home = os.environ["IEM_HOME"]
+
+    @unittest.skipUnless(os.getenv("AWS"), "Define AWS variable to execute")
+    def test_deploy_destroy_aws(self):
+
+        deployment_id = str(uuid.uuid4())
+        a = Iem(
+            Credentials(
+                aws=Aws(
+                    access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
+                    secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
+                )
+            )
+        )
+        with open("tests/resources/aws.zip", "rb") as binary_file:
+            bundle = base64.b64encode(binary_file.read())
+
+        a.deploy(deployment_id=deployment_id, bundle=bundle)
+
+        a.destroy(deployment_id=deployment_id)
+
+    @unittest.skipUnless(
+        os.getenv("INTEGRATION"), "Define INTEGRATION variable to execute"
+    )
+    def test_deploy_destroy_openstack(self):
+
+        deployment_id = str(uuid.uuid4())
+        a = Iem(
+            Credentials(
+                openstack=Openstack(
+                    user_name=os.environ["OS_USERNAME"],
+                    password=os.environ["OS_PASSWORD"],
+                    auth_url=os.environ["OS_AUTH_URL"],
+                    project_name=os.environ["OS_PROJECT_NAME"],
+                )
+            )
+        )
+        with open("tests/resources/openstack.zip", "rb") as binary_file:
+            bundle = base64.b64encode(binary_file.read())
+
+        a.deploy(deployment_id=deployment_id, bundle=bundle)
+
+        a.destroy(deployment_id=deployment_id)
+
+    @unittest.skipUnless(
+        os.getenv("INTEGRATION"), "Define INTEGRATION variable to execute"
+    )
+    def test_deploy_destroy_docker(self):
+
+        deployment_id = str(uuid.uuid4())
+        a = Iem(
+            Credentials(
+                openstack=Openstack(
+                    user_name=os.environ["OS_USERNAME"],
+                    password=os.environ["OS_PASSWORD"],
+                    auth_url=os.environ["OS_AUTH_URL"],
+                    project_name=os.environ["OS_PROJECT_NAME"],
+                )
+            )
+        )
+        with open("tests/resources/docker.zip", "rb") as binary_file:
+            bundle = base64.b64encode(binary_file.read())
+
+        a.deploy(deployment_id=deployment_id, bundle=bundle)
+
+        a.destroy(deployment_id=deployment_id)
diff --git a/iem-api/tests/resources/aws.zip b/iem-api/tests/resources/aws.zip
new file mode 100644
index 0000000000000000000000000000000000000000..0cc3d7449f463683c68ed584cc500b8d1a9d4014
Binary files /dev/null and b/iem-api/tests/resources/aws.zip differ
diff --git a/iem-api/tests/resources/docker.zip b/iem-api/tests/resources/docker.zip
new file mode 100644
index 0000000000000000000000000000000000000000..888efb0b9697997713ed9d800eb25fcf8bf989c9
Binary files /dev/null and b/iem-api/tests/resources/docker.zip differ
diff --git a/iem-api/tests/resources/dummy.zip b/iem-api/tests/resources/dummy.zip
new file mode 100644
index 0000000000000000000000000000000000000000..20d2bc346232701a6a840e0834e7709fdc67bab3
Binary files /dev/null and b/iem-api/tests/resources/dummy.zip differ
diff --git a/iem-api/tests/resources/openstack.zip b/iem-api/tests/resources/openstack.zip
new file mode 100644
index 0000000000000000000000000000000000000000..7d2d24ddd670766af89958f489b8cfceff66c99a
Binary files /dev/null and b/iem-api/tests/resources/openstack.zip differ
diff --git a/iem-api/tests/unit/__init__.py b/iem-api/tests/unit/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/iem-api/tests/unit/test_iem.py b/iem-api/tests/unit/test_iem.py
new file mode 100644
index 0000000000000000000000000000000000000000..97fa04dad9e5c134fc3c5ed2996bc7a5ebdf6746
--- /dev/null
+++ b/iem-api/tests/unit/test_iem.py
@@ -0,0 +1,31 @@
+import logging
+import os
+import unittest
+import uuid
+
+from src.core.iem import Iem
+from src.core.utils import Aws, Credentials, Openstack
+
+LOGGER = logging.getLogger(__name__)
+
+
+class TestIem(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        super(TestIem, self).__init__(*args, **kwargs)
+
+        # Check IEM_HOME variable
+        self._iem_home = os.environ["IEM_HOME"]
+
+    def test_get_all_deployments(self):
+        i = Iem()
+        all_deployments = i.get_all_deployments()
+        self.assertGreaterEqual(len(list(all_deployments)), 0)
+
+    def test_get_deployment_ok(self):
+        i = Iem()
+        i.get_deployment(deployment_id="deployment2")
+
+    def test_get_deployment_not_ok(self):
+        i = Iem()
+        deployment_id = str(uuid.uuid4())
+        i.get_deployment(deployment_id=deployment_id)
diff --git a/iem-api/tests/unit/test_main.py b/iem-api/tests/unit/test_main.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d05433f46c71e7afa50338565d3fbf90d9ea5ad
--- /dev/null
+++ b/iem-api/tests/unit/test_main.py
@@ -0,0 +1,106 @@
+import base64
+import os
+import unittest
+import uuid
+from unittest.mock import Mock, patch
+
+from fastapi.testclient import TestClient
+
+from main import app
+
+
+class TestMain(unittest.TestCase):
+    def __init__(self, *args, **kwargs):
+        super(TestMain, self).__init__(*args, **kwargs)
+
+        self.client = TestClient(app)
+
+        # Check API_KEY variable
+        self._api_key = os.environ["API_KEY"]
+
+    def test_root_no_apikey(self):
+        response = self.client.get("/")
+        assert response.status_code == 403
+
+    def test_root(self):
+        response = self.client.get("/", headers={"x-api-key": self._api_key})
+        assert response.status_code == 200
+
+    def test_get_all_deployments(self):
+        response = self.client.get(
+            f"/deployments/", headers={"x-api-key": self._api_key}
+        )
+        assert response.status_code == 200
+
+    def test_get_one_deployment(self):
+        response = self.client.get(
+            f"/deployments/25/", headers={"x-api-key": self._api_key}
+        )
+        assert response.status_code == 200
+
+    @patch("subprocess.run")
+    def test_deployments(self, mock_run):
+        mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}")
+        with open("tests/resources/dummy.zip", "rb") as binary_file:
+            bundle = base64.b64encode(binary_file.read())
+        response = self.client.post(
+            f"/deployments/",
+            headers={"x-api-key": self._api_key},
+            json={
+                "deployment_id": str(uuid.uuid4()),
+                "credentials": {
+                    "openstack": {
+                        "user_name": "string",
+                        "password": "string",
+                        "auth_url": "string",
+                        "project_name": "string",
+                        "region_name": "string",
+                        "domain_name": "string",
+                        "project_domain_name": "string",
+                        "user_domain_name": "string",
+                    }
+                },
+                "bundle": {"base64": bundle.decode("utf-8")},
+            },
+        )
+        assert response.status_code == 201
+
+    @patch("subprocess.run")
+    def test_undeploy(self, mock_run):
+        mock_run.return_value = Mock(returncode=0, stdout=b"{}", stderr=b"{}")
+
+        deployment_id = str(uuid.uuid4())
+
+        with open("tests/resources/dummy.zip", "rb") as binary_file:
+            bundle = base64.b64encode(binary_file.read())
+        response = self.client.post(
+            f"/deployments/",
+            headers={"x-api-key": self._api_key},
+            json={
+                "deployment_id": deployment_id,
+                "credentials": {
+                    "openstack": {
+                        "user_name": "string",
+                        "password": "string",
+                        "auth_url": "string",
+                        "project_name": "string",
+                        "region_name": "string",
+                        "domain_name": "string",
+                        "project_domain_name": "string",
+                        "user_domain_name": "string",
+                    }
+                },
+                "bundle": {"base64": bundle.decode("utf-8")},
+            },
+        )
+        assert response.status_code == 201
+
+        response = self.client.post(
+            f"/undeploy/",
+            headers={"x-api-key": self._api_key},
+            json={
+                "deployment_id": deployment_id,
+                "credentials": {},
+            },
+        )
+        assert response.status_code == 202
diff --git a/iem-api/tests/unit/test_persistence.py b/iem-api/tests/unit/test_persistence.py
new file mode 100644
index 0000000000000000000000000000000000000000..6683e8b6e740dca89d0d0c98358ba7f0b4e23c87
--- /dev/null
+++ b/iem-api/tests/unit/test_persistence.py
@@ -0,0 +1,32 @@
+import unittest
+import uuid
+
+from src.core.persistence import Persistence
+
+
+class TestPersistence(unittest.TestCase):
+    @classmethod
+    def setUpClass(self):
+        self.engine_url = "sqlite:///:memory:"
+
+    def test_insert_deployment(self):
+        Persistence(engine_url=self.engine_url).insert_deployment(
+            "deployment1", "STARTED", "stdout", "stderr"
+        )
+
+    def test_get_deployment(self):
+        p = Persistence(engine_url=self.engine_url)
+        p.insert_deployment("deployment2", "STOPPED", "stdout", "stderr")
+        p.insert_deployment("deployment2", "STARTED", "stdout", "stderr")
+        d = p.get_deployment("deployment2")
+        self.assertEqual(d.status, "STARTED")
+
+    def test_get_deployment_not_ok(self):
+        row = Persistence(engine_url=self.engine_url).get_deployment(str(uuid.uuid4()))
+        self.assertIsNone(row)
+
+    def test_get_all_deployments(self):
+        persistence = Persistence(engine_url=self.engine_url)
+        persistence.insert_deployment("deployment3", "STARTED", "stdout", "stderr")
+        rows = persistence.get_all_deployments()
+        self.assertGreaterEqual(len(list(rows)), 1)