diff --git a/uwsgi-python/handlers/main.yml b/uwsgi-python/handlers/main.yml
index 90c3d24201922b224921c2ad41060256e2ca51e4..0bce1092963289a15b9dab94804371bec847649b 100644
--- a/uwsgi-python/handlers/main.yml
+++ b/uwsgi-python/handlers/main.yml
@@ -133,3 +133,6 @@
 
 - name: restart uwsgi instance etherpad-api-proxy
   service: name="uwsgi@etherpad-api-proxy" state=restarted
+
+- name: restart uwsgi instance schilder2000
+  service: name="uwsgi@schilder2000" state=restarted
diff --git a/uwsgi-python/tasks/app.yml b/uwsgi-python/tasks/app.yml
index 0b3f005db34830d57aa765d0d79e528e280cb92d..b116aa8bf5378882ef26944517662c3cfed0ebaa 100644
--- a/uwsgi-python/tasks/app.yml
+++ b/uwsgi-python/tasks/app.yml
@@ -155,6 +155,7 @@
     - app_deploy_key is defined
     - app_deploy_key|string != ''  # noqa 602
     - (app_git_pip is not defined) or (not app_git_pip)
+    - app_pip_package is not defined or app_pip_package == "" # noqa 602
   tags:
     - uwsgi-app
     - "{{ app.app }}"
@@ -177,6 +178,7 @@
     - app_deploy_key|string != ''  # noqa 602
     - (app_git_pip is not defined) or (not app_git_pip)
     - app_git_url|string != ''  # noqa 602
+    - app_pip_package is not defined or app_pip_package == "" # noqa 602
   register: git
   tags:
     - uwsgi-app
@@ -220,6 +222,7 @@
     - app_requirements_file|string != ''  # noqa 602
     - (app_git_pip is not defined) or (not app_git_pip)
     - app_lang == "python"
+    - app_pip_package is not defined or app_pip_package == "" # noqa 602
   tags:
     - uwsgi-app
     - "{{ app.app }}"
@@ -238,6 +241,32 @@
     - app_git_pip is defined
     - app_git_pip
     - app_lang == "python"
+    - app_pip_package is not defined or app_pip_package == "" # noqa 602
+  tags:
+    - uwsgi-app
+    - "{{ app.app }}"
+    - "{{ app.instance }}"
+
+- name: ensure we have a virtualenv (app package)  # noqa 403
+  pip:
+    name: >-
+      {{ app_pip_package }}
+      {%- if app_pip_extras | default([]) != [] -%}
+      [{{ app_pip_extras | join(",") }}]
+      {%- endif -%}
+    extra_args: >-
+      {%- if app_pip_index_url is defined -%}
+      --extra-index-url {{ app_pip_index_url }}
+      {%- endif -%}
+    virtualenv: "{{ app_venv }}"
+    virtualenv_python: "python{{ app_python_version }}"
+    state: latest
+  notify:
+    - "restart uwsgi instance {{ app.instance }}"
+  when:
+    - app_lang == "python"
+    - app_pip_package is defined
+    - app_pip_package != "" # noqa 602
   tags:
     - uwsgi-app
     - "{{ app.app }}"
diff --git a/uwsgi-python/tasks/apps/schilder2000.yml b/uwsgi-python/tasks/apps/schilder2000.yml
new file mode 100644
index 0000000000000000000000000000000000000000..977dfffd805f18536453608a6205124fdb5a1731
--- /dev/null
+++ b/uwsgi-python/tasks/apps/schilder2000.yml
@@ -0,0 +1,44 @@
+---
+
+- name: Create instance directory
+  file:
+    path: "{{ app_venv }}/var/schilder2000-instance"
+    state: directory
+    owner: root
+    group: "{{ app_group }}"
+    mode: "0751"
+  tags:
+    - uwsgi-app
+    - "{{ app.app }}"
+    - "{{ app.instance }}"
+
+- name: Generate secret config
+  template:
+    src: secret_config.py.j2
+    dest: "{{ app_venv }}/var/schilder2000-instance/secret_config.py"
+    owner: root
+    group: "{{ app_group }}"
+    mode: "0640"
+    force: false
+  notify:
+    - restart uwsgi instance {{ app.instance }}
+
+- name: Install app config
+  template:
+    src: apps/{{ app.app }}.j2
+    dest: "{{ app_venv }}/var/schilder2000-instance/config.py"
+    owner: root
+    group: "{{ app_group }}"
+    mode: "0640"
+  notify:
+    - restart uwsgi instance {{ app.instance }}
+
+  # Skip linter 301 as this command is idempotent per se
+- name: Migrate database  # noqa 301
+  command: "{{ app_venv }}/bin/flask alembic upgrade head"
+  environment:
+    FLASK_APP: schilder2000
+  become: true
+  become_user: "{{ app_user }}"
+  notify:
+    - restart uwsgi instance {{ app.instance }}
diff --git a/uwsgi-python/templates/apps/schilder2000.j2 b/uwsgi-python/templates/apps/schilder2000.j2
new file mode 100644
index 0000000000000000000000000000000000000000..c042bb6652fac029e15f826d9c08777c3d4b2c7f
--- /dev/null
+++ b/uwsgi-python/templates/apps/schilder2000.j2
@@ -0,0 +1,17 @@
+import json
+from pathlib import Path
+
+exec((Path(__file__).parent / "secret_config.py").read_text())
+SECRET_KEY = secret_key
+
+SQLALCHEMY_DATABASE_URI = "postgresql+psycopg:///schilder2000"
+
+TEMPLATES_AUTO_RELOAD = True
+
+PRINTERS = json.loads(r"""
+{{ schilder2000_printers | to_json }}
+""")
+
+REQUIRE_LOGIN = {{ schilder2000_require_login | bool | title }}
+
+{{ schilder2000_extra }}
diff --git a/uwsgi-python/vars/default.yml b/uwsgi-python/vars/default.yml
index ca17fa11cc28c0fdf2168f5ee65661fc1b2c5235..437a300b5cd25ccda6757e2ec32935e6ac9b655e 100644
--- a/uwsgi-python/vars/default.yml
+++ b/uwsgi-python/vars/default.yml
@@ -39,6 +39,10 @@ app_git_version: HEAD
 app_git_pip: false
 app_git_pip_query: ''
 
+# app_pip_package: ""
+# app_pip_index_url: ""
+# app_pip_extras: []
+
 app_requirements_file: requirements.txt
 app_config_file: config.py
 app_secret_config: true
diff --git a/uwsgi-python/vars/schilder2000.yml b/uwsgi-python/vars/schilder2000.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2ab9b92c7995256b4011c7f3b142d3d8403477b5
--- /dev/null
+++ b/uwsgi-python/vars/schilder2000.yml
@@ -0,0 +1,23 @@
+---
+
+app_module: "schilder2000:create_app()"
+
+app_db_name: schilder2000
+app_db_type: postgres
+
+#app_additional_software:
+#  - libldap-dev  # for auth-ldap
+#  - default-libmysqlclient-dev  # for db-mysql
+
+app_pip_package: schilder2000
+app_pip_index_url: >-
+  https://git.fsmpi.rwth-aachen.de/api/v4/projects/305/packages/pypi/simple
+app_pip_extras:
+  - db-postgres
+
+app_config_file: ""
+app_secret_config: false
+
+schilder2000_require_login: false
+schilder2000_printers: {}
+schilder2000_extra: ""