diff --git a/envri-hub/ansible.cfg b/envri-hub/ansible.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..c3a73bec9aa17dbdd27c77947d8813866f7036e6
--- /dev/null
+++ b/envri-hub/ansible.cfg
@@ -0,0 +1,5 @@
+[defaults]
+inventory=inventory
+
+[diff]
+always=true
diff --git a/envri-hub/deploy.sh b/envri-hub/deploy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c244c78a23d9d210cce68cd671066c00875949ff
--- /dev/null
+++ b/envri-hub/deploy.sh
@@ -0,0 +1,26 @@
+#! /bin/bash -xe
+
+#
+# Deploy ENVRI-HUB VRE
+#
+
+ip=admin.envri-vre.cloud.cesnet.cz
+# wait for ping and ssh
+{
+	while ! ping -c 1 "$ip"; do sleep 5; done
+	ssh-keygen -R "$ip"
+	while ! ssh ubuntu@"$ip" -o ConnectTimeout=10 -o PreferredAuthentications=publickey -o StrictHostKeyChecking=no :; do sleep 10; done
+}
+
+# check ssh access
+ansible -m command -a 'uname -a' allnodes
+
+# wait cloud-init
+ansible -m shell -a 'while ! test -f /var/lib/cloud/instance/boot-finished; do sleep 2; done' allnodes
+
+# kubernetes
+# ansible-playbook playbooks/squid.yaml
+# ansible-playbook playbooks/cvmfs.yaml
+
+# wait for finish
+# while ansible -m command -a 'kubectl get pods --all-namespaces' master | tail -n +3 | grep -Ev ' (Running|Completed) '; do sleep 5; done
diff --git a/envri-hub/deployments/envri-hub.yaml b/envri-hub/deployments/envri-hub.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..033d2d763b6d16e5d695a2c95bc3e2628a1d4a74
--- /dev/null
+++ b/envri-hub/deployments/envri-hub.yaml
@@ -0,0 +1,124 @@
+---
+proxy:
+  service:
+    type: NodePort
+
+ingress:
+  enabled: true
+  annotations:
+    kubernetes.io/ingress.class: "nginx"
+    kubernetes.io/tls-acme: "true"
+  hosts:
+    - "{{ notebooks_hostname }}"
+  tls:
+    - hosts:
+        - "{{ notebooks_hostname }}"
+      secretName: acme-tls-envri-hub
+
+singleuser:
+  # keep resource limits in sync with:
+  # - profileList
+  storage:
+    capacity: 10Gi
+    dynamic:
+      pvcNameTemplate: claim-{userid}{servername}
+      volumeNameTemplate: vol-{userid}{servername}
+      storageClass: csi-sc-cinderplugin
+    extraVolumes:
+      # - name: cvmfs-host
+      #   hostPath:
+      #     path: /cvmfs
+      #     type: Directory
+    extraVolumeMounts:
+      # - name: cvmfs-host
+      #   mountPath: "/cvmfs:shared"
+  memory:
+    limit: 4G
+    guarantee: 128M
+  cpu:
+    limit: 2
+    guarantee: .2
+  defaultUrl: "/lab"
+  image:
+    name: eginotebooks/single-user
+    tag: "sha-0f4a63c"
+  profileList:
+    - display_name: Default environment - 4 GB RAM / 2 core
+      description: >
+        The default notebook environment includes Python, R, Julia and Octave kernels.
+      default: true
+      kubespawner_override:
+        args:
+          - "--CondaKernelSpecManager.env_filter='/opt/conda$'"
+
+hub:
+  db:
+    pvc:
+      storageClassName: csi-sc-cinderplugin
+  services:
+    status:
+      url: "http://status-web/"
+      admin: true
+  image:
+    name: eginotebooks/hub
+    tag: "sha-2fa0db6"
+  config:
+    Authenticator:
+      enable_auth_state: true
+      admin_users:
+        # valtri@civ.zcu.cz
+        - 52cc7599bd1553c9d63e34e4c90b7e84d44967490c28bb4c53fe97b0c881d677@egi.eu
+      allowed_groups:
+        - urn:mace:egi.eu:group:envri-hub-next-all#sso.egi.eu
+      claim_groups_key: "eduperson_entitlement"
+    EGICheckinAuthenticator:
+      checkin_host: "{{ secrets['checkin_host']}}"
+      authorize_url: "https://{{ secrets['checkin_host']}}/auth/realms/egi/protocol/openid-connect/auth"
+      token_url: "https://{{ secrets['checkin_host']}}/auth/realms/egi/protocol/openid-connect/token"
+      userdata_url: "https://{{ secrets['checkin_host']}}/auth/realms/egi/protocol/openid-connect/userinfo"
+      client_id: "{{ secrets['client_id'] }}"
+      client_secret: "{{ secrets['client_secret'] }}"
+      oauth_callback_url: "https://{{ notebooks_hostname }}/hub/oauth_callback"
+      # (unused, for JWT wrapper)
+      openid_configuration_url: "https://{{ secrets['checkin_host'] }}/.well-known/openid-configuration"
+      scope: ["openid", "profile", "email", "offline_access", "eduperson_entitlement"]
+      username_key: "sub"
+    JupyterHub:
+      admin_access: true
+      authenticate_prometheus: false
+      authenticator_class: egi_notebooks_hub.egiauthenticator.EGICheckinAuthenticator
+      spawner_class: egi_notebooks_hub.onedata.OnedataSpawner
+    OnedataSpawner:
+      http_timeout: 60
+      token_mount_path: "/var/run/secrets/oidc/"
+      args:
+        - "--FileCheckpoints.checkpoint_dir='/home/jovyan/.notebookCheckpoints'"
+    LabApp:
+      check_for_updates_class: jupyterlab.NeverCheckForUpdate
+  extraConfig:
+    egi-notebooks-welcome: |-
+      from egi_notebooks_hub.welcome import WelcomeHandler
+      c.JupyterHub.default_url = "/welcome"
+      c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)]
+  templatePaths:
+    - /egi-notebooks-hub/egi-templates
+  extraFiles:
+    welcome.html:
+      mountPath: /usr/local/share/jupyterhub/templates/welcome.html
+      stringData: |-
+{%- raw %}
+        {% extends "login.html" %}
+{% endraw %}
+    403.html:
+      mountPath: /usr/local/share/jupyterhub/templates/403.html
+      stringData: |-
+{%- raw %}
+        {% extends "error.html" %}
+        {% block main %}
+        <div class="error">
+          <h1>Unauthorized</h1>
+          <p>You don't have the correct entitlements to access this service.</p>
+          <p>If you think you should be granted access, please open an issue!</p>
+        </div>
+        {% endblock %}
+{% endraw %}
diff --git a/envri-hub/inventory/1-envri-hub.yaml b/envri-hub/inventory/1-envri-hub.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4f6426c9963da1f9bb63ee7b85e90f283d7b17ff
--- /dev/null
+++ b/envri-hub/inventory/1-envri-hub.yaml
@@ -0,0 +1,29 @@
+---
+# fip:
+#   hosts:
+#     147.251.245.108:
+
+ingress_0:
+  hosts:
+
+ingress:
+  hosts:
+
+master:
+  hosts:
+    10.0.0.11:
+    10.0.0.12:
+    10.0.0.13:
+
+nfs:
+  hosts:
+
+worker:
+  hosts:
+    10.0.0.70:
+    10.0.0.89:
+    10.0.0.251:
+    10.0.0.211:
+
+gpu:
+  hosts:
diff --git a/envri-hub/inventory/99-all.yaml b/envri-hub/inventory/99-all.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..23588a1b9ee2199a0eb99fc9a6e62f268d9026e6
--- /dev/null
+++ b/envri-hub/inventory/99-all.yaml
@@ -0,0 +1,20 @@
+---
+allnodes:
+  children:
+    master:
+    ingress:
+    nfs:
+    worker:
+    gpu:
+
+all:
+  vars:
+    ansible_become: yes
+    ansible_user: ubuntu
+    ansible_ssh_common_args: '-o ProxyCommand="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -W %h:%p -q ubuntu@admin.envri-vre.cloud.cesnet.cz" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
+
+    site_name: envri-hub
+    vault_mount_point: secrets/users/e1662e20-e34b-468c-b0ce-d899bc878364@egi.eu/envri-hub
+
+    notebooks_hostname: envri-vre.cloud.cesnet.cz
+    grafana_hostname: grafana.envri-vre.cloud.cesnet.cz
diff --git a/envri-hub/playbooks/files/etc b/envri-hub/playbooks/files/etc
new file mode 120000
index 0000000000000000000000000000000000000000..ed53b8742792e16bb4bae2ed49d02c79d79de146
--- /dev/null
+++ b/envri-hub/playbooks/files/etc
@@ -0,0 +1 @@
+../../../common/playbooks/files/etc
\ No newline at end of file
diff --git a/envri-hub/playbooks/k8s.yaml b/envri-hub/playbooks/k8s.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..4af01883d1de007fed0a1cdfa8785f008d3342b7
--- /dev/null
+++ b/envri-hub/playbooks/k8s.yaml
@@ -0,0 +1,110 @@
+---
+- name: Basic setup
+  hosts: allnodes
+  become: true
+  tasks:
+    - name: Add SSH keys
+      authorized_key:
+        user: ubuntu
+        state: present
+        key: '{{ item }}'
+      with_file:
+        - public_keys/jhradil
+        - public_keys/pailozian
+        - public_keys/sustr
+        - public_keys/valtri
+    - name: Site install packages
+      package:
+        name:
+          - atop
+          - git
+          - mc
+          - vim
+    - name: Site touch
+      file:
+        path: "/{{ site_name | upper }}"
+        state: touch
+        mode: 0644
+
+- name: K8s customization
+  hosts: master[0]
+  become: true
+  tasks:
+    - name: Site k8s cheat sheets
+      copy:
+        dest: /etc/profile.d/k8s-cheats.sh
+        src: files//etc/profile.d/k8s-cheats.sh
+        mode: preserve
+    - name: Wait for helm
+      command: helm version
+      register: result
+      until: result.rc == 0
+      retries: 20
+      delay: 10
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+      when: true
+    - name: Create custom fact directory
+      file:
+        path: "/etc/ansible/facts.d"
+        mode: 0755
+        recurse: true
+        state: "directory"
+    - name: Create helm repos custom fact
+      copy:
+        src: files/etc/ansible/facts.d/helm_repos.fact
+        dest: /etc/ansible/facts.d/helm_repos.fact
+        mode: 0755
+    - name: Reload custom facts
+      setup:
+        filter: ansible_local
+    - name: Cert-manager
+      vars:
+        version: 1.16.1
+        config: >-
+          --version={{ version }}
+          --set ingressShim.defaultIssuerName=letsencrypt-prod
+          --set ingressShim.defaultIssuerKind=ClusterIssuer
+          --set ingressShim.defaultIssuerGroup=cert-manager.io
+      shell: |-
+        helm status --namespace cert-manager certs-man
+        if [ $? -ne 0 ]; then
+            kubectl create namespace cert-manager
+            kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v{{ version }}/cert-manager.crds.yaml
+            helm repo add jetstack https://charts.jetstack.io
+            helm repo update
+            helm install --namespace cert-manager {{ config }} certs-man jetstack/cert-manager
+        else
+            helm upgrade --namespace cert-manager {{ config }} certs-man jetstack/cert-manager
+        fi
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      when: true
+    - name: Cluster issuer file
+      copy:
+        dest: /tmp/clusterissuer.yaml
+        mode: 0644
+        content: |
+          apiVersion: cert-manager.io/v1
+          kind: ClusterIssuer
+          metadata:
+            name: letsencrypt-prod
+          spec:
+            acme:
+              email: valtri@civ.zcu.cz
+              server: https://acme-v02.api.letsencrypt.org/directory
+              privateKeySecretRef:
+                name: cluster-issuer-account-key
+              # Add a single challenge solver, HTTP01 using nginx
+              solvers:
+              - http01:
+                  ingress:
+                    class: nginx
+    - name: Cluster issuer
+      command:
+        kubectl apply -f /tmp/clusterissuer.yaml
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      when: true
diff --git a/envri-hub/playbooks/notebooks.yaml b/envri-hub/playbooks/notebooks.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..35d5d4dc96cde9434844497255596ebe2c481d55
--- /dev/null
+++ b/envri-hub/playbooks/notebooks.yaml
@@ -0,0 +1,111 @@
+---
+- name: Notebooks deployments
+  hosts: master[0]
+  become: true
+  tasks:
+    - name: Configure helm repo
+      shell: |-
+        helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
+        helm repo add eginotebooks https://egi-federation.github.io/egi-notebooks-chart/
+        helm repo update
+      when: "'jupyterhub' not in ansible_local.helm_repos | map(attribute='name') | list or
+             'eginotebooks' not in ansible_local.helm_repos | map(attribute='name') | list"
+    - name: Get Secrets from Vault for notebooks
+      vars:
+        name: "{{ item | basename | splitext | first }}"
+      set_fact:
+        deployment_secrets: "{{ deployment_secrets|default({}) | combine({name: lookup('community.hashi_vault.hashi_vault',
+          (vault_mount_point, 'deployment-' + name)  | join('/'),  token_validate=false)}) }}"
+      with_fileglob:
+        - "../deployments/*.yaml"
+    - name: Debug Deployments Secrets
+      debug:
+        msg: "{{ item.key }} = {{ item.value }}"
+      loop: "{{ deployment_secrets | dict2items }}"
+    - name: Copy config file to master
+      vars:
+        name: "{{ item | basename | splitext | first }}"
+        secrets: "{{ deployment_secrets[name] }}"
+      template:
+        src: "{{ item }}"
+        dest: "/tmp/{{ item | basename }}"
+        mode: 0600
+      with_fileglob:
+        - "../deployments/*.yaml"
+    - name: Deploy/upgrade notebook instance
+      vars:
+        name: "{{ item | basename | splitext | first }}"
+        version: "3.2.1" # app 4.0.2 (2023-11-27)
+      shell: |-
+        helm status --namespace {{ name }} {{ name }}
+        if [ $? -ne 0 ]; then
+            helm install --create-namespace --namespace {{ name }} \
+                -f /tmp/{{ item | basename }} --version {{ version }} --timeout 2h \
+                 {{ name }} jupyterhub/jupyterhub
+        else
+            helm upgrade --version {{ version }} -f /tmp/{{ item | basename }} --timeout 2h \
+                --namespace {{ name }} {{ name }} jupyterhub/jupyterhub
+        fi
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      when: true
+      with_fileglob:
+        - "../deployments/*.yaml"
+    - name: Deploy/upgrade notebook monitoring instance
+      vars:
+        name: "{{ item | basename | splitext | first }}"
+        monitor_version: "0.3.1"
+      shell: |-
+        helm status --namespace {{ name }} {{ name }}-monitor
+        if [ $? -ne 0 ]; then
+            helm install --namespace {{ name }} \
+                -f /tmp/{{ item | basename }} --version {{ monitor_version }} \
+                {{ name }}-monitor eginotebooks/notebooks-monitor
+        else
+            helm upgrade --version {{ monitor_version }} \
+                -f /tmp/{{ item | basename }} --namespace {{ name }} \
+                {{ name }}-monitor eginotebooks/notebooks-monitor
+        fi
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      when: true
+      with_fileglob:
+        - "../deployments/*.yaml"
+    - name: Configure secrets management for the hub
+      vars:
+        name: "{{ item | basename | splitext | first }}"
+      shell: |-
+        kubectl apply -f - << EOF
+        ---
+        kind: Role
+        apiVersion: rbac.authorization.k8s.io/v1
+        metadata:
+          name: hub-secrets
+          namespace: {{ name }}
+        rules:
+          - apiGroups: [""]       # "" indicates the core API group
+            resources: ["secrets"]
+            verbs: ["get", "watch", "list", "create", "delete", "patch", "update"]
+        ---
+        kind: RoleBinding
+        apiVersion: rbac.authorization.k8s.io/v1
+        metadata:
+          name: hub-secrets
+          namespace: {{ name }}
+        subjects:
+          - kind: ServiceAccount
+            name: hub
+            namespace: {{ name }}
+        roleRef:
+          kind: Role
+          name: hub-secrets
+          apiGroup: rbac.authorization.k8s.io
+        EOF
+      environment:
+        KUBECONFIG: /etc/kubernetes/admin.conf
+        PATH: /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
+      when: true
+      with_fileglob:
+        - "../deployments/*.yaml"
diff --git a/envri-hub/playbooks/public_keys b/envri-hub/playbooks/public_keys
new file mode 120000
index 0000000000000000000000000000000000000000..6ef4918a9eb6aba6c6076f8e4d42570f35735d86
--- /dev/null
+++ b/envri-hub/playbooks/public_keys
@@ -0,0 +1 @@
+../../common/playbooks/public_keys
\ No newline at end of file