diff --git a/nsfarm/lxd/container.py b/nsfarm/lxd/container.py
index 2ebf4dc8075f0309c30cff226d53b506ea1a3318..e067d88ab0602b8977550fce632ebc3b045733a6 100644
--- a/nsfarm/lxd/container.py
+++ b/nsfarm/lxd/container.py
@@ -1,21 +1,13 @@
-"""Containers and images management.
+"""Containers management.
 """
 import os
-import time
 import typing
-import itertools
-import pathlib
-import hashlib
 import logging
 import pexpect
-import pylxd
 from .. import cli
 from .connection import LXDConnection
-from .device import NetInterface
-
-IMAGE_INIT_PATH = "/nsfarm-init.sh"  # Where we deploy initialization script for image
-
-IMGS_DIR = pathlib.Path(__file__).parents[2] / "imgs"
+from .image import Image
+from .device import Device
 
 logger = logging.getLogger(__package__)
 
@@ -25,163 +17,50 @@ class Container:
     """
     # TODO log syslog somehow
 
-    def __init__(self, lxd_connection: LXDConnection, img_name: str, devices: typing.List[NetInterface] = (),
+    def __init__(self, lxd_connection: LXDConnection, image: typing.Union[str, Image], devices: typing.List[Device] = (),
                  internet: bool = True):
+        self.image_name = image.name if isinstance(image, Image) else image
         self._lxd = lxd_connection
-        self._name = img_name
         self._internet = internet
         self._devices = tuple(devices)
-        self._dir_path = IMGS_DIR / img_name
-        self._file_path = self._dir_path.with_suffix(self._dir_path.suffix + ".sh")
-        self._logger = logging.getLogger(f"{__package__}[{img_name}]")
-        # Verify existence of image definition
-        if not self._file_path.is_file():
-            raise Exception(f"There seems to be no file describing image: {self._file_path}")
-        if not self._dir_path.is_dir():
-            self._dir_path = None
-        # Get parent
-        with open(self._file_path) as file:
-            # This reads second line of file while initial hash removed
-            parent = next(itertools.islice(file, 1, 2))[1:].strip()
-        self._parent = None
-        if parent.startswith("nsfarm:"):
-            self._parent = Container(lxd_connection, parent[7:])
-        elif parent.startswith("images:"):
-            self._parent = self._lxd.images.images.get_by_alias(parent[7:])
-        else:
-            raise Exception(f"The file has parent from unknown source: {parent}: {self.fpath}")
-        # Calculate identity hash and generate image name
-        self._hash = self.__identity_hash()
-        self._image_alias = f"nsfarm/{self._name}/{self._hash}"
-        # Some empty handles
-        self._lxd_image = None
-        self._lxd_container = None
 
-    def __identity_hash(self):
-        md5sum = hashlib.md5()
-        # Parent
-        if isinstance(self._parent, Container):
-            md5sum.update(self._parent.hash.encode())
-        else:
-            md5sum.update(self._parent.fingerprint.encode())
-        # File defining container
-        with open(self._file_path, "rb") as file:
-            md5sum.update(file.read())
-        # Additional nodes from directory
-        if self._dir_path:
-            nodes = [path for path in self._dir_path.iterdir() if path.is_dir()]
-            while nodes:
-                node = nodes.pop()
-                path = self._dir_path / node
-                md5sum.update(str(node).encode())
-                if path.is_dir():
-                    nodes += [path for path in node.iterdir() if path.is_dir()]
-                elif path.is_file():
-                    # For plain file include content
-                    with open(path, "rb") as file:
-                        md5sum.update(file.read())
-                elif path.is_link():
-                    # For link include its target as well
-                    md5sum.update(str(path.resolve()).encode())
-        return md5sum.hexdigest()
+        self._logger = logging.getLogger(f"{__package__}[{self.image_name}]")
+        self.image = image if isinstance(image, Image) else Image(lxd_connection, image)
 
-    def prepare_image(self):
-        """Prepare image for this container if not already prepared.
-
-        You can call this explicitly if you want to prepapre image but this method is automatically called when you
-        atempt to prepare container so you don't have to do it.
-        """
-        if self._lxd_image:
-            return
-        if self._lxd.local.images.exists(self._image_alias, alias=True):
-            self._lxd_image = self._lxd.local.images.get_by_alias(self._image_alias)
-            return
-        # We do not have appropriate image so prepare it
-        logger.warning("Bootstrapping image: %s", self._image_alias)
-        image_source = {
-            'type': 'image',
-        }
-        if isinstance(self._parent, Container):
-            # We have NSFarm image to base on
-            self._parent.prepare_image()
-            image_source["alias"] = self._parent._image_alias
-        else:
-            # We have to pull it from images
-            image_source["mode"] = "pull"
-            image_source["server"] = self._lxd.IMAGES_SOURCE
-            image_source["alias"] = self._parent.fingerprint
-        container_name = f"nsfarm-bootstrap-{self._name}-{self._hash}"
-        try:
-            container = self._lxd.local.containers.create({
-                'name': container_name,
-                'profiles': ['nsfarm-root', 'nsfarm-internet'],
-                'source': image_source
-            }, wait=True)
-        except pylxd.exceptions.LXDAPIException as elxd:
-            # TODO found other way to match reason
-            if not str(elxd).endswith("This container already exists"):
-                raise
-            logger.warning("Other instance is already bootsrapping image probably. "
-                            "Waiting for following container to go away: %s", container_name)
-            while self._lxd.local.containers.exists(container_name):
-                time.sleep(1)
-            self.prepare_image()  # possibly get created image or try again
-            return
-        try:
-            # TODO log boostrap process
-            # Copy script and files to container
-            with open(self._file_path) as file:
-                container.files.put(IMAGE_INIT_PATH, file.read(), mode=700)
-            if self._dir_path:
-                container.files.recursive_put(self._dir_path, "/")
-            # Run script to bootstrap image
-            container.start(wait=True)
-            try:
-                res = container.execute([IMAGE_INIT_PATH])
-                if res.exit_code != 0:
-                    # TODO more appropriate exception and possibly use stderr and stdout
-                    raise Exception(f"Image initialization failed: {res}")
-                container.files.delete(IMAGE_INIT_PATH)  # Remove init script
-            finally:
-                container.stop(wait=True)
-            # Create and configure image
-            self._lxd_image = container.publish(wait=True)
-            self._lxd_image.add_alias(self._image_alias, f"NSFarm image: {self._name}")
-        finally:
-            container.delete()
+        self.lxd_container = None
 
     def prepare(self):
         """Create and start container for this object.
         """
-        if self._lxd_container is not None:
+        if self.lxd_container is not None:
             return
-        self.prepare_image()
+        self.image.prepare()
+
         # Collect profiles to be assigned to container
-        profiles = ['nsfarm-root', ]
+        profiles = [self._lxd.ROOT_PROFILE, ]
         if self._internet:
-            profiles.append('nsfarm-internet')
-        # Collect devices to attach
+            profiles.append(self._lxd.INTERNET_PROFILE)
+        # Collect devices to attach to container
         devices = dict()
         for device in self._devices:
             devices.update(device.acquire(self))
+
         # Create and start container
-        self._lxd_container = self._lxd.local.containers.create({
+        self.lxd_container = self._lxd.local.containers.create({
             'name': self._container_name(),
             'ephemeral': True,
             'profiles': profiles,
             'devices': devices,
             'source': {
                 'type': 'image',
-                'alias': self._image_alias,
+                'alias': self.image.alias(),
             },
         }, wait=True)
-        self._lxd_container.start(wait=True)
-        self._logger.debug("Container prepared: %s", self._lxd_container.name)
-        # TODO we could somehow just let it create it and return from this method and wait later on when we realy need
-        # container.
+        self.lxd_container.start(wait=True)
+        logger.debug("Container prepared: %s", self.lxd_container.name)
 
     def _container_name(self, prefix="nsfarm"):
-        name = f"{prefix}-{self._name}-{os.getpid()}"
+        name = f"{prefix}-{self.image_name}-{os.getpid()}"
         if self._lxd.local.containers.exists(name):
             i = 1
             while self._lxd.local.containers.exists(f"{name}-{i}"):
@@ -194,25 +73,25 @@ class Container:
 
         This is intended to be called as a cleanup handler. Please call it when you are removing this container.
         """
-        if self._lxd_container is None:
+        if self.lxd_container is None:
             return  # No cleanup is required
-        self._logger.debug("Removing container: %s", self._lxd_container.name)
+        logger.debug("Removing container: %s", self.lxd_container.name)
         # First freeze and remove devices
-        self._lxd_container.freeze(wait=True)
-        self._lxd_container.devices = dict()
-        self._lxd_container.save()
+        self.lxd_container.freeze(wait=True)
+        self.lxd_container.devices = dict()
+        self.lxd_container.save()
         for device in self._devices:
             device.release(self)
         # Now stop container (Note: container is ephemeral so it is removed automatically after stop)
-        self._lxd_container.stop()
-        self._lxd_container = None
+        self.lxd_container.stop()
+        self.lxd_container = None
 
     def pexpect(self, command=("/bin/sh",)):
         """Returns pexpect handle for command running in container.
         """
-        assert self._lxd_container is not None
+        assert self.lxd_container is not None
         self._logger.debug("Running command: %s", command)
-        pexp = pexpect.spawn('lxc', ["exec", self._lxd_container.name] + list(command))
+        pexp = pexpect.spawn('lxc', ["exec", self.lxd_container.name] + list(command))
         pexp.logfile_read = cli.PexpectLogging(logging.getLogger(self._logger.name + str(command)))
         return pexp
 
@@ -222,43 +101,23 @@ class Container:
 
     def __exit__(self, etype, value, traceback):
         self.cleanup()
-    
-    @property
-    def image_name(self):
-        """Name of NSFarm image this container is initialized for.
-        """
-        return self._name
 
     @property
-    def name(self):
+    def name(self) -> typing.Union[str, None]:
         """Name of container if prepared, otherwise None.
         """
-        if self._lxd_container is None:
+        if self.lxd_container is None:
             return None
-        return self._lxd_container.name
+        return self.lxd_container.name
 
     @property
-    def internet(self):
+    def internet(self) -> bool:
         """If host internet connection should be accessible in this instance.
         """
         return self._internet
 
     @property
-    def devices(self):
+    def devices(self) -> typing.Tuple[Device]:
         """List of passed devices from host.
         """
-        return self._devices
-
-    @property
-    def hash(self):
-        """Identifying Hash of container.
-
-        This is unique identifier generated from container sources and used to check if image can be reused or not.
-        """
-        return self._hash
-
-    @property
-    def image_alias(self):
-        """Alias of image for this container.
-        """
-        return self._image_alias
+        return tuple(self._devices)
diff --git a/nsfarm/lxd/image.py b/nsfarm/lxd/image.py
new file mode 100644
index 0000000000000000000000000000000000000000..c87866eeaf3bc7267c9b02b17b471b550de598b3
--- /dev/null
+++ b/nsfarm/lxd/image.py
@@ -0,0 +1,165 @@
+"""Images management.
+"""
+import time
+import itertools
+import functools
+import pathlib
+import hashlib
+import logging
+import pylxd
+from .connection import LXDConnection
+
+logger = logging.getLogger(__package__)
+
+
+class Image:
+    """Generic Image handle.
+    """
+    IMAGE_INIT_PATH = "/nsfarm-init.sh"  # Where we deploy initialization script for image
+    IMGS_DIR = pathlib.Path(__file__).parents[2] / "imgs"
+
+    def __init__(self, lxd_connection: LXDConnection, img_name: str):
+        self.name = img_name
+        self._lxd = lxd_connection
+        self._dir_path = self.IMGS_DIR / img_name
+        self._file_path = self._dir_path.with_suffix(self._dir_path.suffix + ".sh")
+
+        self.lxd_image = None
+
+        # Verify existence of image definition
+        if not self._file_path.is_file():
+            # TODO better exception object
+            raise Exception(f"There seems to be no file describing image: {self._file_path}")
+        if not self._dir_path.is_dir():
+            self._dir_path = None
+
+        # Get parent
+        with open(self._file_path) as file:
+            # This reads second line of file while initial hash removed
+            parent = next(itertools.islice(file, 1, 2))[1:].strip()
+        self._parent = None
+        if parent.startswith("nsfarm:"):
+            self._parent = Image(lxd_connection, parent[7:])
+        elif parent.startswith("images:"):
+            self._parent = self._lxd.images.images.get_by_alias(parent[7:])
+        else:
+            # TODO better exception object
+            raise Exception(f"The file has parent from unknown source: {parent}: {self._file_path}")
+
+    @functools.lru_cache(maxsize=1)
+    def hash(self) -> str:
+        """Identifying Hash for latest image.
+
+        This is unique identifier generated from image sources and used to check if image can be reused or not.
+        """
+        md5sum = hashlib.md5()
+        # Parent
+        if isinstance(self._parent, Image):
+            md5sum.update(self._parent.hash().encode())
+        else:
+            md5sum.update(self._parent.fingerprint.encode())
+        # File defining container
+        with open(self._file_path, "rb") as file:
+            md5sum.update(file.read())
+        # Additional nodes from directory
+        if self._dir_path:
+            nodes = [path for path in self._dir_path.iterdir() if path.is_dir()]
+            while nodes:
+                node = nodes.pop()
+                path = self._dir_path / node
+                md5sum.update(str(node).encode())
+                if path.is_dir():
+                    nodes += [path for path in node.iterdir() if path.is_dir()]
+                elif path.is_file():
+                    # For plain file include content
+                    with open(path, "rb") as file:
+                        md5sum.update(file.read())
+                elif path.is_link():
+                    # For link include its target as well
+                    md5sum.update(str(path.resolve()).encode())
+        return md5sum.hexdigest()
+
+    def alias(self, img_hash: str = None) -> str:
+        """Alias for latest image. This is name used to identify image in LXD.
+
+        img_hash: specific hash (not latest one) to generate alias for.
+        """
+        if img_hash is None:
+            img_hash = self.hash()
+        return f"nsfarm/{self.name}/{img_hash}"
+
+    def is_prepared(self, img_hash: str = None) -> bool:
+        """Check if image we need is prepared.
+
+        img_hash: specific hash (not latest one) to be checked.
+        """
+        return self._lxd.local.images.exists(self.alias(img_hash), alias=True)
+
+    def prepare(self):
+        """Prepare image. It creates it if necessary and populates lxd_image attribute.
+        """
+        if self.lxd_image is not None:
+            return
+        if self.is_prepared(self.hash()):
+            self.lxd_image = self._lxd.local.images.get_by_alias(self.alias())
+            return
+
+        logger.warning("Bootstrapping image: %s", self.alias())
+
+        image_source = {
+            'type': 'image',
+        }
+        if isinstance(self._parent, Image):
+            # We have NSFarm image to base on
+            self._parent.prepare()
+            image_source["alias"] = self._parent.alias()
+        else:
+            # We have to pull it from images
+            image_source["mode"] = "pull"
+            image_source["server"] = self._lxd.IMAGES_SOURCE
+            image_source["alias"] = self._parent.fingerprint
+
+        container_name = f"nsfarm-bootstrap-{self.name}-{self.hash()}"
+
+        try:
+            container = self._lxd.local.containers.create({
+                'name': container_name,
+                'profiles': ['nsfarm-root', 'nsfarm-internet'],
+                'source': image_source
+            }, wait=True)
+        except pylxd.exceptions.LXDAPIException as elxd:
+            if not str(elxd).endswith("already exists"):
+                raise
+            logger.warning("Other instance is already bootsrapping image probably. "
+                           "Waiting for following container to go away: %s", container_name)
+            while self._lxd.local.containers.exists(container_name):
+                time.sleep(1)
+            self.prepare()  # possibly get created image or try again
+            return
+
+        try:
+            self._deploy_files(container)
+            self._run_bootstrap(container)
+            # Create and configure image
+            self.lxd_image = container.publish(wait=True)
+            self.lxd_image.add_alias(self.alias(), f"NSFarm image: {self.name}")
+        finally:
+            container.delete()
+
+    def _deploy_files(self, container):
+        with open(self._file_path) as file:
+            container.files.put(self.IMAGE_INIT_PATH, file.read(), mode=700)
+        if self._dir_path:
+            container.files.recursive_put(self._dir_path, "/")
+
+    def _run_bootstrap(self, container):
+        # TODO log boostrap process
+        container.start(wait=True)
+        try:
+            res = container.execute([self.IMAGE_INIT_PATH])
+            if res.exit_code != 0:
+                # TODO more appropriate exception and possibly use stderr and stdout
+                raise Exception(f"Image initialization failed: {res}")
+            container.files.delete(self.IMAGE_INIT_PATH)  # Remove init script
+        finally:
+            container.stop(wait=True)
diff --git a/nsfarm/lxd/utils.py b/nsfarm/lxd/utils.py
index 8f770e7c321f2d482475b21a413b5d469a1b8d9e..0c4b750f9380cee48f5352b4189e32172cf3cb73 100644
--- a/nsfarm/lxd/utils.py
+++ b/nsfarm/lxd/utils.py
@@ -5,7 +5,8 @@ import logging
 from datetime import datetime
 import dateutil.parser
 from .connection import LXDConnection
-from .container import Container, IMGS_DIR
+from .container import Container
+from .image import Image
 
 logger = logging.getLogger(__package__)
 
@@ -42,7 +43,7 @@ def all_images():
 
     This collects all *.sh files in imgs directory in root of nsfarm project.
     """
-    return (imgf[:-3] for imgf in os.listdir(IMGS_DIR) if imgf.endswith(".sh"))
+    return (imgf[:-3] for imgf in os.listdir(Image.IMGS_DIR) if imgf.endswith(".sh"))
 
 
 def bootstrap(imgs=None):
@@ -56,5 +57,5 @@ def bootstrap(imgs=None):
     connection = LXDConnection()
     for img in all_images() if imgs is None else imgs:
         logger.info("Trying to bootstrap: %s", img)
-        Container(connection, img).prepare_image()
+        Image(connection, img).prepare()
     return success