diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 241d7579edecffbc2434ced41aad3dc717d40eec..b5ef16b90fcd98111388488637a76867c54a3af5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -6,7 +6,7 @@ stages:
   - staging
 
 variables:
-  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
+  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-$CI_COMMIT_SHA
 
 
 # ========================== Build Testing section =========================== #
@@ -118,6 +118,8 @@ staging:
     on_stop: staging_stop
   script:
     - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
+    - docker-compose stop
+    - docker-compose pull
     - docker-compose up -d --force-recreate
 
 staging_stop:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 053bd6572ce691ad5c3702e86133c104fe057666..7bf469f1f26365df1445dfd8fb87fb528b76cc22 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@
   - id: debug-statements
   - id: flake8
     args:
-    - --exclude=*/migrations/*,docs/*
+    - --exclude=*/migrations/*,docs/*,grady/*
   - id: check-added-large-files
   - id: requirements-txt-fixer
     args:
diff --git a/Dockerfile b/Dockerfile
index 3a33f19c679677ca9f9f92962b3d19e51a8c05f3..2a790dbb1a476fb678ab24e539c84dc360f89852 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,5 +24,6 @@ COPY --from=node /app/dist /code/frontend/dist
 COPY --from=node /app/dist/index.html /code/core/templates/index.html
 
 RUN pip install -r requirements.txt && rm -rf /root/.cache
+RUN python util/format_index.py
 RUN python manage.py collectstatic --noinput
 RUN apk del build-deps
diff --git a/core/urls.py b/core/urls.py
index 3eaaa62d63a7b35e7c17b9c039bb55115ee92394..4853a567b10041c67865b05753a145482a6350c9 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,4 +1,3 @@
-from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.urls import path
 from rest_framework.routers import DefaultRouter
 
@@ -33,5 +32,4 @@ regular_views_urlpatterns = [
 urlpatterns = [
     *router.urls,
     *regular_views_urlpatterns,
-    *staticfiles_urlpatterns()
 ]
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000000000000000000000000000000000000..fd384da7f07f6a49df9697d8a0189052f6f82cf6
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+sleep 1
+python manage.py migrate --noinput
+gunicorn \
+  --bind 0.0.0.0:8000 \
+  --workers=2 \
+  --worker-class=gevent \
+  --log-level debug \
+  grady.wsgi:application
diff --git a/grady/__init__.py b/grady/__init__.py
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/grady/settings/__init__.py b/grady/settings/__init__.py
index f3fadcf6612f28aed805c76b4402cdd8f3d69434..a8e95f7ca800682ef4462350c96bc577dc80d440 100644
--- a/grady/settings/__init__.py
+++ b/grady/settings/__init__.py
@@ -1,8 +1,8 @@
 import os
+from .default import *
 
 dev = os.environ.get('DJANGO_DEV', False)
 
-from .default import *
-
 if not dev:
-    from .live import *
+    from .live import *  # noqa
+    from .url_hack import *  # noqa
diff --git a/grady/settings/default.py b/grady/settings/default.py
index 0f18f2e73e5d89d9cbf4328400ad36923811cc2f..5cda30d18375bb87fc7a462f1bb45182d0a5718a 100644
--- a/grady/settings/default.py
+++ b/grady/settings/default.py
@@ -52,6 +52,7 @@ INSTALLED_APPS = [
     'django.contrib.contenttypes',
     'django.contrib.sessions',
     'django.contrib.messages',
+    'whitenoise.runserver_nostatic',
     'django.contrib.staticfiles',
     'rest_framework',
     'corsheaders',
@@ -132,10 +133,6 @@ STATICFILES_DIRS = (
     'frontend/dist/static',
 )
 
-GRAPH_MODELS = {
-    'all_applications': True,
-    'group_models': True,
-}
 
 LOGIN_REDIRECT_URL = '/'
 LOGIN_URL = '/'
diff --git a/grady/settings/url_hack.py b/grady/settings/url_hack.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4443f3ece6371a91e945f7a298be8676fd3255a
--- /dev/null
+++ b/grady/settings/url_hack.py
@@ -0,0 +1,17 @@
+""" Ok, what the hell? This is especially ugly, hence I keep it hidden in
+this file. We have the requirement that the application instances should
+run under http://$host/$instancename/. And therefore the frontend, whitenoise,
+django, gunicorn and the top http proxy all have to handle this stuff.
+
+Usage: Just set the SCRIPT_NAME env variable to /<name of your instance>
+       and things will work. """
+
+import os
+
+FORCE_SCRIPT_NAME = os.environ.get('SCRIPT_NAME', '')
+if FORCE_SCRIPT_NAME:
+    FORCE_SCRIPT_NAME += '/'
+
+STATIC_URL_BASE = '/static/'
+STATIC_URL = os.path.join(FORCE_SCRIPT_NAME + STATIC_URL_BASE)
+WHITENOISE_STATIC_PREFIX = STATIC_URL_BASE
diff --git a/grady/urls.py b/grady/urls.py
index e36863f62f8c8d3c2d7e610ed1a52e05c11e5832..6d84db555ba003d80d095563ac6ab0d4fbd97129 100644
--- a/grady/urls.py
+++ b/grady/urls.py
@@ -10,6 +10,5 @@ urlpatterns = [
                               namespace='rest_framework')),
     path('api-token-auth/', obtain_jwt_token),
     path('api-token-refresh/', refresh_jwt_token),
-    path('', TemplateView.as_view(template_name='index.html'))
-
+    path('', TemplateView.as_view(template_name='index.html')),
 ]
diff --git a/requirements.txt b/requirements.txt
index c6db510ecba2c16b5247ce29bd6c16fb16d9a57d..9881225c11c7103a2c39218cbeb3b085134e7520 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,10 +2,10 @@ django-cors-headers~=2.1.0
 django-extensions~=1.7.7
 djangorestframework-jwt~=1.11.0
 djangorestframework~=3.7.7
-drf-dynamic-fields~=0.2.0
 Django~=2.0
+drf-dynamic-fields~=0.2.0
 gevent~=1.2.2
 gunicorn~=19.7.0
-psycopg2~=2.7.1
+psycopg2-binary~=2.7.4
 whitenoise~=3.3.1
 xlrd~=1.0.0
diff --git a/util/convert.py b/util/convert.py
index dcfc3e79121e2acca8b841b21a26ab80946ca3e3..82041e1997e20dfb14e7494ca8d76444759d4552 100755
--- a/util/convert.py
+++ b/util/convert.py
@@ -57,15 +57,13 @@ parser.add_argument(
 # one user has one submission (code) per task
 # yes, I know it is possible to name match groups via (?P<name>) but
 # I like this solution better since it gets the job done nicely
-user_head = namedtuple('user_head', 'kohorte, name')
-user_head_re = re.compile(r'^Ergebnisse von Testdurchlauf '
-                          '(?P<kohorte>\d+) für (?P<name>[\w\s\.,-]+)$')
+user_t = namedtuple('user_head', 'name matrikel_no')
 
 # one task has a title and id and hpfly code
-task_head_re = re.compile(r'^Quellcode Frage(?P<title>.*) \d{8}$')
+task_head_re = re.compile(r'^Quellcode Frage (?P<title>.*) ?(\d{8})?$')
 
 # nor parsing the weird mat no
-matno_re = re.compile(r'^(?P<matrikel_no>\d{8})-(\d{3})-(\d{3})$')
+matno_re = re.compile(r'^(?P<matrikel_no>\d{8})-(\d+)-(\d+)$')
 
 
 def converter(infile, usernames=None, number_of_tasks=0,):
@@ -79,13 +77,15 @@ def converter(infile, usernames=None, number_of_tasks=0,):
             yield row[0].value, m.group('matrikel_no') if m else row[1].value
 
     def sheet_iter_data(sheet):
-        """ yields all rows that are not of empty type as one string """
-        for row in (sheet.row(i) for i in range(sheet.nrows)):
-            if any(map(lambda c: c.ctype, row)):
-                yield ''.join(c.value for c in row)
-
-    # meta sheet contains ilias evaluation names usernames etc - data contains
-    # code
+        """ yields all source code titel and code tuples """
+        def row(i):
+            return sheet.row(i)
+        for top, low in ((row(i), row(i + 1)) for i in range(sheet.nrows - 1)):
+            if any(map(lambda c: c.ctype, top)) and 'Quell' in top[0].value:
+                yield (' '.join(c.value for c in top),
+                       ' '.join(c.value for c in low))
+
+    # meta sheet contains ilias names usernames etc - data contains code
     meta, *data = open_workbook(infile, open(os.devnull, 'w')).sheets()
 
     # nice!
@@ -95,16 +95,12 @@ def converter(infile, usernames=None, number_of_tasks=0,):
     # from xls to lists and namedtuples
     # [ [user0, task0_h, code0, ..., taskn, coden ], ..., [...] ]
     root = []
-    for sheet in data:
-        for row in sheet_iter_data(sheet):
-            user = re.search(user_head_re, row)
-            task = re.search(task_head_re, row)
-            if user:
-                root.append([user_head(*user.groups())])
-            elif task:
-                root[-1].append(task.group('title'))
-            else:  # should be code
-                root[-1].append(urllib.parse.unquote(row).strip())
+    for user, sheet in zip(sheet_iter_meta(meta), data):
+        root.append([user_t(*user)])
+        for task, code in sheet_iter_data(sheet):
+            task = re.search(task_head_re, task)
+            root[-1].append(task.group('title'))
+            root[-1].append(urllib.parse.unquote(code).strip())
 
     if number_of_tasks:
         for (user, *task_list) in sorted(root, key=lambda u: u[0].name):
@@ -127,7 +123,7 @@ def converter(infile, usernames=None, number_of_tasks=0,):
     # {id:, ..., id:}}}
     return {
         usernames[user.name]: {
-            'name': user.name,
+            'fullname': user.name,
             'email': mat_to_email[name2mat[user.name]],
             'matrikel_no': name2mat[user.name],
             'submissions': [
@@ -144,7 +140,7 @@ def converter(infile, usernames=None, number_of_tasks=0,):
 def write_to_file(json_dict, outfile):
     # just encode python style
     with open(outfile, "w") as out:
-        out.write(json.JSONEncoder().encode(json_dict))
+        json.dump(json_dict, out, indent=2)
 
     print(f"Wrote data to {outfile}. Done.")
 
diff --git a/util/format_index.py b/util/format_index.py
new file mode 100644
index 0000000000000000000000000000000000000000..c3f1f7d6fd702c637d49ccbbca4f3e64feaade24
--- /dev/null
+++ b/util/format_index.py
@@ -0,0 +1,16 @@
+import sys
+import fileinput
+
+file = 'core/templates/index.html'
+
+with open(file, "r+") as f:
+    s = f.read()
+    f.seek(0)
+    f.write("{% load staticfiles %}\n" + s)
+
+for i, line in enumerate(fileinput.input(file, inplace=1)):
+    sys.stdout.write(line.replace('/static/', "{% static '"))
+for i, line in enumerate(fileinput.input(file, inplace=1)):
+    sys.stdout.write(line.replace('.css', ".css' %}"))
+for i, line in enumerate(fileinput.input(file, inplace=1)):
+    sys.stdout.write(line.replace('.js', ".js' %}"))
diff --git a/util/importer.py b/util/importer.py
index 9f60042b62339373c21f7a43fb1a95da937202e1..683dccc73add4a623dc8643491c211dc93bf5ecc 100644
--- a/util/importer.py
+++ b/util/importer.py
@@ -110,7 +110,7 @@ def add_tests(submission_obj, tests):
 
     for name, test_data in ((name, tests[name]) for name in TEST_ORDER):
         test_obj, created = Test.objects.update_or_create(
-            name=test_data['name'],
+            name=test_data['fullname'],
             submission=submission_obj,
             defaults={
                 'label': test_data['label'],