diff --git a/.gitignore b/.gitignore
index 6b2411b4de447d50510d630dcd7e92860816f883..0973438037150f95f1e9c26b726a05eb391fa546 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,3 +14,5 @@ __pycache__/
 # Data
 data*
 
+# DB conf
+images/webapp/db_conf.sh
diff --git a/README.md b/README.md
index c27286cc496cc27b6992174dbc5558a70b3fd1dc..5eb036bd670c671d69ded1c6cb4c942c6481dc75 100755
--- a/README.md
+++ b/README.md
@@ -27,13 +27,53 @@ Run
 
 	$ rosetta/run
 
-Check status
 
-	$ rosetta/ps
+Play
+
+    You can now point your browser to http://localhost:8080
+
+Clean
+
+	# rosetta/clean
+
+### Extras
+
+Check status (not yet fully supported)
+
+    # rosetta/status
+
+
+Run Web App unit tests (with Rosetta running)
+
+    ./run_webapp_unit_tests.sh
 
 
 ### Building errors
 
 It is common for the build process to fail with a "404 not found" error on an apt-get instrucions, as apt repositories often change their IP addresses. In such case, try:
 
-    $ rosetta/build nocache
\ No newline at end of file
+    $ rosetta/build nocache
+
+
+### Development mode
+
+Django development server is running on port 8080 of the "webapp" service.
+
+To enable live code changes, add or comment out the following in docker-compose.yaml under the "volumes" section of the "webapp" service:
+
+    - ./images/webapp/code:/opt/webapp_code
+    
+This will mount the code from images/webapp/code as a volume inside the webapp container itself allowing to make immediately effective codebase edits.
+
+Note that when you edit the Django ORM model, you need to rerun the migrate the database, either by just rerunning the webapp service:
+
+    $ rosetta/rerun webapp
+
+..ora by entering in the webapp service container and manually migrate:
+
+    $ rosetta/shell webapp
+    $ source /env.sh
+    $ source /db_conf.sh
+    $ cd /opt/webapp_code
+    $ python3 manage.py makemigrations
+    $ python3 manage.py migrate  
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 474fb751ac908b6fc612bb03928b0651e47233ac..b8b5435840b5d90936bf9750f3fb28a5ff6b57cb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,3 +33,26 @@ services:
       - ./data/dregistry:/var/lib/registry
     ports:
       - "5000:5000"
+
+  webapp:
+    image: "rosetta/webapp"
+    container_name: webapp
+    hostname: webapp
+    environment:
+      - SAFEMODE=False
+      - DJANGO_LOG_LEVEL=CRITICAL
+      - ROSETTA_LOG_LEVEL=DEBUG 
+    ports:
+      - "8080:8080"
+    volumes:
+      - ./data_rosetta/webapp/data:/data
+      - ./data_rosetta/webapp/log:/var/log/webapp
+      #- ./images/webapp/code:/opt/webapp_code
+
+
+
+
+
+
+
+
diff --git a/images/webapp/.gitignore b/images/webapp/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..d007c6ac92e4d87d7e0d456d52572eac7d5b0b2c
--- /dev/null
+++ b/images/webapp/.gitignore
@@ -0,0 +1,129 @@
+# From https://github.com/github/gitignore/blob/master/Python.gitignore
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don’t work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+virtual/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# 
+.docker_bash_history
diff --git a/images/webapp/Dockerfile b/images/webapp/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..b8cb025cc103805b7cca8de6fead32b6ab662795
--- /dev/null
+++ b/images/webapp/Dockerfile
@@ -0,0 +1,90 @@
+FROM rosetta/base
+MAINTAINER Stefano Alberto Russo <stefano.russo@gmail.com>
+
+# Always start with an apt-get update when extending base images,
+# otherwise apt repositories might get outdated (404 not found)
+# and building without cache does not re-build base images.
+RUN apt-get update
+
+#------------------------------
+# Apt requirements
+#------------------------------
+
+# Install Curl
+RUN apt-get install curl -y
+
+# Download get-pip script
+RUN curl -O https://bootstrap.pypa.io/get-pip.py
+
+# Install Python3 and Pip3 (python3-distutils required for pip3)
+RUN apt-get install python3 python3-distutils -y 
+
+# Install Python and pip in this order (first Python 3 and then Python 2), or 
+# you will end ap with python defaulting to python2 and pip defaulting to pip3
+# Otherwise, do somethign like "ln -s /usr/local/bin/pip3 /usr/local/bin/pip"
+
+# Install Python3 and Pip3 (ython3-distutils required for pip3)
+RUN apt-get install python3 python3-distutils -y 
+RUN python3 get-pip.py 'pip==10.0.1'
+
+# Install Python2 and Pip2
+RUN apt-get install python -y
+RUN python get-pip.py 'pip==10.0.1'
+
+# Python 3 dev (for pycrypto)
+RUN apt-get install python3-dev -y
+
+# Install postgres driver required for psycopg2
+RUN apt-get install libpq-dev -y
+
+
+#------------------------------
+# Install Django project
+#------------------------------
+
+# Prepare dir
+RUN mkdir /opt/webapp_code
+
+# Install Python requirements..
+COPY requirements.txt /tmp/
+RUN cd /opt/webapp_code && pip3 install -r /tmp/requirements.txt
+
+# Patch Django 2.2 non-ascii chars in /usr/local/lib/python3.6/dist-packages/django/views/templates/technical_500.html
+RUN sed -i 's/[\x80-\xFF]/./g' /usr/local/lib/python3.6/dist-packages/django/views/templates/technical_500.html
+
+# Install App code
+COPY code /opt/webapp_code
+
+# Fix permissions
+RUN chown -R rosetta:rosetta /opt/webapp_code
+
+# Copy db conf
+COPY db_conf.sh /db_conf.sh
+
+# Prepare for logs
+RUN mkdir /var/log/webapp/ && chown rosetta:rosetta /var/log/webapp/
+
+
+#------------------------------
+# Supervisord
+#------------------------------
+
+COPY run_webapp.sh /etc/supervisor/conf.d/
+RUN chmod 755 /etc/supervisor/conf.d/run_webapp.sh
+COPY supervisord_webapp.conf /etc/supervisor/conf.d/
+
+
+#------------------------------
+# Prestartup
+#------------------------------
+
+COPY prestartup_webapp.sh /prestartup/
+
+
+
+
+
+
+
+
+
diff --git a/images/webapp/code/manage.py b/images/webapp/code/manage.py
new file mode 100755
index 0000000000000000000000000000000000000000..ddb292f3784b73c511921d727b8665ba6277b522
--- /dev/null
+++ b/images/webapp/code/manage.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rosetta.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/images/webapp/code/migrations_vanilla/README.txt b/images/webapp/code/migrations_vanilla/README.txt
new file mode 100644
index 0000000000000000000000000000000000000000..dd641869d4ab930e2c8585f5a412fa96d618a5bf
--- /dev/null
+++ b/images/webapp/code/migrations_vanilla/README.txt
@@ -0,0 +1,3 @@
+
+This is a folder with just an __init__.py file, used to initialize the migrations.
+
diff --git a/images/webapp/code/migrations_vanilla/__init__.py b/images/webapp/code/migrations_vanilla/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/images/webapp/code/rosetta/__init__.py b/images/webapp/code/rosetta/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/images/webapp/code/rosetta/base_app/__init__.py b/images/webapp/code/rosetta/base_app/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/images/webapp/code/rosetta/base_app/api.py b/images/webapp/code/rosetta/base_app/api.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2b785867aa45a1a6b7ec6269cf7bfbdfc566568
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/api.py
@@ -0,0 +1,220 @@
+import logging
+ 
+# Django imports
+from django.http import HttpResponse
+from django.utils import timezone
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.models import User, Group
+
+from rest_framework.response import Response
+from rest_framework import status, serializers, viewsets
+from rest_framework.views import APIView
+
+ 
+# Project imports
+from rosetta.common import format_exception
+from rosetta.base_app.models import Profile
+ 
+# Setup logging
+logger = logging.getLogger(__name__)
+
+
+#==============================
+#  Common returns
+#==============================
+ 
+# Ok (with data)
+def ok200(data=None):
+    return Response({"results": data}, status=status.HTTP_200_OK)
+ 
+# Error 400
+def error400(data=None):
+    return Response({"detail": data}, status=status.HTTP_400_BAD_REQUEST)
+ 
+# Error 401
+def error401(data=None):
+    return Response({"detail": data}, status=status.HTTP_401_UNAUTHORIZED)
+ 
+# Error 404
+def error404(data=None):
+    return Response({"detail": data}, status=status.HTTP_404_NOT_FOUND)
+ 
+# Error 500
+def error500(data=None):
+    return Response({"detail": data}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+
+ 
+#==============================
+#  Authentication helper
+#==============================
+ 
+def rosetta_authenticate(request):
+
+    # Get data
+    user      = request.user if request.user.is_authenticated else None
+    username  = request.data.get('username', None)
+    password  = request.data.get('password', None)
+    authtoken = request.data.get('authtoken', None)
+
+    # Try standard user authentication
+    if user:
+        return user
+
+    # Try username/password  authentication
+    elif username or password:
+        
+        # Check we got both
+        if not username:
+            return error400('Got empty username')
+        if not password:
+            return error400('Got empty password')
+ 
+        # Authenticate
+        user = authenticate(username=username, password=password)
+        if not user:
+            return error401('Wrong username/password')  
+        else:
+            login(request, user)
+            return user
+
+    # Try auth toekn authentication 
+    elif authtoken:
+        try:
+            profile = Profile.objects.get(authtoken=authtoken)
+        except Profile.DoesNotExist:
+            return error400('Wrong auth token')
+        login(request, profile.user)
+        return profile.user
+    else:
+        return error401('This is a private API. Login or provide username/password or auth token')
+
+
+
+#==============================
+#  Base public API class
+#==============================
+ 
+class PublicPOSTAPI(APIView):
+    '''Base public POST API class'''
+ 
+    # POST
+    def post(self, request):
+        try:
+            return self._post(request)
+        except Exception as e:
+            logger.error(format_exception(e))
+            return error500('Got error in processing request: {}'.format(e))
+ 
+class PublicGETAPI(APIView):
+    '''Base public GET API class''' 
+    # GET
+    def get(self, request):
+        try:
+            return self._get(request)
+        except Exception as e:
+            logger.error(format_exception(e))
+            return error500('Got error in processing request: {}'.format(e))
+
+
+
+#==============================
+#  Base private API class
+#==============================
+ 
+class PrivatePOSTAPI(APIView):
+    '''Base private POST API class'''
+ 
+    # POST
+    def post(self, request):
+        try:
+            # Authenticate using rosetta authentication
+            response = rosetta_authenticate(request)
+             
+            # If we got a response return it, otherwise set it as the user.
+            if isinstance(response, Response):
+                return response
+            else:
+                self.user = response
+             
+            # Call API logic
+            return self._post(request)
+        except Exception as e:
+            logger.error(format_exception(e))
+            return error500('Got error in processing request: {}'.format(e))
+ 
+class PrivateGETAPI(APIView):
+    '''Base private GET API class'''
+
+    # GET  
+    def get(self, request):
+        try:
+            # Authenticate using rosetta authentication
+            response = rosetta_authenticate(request)
+             
+            # If we got a response return it, otherwise set it as the user.
+            if isinstance(response, Response):
+                return response
+            else:
+                self.user = response
+             
+            # Call API logic
+            return self._get(request)
+        except Exception as e:
+            logger.error(format_exception(e))
+            return error500('Got error in processing request: {}'.format(e))
+
+
+
+#==============================
+#  User & profile APIs
+#==============================
+
+class login_api(PrivateGETAPI, PrivatePOSTAPI):
+    """
+    get:
+    Returns the auth token.
+
+    post:
+    Authorize and returns the auth token.
+    """
+         
+    def _post(self, request):
+        return ok200({'authtoken': self.user.profile.authtoken})
+
+    def _get(self, request):
+        return ok200({'authtoken': self.user.profile.authtoken}) 
+ 
+ 
+class logout_api(PrivateGETAPI):
+    """
+    get:
+    Logout the user
+    """
+         
+    def _get(self, request):
+        logout(request)
+        return ok200()
+
+
+class UserViewSet(viewsets.ModelViewSet):
+    """
+    API endpoint that allows Users to be viewed or edited.
+    """
+
+    class UserSerializer(serializers.HyperlinkedModelSerializer):
+        class Meta:
+            model = User
+            fields = ('url', 'username', 'email', 'groups')
+
+    queryset = User.objects.all().order_by('-date_joined')    
+    serializer_class = UserSerializer
+
+
+
+
+
+
+
+
+
diff --git a/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py
new file mode 100644
index 0000000000000000000000000000000000000000..02666637130396d35347bc9c9da8f38b92144841
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/management/commands/base_app_populate.py
@@ -0,0 +1,25 @@
+from django.core.management.base import BaseCommand
+from django.contrib.auth.models import User
+from ...models import Profile
+
+class Command(BaseCommand):
+    help = 'Adds the admin superuser with \'a\' password.'
+
+    def handle(self, *args, **options):
+
+        try:
+            User.objects.get(username='admin')
+            print('Not creating admin user as it already exist')
+        except User.DoesNotExist:
+            print('Creating admin user with default password')
+            admin = User.objects.create_superuser('admin', 'admin@example.com', 'a')
+            Profile.objects.create(user=admin)
+
+        try:
+            User.objects.get(username='testuser')
+            print('Not creating test user as it already exist')
+        except User.DoesNotExist:
+            print('Creating test user with default password')
+            testuser = User.objects.create_user('testuser', 'testuser@rosetta.platform', 'testpass')
+            Profile.objects.create(user=testuser, authtoken='129aac94-284a-4476-953c-ffa4349b4a50')
+            
\ No newline at end of file
diff --git a/images/webapp/code/rosetta/base_app/migrations b/images/webapp/code/rosetta/base_app/migrations
new file mode 120000
index 0000000000000000000000000000000000000000..34d39117ff8ff780d1513412adb3dd1d68df3d65
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/migrations
@@ -0,0 +1 @@
+/data/migrations/base_app
\ No newline at end of file
diff --git a/images/webapp/code/rosetta/base_app/models.py b/images/webapp/code/rosetta/base_app/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..4cb132d3336faf5a7d76a9a36758f83d340b1114
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/models.py
@@ -0,0 +1,31 @@
+import uuid
+from django.db import models
+from django.contrib.auth.models import User
+
+
+#=========================
+#  Profile 
+#=========================
+
+class Profile(models.Model):
+    user      = models.OneToOneField(User, on_delete=models.CASCADE)
+    timezone  = models.CharField('User Timezone', max_length=36, default='UTC')
+    authtoken = models.CharField('User auth token', max_length=36, blank=True, null=True)
+
+    def save(self, *args, **kwargs):
+        if not self.authtoken:
+            self.authtoken = str(uuid.uuid4())
+        super(Profile, self).save(*args, **kwargs)
+
+    def __unicode__(self):
+        return str('Profile of user "{}"'.format(self.user.username))
+
+
+
+
+
+
+
+
+
+
diff --git a/images/webapp/code/rosetta/base_app/templates/login.html b/images/webapp/code/rosetta/base_app/templates/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..ec1d91f66babd6c46a26053ab6c8521200d472e8
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/templates/login.html
@@ -0,0 +1,30 @@
+<html>
+    <head>
+        <title>Rosetta WebApp</title>
+    </head>
+    <body>
+        <div style="text-align:center; margin-top:50px">
+            {% if data.error %}
+            <div style="color:red">
+              {{ data.error }}
+            </div>
+            {% elif data.success %}
+            <div style="color:green">
+              {{data.success}}
+            </div>
+            {% else %}            
+            <br/>
+            {% endif %} 
+            
+            <form class="form-signin" role="form" action='/login/' method='POST'>
+            {% csrf_token %}
+            <input style="max-width:300px; margin: 5px auto" type="username" class="form-control" placeholder="Username" name='username' required autofocus>
+            <input style="max-width:300px; margin: 5px auto" type="password" class="form-control" placeholder="Password" name='password'>
+            <input style="width:100px; height:35px; margin: 5px auto; font-size:16px" type='submit' class="btn btn-success" value='Log in' />
+            </form>
+        </div>
+    </body>
+    
+
+</html>
+
diff --git a/images/webapp/code/rosetta/base_app/templates/main.html b/images/webapp/code/rosetta/base_app/templates/main.html
new file mode 100644
index 0000000000000000000000000000000000000000..e679084dabfad41d0874a128a0ca6dbce7214e8b
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/templates/main.html
@@ -0,0 +1,51 @@
+<html>
+    <head>
+        <title>Rosetta WebApp</title>
+    </head>
+    <body>
+        <div style="text-align:center; margin-top:50px">
+            <h1>Rosetta WebApp</h1>
+            {% if data.error %}
+            <div style="color:red">
+              {{ data.error }}
+            </div>
+            {% elif data.success %}
+            <div style="color:green">
+              {{data.success}}
+            </div>
+            {% else %}            
+            <br/>
+            {% endif %} 
+            
+            {% if request.user.is_authenticated %}
+            Logged in as <b>{{ request.user.username }}</b> | <a href="/logout">Logout</a>
+            {% else %}
+            <form class="form-signin" role="form" action='/login/' method='POST'>
+            {% csrf_token %}
+            <input style="max-width:300px; margin: 5px auto" type="username" class="form-control" placeholder="Username" name='username' required autofocus>
+            <input style="max-width:300px; margin: 5px auto" type="password" class="form-control" placeholder="Password" name='password'>
+            <input style="width:100px; height:35px; margin: 5px auto; font-size:16px" type='submit' class="btn btn-success" value='Log in' />
+            </form>
+            {% endif %}
+        </div>
+
+        <div style="max-width:500px; margin: 30px auto">
+            Modules:
+            <ul style="margin-top:5px; margin-bottom:20px">
+                <li><a href="/admin">Admin</a></li>
+                <li><a href="/api/v1/doc">Swagger APIs v1 Doc</a></li>
+            </ul>
+
+            REST APIs v1:
+            <ul style="margin-top:5px">
+                <li><a href="/api/v1/base/login/">/api/v1/base/login/</a></li>
+                <li><a href="/api/v1/base/logout/">/api/v1/base/logout/</a></li>
+            </ul>
+            
+        </div>
+        
+    </body>
+    
+
+</html>
+
diff --git a/images/webapp/code/rosetta/base_app/tests/__init__.py b/images/webapp/code/rosetta/base_app/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/images/webapp/code/rosetta/base_app/tests/common.py b/images/webapp/code/rosetta/base_app/tests/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..75a1a7ee302c96dd1a3b1d379834a0383bdcf0d3
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/tests/common.py
@@ -0,0 +1,93 @@
+import json
+from django.test import TestCase
+from rest_framework.test import APIClient as Client
+from django.test.client import MULTIPART_CONTENT
+from rest_framework import status
+from rest_framework.reverse import reverse
+from django.contrib.auth.models import User
+
+class APIClient(Client):
+    # Add patch to test client object
+    def patch(self, path, data='', content_type=MULTIPART_CONTENT, follow=False, **extra):
+        return self.generic('PATCH', path, data, content_type, **extra)
+
+    # Add options to test client object
+    def options(self, path, data='', content_type=MULTIPART_CONTENT, follow=False, **extra):
+        return self.generic('OPTIONS', path, data, content_type, **extra)
+
+
+class BaseAPITestCase(TestCase):
+
+    def __init__(self, *args, **kwargs):
+        self.maxDiff = None
+        super(TestCase, self).__init__(*args, **kwargs)
+
+    def send_request(self, request_method, *args, **kwargs):
+
+        request_func = getattr(self.client, request_method)
+        status_code  = None
+       
+        if 'multipart' in kwargs and kwargs['multipart'] is True:
+            # Do nothing, this is a "special", multipart request 
+            pass
+        else:
+            if 'content_type' not in kwargs and request_method != 'get':
+                kwargs['content_type'] = 'application/json'
+            
+            if 'data' in kwargs and request_method != 'get' and kwargs['content_type'] == 'application/json':
+                data = kwargs.get('data', '')
+                kwargs['data'] = json.dumps(data)
+
+        if 'status_code' in kwargs:
+            status_code = kwargs.pop('status_code')
+
+        self.response = request_func(*args, **kwargs)
+
+        # Parse response
+        is_json = False
+
+        if 'content-type' in self.response._headers:
+            is_json = bool(filter(lambda x: 'json' in x, self.response._headers['content-type']))
+
+        try:
+            if is_json and self.response.content:
+                self.response.content_dict = json.loads(self.response.content)
+            else:
+                self.response.content_dict = {}
+            
+        except:
+            self.response.content_dict = {}
+        
+        if status_code:
+            if not self.response.status_code == status_code:
+                raise Exception('Error with response:' + str(self.response))
+        return self.response
+
+    def post(self, *args, **kwargs):
+        return self.send_request('post', *args, **kwargs)
+
+    def get(self, *args, **kwargs):
+        return self.send_request('get', *args, **kwargs)
+
+    def put(self, *args, **kwargs):
+        return self.send_request('put', *args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        return self.send_request('delete', *args, **kwargs)
+
+    def patch(self, *args, **kwargs):
+        return self.send_request('patch', *args, **kwargs)
+
+    def init(self):
+        self.client = APIClient()
+
+    def assertRedirects(self, *args, **kwargs):
+        super(BaseAPITestCase, self).assertRedirects(*args, **kwargs)
+
+
+
+
+
+
+
+
diff --git a/images/webapp/code/rosetta/base_app/tests/test_apis.py b/images/webapp/code/rosetta/base_app/tests/test_apis.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ef1ed7aedb7ee3dc830e4aa3e326710dbf77832
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/tests/test_apis.py
@@ -0,0 +1,49 @@
+import json
+
+from django.contrib.auth.models import User
+        
+from .common import BaseAPITestCase
+from ..models import Profile
+
+class ApiTests(BaseAPITestCase):
+
+    def setUp(self):
+        
+        # Create test users
+        self.user = User.objects.create_user('testuser', password='testpass')
+        self.anotheruser = User.objects.create_user('anotheruser', password='anotherpass')
+
+        # Create test profile
+        Profile.objects.create(user=self.user, authtoken='ync719tce917tec197t29cn712eg')
+
+
+    def test_api_web_auth(self):
+        '''Test auth using login api''' 
+         
+        # No user at all
+        resp = self.post('/api/v1/base/login/', data={})
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(json.loads(resp.content), {"detail": "This is a private API. Login or provide username/password or auth token"})
+
+        # Wrong user
+        resp = self.post('/api/v1/base/login/', data={'username':'wronguser', 'password':'testpass'})
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(json.loads(resp.content), {"detail": "Wrong username/password"})
+
+        # Wrong pass
+        resp = self.post('/api/v1/base/login/', data={'username':'testuser', 'password':'wrongpass'})
+        self.assertEqual(resp.status_code, 401)
+        self.assertEqual(json.loads(resp.content), {"detail": "Wrong username/password"})
+
+        # Correct user
+        resp = self.post('/api/v1/base/login/', data={'username': 'testuser', 'password':'testpass'})
+        self.assertEqual(resp.status_code, 200)
+
+
+
+
+
+
+
+
+        
\ No newline at end of file
diff --git a/images/webapp/code/rosetta/base_app/views.py b/images/webapp/code/rosetta/base_app/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b8ec33504a96dbccc248f087cbe31bc5f460c94
--- /dev/null
+++ b/images/webapp/code/rosetta/base_app/views.py
@@ -0,0 +1,55 @@
+from django.shortcuts import render
+from django.http import HttpResponseRedirect
+from django.contrib.auth import authenticate, login, logout
+
+# Setup logging
+import logging
+logger = logging.getLogger(__name__)
+
+class ErrorMessage(Exception):
+    pass
+
+
+def main_view(request):
+    return render(request, 'main.html')
+ 
+
+def login_view(request):
+
+    data={}
+
+    if request.method == 'POST':
+        username = request.POST.get('username')
+        password = request.POST.get('password')
+        
+        if (not username) or (not password):
+            data['error'] = 'Empty username or password'
+        
+        if request.user.is_authenticated:
+            logout(request)
+            
+        user = authenticate(username=username, password=password)
+        if user:
+            login(request, user)
+            return HttpResponseRedirect('/')
+        else:
+            data['error'] = 'Check username and password'
+
+    
+    # Render the login page again with no other data than title
+    return render(request, 'login.html', {'data': data})
+
+
+
+def logout_view(request):
+    logout(request)
+    return HttpResponseRedirect('/')
+
+
+
+
+
+
+
+
+
diff --git a/images/webapp/code/rosetta/common.py b/images/webapp/code/rosetta/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..418fd24eef319bf745c857b31c1701db79d05246
--- /dev/null
+++ b/images/webapp/code/rosetta/common.py
@@ -0,0 +1,196 @@
+import pytz
+import time
+import calendar
+import logging
+from datetime import datetime
+import traceback
+from rest_framework import serializers
+
+try:
+    from dateutil.tz import tzoffset
+except ImportError:
+    tzoffset = None
+    
+class ConsistencyException(Exception):
+    pass
+
+class AlreadyExistentException(Exception):
+    pass
+
+class DoNotCommitTransactionException(Exception):
+    pass
+
+def format_exception(e):
+    return 'Exception: ' + str(e) + '; Traceback: ' + traceback.format_exc().replace('\n','|')
+
+class HyperlinkedModelSerializerWithId(serializers.HyperlinkedModelSerializer):
+    """Extend the HyperlinkedModelSerializer to add IDs as well for the best of
+    both worlds.
+    """
+    id = serializers.ReadOnlyField()
+
+
+# def setup_logger(logger, loglevel):
+#     handler = logging.StreamHandler()
+#     formatter = logging.Formatter('%(name)s - %(levelname)s: %(message)s')
+#     handler.setFormatter(formatter)
+#     logger.addHandler(handler)
+#     logger.setLevel(loglevel)
+#     return logger
+
+
+#===================================
+#  Time management
+#===================================
+
+# Note: most of the following routines are extrapolated from the
+# time package of the Luna project (https://github.com/sarusso/Luna)
+# by courtesy of Stefano Alberto Russo. If you find and fix any bug,
+# please open a pull request with the fix for Luna as well. Thank you!
+
+def timezonize(timezone):
+    if not 'pytz' in str(type(timezone)):
+        timezone = pytz.timezone(timezone)
+    return timezone
+
+def t_now():
+    return time.time()  
+
+
+def dt(*args, **kwargs):
+    '''Initialize a datetime object in the proper way. Using the standard datetime leads to a lot of
+     problems with the tz package. Also, it forces UTC timezone if no timezone is specified'''
+    
+    if 'tz' in kwargs:
+        tzinfo = kwargs.pop('tz')
+    else:
+        tzinfo  = kwargs.pop('tzinfo', None)
+        
+    offset_s  = kwargs.pop('offset_s', None)   
+    trustme   = kwargs.pop('trustme', None)
+    
+    if kwargs:
+        raise Exception('Unhandled arg: "{}".'.format(kwargs))
+        
+    if (tzinfo is None):
+        # Force UTC if None
+        timezone = timezonize('UTC')
+        
+    else:
+        timezone = timezonize(tzinfo)
+    
+    if offset_s:
+        # Special case for the offset
+        if not tzoffset:
+            raise Exception('For ISO date with offset please install dateutil')
+        time_dt = datetime(*args, tzinfo=tzoffset(None, offset_s))
+    else:
+        # Standard  timezone
+        time_dt = timezone.localize(datetime(*args))
+
+    # Check consistency    
+    if not trustme and timezone != pytz.UTC:
+        if not check_dt_consistency(time_dt):
+            raise Exception('Sorry, time {} does not exists on timezone {}'.format(time_dt, timezone))
+
+    return  time_dt
+
+def dt_from_s(timestamp_s, tz=None):
+    if not tz:
+        tz = "UTC"
+    try:
+        timestamp_dt = datetime.utcfromtimestamp(float(timestamp_s))
+    except TypeError:
+        raise Exception('timestamp_s argument must be string or number, got {}'.format(type(timestamp_s)))
+
+    pytz_tz = timezonize(tz)
+    timestamp_dt = timestamp_dt.replace(tzinfo=pytz.utc).astimezone(pytz_tz)
+    
+    return timestamp_dt
+
+def s_from_dt(dt):
+    if not (isinstance(dt, datetime)):
+        raise Exception('s_from_dt function called without datetime argument, got type "{}" instead.'.format(dt.__class__.__name__))
+    microseconds_part = (dt.microsecond/1000000.0) if dt.microsecond else 0
+    return  ( calendar.timegm(dt.utctimetuple()) + microseconds_part)
+
+def check_dt_consistency(date_dt):
+    if date_dt.tzinfo is None:
+        return True
+    else: 
+        if date_dt.utcoffset() != dt_from_s(s_from_dt(date_dt), tz=date_dt.tzinfo).utcoffset():
+            return False
+        else:
+            return True
+
+def dt_from_str(string, timezone=None):
+
+    # Supported formats on UTC
+    # 1) YYYY-MM-DDThh:mm:ssZ
+    # 2) YYYY-MM-DDThh:mm:ss.{u}Z
+
+    # Supported formats with offset    
+    # 3) YYYY-MM-DDThh:mm:ss+ZZ:ZZ
+    # 4) YYYY-MM-DDThh:mm:ss.{u}+ZZ:ZZ
+    
+    # Also:
+    # 5) YYYY-MM-DDThh:mm:ss (without the trailing Z, and assume it on UTC)
+
+    # Split and parse standard part
+    date, time = string.split('T')
+    
+    if time.endswith('Z'):
+        # UTC
+        offset_s = 0
+        time = time[:-1]
+        
+    elif ('+') in time:
+        # Positive offset
+        time, offset = time.split('+')
+        # Set time and extract positive offset
+        if ':' in offset:
+            offset_s = (int(offset[0:2])*60 + int(offset[3:5]))* 60
+        else:
+            offset_s = (int(offset[0:2])*60 + int(offset[2:4]))* 60
+               
+        
+    elif ('-') in time:
+        # Negative offset
+        time, offset = time.split('-')
+        # Set time and extract negative offset
+        if ':' in offset:
+            offset_s = -1 * (int(offset[0:2])*60 + int(offset[3:5]))* 60
+        else:
+            offset_s = -1 * (int(offset[0:2])*60 + int(offset[2:4]))* 60
+    
+    
+    else:
+        # Assume UTC
+        offset_s = 0
+        #raise InputException('Format error')
+    
+    # Handle time
+    hour, minute, second = time.split(':')
+    
+    # Now parse date (easy)
+    year, month, day = date.split('-') 
+
+    # Convert everything to int
+    year    = int(year)
+    month   = int(month)
+    day     = int(day)
+    hour    = int(hour)
+    minute  = int(minute)
+    if '.' in second:
+        usecond = int(second.split('.')[1])
+        second  = int(second.split('.')[0])
+    else:
+        second  = int(second)
+        usecond = 0
+    
+    return dt(year, month, day, hour, minute, second, usecond, offset_s=offset_s)
+
+
+def dt_to_str(dt):
+    '''Return the ISO representation of the datetime as argument'''
+    return dt.isoformat()
diff --git a/images/webapp/code/rosetta/settings.py b/images/webapp/code/rosetta/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..58521f810bcc40d8ed391282e3f997d203bff6a5
--- /dev/null
+++ b/images/webapp/code/rosetta/settings.py
@@ -0,0 +1,211 @@
+"""
+Django settings for rosetta project.
+
+Generated by 'django-admin startproject' using Django 2.2.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = '-3byo^nd6-x82fuj*#68mj=5#qp*gagg58sc($u$r-=g8ujxu4'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = ['*']
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'rosetta.base_app',
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'rest_framework',
+    'rest_framework_swagger',  
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'rosetta.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'rosetta.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+default_db_engine = 'django.db.backends.sqlite3'
+default_db_name   = os.path.join(BASE_DIR, '../rosetta_database.sqlite3')
+
+DATABASES = {
+    'default': {
+        'ENGINE': os.environ.get('DJANGO_DB_ENGINE', default_db_engine),
+        'NAME': os.environ.get('DJANGO_DB_NAME', default_db_name),
+        'USER': os.environ.get('DJANGO_DB_USER', None),
+        'PASSWORD': os.environ.get('DJANGO_DB_PASSWORD', None),
+        'HOST': os.environ.get('DJANGO_DB_HOST', None),
+        'PORT': os.environ.get('DJANGO_DB_PORT',None),
+    }
+}
+
+
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'
+
+
+# REST framework settings
+REST_FRAMEWORK = {
+    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+    'PAGE_SIZE': 100000
+}
+
+# Swagger settings
+# See https://django-rest-swagger.readthedocs.io/en/latest/settings/
+
+SWAGGER_SETTINGS = {
+    'SECURITY_DEFINITIONS': {},
+    'USE_SESSION_AUTH': False
+}
+
+# Data path for resources etc.
+DATA_PATH  = '/data/'
+TMP_PATH   = '/tmp/'
+
+#===============================
+#  Logging
+#===============================
+
+DJANGO_LOG_LEVEL  = os.environ.get('DJANGO_LOG_LEVEL','ERROR')
+ROSETTA_LOG_LEVEL = os.environ.get('ROSETTA_LOG_LEVEL','ERROR')
+
+LOGGING = {
+    'version': 1,
+    'disable_existing_loggers': False,
+ 
+    'formatters': {
+        'verbose': {
+            'format': '%(levelname)s %(asctime)s %(module)s %(process)d '
+                      '%(thread)d %(message)s',
+        },
+        'halfverbose': {
+            'format': '%(asctime)s, %(name)s: [%(levelname)s] - %(message)s',
+            'datefmt': '%m/%d/%Y %I:%M:%S %p'
+        }
+    },
+ 
+    'filters': {
+        'require_debug_false': {
+            '()': 'django.utils.log.RequireDebugFalse'
+        }
+    },
+ 
+    'handlers': {
+        'mail_admins': {
+            'level': 'ERROR',
+            'filters': ['require_debug_false'],
+            'class': 'django.utils.log.AdminEmailHandler'
+        },
+        'console': {
+            'level': 'DEBUG',
+            'class': 'logging.StreamHandler',
+            'formatter': 'halfverbose',
+        },
+    },
+ 
+    'loggers': {
+        'rosetta': {
+            'handlers': ['console'],
+            'level': ROSETTA_LOG_LEVEL,
+            'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. 
+        },
+        'django': {
+            'handlers': ['console'],
+            'level': DJANGO_LOG_LEVEL,
+            'propagate': False, # Do not propagate or the root logger will emit as well, and even at lower levels. 
+        }, 
+        # Read more about the 'django' logger: https://docs.djangoproject.com/en/2.2/topics/logging/#django-logger
+        # Read more about logging in the right way: https://lincolnloop.com/blog/django-logging-right-way/
+    }
+}
+
+
+
diff --git a/images/webapp/code/rosetta/urls.py b/images/webapp/code/rosetta/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..098d5998859a081c3d5df011023526f510fce379
--- /dev/null
+++ b/images/webapp/code/rosetta/urls.py
@@ -0,0 +1,54 @@
+"""rosetta URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import include, path
+from django.conf.urls import url
+import logging
+
+logger = logging.getLogger(__name__)
+
+# Base App
+from rosetta.base_app import api as base_app_api
+from rosetta.base_app import views as base_app_views
+
+# REST Framework & Swagger
+from rest_framework import routers
+from rest_framework.documentation import include_docs_urls
+from rest_framework_swagger.views import get_swagger_view
+
+base_app_api_router = routers.DefaultRouter()
+base_app_api_router.register(r'users', base_app_api.UserViewSet)
+
+urlpatterns = [
+               
+    # Webpages
+    path('', base_app_views.main_view),
+    path('login/', base_app_views.login_view),
+    path('logout/', base_app_views.logout_view),
+
+    # Modules
+    path('admin/', admin.site.urls),
+    path('api/v1/doc/', get_swagger_view(title="Swagger Documentation")),
+    
+    # ViewSet APIs
+    path('api/v1/base/login/', base_app_api.login_api.as_view(), name='login_api'),
+    path('api/v1/base/logout/', base_app_api.logout_api.as_view(), name='logout_api'),
+  
+]
+
+# This message here is quite useful when developing in autoreload mode
+logger.info('Loaded URLs')
+
diff --git a/images/webapp/code/rosetta/wsgi.py b/images/webapp/code/rosetta/wsgi.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c8d40e667ac975d6bdf4f7c9d9a7d4db67130af
--- /dev/null
+++ b/images/webapp/code/rosetta/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for rosetta project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rosetta.settings')
+
+application = get_wsgi_application()
diff --git a/images/webapp/db_conf-dev.sh b/images/webapp/db_conf-dev.sh
new file mode 100644
index 0000000000000000000000000000000000000000..242ede53b4bc3a831241955e9d7491996cc86338
--- /dev/null
+++ b/images/webapp/db_conf-dev.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+# SQlite Django DB conf
+export DJANGO_DB_ENGINE="django.db.backends.sqlite3"
+export DJANGO_DB_NAME="/data/database.sqlite3"
+
+# Postgres Django DB conf
+#export DJANGO_DB_ENGINE="django.db.backends.postgresql_psycopg2"
+#export DJANGO_DB_NAME="rosetta"
+#export DJANGO_DB_USER="rosetta"
+#export DJANGO_DB_PASSWORD=""
+#export DJANGO_DB_HOST="postgres"
+#export DJANGO_DB_PORT=5432
diff --git a/images/webapp/prestartup_webapp.sh b/images/webapp/prestartup_webapp.sh
new file mode 100644
index 0000000000000000000000000000000000000000..befdd9ac85231f6e79973421843ea6c12774b324
--- /dev/null
+++ b/images/webapp/prestartup_webapp.sh
@@ -0,0 +1,49 @@
+#!/bin/bash
+set -e
+
+# Set proper permissions to the log dir
+chown rosetta:rosetta /var/log/webapp
+
+# Create and set proper permissions to the data/resources
+mkdir -p /data/resources 
+chown rosetta:rosetta /data/resources
+
+#-----------------------------
+# Set migrations data folder
+#-----------------------------
+
+if [[ "x$(mount | grep /devmigrations)" == "x" ]] ; then
+    # If the migrations folder is not mounted (not a Docker volume), use the /data directory via links to use data persistency
+    MIGRATIONS_DATA_FOLDER="/data/migrations"
+    # Also if the migrations folder in /data does not exist, create it now
+    mkdir -p /data/migrations
+else
+    # If the migrations folder is mounted (a Docker volume), use it as we are in dev mode
+    MIGRATIONS_DATA_FOLDER="/devmigrations"
+fi
+echo "Persisting migrations in $MIGRATIONS_DATA_FOLDER"
+
+
+#-----------------------------
+# Handle Base App migrations
+#-----------------------------
+
+# Remove potential leftovers
+rm -f /opt/webapp_code/rosetta/base_app/migrations
+if [ ! -d "$MIGRATIONS_DATA_FOLDER/base_app" ] ; then
+    # If migrations were not already initialized, do it now
+    echo "Initializing migrations for base_app"...
+    mkdir $MIGRATIONS_DATA_FOLDER/base_app && chown rosetta:rosetta $MIGRATIONS_DATA_FOLDER/base_app
+    touch $MIGRATIONS_DATA_FOLDER/base_app/__init__.py && chown rosetta:rosetta $MIGRATIONS_DATA_FOLDER/base_app/__init__.py
+fi
+
+# Use the persisted migrations
+ln -s $MIGRATIONS_DATA_FOLDER/base_app /opt/webapp_code/rosetta/base_app/migrations
+
+
+
+
+
+
+
+
diff --git a/images/webapp/requirements.txt b/images/webapp/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3b7edee7b390437c05b57864ff26db11d8372a01
--- /dev/null
+++ b/images/webapp/requirements.txt
@@ -0,0 +1,6 @@
+Django==2.2.1
+psycopg2==2.8
+pytz==2018.9
+djangorestframework==3.9.3
+django-rest-swagger==2.2.0
+dateutils==0.6.6
diff --git a/images/webapp/run_webapp.sh b/images/webapp/run_webapp.sh
new file mode 100644
index 0000000000000000000000000000000000000000..2e5677800874c489163a5b1bef71ac6ba354e4a0
--- /dev/null
+++ b/images/webapp/run_webapp.sh
@@ -0,0 +1,88 @@
+#!/bin/bash
+
+DATE=$(date)
+
+echo ""
+echo "==================================================="
+echo "  Starting Backend @ $DATE"
+echo "==================================================="
+echo ""
+
+echo "1) Loading/sourcing env and settings"
+
+# Load env
+source /env.sh
+
+# Database conf
+source /db_conf.sh
+
+# Django Project conf
+if [[ "x$DJANGO_PROJECT_NAME" == "x" ]] ; then
+    export DJANGO_PROJECT_NAME="Rosetta"
+fi
+
+if [[ "x$DJANGO_PUBLIC_HTTP_HOST" == "x" ]] ; then
+    export DJANGO_PUBLIC_HTTP_HOST="https://rosetta.platform"
+fi
+
+if [[ "x$DJANGO_EMAIL_SERVICE" == "x" ]] ; then
+    export DJANGO_EMAIL_SERVICE="Sendgrid"
+fi
+
+if [[ "x$DJANGO_EMAIL_FROM" == "x" ]] ; then
+    export DJANGO_EMAIL_FROM="Rosetta <rosetta@rosetta.platform>"
+fi
+
+if [[ "x$DJANGO_EMAIL_APIKEY" == "x" ]] ; then
+    export DJANGO_EMAIL_APIKEY=""
+fi
+
+# Set log levels
+export DJANGO_LOG_LEVEL="CRITICAL"
+export ROSETTA_LOG_LEVEL="CRITICAL"
+
+# Stay quiet on Python warnings
+export PYTHONWARNINGS=ignore
+
+# To Python3 (unbuffered). P.s. "python3 -u" does not work..
+export DJANGO_PYTHON=python3
+export PYTHONUNBUFFERED=on
+
+# Check if there is something to migrate or populate
+echo ""
+echo "2) Making migrations..."
+cd /opt/webapp_code && $DJANGO_PYTHON manage.py makemigrations --noinput
+EXIT_CODE=$?
+echo "Exit code: $EXIT_CODE"
+if [[ "x$EXIT_CODE" != "x0" ]] ; then
+    echo "This exit code is an error, sleeping 5s and exiting." 
+    sleep 5
+    exit $?
+fi
+echo ""
+
+echo "3) Migrating..."
+cd /opt/webapp_code && $DJANGO_PYTHON manage.py migrate --noinput
+EXIT_CODE=$?
+echo "Exit code: $EXIT_CODE"
+if [[ "x$EXIT_CODE" != "x0" ]] ; then
+    echo "This exit code is an error, sleeping 5s and exiting." 
+    sleep 5
+    exit $?
+fi
+echo ""
+
+echo "4) Populating base app..."
+cd /opt/webapp_code && $DJANGO_PYTHON manage.py base_app_populate  
+EXIT_CODE=$?
+echo "Exit code: $EXIT_CODE"
+if [[ "x$EXIT_CODE" != "x0" ]] ; then
+    echo "This exit code is an error, sleeping 5s and exiting." 
+    sleep 5
+    exit $?
+fi
+echo ""
+
+# Run the (development) server
+echo "6) Now starting the server and logging in /var/log/cloud_server.log."
+exec $DJANGO_PYTHON manage.py runserver 0.0.0.0:8080 2>> /var/log/webapp/server.log
diff --git a/images/webapp/supervisord_webapp.conf b/images/webapp/supervisord_webapp.conf
new file mode 100644
index 0000000000000000000000000000000000000000..387a55dc15babcb266d2b30124bccdb125d5434f
--- /dev/null
+++ b/images/webapp/supervisord_webapp.conf
@@ -0,0 +1,17 @@
+[program:webapp]
+
+; Process definition
+process_name = webapp
+command      = /etc/supervisor/conf.d/run_webapp.sh
+autostart    = true
+autorestart  = true
+startsecs    = 5
+stopwaitsecs = 10
+user         = rosetta
+environment  =HOME=/rosetta
+
+; Log files
+stdout_logfile          = /var/log/webapp/startup.log
+stdout_logfile_maxbytes = 100MB
+stdout_logfile_backups  = 100
+redirect_stderr         = true
diff --git a/rosetta/build b/rosetta/build
index 347a2c139050aafe99cd531b4674f7876289e4b6..ab6de174d7048664c0453054b496a338c6642c38 100755
--- a/rosetta/build
+++ b/rosetta/build
@@ -35,7 +35,9 @@ if [[ "x$SERVICE" == "x" ]] ; then
     $BUILD_COMMAND images/slurmclustermaster -t rosetta/slurmclustermaster    
     $BUILD_COMMAND images/slurmclusterworker -t rosetta/slurmclusterworker    
     $BUILD_COMMAND images/dregistry -t rosetta/dregistry
-  
+    $BUILD_COMMAND images/webapp -t rosetta/webapp
+
+    
 else
 
     # Build a specific image
diff --git a/rosetta/setup b/rosetta/setup
new file mode 100755
index 0000000000000000000000000000000000000000..13cd1389375cbc2d844f4c87113bd20f1828e28c
--- /dev/null
+++ b/rosetta/setup
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Use dev (local) database for backend
+if [ ! -f images/webapp/db_conf.sh ]; then
+    echo "Using dev webapp database settings."
+    cp images/webapp/db_conf-dev.sh  images/webapp/db_conf.sh
+else
+    echo "Not using dev webapp database settings as settings are already present."
+fi
diff --git a/run_webapp_unit_tests.sh b/run_webapp_unit_tests.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b6f78879e7c33d26897f41981332fa90ed17b4ed
--- /dev/null
+++ b/run_webapp_unit_tests.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+# Example: ./run_webapp_unit_tests.sh rosetta.base_app.tests.test_apis.ApiTests.test_api_web_auth
+
+# You probably want to set DJANGO_LOG_LEVEL to ERROR and ROSETTA_LOG_LEVEL to DEBUG if you are doing tdd.
+DJANGO_LOG_LEVEL="CRITICAL"
+ROSETTA_LOG_LEVEL="CRITICAL"
+
+rosetta/shell webapp "cd /opt/webapp_code && DJANGO_LOG_LEVEL=$DJANGO_LOG_LEVEL ROSETTA_LOG_LEVEL=$ROSETTA_LOG_LEVEL python3 manage.py test $@"
diff --git a/view_webapp_server_log.sh b/view_webapp_server_log.sh
new file mode 100755
index 0000000000000000000000000000000000000000..0572279206088fc14d6077ea262ddbd5a109d0ac
--- /dev/null
+++ b/view_webapp_server_log.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+tail -f -n 1000 data_rosetta/webapp/log/server.log
diff --git a/view_webapp_startup_log.sh b/view_webapp_startup_log.sh
new file mode 100755
index 0000000000000000000000000000000000000000..5e57e64cb4a9dbd06874283765aedf6813259e58
--- /dev/null
+++ b/view_webapp_startup_log.sh
@@ -0,0 +1,2 @@
+#!/bin/bash
+tail -f -n 1000 data_rosetta/webapp/log/startup.log