diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 6bf9dca1112..e42a3632e79 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -211,11 +211,12 @@ export default class Clusters {
     }
   }
 
-  installApplication(appId) {
+  installApplication(data) {
+    const appId = data.id;
     this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
     this.store.updateAppProperty(appId, 'requestReason', null);
 
-    this.service.installApplication(appId)
+    this.service.installApplication(appId, data.params)
       .then(() => {
         this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
       })
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index fae580c091b..ff4fd0b118c 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -52,6 +52,16 @@
         type: String,
         required: false,
       },
+      disableInstallButton: {
+        type: Boolean,
+        required: false,
+        default: false,
+      },
+      installApplicationRequestParams: {
+        type: Object,
+        required: false,
+        default: () => ({}),
+      },
     },
     computed: {
       rowJsClass() {
@@ -67,7 +77,7 @@
         // Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
         // we already made a request to install and are just waiting for the real-time
         // to sync up.
-        return (this.status !== APPLICATION_INSTALLABLE
+        return this.disableInstallButton || (this.status !== APPLICATION_INSTALLABLE
           && this.status !== APPLICATION_ERROR) ||
           this.requestStatus === REQUEST_LOADING ||
           this.requestStatus === REQUEST_SUCCESS;
@@ -109,7 +119,10 @@
     },
     methods: {
       installClicked() {
-        eventHub.$emit('installApplication', this.id);
+        eventHub.$emit('installApplication', {
+          id: this.id,
+          params: this.installApplicationRequestParams,
+        });
       },
     },
   };
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index e03db7b8974..5b127c7911c 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -37,11 +37,6 @@ export default {
       default: '',
     },
   },
-  data() {
-    return {
-      jupyterSuggestHostnameValue: '',
-    };
-  },
   computed: {
     generalApplicationDescription() {
       return sprintf(
@@ -132,14 +127,6 @@ export default {
     jupyterHostname() {
       return this.applications.jupyter.hostname;
     },
-    jupyterSuggestHostname() {
-      return `jupyter.${this.applications.ingress.externalIp}.xip.io`;
-    },
-  },
-  watch: {
-    jupyterSuggestHostname() {
-      this.jupyterSuggestHostnameValue = this.jupyterSuggestHostname;
-    },
   },
 };
 
@@ -305,6 +292,8 @@ export default {
           :status-reason="applications.jupyter.statusReason"
           :request-status="applications.jupyter.requestStatus"
           :request-reason="applications.jupyter.requestReason"
+          :disable-install-button="!ingressInstalled"
+          :install-application-request-params="{ hostname: applications.jupyter.hostname }"
         >
           
             
@@ -314,45 +303,23 @@ export default {
                 notebooks to a class of students, a corporate data science group,
                 or a scientific research group.`) }}
             
-            
-              
-            
-            
+
+            
               
-              
+              
                 {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
                 If you do so, point hostname to Ingress IP Address from above.`) }}
                  {
       spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
       expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
 
-      cluster.installApplication('helm');
+      cluster.installApplication({ id: 'helm' });
 
       expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
       expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
-      expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
+      expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
 
       getSetTimeoutPromise()
         .then(() => {
@@ -226,11 +226,11 @@ describe('Clusters', () => {
       spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
       expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
 
-      cluster.installApplication('ingress');
+      cluster.installApplication({ id: 'ingress' });
 
       expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
       expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
-      expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
+      expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
 
       getSetTimeoutPromise()
         .then(() => {
@@ -245,11 +245,11 @@ describe('Clusters', () => {
       spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
       expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
 
-      cluster.installApplication('runner');
+      cluster.installApplication({ id: 'runner' });
 
       expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
       expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
-      expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
+      expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
 
       getSetTimeoutPromise()
         .then(() => {
@@ -260,11 +260,29 @@ describe('Clusters', () => {
         .catch(done.fail);
     });
 
+    it('tries to install jupyter', (done) => {
+      spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
+      expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
+      cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } });
+
+      expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING);
+      expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+      expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname });
+
+      getSetTimeoutPromise()
+      .then(() => {
+        expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS);
+        expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
+      })
+      .then(done)
+      .catch(done.fail);
+    });
+
     it('sets error request status when the request fails', (done) => {
       spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
       expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
 
-      cluster.installApplication('helm');
+      cluster.installApplication({ id: 'helm' });
 
       expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
       expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
index 2c4707bb856..fed8f145ef1 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -174,7 +174,27 @@ describe('Application Row', () => {
 
       installButton.click();
 
-      expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
+      expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+        id: DEFAULT_APPLICATION_STATE.id,
+        params: {},
+      });
+    });
+
+    it('clicking install button when installApplicationRequestParams are provided emits event', () => {
+      spyOn(eventHub, '$emit');
+      vm = mountComponent(ApplicationRow, {
+        ...DEFAULT_APPLICATION_STATE,
+        status: APPLICATION_INSTALLABLE,
+        installApplicationRequestParams: { hostname: 'jupyter' },
+      });
+      const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
+
+      installButton.click();
+
+      expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
+        id: DEFAULT_APPLICATION_STATE.id,
+        params: { hostname: 'jupyter' },
+      });
     });
 
     it('clicking disabled install button emits nothing', () => {
@@ -191,6 +211,16 @@ describe('Application Row', () => {
 
       expect(eventHub.$emit).not.toHaveBeenCalled();
     });
+
+    it('is disabled when disableInstallButton prop is provided', () => {
+      vm = mountComponent(ApplicationRow, {
+        ...DEFAULT_APPLICATION_STATE,
+        status: APPLICATION_INSTALLING,
+        disableInstallButton: true,
+      });
+
+      expect(vm.installButtonDisabled).toEqual(true);
+    });
   });
 
   describe('Error block', () => {
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index d546543d273..4619322bd4b 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -22,6 +22,7 @@ describe('Applications', () => {
           ingress: { title: 'Ingress' },
           runner: { title: 'GitLab Runner' },
           prometheus: { title: 'Prometheus' },
+          jupyter: { title: 'JupyterHub' },
         },
       });
     });
@@ -41,6 +42,10 @@ describe('Applications', () => {
     it('renders a row for GitLab Runner', () => {
       expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
     });
+
+    it('renders a row for Jupyter', () => {
+      expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
+    });
   });
 
   describe('Ingress application', () => {
@@ -57,12 +62,11 @@ describe('Applications', () => {
               helm: { title: 'Helm Tiller' },
               runner: { title: 'GitLab Runner' },
               prometheus: { title: 'Prometheus' },
+              jupyter: { title: 'JupyterHub', hostname: '' },
             },
           });
 
-          expect(
-            vm.$el.querySelector('.js-ip-address').value,
-          ).toEqual('0.0.0.0');
+          expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
 
           expect(
             vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@@ -81,12 +85,11 @@ describe('Applications', () => {
               helm: { title: 'Helm Tiller' },
               runner: { title: 'GitLab Runner' },
               prometheus: { title: 'Prometheus' },
+              jupyter: { title: 'JupyterHub', hostname: '' },
             },
           });
 
-          expect(
-            vm.$el.querySelector('.js-ip-address').value,
-          ).toEqual('?');
+          expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
 
           expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
         });
@@ -101,6 +104,7 @@ describe('Applications', () => {
             ingress: { title: 'Ingress' },
             runner: { title: 'GitLab Runner' },
             prometheus: { title: 'Prometheus' },
+            jupyter: { title: 'JupyterHub', hostname: '' },
           },
         });
 
@@ -108,5 +112,66 @@ describe('Applications', () => {
         expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
       });
     });
+
+    describe('Jupyter application', () => {
+      describe('with ingress installed & jupyter not installed', () => {
+        it('renders hostname active input', () => {
+          vm = mountComponent(Applications, {
+            applications: {
+              helm: { title: 'Helm Tiller', status: 'installed' },
+              ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+              runner: { title: 'GitLab Runner' },
+              prometheus: { title: 'Prometheus' },
+              jupyter: { title: 'JupyterHub', hostname: '' },
+            },
+          });
+
+          expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
+        });
+        describe('with ingress & jupyter installed', () => {
+          it('renders readonly input', () => {
+            vm = mountComponent(Applications, {
+              applications: {
+                helm: { title: 'Helm Tiller', status: 'installed' },
+                ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+                runner: { title: 'GitLab Runner' },
+                prometheus: { title: 'Prometheus' },
+                jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+              },
+            });
+
+            expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
+          });
+        });
+      });
+
+      describe('without ingress installed', () => {
+        beforeEach(() => {
+          vm = mountComponent(Applications, {
+            applications: {
+              helm: { title: 'Helm Tiller' },
+              ingress: { title: 'Ingress' },
+              runner: { title: 'GitLab Runner' },
+              prometheus: { title: 'Prometheus' },
+              jupyter: { title: 'JupyterHub' },
+            },
+          });
+        });
+
+        it('does not render input', () => {
+          expect(vm.$el.querySelector('.js-hostname')).toBe(null);
+        });
+
+        it('renders disabled install button', () => {
+          expect(
+            vm.$el
+              .querySelector(
+                '.js-cluster-application-row-jupyter .js-cluster-application-install-button',
+              )
+              .getAttribute('disabled'),
+          ).toEqual('disabled');
+        });
+      });
+    });
   });
 });
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 6ae7a792329..b2b0ebf840b 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -1,4 +1,5 @@
 import {
+  APPLICATION_INSTALLED,
   APPLICATION_INSTALLABLE,
   APPLICATION_INSTALLING,
   APPLICATION_ERROR,
@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
           name: 'prometheus',
           status: APPLICATION_ERROR,
           status_reason: 'Cannot connect',
+        }, {
+          name: 'jupyter',
+          status: APPLICATION_INSTALLING,
+          status_reason: 'Cannot connect',
+        }],
+      },
+    },
+    '/gitlab-org/gitlab-shell/clusters/2/status.json': {
+      data: {
+        status: 'errored',
+        status_reason: 'Failed to request to CloudPlatform.',
+        applications: [{
+          name: 'helm',
+          status: APPLICATION_INSTALLED,
+          status_reason: null,
+        }, {
+          name: 'ingress',
+          status: APPLICATION_INSTALLED,
+          status_reason: 'Cannot connect',
+          external_ip: '1.1.1.1',
+        }, {
+          name: 'runner',
+          status: APPLICATION_INSTALLING,
+          status_reason: null,
+        },
+        {
+          name: 'prometheus',
+          status: APPLICATION_ERROR,
+          status_reason: 'Cannot connect',
+        }, {
+          name: 'jupyter',
+          status: APPLICATION_INSTALLABLE,
+          status_reason: 'Cannot connect',
         }],
       },
     },
@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
     '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
     '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
     '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
+    '/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
   },
 };
 
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 8028faf2f02..6854b016852 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
             requestStatus: null,
             requestReason: null,
           },
+          jupyter: {
+            title: 'JupyterHub',
+            status: mockResponseData.applications[4].status,
+            statusReason: mockResponseData.applications[4].status_reason,
+            requestStatus: null,
+            requestReason: null,
+            hostname: '',
+          },
         },
       });
     });
+
+    it('sets default hostname for jupyter when ingress has a ip address', () => {
+      const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+
+      store.updateStateFromServer(mockResponseData);
+
+      expect(
+        store.state.applications.jupyter.hostname,
+      ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`);
+    });
   });
 });