diff --git a/common/playbooks/repository-nexus.yaml b/common/playbooks/repository-nexus.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d3f40c18f022bad65b298ce7e94065138daa0454
--- /dev/null
+++ b/common/playbooks/repository-nexus.yaml
@@ -0,0 +1,186 @@
+---
+# Secrets in "/nexus" (user passwords):
+#
+# * admin
+# * binder
+# * notebooks-reader
+# * notebooks-writer
+#
+# (the list is dynamic based on templates/nexus/user-*.yaml)
+#
+- name: Sonatype Nexus Deployment
+  hosts: master[0]
+  vars:
+    nexus_url: "https://{{ nexus_hostname }}/service/rest/v1"
+    nexus_blobstore_name: default
+    nexus_blobstore_type: file
+    nexus_repository_binder_name: binder
+    nexus_repository_notebooks_name: notebooks
+    nexus_repository_port: 8082
+    secrets: "{{ lookup('community.hashi_vault.hashi_vault', (vault_mount_point, 'nexus') | join('/'), token_validate=false) }}"
+  become: true
+  tasks:
+    - name: Debug Secrets
+      debug:
+        msg: "{{ item.key }} = {{ item.value }}"
+      loop: "{{ secrets | dict2items }}"
+    - name: Create Nexus configuration file on master
+      vars:
+        name: nexus
+      template:
+        src: templates/nexus.yaml
+        dest: /tmp/nexus.yaml
+        mode: 0600
+    - name: Deploy/update Nexus instance
+      command: kubectl apply -f /tmp/nexus.yaml
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      changed_when: true
+      when: true
+    - name: Wait for Nexus pod ready
+      command: kubectl wait pod --all --namespace nexus --for condition=ready --timeout=5m
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+      changed_when: false
+      when: true
+      # workaround problem with kubectl wait
+      retries: 1
+    - name: Wait for Nexus REST API
+      uri:
+        url: "{{ nexus_url }}/status"
+        status_code: 200
+        method: GET
+      register: _result
+      until: _result.status == 200
+      retries: 120
+      delay: 15
+    - name: Check the admin password
+      uri:
+        url: "{{ nexus_url }}/status"
+        force_basic_auth: true
+        method: HEAD
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        status_code: 200, 401
+      register: nexus_admin_password_check
+    - name: Admin password setup
+      when:
+        - nexus_admin_password_check.status == 401
+      block:
+        - name: Get initial admin password
+          shell: 'kubectl exec -it -n nexus $(kubectl get pod -n nexus -l app=sonatype-nexus -o name) -- cat /nexus-data/admin.password'
+          register: nexus_admin_password_initial
+          changed_when: false
+          environment:
+            KUBECONFIG: /etc/kubernetes/admin.conf
+        - name: Set the admin password
+          uri:
+            url: "{{ nexus_url }}/security/users/admin/change-password"
+            force_basic_auth: true
+            headers:
+              Content-Type: text/plain
+            method: PUT
+            user: 'admin'
+            password: "{{ nexus_admin_password_initial.stdout }}"
+            body: "{{ secrets['admin'] }}"
+            body_format: raw
+            status_code: [200, 204]
+      rescue:
+        - name: Admin Password Setup Fail
+          fail:
+            msg: "Failed admin password setup"
+    - name: Check blobstore
+      uri:
+        url: "{{ nexus_url }}/blobstores/{{ nexus_blobstore_type }}/{{ nexus_blobstore_name }}"
+        force_basic_auth: true
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        # XXX: workaround REST API bug for S3 (Nexus 3.33.0-01)
+        status_code: [200, 400, 404, 500]
+      register: nexus_blobstore_check
+    # XXX: REST API bug II - needs to be created manually
+    - name: Create blobstore
+      when: &blobstore_changed
+        - nexus_blobstore_check.status == 404 or nexus_blobstore_check.status == 400
+      uri:
+        url: "{{ nexus_url }}/blobstores/{{ nexus_blobstore_type }}"
+        force_basic_auth: true
+        method: POST
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        body: "{{ lookup('template', 'templates/nexus/blobstore.yaml') | from_yaml }}"
+        body_format: json
+        status_code: [200, 201]
+      changed_when: *blobstore_changed
+    - name: Check repository
+      uri:
+        url: "{{ nexus_url }}/repositories/docker/hosted/{{ nexus_repository_binder_name }}"
+        force_basic_auth: true
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        status_code: [200, 404]
+      register: nexus_repository_check
+    - name: Delete original repositories
+      when: &repositories_deleted
+        - nexus_repository_check.status == 404
+      uri:
+        url: "{{ nexus_url }}/repositories/{{ item }}"
+        force_basic_auth: true
+        method: DELETE
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        status_code: [200, 204, 404]
+      register: _result
+      loop:
+        - maven-central
+        - maven-public
+        - maven-releases
+        - maven-snapshots
+        - nuget-group
+        - nuget-hosted
+        - nuget.org-proxy
+      changed_when: _result.status == 200 or _result.status == 204
+    - name: Create repositories
+      include_tasks: subtasks/nexus-repository.yaml
+      loop:
+        - name: "{{ nexus_repository_binder_name }}"
+          type: docker/hosted
+        - name: "{{ nexus_repository_notebooks_name }}"
+          type: docker/hosted
+    - name: Create roles
+      include_tasks: subtasks/nexus-role.yaml
+      vars:
+        rolename: "{{ item | basename | splitext | first | regex_replace('^role-', '') }}"
+      with_fileglob:
+        - "templates/nexus/role-*.yaml"
+    - name: Create users
+      include_tasks: subtasks/nexus-user.yaml
+      vars:
+        username: "{{ item | basename | splitext | first | regex_replace('^user-', '') }}"
+      with_fileglob:
+        - "templates/nexus/user-*.yaml"
+    - name: Check security realms
+      uri:
+        url: "{{ nexus_url }}/security/realms/active"
+        force_basic_auth: true
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        return_content: true
+      register: nexus_realms_check
+    - name: Update securty realms
+      when: &realms_changed
+        - '"DockerToken" not in nexus_realms_check.content'
+      uri:
+        url: "{{ nexus_url }}/security/realms/active"
+        force_basic_auth: true
+        headers:
+          accept: application/json
+          Content-Type: application/json
+        method: PUT
+        user: 'admin'
+        password: "{{ secrets['admin'] }}"
+        body: "{{ lookup('template', 'templates/nexus/realms.yaml') | from_yaml }}"
+        body_format: json
+        status_code: [200, 204]
+      changed_when: *realms_changed
diff --git a/common/playbooks/subtasks/nexus-repository.yaml b/common/playbooks/subtasks/nexus-repository.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4787166250c18baf9785c5860598fa4b870e35af
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-repository.yaml
@@ -0,0 +1,23 @@
+---
+- name: Check repository {{ item.name }}
+  uri:
+    url: "{{ nexus_url }}/repositories/{{ item.type }}/{{ item.name }}"
+    force_basic_auth: true
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    status_code: [200, 404]
+  register: nexus_repository_check
+
+- name: Create repository {{ item.name }}
+  when: &repository_created
+    - nexus_repository_check.status == 404
+  uri:
+    url: "{{ nexus_url }}/repositories/{{ item.type }}"
+    force_basic_auth: true
+    method: POST
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    body: "{{ lookup('template', 'templates/nexus/repository-' + item.name + '.yaml') | from_yaml }}"
+    body_format: json
+    status_code: [200, 201]
+  changed_when: *repository_created
diff --git a/common/playbooks/subtasks/nexus-role.yaml b/common/playbooks/subtasks/nexus-role.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..498c4989f966355cedb16effcc3e5ff16150d8e6
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-role.yaml
@@ -0,0 +1,23 @@
+---
+- name: Check role {{ rolename }}
+  uri:
+    url: "{{ nexus_url }}/security/roles/{{ rolename }}"
+    force_basic_auth: true
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    status_code: [200, 404]
+  register: nexus_role_check
+
+- name: Create role {{ rolename }}
+  when: &role_created
+    - nexus_role_check.status == 404
+  uri:
+    url: "{{ nexus_url }}/security/roles"
+    force_basic_auth: true
+    method: POST
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    body: "{{ lookup('template', item) | from_yaml }}"
+    body_format: json
+    status_code: [200, 201]
+  changed_when: *role_created
diff --git a/common/playbooks/subtasks/nexus-user.yaml b/common/playbooks/subtasks/nexus-user.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0d3a7a1cab476674c95f7e0672e893087793588b
--- /dev/null
+++ b/common/playbooks/subtasks/nexus-user.yaml
@@ -0,0 +1,27 @@
+---
+- name: Check user {{ username }}
+  uri:
+    url: "{{ nexus_url }}/security/users?userId={{ username }}"
+    force_basic_auth: true
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    return_content: true
+    status_code: [200, 404]
+  register: nexus_user_check
+
+- name: Create user {{ username }}
+  when: &user_created
+    - username not in nexus_user_check.content
+  uri:
+    url: "{{ nexus_url }}/security/users"
+    force_basic_auth: true
+    headers:
+      accept: application/json
+      Content-Type: application/json
+    method: POST
+    user: 'admin'
+    password: "{{ secrets['admin'] }}"
+    body: "{{ lookup('template', item) | from_yaml }}"
+    body_format: json
+    status_code: [200, 201]
+  changed_when: *user_created
diff --git a/common/playbooks/templates/nexus.yaml b/common/playbooks/templates/nexus.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6208be20aaffcdae41b717cf1f34867191843510
--- /dev/null
+++ b/common/playbooks/templates/nexus.yaml
@@ -0,0 +1,149 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ name }}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: nexus-pvc
+  namespace: {{ name }}
+  labels:
+    app: sonatype-nexus
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 500Gi
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nexus
+  namespace: {{ name }}
+  labels:
+    app: sonatype-nexus
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: sonatype-nexus
+  template:
+    metadata:
+      labels:
+        app: sonatype-nexus
+    spec:
+      containers:
+        - image: sonatype/nexus3
+          imagePullPolicy: Always
+          name: nexus
+          ports:
+            - containerPort: 8081
+            - containerPort: {{ nexus_repository_port }}
+            - containerPort: {{ nexus_repository_port + 1 }}
+          resources:
+            requests:
+              cpu: 2
+            limits:
+              cpu: 4
+          volumeMounts:
+            - mountPath: /nexus-data
+              name: nexus-data-volume
+      volumes:
+        - name: nexus-data-volume
+          persistentVolumeClaim:
+            claimName: nexus-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: nexus
+  namespace: {{ name }}
+spec:
+  ports:
+    - port: 80
+      targetPort: 8081
+      protocol: TCP
+      name: http
+    - port: 5000
+      targetPort: {{ nexus_repository_port }}
+      protocol: TCP
+      name: docker-container-notebooks
+    - port: 5001
+      targetPort: {{ nexus_repository_port + 1 }}
+      protocol: TCP
+      name: docker-repository
+  selector:
+    app: sonatype-nexus
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: nexus-ingress
+  namespace: nexus
+  annotations:
+    kubernetes.io/ingress.class: "nginx"
+    kubernetes.io/tls-acme: "true"
+    ingress.kubernetes.io/proxy-body-size: 100m
+    nginx.ingress.kubernetes.io/proxy-connect-timeout: "15"
+    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
+    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
+    nginx.ingress.kubernetes.io/proxy-next-upstream-timeout: "3600"
+    nginx.ingress.kubernetes.io/proxy-request-buffering: "on"
+spec:
+  tls:
+    - hosts:
+        - {{ nexus_hostname }}
+        - {{ registry_binder_hostname }}
+        - {{ registry_notebooks_hostname }}
+      secretName: acme-tls-{{ name }}
+  rules:
+    - host: {{ nexus_hostname }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: nexus
+                port:
+                  number: 80
+            path: /
+            pathType: Prefix
+    - host: {{ registry_binder_hostname }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: nexus
+                port:
+                  number: 5000
+            path: /
+            pathType: Prefix
+    - host: {{ registry_notebooks_hostname }}
+      http:
+        paths:
+          - backend:
+              service:
+                name: nexus
+                port:
+                  number: 5001
+            path: /
+            pathType: Prefix
+# direct access without nginx layer and SSL (for debugging)
+# ---
+# apiVersion: v1
+# kind: Service
+# metadata:
+#   name: nexus-repository-direct
+#   namespace: {{ name }}
+# spec:
+#   type: NodePort
+#   selector:
+#     app: sonatype-nexus
+#   ports:
+#     - port: 5002
+#       targetPort: {{ nexus_repository_port + 1 }}
+#       protocol: TCP
+#       nodePort: 31444
+#   externalIPs: {{ groups['ingress'] }}
diff --git a/common/playbooks/templates/nexus/blobstore.yaml b/common/playbooks/templates/nexus/blobstore.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5c46365df74a4d3c86bf6c5b1949382f332aedb1
--- /dev/null
+++ b/common/playbooks/templates/nexus/blobstore.yaml
@@ -0,0 +1,4 @@
+---
+name: {{ nexus_blobstore_name }}
+
+path: default
diff --git a/common/playbooks/templates/nexus/realms.yaml b/common/playbooks/templates/nexus/realms.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..c49f88fc96a6193fc06a48897e2dbdeb26c3067e
--- /dev/null
+++ b/common/playbooks/templates/nexus/realms.yaml
@@ -0,0 +1,3 @@
+---
+- NexusAuthenticatingRealm
+- DockerToken
diff --git a/common/playbooks/templates/nexus/repository-binder.yaml b/common/playbooks/templates/nexus/repository-binder.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5cf07bddac8402390dc059a6d24422c1187d1020
--- /dev/null
+++ b/common/playbooks/templates/nexus/repository-binder.yaml
@@ -0,0 +1,12 @@
+---
+name: {{ nexus_repository_binder_name }}
+online: true
+storage:
+  blobStoreName: {{ nexus_blobstore_name }}
+  strictContentTypeValidation: true
+  writePolicy: allow
+docker:
+  v1Enabled: false
+  # basic-auth worked only with binder 0.2.0-n577.h14cc6c7 + jupyterhub 0.11.1
+  forceBasicAuth: false
+  httpPort: {{ nexus_repository_port }}
diff --git a/common/playbooks/templates/nexus/repository-notebooks.yaml b/common/playbooks/templates/nexus/repository-notebooks.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7447e5f663e2e514e18615498aeb6bc3bbfd66f8
--- /dev/null
+++ b/common/playbooks/templates/nexus/repository-notebooks.yaml
@@ -0,0 +1,11 @@
+---
+name: {{ nexus_repository_notebooks_name }}
+online: true
+storage:
+  blobStoreName: {{ nexus_blobstore_name }}
+  strictContentTypeValidation: true
+  writePolicy: allow
+docker:
+  v1Enabled: false
+  forceBasicAuth: true
+  httpPort: {{ nexus_repository_port + 1 }}
diff --git a/common/playbooks/templates/nexus/role-anonymous.yaml b/common/playbooks/templates/nexus/role-anonymous.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..f88aaf0e1577c9843bc5b6db948c329a95d6045f
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-anonymous.yaml
@@ -0,0 +1,10 @@
+---
+id: anonymous
+name: anonymous
+description: Anonymous Role for Jupyter Binder repository manager
+# only explicit repository read roles to avoid access to the internal repository
+privileges:
+  - nx-healthcheck-read
+  - nx-repository-view-docker-{{ nexus_repository_binder_name }}-browse
+  - nx-repository-view-docker-{{ nexus_repository_binder_name }}-read
+  - nx-search-read
diff --git a/common/playbooks/templates/nexus/role-binder.yaml b/common/playbooks/templates/nexus/role-binder.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..977d88721bc65bcca1c14fc15b08c5a0ee11a7f4
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-binder.yaml
@@ -0,0 +1,10 @@
+---
+id: binder
+name: binder
+description: Jupyter Binder
+privileges:
+  - nx-repository-view-docker-{{ nexus_repository_binder_name }}-add
+  - nx-repository-view-docker-{{ nexus_repository_binder_name }}-edit
+  - nx-repository-view-docker-{{ nexus_repository_binder_name }}-read
+roles:
+  - anonymous
diff --git a/common/playbooks/templates/nexus/role-notebooks-read.yaml b/common/playbooks/templates/nexus/role-notebooks-read.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..13a2e7bcb5a5ea3ef70a6e790e36503309803776
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-notebooks-read.yaml
@@ -0,0 +1,7 @@
+---
+id: {{ nexus_repository_notebooks_name }}-read
+name: {{ nexus_repository_notebooks_name }}-read
+description: Jupyter Notebooks internal repository read access
+privileges:
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-browse
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/role-notebooks-write.yaml b/common/playbooks/templates/nexus/role-notebooks-write.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..38f5883ca5b9da706212ca57357a234ddd4a7eb8
--- /dev/null
+++ b/common/playbooks/templates/nexus/role-notebooks-write.yaml
@@ -0,0 +1,10 @@
+---
+id: {{ nexus_repository_notebooks_name }}-write
+name: {{ nexus_repository_notebooks_name }}-write
+description: Jupyter Notebooks internal repository write access
+privileges:
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-add
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-browse
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-delete
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-edit
+  - nx-repository-view-docker-{{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/user-binder.yaml b/common/playbooks/templates/nexus/user-binder.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..032c8fd512ade0a168a51f24957dd531f6315b64
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-binder.yaml
@@ -0,0 +1,9 @@
+---
+userId: binder
+firstName: Jupyter Binder
+lastName: Writer
+emailAddress: binder@{{ registry_binder_hostname }}
+password: {{ secrets['binder'] }}
+status: active
+roles:
+ - binder
diff --git a/common/playbooks/templates/nexus/user-notebooks-reader.yaml b/common/playbooks/templates/nexus/user-notebooks-reader.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0329bc03b0a0178f2146523b0107be53ed3b488d
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-notebooks-reader.yaml
@@ -0,0 +1,9 @@
+---
+userId: notebooks-reader
+firstName: Jupyter Notebooks
+lastName: Reader
+emailAddress: notebooks-reader@{{ registry_notebooks_hostname }}
+password: {{ secrets['notebooks-reader'] }}
+status: active
+roles:
+ - {{ nexus_repository_notebooks_name }}-read
diff --git a/common/playbooks/templates/nexus/user-notebooks-writer.yaml b/common/playbooks/templates/nexus/user-notebooks-writer.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..40c3fc0a6b38eeb5c4ad7ce0de104419b954dcb3
--- /dev/null
+++ b/common/playbooks/templates/nexus/user-notebooks-writer.yaml
@@ -0,0 +1,9 @@
+---
+userId: notebooks-writer
+firstName: Jupyter Notebooks
+lastName: Writer
+emailAddress: notebooks-writer@{{ registry_notebooks_hostname }}
+password: {{ secrets['notebooks-writer'] }}
+status: active
+roles:
+ - {{ nexus_repository_notebooks_name }}-write