diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 56997d6ea2f5f2128d9e455ed95a84de36409131..2e871216b364b99218c112e33a63d3cd2c992708 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -77,6 +77,7 @@ install:
     paths:
     - ./clusters
     expire_in: 1 month
+    when: always
   only:
     changes:
       - .gitlab-ci.yml
diff --git a/README.md b/README.md
index 56db7b8bed6229f6bb3155c3fa04d0502b3fbee8..eeb2559abc4d8811679a4098ba4eb0ce76480807 100644
--- a/README.md
+++ b/README.md
@@ -9,4 +9,3 @@ cluster.
 Please refer to https://docs.openappstack.net for further details,
 and to [the installation tutorial](docs/installation_instructions.md) for a step
 by step installation tutorial how to bootstrap your cluster.
-
diff --git a/ansible/group_vars/all/oas.yml b/ansible/group_vars/all/oas.yml
index f1f621fef63fe5e0911aabcd10179b06c9c631b8..2a0dd84125561a9cef70a4dae9f843d6897179bd 100644
--- a/ansible/group_vars/all/oas.yml
+++ b/ansible/group_vars/all/oas.yml
@@ -14,11 +14,12 @@ ansible_python_interpreter: "/usr/bin/env python3"
 
 # Nextcloud administrator password
 nextcloud_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/nextcloud_admin_password chars=ascii_letters') }}"
-# Nextcloud mariadb password for nextcloud db
 nextcloud_mariadb_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/nextcloud_mariadb_password chars=ascii_letters') }}"
-# Nextcloud mariadb root password
 nextcloud_mariadb_root_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/nextcloud_mariadb_root_password chars=ascii_letters') }}"
-# Grafana administrator password
+onlyoffice_jwt_secret: "{{ lookup('password', '{{ cluster_dir }}/secrets/onlyoffice_jwt_secret chars=ascii_letters') }}"
+onlyoffice_postgresql_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/onlyoffice_postgresql_password chars=ascii_letters') }}"
+onlyoffice_rabbitmq_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/onlyoffice_rabbitmq_password chars=ascii_letters') }}"
+
 grafana_admin_password: "{{ lookup('password', '{{ cluster_dir }}/secrets/grafana_admin_password chars=ascii_letters') }}"
 
 # Kubernetes version
@@ -27,7 +28,8 @@ kubernetes_version: "v1.14.3-rancher1-1"
 # git repo versions
 git_charts_version: 'HEAD'
 git_local_storage_version: 'HEAD'
-git_nextcloud_version: '21aac1909edf5dc84eae067a536a16e16ca897fb'
+# version of the https://open.greenhost.net/openappstack/nextcloud repo
+git_nextcloud_version: '569aedb4e8831fb81c84f4087c142b739b9e6521'
 
 # Application versions
 # https://github.com/kubernetes-sigs/krew/releases
diff --git a/ansible/group_vars/all/settings.yml.example b/ansible/group_vars/all/settings.yml.example
index b5e08491e872f38a286ed35c61ddf4b7b4f4c0fe..51b2984df49020e3e0f75f7c2f206629247a552c 100644
--- a/ansible/group_vars/all/settings.yml.example
+++ b/ansible/group_vars/all/settings.yml.example
@@ -19,3 +19,20 @@ helmfiles:
   - 10-nginx
   - 15-monitoring
   - 20-nextcloud
+
+# Optional, custom rke config.
+# I.e. you can set the desired Kubernetes version but please be aware of
+# the [every rke release has only a few supported kubernetes versions](https://rancher.com/docs/rke/latest/en/config-options/#kubernetes-version).
+#
+# rke_custom_config:
+#   kubernetes_version: "v1.14.3-rancher1-1"
+#
+# Another example is allowing to disable ipv6 in pods by
+# passing adding an additional argument to the kubelet:
+# `--allowed-unsafe-sysctls net.ipv6.conf.all.disable_ipv6`
+#
+# rke_custom_config:
+#   services:
+#     kubelet:
+#       extra_args:
+#         allowed-unsafe-sysctls: 'net.ipv6.conf.all.disable_ipv6'
diff --git a/ansible/roles/apps/tasks/helmfiles.yml b/ansible/roles/apps/tasks/helmfiles.yml
index 657726af974af1742e179d802ab9cdc022e06d1a..309989fd068ccdd87392e58012cba5805d976e82 100644
--- a/ansible/roles/apps/tasks/helmfiles.yml
+++ b/ansible/roles/apps/tasks/helmfiles.yml
@@ -2,6 +2,7 @@
 - name: Clone nextcloud repo
   tags:
     - git
+    - nextcloud
   git:
     repo: 'https://open.greenhost.net/openappstack/nextcloud'
     dest: '{{ data_directory }}/source/repos/nextcloud'
@@ -10,6 +11,7 @@
 - name: Clone local-storage repo
   tags:
     - git
+    - local-storage
   git:
     repo: 'https://open.greenhost.net/openappstack/local-storage'
     dest: '{{ data_directory }}/source/repos/local-storage'
@@ -22,6 +24,9 @@
     - NEXTCLOUD_PASSWORD: "{{ nextcloud_password }}"
     - NEXTCLOUD_MARIADB_PASSWORD: "{{ nextcloud_mariadb_password }}"
     - NEXTCLOUD_MARIADB_ROOT_PASSWORD: "{{ nextcloud_mariadb_root_password }}"
+    - ONLYOFFICE_JWT_SECRET: "{{ onlyoffice_jwt_secret }}"
+    - ONLYOFFICE_POSTGRESQL_PASSWORD: "{{ onlyoffice_postgresql_password }}"
+    - ONLYOFFICE_RABBITMQ_PASSWORD: "{{ onlyoffice_rabbitmq_password }}"
     - GRAFANA_ADMIN_PASSWORD: "{{ grafana_admin_password }}"
   shell: |
     set -e -x -o pipefail
diff --git a/ansible/roles/apps/tasks/init.yml b/ansible/roles/apps/tasks/init.yml
index 07ff14dedabcb4b2f966ade8d8086207e1d7a599..4a7ed7210494a6758c69debd2c3d20040c155401 100644
--- a/ansible/roles/apps/tasks/init.yml
+++ b/ansible/roles/apps/tasks/init.yml
@@ -47,6 +47,9 @@
     - config
     - helmfile
     - oas
+    - nextcloud
+    - prometheus
+    - nginx
   file:
     state: touch
     path: "{{ configuration_directory }}/values/apps/{{ item }}.yaml.gotmpl"
diff --git a/ansible/roles/rke_configuration/defaults/main.yml b/ansible/roles/rke_configuration/defaults/main.yml
deleted file mode 100644
index ad9de7c102005628cbd3a1ebb76e35284c90ac2d..0000000000000000000000000000000000000000
--- a/ansible/roles/rke_configuration/defaults/main.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-rke_configuration_location: "{{ data_directory }}/rke/cluster.yml"
-rke_ssh_key_path: "{{ data_directory }}/ssh/ssh_key"
-rke_ssh_agent_auth: "false"
-# Whether to support customer flexvolume driver plugins, by mounting the path
-# /usr/libexec/kubernetes/kubelet-plugins/volume/exec into kubelet.
-flexvolume_plugins: false
diff --git a/ansible/roles/rke_configuration/files/cluster-defaults.yml b/ansible/roles/rke_configuration/files/cluster-defaults.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4e3f9b50aeb01783c183e48a75120eda52538fd9
--- /dev/null
+++ b/ansible/roles/rke_configuration/files/cluster-defaults.yml
@@ -0,0 +1,72 @@
+addon_job_timeout: 0
+addons: ''
+addons_include: []
+authentication:
+  options: {}
+  sans: []
+  strategy: x509
+authorization:
+  mode: rbac
+  options: {}
+bastion_host:
+  address: ''
+  port: ''
+  ssh_key: ''
+  ssh_key_path: ''
+  user: ''
+cloud_provider:
+  name: ''
+cluster_name: ''
+ignore_docker_version: false
+ingress:
+  extra_args: {}
+  node_selector: {}
+  options: {}
+  # Set this to none, so we can install nginx ourselves.
+  provider: none
+kubernetes_version: 'v1.14.3-rancher1-1'
+monitoring:
+  options: {}
+  provider: ''
+network:
+  options: {}
+  plugin: canal
+prefix_path: ''
+private_registries: []
+services:
+  etcd:
+    ca_cert: ''
+    cert: ''
+    creation: ''
+    external_urls: []
+    image: ''
+    key: ''
+    path: ''
+    retention: ''
+    snapshot: false
+  kube-api:
+    image: ''
+    pod_security_policy: false
+    service_cluster_ip_range: 10.43.0.0/16
+    service_node_port_range: ''
+  kube-controller:
+    cluster_cidr: 10.42.0.0/16
+    image: ''
+    service_cluster_ip_range: 10.43.0.0/16
+  kubelet:
+    cluster_dns_server: 10.43.0.10
+    cluster_domain: cluster.local
+    extra_args:
+      containerized: 'true'
+    extra_binds:
+    # Make local storage work with persistent volumes that use `subpath`
+    # see https://open.greenhost.net/openappstack/openappstack/issues/236
+    - /:/rootfs:rshared
+    fail_swap_on: false
+    image: ''
+    infra_container_image: ''
+  kubeproxy:
+    image: ''
+  scheduler:
+    image: ''
+ssh_agent_auth: false
diff --git a/ansible/roles/rke_configuration/tasks/main.yml b/ansible/roles/rke_configuration/tasks/main.yml
index 8dbac5beb167783029b9910467a8a748b0f4dc51..54a7859ad37ec0b85956427bacfc6ec991df25f2 100644
--- a/ansible/roles/rke_configuration/tasks/main.yml
+++ b/ansible/roles/rke_configuration/tasks/main.yml
@@ -29,8 +29,24 @@
   become: true
 
 
-- name: Copy rke cluster configuration file
-  template:
-    src: "cluster.yml.j2"
-    dest: "{{ rke_configuration_location }}"
+- name: Deploy rke cluster configuration file
+  tags:
+    - tmp
+    - rke
+  vars:
+    additional_config:
+      nodes:
+        - address: "{{ ansible_host }}"
+          hostname_override: "{{ inventory_hostname }}"
+          role:
+            - controlplane
+            - etcd
+            - worker
+          ssh_key_path: '/var/lib/OpenAppStack/ssh/ssh_key'
+          user: "{{ ansible_user }}"
+    # Allow undefined rke_custom_config variable
+    custom_config: "{{ rke_custom_config | default({}) }}"
+  copy:
+    content: "{{ lookup('file', 'cluster-defaults.yml') | from_yaml | combine(additional_config, custom_config, recursive=True) | to_nice_yaml(indent=2) }}"
+    dest: "{{ data_directory }}/rke/cluster.yml"
   become: true
diff --git a/ansible/roles/rke_configuration/templates/cluster.yml.j2 b/ansible/roles/rke_configuration/templates/cluster.yml.j2
deleted file mode 100644
index 07d90dccbb5d258c2af1131eb80817dd021bb858..0000000000000000000000000000000000000000
--- a/ansible/roles/rke_configuration/templates/cluster.yml.j2
+++ /dev/null
@@ -1,102 +0,0 @@
-nodes:
-{% for node in groups['all'] %}
-- address: {{ hostvars[node]['ansible_host'] }}
-  # port: "22"
-  # internal_address: ""
-  role:
-{% if hostvars[node]['inventory_hostname'] in groups.master %}
-  - controlplane
-  - etcd
-{% endif %}
-{% if hostvars[node]['inventory_hostname'] in groups.worker %}
-  - worker
-{% endif %}
-  hostname_override: {{ hostvars[node]['inventory_hostname'] }}
-  user: {{ hostvars[node]['ansible_user'] }}
-  # docker_socket: /var/run/docker.sock
-  # ssh_key: ""
-{% if rke_ssh_key_path is defined %}
-  ssh_key_path: {{ rke_ssh_key_path }}
-{% else %}
-  # ssh_key_path: ""
-{% endif %}
-  # labels: {}
-{% endfor %}
-services:
-  etcd:
-    image: ""
-    external_urls: []
-    ca_cert: ""
-    cert: ""
-    key: ""
-    path: ""
-    snapshot: false
-    retention: ""
-    creation: ""
-  kube-api:
-    image: ""
-    service_cluster_ip_range: 10.43.0.0/16
-    service_node_port_range: ""
-    pod_security_policy: false
-  kube-controller:
-    image: ""
-    cluster_cidr: 10.42.0.0/16
-    service_cluster_ip_range: 10.43.0.0/16
-  scheduler:
-    image: ""
-  kubelet:
-    image: ""
-    extra_args:
-      containerized: "true"
-{% if flexvolume_plugins %}
-      volume-plugin-dir: /usr/libexec/kubernetes/kubelet-plugins/volume/exec
-{% endif %}
-    extra_binds:
-    # Make local storage work with persistent volumes that use `subpath`
-    # see https://open.greenhost.net/openappstack/openappstack/issues/236
-    - "/:/rootfs:rshared"
-{% if flexvolume_plugins %}
-    - /usr/libexec/kubernetes/kubelet-plugins/volume/exec:/usr/libexec/kubernetes/kubelet-plugins/volume/exec
-{% endif %}
-    cluster_domain: cluster.local
-    infra_container_image: ""
-    cluster_dns_server: 10.43.0.10
-    fail_swap_on: false
-  kubeproxy:
-    image: ""
-network:
-  plugin: canal
-  options: {}
-authentication:
-  strategy: x509
-  options: {}
-  sans: []
-addons: ""
-addons_include: []
-ssh_agent_auth: {{ rke_ssh_agent_auth }}
-authorization:
-  mode: rbac
-  options: {}
-ignore_docker_version: false
-kubernetes_version: {{ kubernetes_version }}
-private_registries: []
-ingress:
-  # Set this to none, so we can install nginx ourselves.
-  provider: none
-  options: {}
-  node_selector: {}
-  extra_args: {}
-cluster_name: ""
-cloud_provider:
-  name: ""
-prefix_path: ""
-addon_job_timeout: 0
-bastion_host:
-  address: ""
-  port: ""
-  user: ""
-  ssh_key: ""
-  ssh_key_path: ""
-monitoring:
-  provider: ""
-  options: {}
diff --git a/docs/installation_instructions.md b/docs/installation_instructions.md
index 31e265a836f60903b3d8b364f674217eecb962ed..4cdc9a72d1579398f8151b22a54c36d760ed200a 100644
--- a/docs/installation_instructions.md
+++ b/docs/installation_instructions.md
@@ -4,9 +4,9 @@ This document describes how you can set up a single-node OpenAppStack cluster.
 Support for multi-node clusters will come in the future.
 
 > **NOTE:** All commands in these installation instructions need to be run on a
-> trusted machine that is *not* the VPS that will run OpenAppStack. The
-> installation process will generate some secrets that will be saved to this
-> machine.
+> trusted provisioning machine (i.e., your laptop) that is *not* the VPS that
+> will run OpenAppStack. The installation process will generate some secrets
+> that will be saved to this machine.
 
 If you encounter any difficulties while following these instructions, please
 [open an issue following our contact
@@ -40,27 +40,26 @@ guide][https://openappstack.net/contact.html).
 
 ## Install OpenAppStack command line tool
 
-On your **local machine**, clone the OpenAppStack git repository:
+On your **provisioning machine**, clone the OpenAppStack git repository:
 
     $ git clone https://open.greenhost.net/openappstack/openappstack.git
     $ cd openappstack
 
-If you installed `virtualenv`, create and activate it:
 
-    $ python3 -m venv env
-    $ . env/bin/activate
-
-Next, install the OpenAppStack CLI client by running the following commands: 
-
-First, create a python virtual environment called "env" that uses python 3. This
-makes sure we do not change any of your other python projects. The second command
-"activates" the virtualenv. 
+If you installed `virtualenv`, create a python virtual environment called "env"
+that uses python 3. This makes sure we do not change any of your other python
+projects. The second command "activates" the virtualenv. 
 
 > **NOTE:** Activating the virtualenv means you will use that environment to
 > install and run python programs instead of your global environment. If you
 > close your terminal or open a new one, you need to activate the virtualenv
 > again.
 
+    $ python3 -m venv env
+    $ . env/bin/activate
+
+Next, install the OpenAppStack CLI client by running the following commands: 
+
     $ pip3 install -r requirements.txt
 
 > *Hint:* if the `pip3 install` command results in a [segmentation
@@ -182,12 +181,21 @@ issue](https://open.greenhost.net/openappstack/openappstack/issues/317)
 Before you continue, if you have not made DNS entries with the CLI tool, you
 need to make them now. It is important to start with configuring DNS because
 depending on your DNS setup/provider, it takes a while (sometimes hours) to
-propagate. You need one dedicated (sub)domain entry and a wild card entry for
-everything inside it. For example, create these two records for your domain:
+propagate. 
+
+You need one dedicated (sub)domain entry and a wildcard entry for everything
+inside it. For example, create an A record for these domains:
 
 - An `A` record `oas.example.org` pointing to the VPSs IP address
 - A `CNAME` record `*.oas.example.org` pointing to `oas.example.org`.
 
+OpenAppStack will fetch https certificates with [Let's
+Encrypt](https://letsencrypt.org) by default. In order to do this DNS entries
+need to be created.  If you don't need https certificates for your cluster while
+testing you can skip this step. Please be aware of the limitations of this:
+
+- Onlyoffice won't work since it requires a valid certificate connecting to Nextcloud.
+- You need to be able to resolve the domain names locally.
 
 ### Installation
 
@@ -226,7 +234,7 @@ When the installation is completed, you will have access to these applications:
   your cluster. Read more about Grafana in the [monitoring chapter
   below](#monitoring)
 
-You can access Nextcloud via https://files.oas.example.org. Use the username
+You can access Nextcloud via https://files.example.org. Use the username
 `admin` with the automatically generated Nextcloud password that you can find in
 `clusters/maarten/secrets/nextcloud_admin_password` on your local machine.
 ONLYOFFICE is already integrated in your Nextcloud installation which allows you
diff --git a/helmfiles/values/nextcloud.yaml.gotmpl b/helmfiles/values/nextcloud.yaml.gotmpl
index 376beded449485e9c6a33914d701cd09326d2090..82e9c7f0739842d768403574edc87c33c7c0ee8e 100644
--- a/helmfiles/values/nextcloud.yaml.gotmpl
+++ b/helmfiles/values/nextcloud.yaml.gotmpl
@@ -27,9 +27,6 @@ nextcloud:
           log_not_found off;
           access_log off;
         }
-        location ~ \.(?:png|html|ttf|ico|jpg|jpeg)$ {
-          try_files $uri /index.php$request_uri;
-        }
     hosts:
       - "files.{{ .Environment.Values.domain }}"
     tls:
@@ -82,3 +79,9 @@ onlyoffice-documentserver:
 
   onlyoffice:
     server_name: "office.{{ .Environment.Values.domain }}"
+  jwtSecret: "{{ requiredEnv "ONLYOFFICE_JWT_SECRET" }}"
+  postgresql:
+    postgresqlPassword: "{{ requiredEnv "ONLYOFFICE_POSTGRESQL_PASSWORD" }}"
+  rabbitmq:
+    rabbitmq:
+      password: "{{ requiredEnv "ONLYOFFICE_RABBITMQ_PASSWORD" }}"
diff --git a/openappstack/__main__.py b/openappstack/__main__.py
index 64b7638d9790b252ef7667de0f62a6cd26e4da41..c9c6f32f3f30e272d071b3cf7dd5fe81b61de546 100755
--- a/openappstack/__main__.py
+++ b/openappstack/__main__.py
@@ -296,11 +296,12 @@ def test(clus, args):
             log.info(command)
             command.append('-t {tag}'.format(tag=tag))
         log.info('Running behave command %s', command)
-        behave_main(command)
+        return_code = behave_main(command)
 
         # Remove behave.ini so we don't leave secrets hanging around.
         os.remove(behave_ini)
 
+        sys.exit(return_code)
 
 def create_domain_records(domain, droplet_ip, subdomain=None):
     """
diff --git a/openappstack/cluster.py b/openappstack/cluster.py
index d728c54a1a37dc19433a079688ccc929de5a5400..0b6b91dc1aa2435d7f1fe5ec0ce8cde4f6dfbd08 100644
--- a/openappstack/cluster.py
+++ b/openappstack/cluster.py
@@ -199,6 +199,8 @@ class Cluster:
         behave_config['behave.userdata']['nextcloud.username'] = 'admin'
         behave_config['behave.userdata']['nextcloud.password'] = \
             nextcloud_admin_password
+        behave_config['behave.userdata']['onlyoffice.url'] = \
+            'https://office.{}/welcome'.format(self.domain)
 
         behave_config['behave.userdata']['grafana.url'] = \
             'https://grafana.{}'.format(self.domain)
diff --git a/test/behave/features/nextcloud.feature b/test/behave/features/nextcloud.feature
index 2482af3ec14e76f42996810dae6fc152572bcf3d..e68c879459e37ff55d7dc3335a9826b3d07eb8bf 100644
--- a/test/behave/features/nextcloud.feature
+++ b/test/behave/features/nextcloud.feature
@@ -4,6 +4,10 @@ Feature: Test nextcloud admin login
   I want to be able to login to nextcloud as the user admin
   And I want to be able to open a document in OnlyOffice
 
+Scenario: Test OnlyOffice welcome screen
+  When I open the onlyoffice URL
+  Then I expect that element "#status-ok-icon" is visible
+
 Scenario: Open nextcloud
   When I open the nextcloud URL
   Then I wait on element "input#user" for 25000ms to be visible
@@ -22,6 +26,7 @@ Scenario: Login to nextcloud
 Scenario: Close welcome to nextcloud modal
   When I close the nextcloud welcome wizard if it exists
   Then I wait on element "#firstrunwizard" for 1000ms to not exist
+  And I wait on element ".actions a.button.new" for 5000ms to be visible
 
 Scenario: Create a new document in OnlyOffice
   When I click on the element ".actions a.button.new"
diff --git a/test/behave/features/steps/login.py b/test/behave/features/steps/login.py
index 5eb11075b1a64f050e87eff28d498e481e337e75..b45ad5b90eb67616aa108e61e3c212cba674a031 100644
--- a/test/behave/features/steps/login.py
+++ b/test/behave/features/steps/login.py
@@ -10,6 +10,13 @@ from behave_webdriver.steps import *
 def before_all(context):
     pass  # login and save cookies here
 
+@when(u'I open the onlyoffice URL')
+@given(u'I open the onlyoffice URL')
+def step_impl(context):
+    """Open onlyoffice URL."""
+    print(context.nextcloud)
+    context.behave_driver.get(context.config.userdata.get('onlyoffice.url'))
+
 @when(u'I open the nextcloud URL')
 @given(u'I open the nextcloud URL')
 def step_impl(context):
@@ -23,7 +30,6 @@ def step_impl(context):
     """Open grafana URL."""
     context.behave_driver.get(context.grafana['url'])
 
-
 @when(u'I enter the "{section}" "{cred_type}" in the inputfield "{element}"')
 def step_impl(context, section, cred_type, element):
     """Enter username/password into login inputfields."""