diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..3b8482a503ff6802c13466afeb92140d09812466
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,35 @@
+FROM jupyter/minimal-notebook
+USER root
+
+RUN mkdir /software
+WORKDIR /software
+
+RUN wget 'https://www.paraview.org/files/v5.7/ParaView-5.7.0-osmesa-MPI-Linux-Python3.7-64bit.tar.gz' \
+    && tar xf ParaView-5.7.0-osmesa-MPI-Linux-Python3.7-64bit.tar.gz \
+    && rm ParaView-5.7.0-osmesa-MPI-Linux-Python3.7-64bit.tar.gz
+
+ENV PATH="/software/ParaView-5.7.0-osmesa-MPI-Linux-Python3.7-64bit/bin:${PATH}"
+RUN apt-get update && apt-get -y install openssh-server net-tools \
+    && rm -rf /var/lib/apt/lists/*
+
+USER $NB_UID
+WORKDIR /home/$NB_USER/work
+
+COPY --chown=1000:100 pvlink /home/$NB_USER/.jupyter/pvlink
+COPY --chown=1000:100 pv_server.py /home/$NB_USER/work/
+COPY --chown=1000:100 remoterenderer_test.ipynb /home/$NB_USER/work/
+
+RUN pip install service_identity \
+    voila \
+    ipyvuetify voila-vuetify \
+    /home/$NB_USER/.jupyter/pvlink \
+    && jupyter labextension install --no-build \
+    @jupyter-widgets/jupyterlab-manager \
+    @jupyter-voila/jupyterlab-preview \
+    jupyter-vuetify \
+    /home/$NB_USER/.jupyter/pvlink/js \
+    && jupyter lab build && jupyter lab clean \
+    && npm cache clean --force \
+    && rm -rf $CONDA_DIR/share/jupyter/lab/staging \
+    && rm -rf /home/$NB_USER/.cache/yarn \
+    && rm -rf /home/$NB_USER/.node-gyp
\ No newline at end of file
diff --git a/docker/pv_server.py b/docker/pv_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..d429c47016b11519b113d35c138c9fbc2be45744
--- /dev/null
+++ b/docker/pv_server.py
@@ -0,0 +1,62 @@
+# add paraview modules
+import sys
+sys.path.append('/home/grosch/Devel/install/ParaView-v5.6.0/build/lib/python3.6/site-packages/')
+
+# import to process args
+import os
+
+# import paraview modules.
+from paraview.web import pv_wslink
+from paraview.web import protocols as pv_protocols
+
+from paraview import simple
+from wslink import server
+
+try:
+    import argparse
+except ImportError:
+    # since  Python 2.6 and earlier don't have argparse, we simply provide
+    # the source for the same as _argparse and we use it instead.
+    from vtk.util import _argparse as argparse
+
+# =============================================================================
+# Create custom PVServerProtocol class to handle clients requests
+# =============================================================================
+
+class _DemoServer(pv_wslink.PVServerProtocol):
+    authKey = "wslink-secret"
+    def initialize(self):
+        # Bring used components
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebMouseHandler())
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPort())
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPortImageDelivery())
+        self.updateSecret(_DemoServer.authKey)
+
+        # Disable interactor-based render calls
+        simple.GetRenderView().EnableRenderOnInteraction = 0
+        simple.GetRenderView().Background = [0,0,0]
+        cone = simple.Cone()
+        simple.Show(cone)
+        simple.Render()
+
+        # Update interaction mode
+        pxm = simple.servermanager.ProxyManager()
+        interactionProxy = pxm.GetProxy('settings', 'RenderViewInteractionSettings')
+        interactionProxy.Camera3DManipulators = ['Rotate', 'Pan', 'Zoom', 'Pan', 'Roll', 'Pan', 'Zoom', 'Rotate', 'Zoom']
+
+# =============================================================================
+# Main: Parse args and start server
+# =============================================================================
+
+if __name__ == "__main__":
+    # Create argument parser
+    parser = argparse.ArgumentParser(description="ParaViewWeb Demo")
+
+    # Add default arguments
+    server.add_arguments(parser)
+
+    # Extract arguments
+    args = parser.parse_args()
+
+    # Start server
+    server.start_webserver(options=args, protocol=_DemoServer)
\ No newline at end of file
diff --git a/docker/pvlink/MANIFEST.in b/docker/pvlink/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..a5ba7c1212ed3cbf612464a290d05db4a796c8fa
--- /dev/null
+++ b/docker/pvlink/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include pvlink/static *.*
+include pvlink.json
diff --git a/docker/pvlink/README.md b/docker/pvlink/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d24623785198c7583b558ebb0f7e976ebad028de
--- /dev/null
+++ b/docker/pvlink/README.md
@@ -0,0 +1,35 @@
+pvlink
+===============================
+
+Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook
+
+Installation
+------------
+
+To install use pip:
+
+    $ pip install pvlink
+    $ jupyter nbextension enable --py --sys-prefix pvlink
+
+To install for jupyterlab
+
+    $ jupyter labextension install pvlink
+
+For a development installation (requires npm),
+
+    $ git clone https://github.com//pvlink.git
+    $ cd pvlink
+    $ pip install -e .
+    $ jupyter nbextension install --py --symlink --sys-prefix pvlink
+    $ jupyter nbextension enable --py --sys-prefix pvlink
+    $ jupyter labextension install js
+
+When actively developing your extension, build Jupyter Lab with the command:
+
+    $ jupyter lab --watch
+
+This take a minute or so to get started, but then allows you to hot-reload your javascript extension.
+To see a change, save your javascript, watch the terminal for an update.
+
+Note on first `jupyter lab --watch`, you may need to touch a file to get Jupyter Lab to open.
+
diff --git a/docker/pvlink/RELEASE.md b/docker/pvlink/RELEASE.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6bc7932c158377af05f436e11468dccc7ec070f
--- /dev/null
+++ b/docker/pvlink/RELEASE.md
@@ -0,0 +1,20 @@
+- To release a new version of pvlink on PyPI:
+
+Update _version.py (set release version, remove 'dev')
+git add the _version.py file and git commit
+`python setup.py sdist upload`
+`python setup.py bdist_wheel upload`
+`git tag -a X.X.X -m 'comment'`
+Update _version.py (add 'dev' and increment minor)
+git add and git commit
+git push
+git push --tags
+
+- To release a new version of pvlink on NPM:
+
+```
+# clean out the `dist` and `node_modules` directories
+git clean -fdx
+npm install
+npm publish
+```
\ No newline at end of file
diff --git a/docker/pvlink/js/README.md b/docker/pvlink/js/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2d42d8fbc010645bbc3768e2071ef06f75bb46c
--- /dev/null
+++ b/docker/pvlink/js/README.md
@@ -0,0 +1,11 @@
+Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook
+
+Package Install
+---------------
+
+**Prerequisites**
+- [node](http://nodejs.org/)
+
+```bash
+npm install --save pvlink
+```
diff --git a/docker/pvlink/js/lib/embed.js b/docker/pvlink/js/lib/embed.js
new file mode 100644
index 0000000000000000000000000000000000000000..84c6809d485106d85af765bfb60ac3293b4cb078
--- /dev/null
+++ b/docker/pvlink/js/lib/embed.js
@@ -0,0 +1,9 @@
+// Entry point for the unpkg bundle containing custom model definitions.
+//
+// It differs from the notebook bundle in that it does not need to define a
+// dynamic baseURL for the static assets and may load some css that would
+// already be loaded by the notebook otherwise.
+
+// Export widget models and views, and the npm package version number.
+module.exports = require('./remoterenderer.js');
+module.exports['version'] = require('../package.json').version;
diff --git a/docker/pvlink/js/lib/extension.js b/docker/pvlink/js/lib/extension.js
new file mode 100644
index 0000000000000000000000000000000000000000..bdbe3e4ca3e18b468c60e8025a73fea332971f04
--- /dev/null
+++ b/docker/pvlink/js/lib/extension.js
@@ -0,0 +1,25 @@
+// This file contains the javascript that is run when the notebook is loaded.
+// It contains some requirejs configuration and the `load_ipython_extension`
+// which is required for any notebook extension.
+//
+// Some static assets may be required by the custom widget javascript. The base
+// url for the notebook is not known at build time and is therefore computed
+// dynamically.
+__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/pvlink';
+
+
+// Configure requirejs
+if (window.require) {
+    window.require.config({
+        map: {
+            "*" : {
+                "pvlink": "nbextensions/pvlink/index",
+            }
+        }
+    });
+}
+
+// Export the required load_ipython_extension
+module.exports = {
+    load_ipython_extension: function() {}
+};
diff --git a/docker/pvlink/js/lib/index.js b/docker/pvlink/js/lib/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c04d96da084ffef45f7f012e6c2755888ff3d54
--- /dev/null
+++ b/docker/pvlink/js/lib/index.js
@@ -0,0 +1,3 @@
+// Export widget models and views, and the npm package version number.
+module.exports = require('./remoterenderer.js');
+module.exports['version'] = require('../package.json').version;
diff --git a/docker/pvlink/js/lib/labplugin.js b/docker/pvlink/js/lib/labplugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5318e98dcfc0ea740c7c6916b4b4d929009c520
--- /dev/null
+++ b/docker/pvlink/js/lib/labplugin.js
@@ -0,0 +1,16 @@
+var plugin = require('./index');
+var base = require('@jupyter-widgets/base');
+
+module.exports = {
+  id: 'pvlink',
+  requires: [base.IJupyterWidgetRegistry],
+  activate: function(app, widgets) {
+      widgets.registerWidget({
+          name: 'pvlink',
+          version: plugin.version,
+          exports: plugin
+      });
+  },
+  autoStart: true
+};
+
diff --git a/docker/pvlink/js/lib/remoterenderer.js b/docker/pvlink/js/lib/remoterenderer.js
new file mode 100644
index 0000000000000000000000000000000000000000..8fc7f085e032dcb44fea4bba794f56a9a3158d19
--- /dev/null
+++ b/docker/pvlink/js/lib/remoterenderer.js
@@ -0,0 +1,73 @@
+var widgets = require('@jupyter-widgets/base');
+var _ = require('lodash');
+
+import ParaViewWebClient from 'paraviewweb/src/IO/WebSocket/ParaViewWebClient';
+import RemoteRenderer from 'paraviewweb/src/NativeUI/Canvas/RemoteRenderer';
+import SizeHelper from "paraviewweb/src/Common/Misc/SizeHelper";
+import SmartConnect from 'wslink/src/SmartConnect';
+
+export var RemoteRendererModel = widgets.DOMWidgetModel.extend({
+  defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), {
+    _model_name: 'RemoteRendererModel',
+    _view_name: 'RemoteRendererView',
+    _model_module: 'pvlink',
+    _view_module: 'pvlink',
+    _model_module_version: '0.1.0',
+    _view_module_version: '0.1.0',
+  })
+});
+
+export var RemoteRendererView = widgets.DOMWidgetView.extend({
+  render: function () {
+    var that = this;
+    
+    // div to hold the canvas of the RemoteRenderer.
+    var render_div = document.createElement('div');
+    render_div.style.height = '100%';
+    render_div.style.width = '100%';
+    this.el.appendChild(render_div);
+
+    /* Get configuration for SmartConnect.
+    *  SmartConnect will establish a direct
+    *  WebSocket connection using Autobahn. 
+    */
+    var config = {
+      sessionURL: this.model.get('sessionURL'),
+      secret: this.model.get('authKey')
+    };
+    var smartConnect = SmartConnect.newInstance({ config: config });
+
+    smartConnect.onConnectionReady(function (connection) {
+      // Create the RemoteRenderer
+      var pvwClient = ParaViewWebClient.createClient(connection, [
+        'MouseHandler',
+        'ViewPort',
+        'ViewPortImageDelivery']
+      );
+      var renderer = new RemoteRenderer(pvwClient);
+      renderer.setContainer(render_div);
+      renderer.setView(that.model.get('viewID'));
+      renderer.onImageReady(function () {
+        // Resize when the renderer is placed within a widget.
+        if (that.el.style.width != '100%') {
+          that.el.style.width = '100%';
+          renderer.resize();
+        }
+        console.log("We are good.");
+      });
+
+      // Handle size changes when the entire window is resized.
+      SizeHelper.onSizeChange(function () {
+        renderer.resize();
+      });
+      SizeHelper.startListening();
+
+      // Explicit render called from python side.
+      that.model.on('change:_update', function () {
+        renderer.render(true);
+      }, that);
+    });
+
+    smartConnect.connect();
+  },
+});
diff --git a/docker/pvlink/js/package.json b/docker/pvlink/js/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..6a4417948970b58eb43e8393f1ad6640b80f6c80
--- /dev/null
+++ b/docker/pvlink/js/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "pvlink",
+  "version": "0.1.0",
+  "description": "Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook",
+  "author": "Alice Grosch",
+  "main": "lib/index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com//pvlink.git"
+  },
+  "keywords": [
+    "jupyter",
+    "widgets",
+    "ipython",
+    "ipywidgets",
+    "jupyterlab-extension"
+  ],
+  "files": [
+    "lib/**/*.js",
+    "dist/*.js"
+  ],
+  "scripts": {
+    "clean": "rimraf dist/",
+    "prepublish": "webpack",
+    "build": "webpack",
+    "watch": "webpack --watch --mode=development",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "devDependencies": {
+    "webpack": "^3.5.5",
+    "rimraf": "^2.6.1"
+  },
+  "dependencies": {
+    "@jupyter-widgets/base": "^1.1 || ^2",
+    "lodash": "^4.17.4",
+    "hammerjs": "^2.0.8",
+    "monologue.js": "^0.3.5",
+    "paraviewweb": "^3.2.12",
+    "wslink": "^0.1.12"
+  },
+  "jupyterlab": {
+    "extension": "lib/labplugin"
+  }
+}
diff --git a/docker/pvlink/js/webpack.config.js b/docker/pvlink/js/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..372bbe234cdc00cb05c77ebb80b94cdc5597b084
--- /dev/null
+++ b/docker/pvlink/js/webpack.config.js
@@ -0,0 +1,72 @@
+var path = require('path');
+var version = require('./package.json').version;
+
+// Custom webpack rules are generally the same for all webpack bundles, hence
+// stored in a separate local variable.
+var rules = [
+    { test: /\.css$/, use: ['style-loader', 'css-loader']}
+]
+
+
+module.exports = [
+    {// Notebook extension
+     //
+     // This bundle only contains the part of the JavaScript that is run on
+     // load of the notebook. This section generally only performs
+     // some configuration for requirejs, and provides the legacy
+     // "load_ipython_extension" function which is required for any notebook
+     // extension.
+     //
+        entry: './lib/extension.js',
+        output: {
+            filename: 'extension.js',
+            path: path.resolve(__dirname, '..', 'pvlink', 'static'),
+            libraryTarget: 'amd'
+        }
+    },
+    {// Bundle for the notebook containing the custom widget views and models
+     //
+     // This bundle contains the implementation for the custom widget views and
+     // custom widget.
+     // It must be an amd module
+     //
+        entry: './lib/index.js',
+        output: {
+            filename: 'index.js',
+            path: path.resolve(__dirname, '..', 'pvlink', 'static'),
+            libraryTarget: 'amd'
+        },
+        devtool: 'source-map',
+        module: {
+            rules: rules
+        },
+        externals: ['@jupyter-widgets/base']
+    },
+    {// Embeddable pvlink bundle
+     //
+     // This bundle is generally almost identical to the notebook bundle
+     // containing the custom widget views and models.
+     //
+     // The only difference is in the configuration of the webpack public path
+     // for the static assets.
+     //
+     // It will be automatically distributed by unpkg to work with the static
+     // widget embedder.
+     //
+     // The target bundle is always `dist/index.js`, which is the path required
+     // by the custom widget embedder.
+     //
+        entry: './lib/embed.js',
+        output: {
+            filename: 'index.js',
+            path: path.resolve(__dirname, 'dist'),
+            libraryTarget: 'amd',
+            publicPath: 'https://unpkg.com/pvlink@' + version + '/dist/'
+        },
+        devtool: 'source-map',
+        module: {
+            rules: rules
+        },
+        externals: ['@jupyter-widgets/base']
+    }
+];
diff --git a/docker/pvlink/pvlink.json b/docker/pvlink/pvlink.json
new file mode 100644
index 0000000000000000000000000000000000000000..ce45f56f6b937a77442d9c1530747c16508750b3
--- /dev/null
+++ b/docker/pvlink/pvlink.json
@@ -0,0 +1,5 @@
+{
+  "load_extensions": {
+    "pvlink/extension": true
+  }
+}
diff --git a/docker/pvlink/pvlink/MANIFEST.in b/docker/pvlink/pvlink/MANIFEST.in
new file mode 100644
index 0000000000000000000000000000000000000000..a5ba7c1212ed3cbf612464a290d05db4a796c8fa
--- /dev/null
+++ b/docker/pvlink/pvlink/MANIFEST.in
@@ -0,0 +1,2 @@
+recursive-include pvlink/static *.*
+include pvlink.json
diff --git a/docker/pvlink/pvlink/README.md b/docker/pvlink/pvlink/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..d24623785198c7583b558ebb0f7e976ebad028de
--- /dev/null
+++ b/docker/pvlink/pvlink/README.md
@@ -0,0 +1,35 @@
+pvlink
+===============================
+
+Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook
+
+Installation
+------------
+
+To install use pip:
+
+    $ pip install pvlink
+    $ jupyter nbextension enable --py --sys-prefix pvlink
+
+To install for jupyterlab
+
+    $ jupyter labextension install pvlink
+
+For a development installation (requires npm),
+
+    $ git clone https://github.com//pvlink.git
+    $ cd pvlink
+    $ pip install -e .
+    $ jupyter nbextension install --py --symlink --sys-prefix pvlink
+    $ jupyter nbextension enable --py --sys-prefix pvlink
+    $ jupyter labextension install js
+
+When actively developing your extension, build Jupyter Lab with the command:
+
+    $ jupyter lab --watch
+
+This take a minute or so to get started, but then allows you to hot-reload your javascript extension.
+To see a change, save your javascript, watch the terminal for an update.
+
+Note on first `jupyter lab --watch`, you may need to touch a file to get Jupyter Lab to open.
+
diff --git a/docker/pvlink/pvlink/RELEASE.md b/docker/pvlink/pvlink/RELEASE.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6bc7932c158377af05f436e11468dccc7ec070f
--- /dev/null
+++ b/docker/pvlink/pvlink/RELEASE.md
@@ -0,0 +1,20 @@
+- To release a new version of pvlink on PyPI:
+
+Update _version.py (set release version, remove 'dev')
+git add the _version.py file and git commit
+`python setup.py sdist upload`
+`python setup.py bdist_wheel upload`
+`git tag -a X.X.X -m 'comment'`
+Update _version.py (add 'dev' and increment minor)
+git add and git commit
+git push
+git push --tags
+
+- To release a new version of pvlink on NPM:
+
+```
+# clean out the `dist` and `node_modules` directories
+git clean -fdx
+npm install
+npm publish
+```
\ No newline at end of file
diff --git a/docker/pvlink/pvlink/__init__.py b/docker/pvlink/pvlink/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb1c0b2cc962d83068190085d8d66ba7e11f4d72
--- /dev/null
+++ b/docker/pvlink/pvlink/__init__.py
@@ -0,0 +1,11 @@
+from ._version import version_info, __version__
+
+from .remoterenderer import *
+
+def _jupyter_nbextension_paths():
+    return [{
+        'section': 'notebook',
+        'src': 'static',
+        'dest': 'pvlink',
+        'require': 'pvlink/extension'
+    }]
diff --git a/docker/pvlink/pvlink/_version.py b/docker/pvlink/pvlink/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed74cad81423128d33c830d30531df907ca88a33
--- /dev/null
+++ b/docker/pvlink/pvlink/_version.py
@@ -0,0 +1,6 @@
+version_info = (0, 1, 0, 'alpha', 0)
+
+_specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''}
+
+__version__ = '%s.%s.%s%s'%(version_info[0], version_info[1], version_info[2],
+  '' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4]))
diff --git a/docker/pvlink/pvlink/js/README.md b/docker/pvlink/pvlink/js/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..a2d42d8fbc010645bbc3768e2071ef06f75bb46c
--- /dev/null
+++ b/docker/pvlink/pvlink/js/README.md
@@ -0,0 +1,11 @@
+Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook
+
+Package Install
+---------------
+
+**Prerequisites**
+- [node](http://nodejs.org/)
+
+```bash
+npm install --save pvlink
+```
diff --git a/docker/pvlink/pvlink/js/lib/embed.js b/docker/pvlink/pvlink/js/lib/embed.js
new file mode 100644
index 0000000000000000000000000000000000000000..84c6809d485106d85af765bfb60ac3293b4cb078
--- /dev/null
+++ b/docker/pvlink/pvlink/js/lib/embed.js
@@ -0,0 +1,9 @@
+// Entry point for the unpkg bundle containing custom model definitions.
+//
+// It differs from the notebook bundle in that it does not need to define a
+// dynamic baseURL for the static assets and may load some css that would
+// already be loaded by the notebook otherwise.
+
+// Export widget models and views, and the npm package version number.
+module.exports = require('./remoterenderer.js');
+module.exports['version'] = require('../package.json').version;
diff --git a/docker/pvlink/pvlink/js/lib/extension.js b/docker/pvlink/pvlink/js/lib/extension.js
new file mode 100644
index 0000000000000000000000000000000000000000..bdbe3e4ca3e18b468c60e8025a73fea332971f04
--- /dev/null
+++ b/docker/pvlink/pvlink/js/lib/extension.js
@@ -0,0 +1,25 @@
+// This file contains the javascript that is run when the notebook is loaded.
+// It contains some requirejs configuration and the `load_ipython_extension`
+// which is required for any notebook extension.
+//
+// Some static assets may be required by the custom widget javascript. The base
+// url for the notebook is not known at build time and is therefore computed
+// dynamically.
+__webpack_public_path__ = document.querySelector('body').getAttribute('data-base-url') + 'nbextensions/pvlink';
+
+
+// Configure requirejs
+if (window.require) {
+    window.require.config({
+        map: {
+            "*" : {
+                "pvlink": "nbextensions/pvlink/index",
+            }
+        }
+    });
+}
+
+// Export the required load_ipython_extension
+module.exports = {
+    load_ipython_extension: function() {}
+};
diff --git a/docker/pvlink/pvlink/js/lib/index.js b/docker/pvlink/pvlink/js/lib/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..8c04d96da084ffef45f7f012e6c2755888ff3d54
--- /dev/null
+++ b/docker/pvlink/pvlink/js/lib/index.js
@@ -0,0 +1,3 @@
+// Export widget models and views, and the npm package version number.
+module.exports = require('./remoterenderer.js');
+module.exports['version'] = require('../package.json').version;
diff --git a/docker/pvlink/pvlink/js/lib/labplugin.js b/docker/pvlink/pvlink/js/lib/labplugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5318e98dcfc0ea740c7c6916b4b4d929009c520
--- /dev/null
+++ b/docker/pvlink/pvlink/js/lib/labplugin.js
@@ -0,0 +1,16 @@
+var plugin = require('./index');
+var base = require('@jupyter-widgets/base');
+
+module.exports = {
+  id: 'pvlink',
+  requires: [base.IJupyterWidgetRegistry],
+  activate: function(app, widgets) {
+      widgets.registerWidget({
+          name: 'pvlink',
+          version: plugin.version,
+          exports: plugin
+      });
+  },
+  autoStart: true
+};
+
diff --git a/docker/pvlink/pvlink/js/lib/remoterenderer.js b/docker/pvlink/pvlink/js/lib/remoterenderer.js
new file mode 100644
index 0000000000000000000000000000000000000000..8fc7f085e032dcb44fea4bba794f56a9a3158d19
--- /dev/null
+++ b/docker/pvlink/pvlink/js/lib/remoterenderer.js
@@ -0,0 +1,73 @@
+var widgets = require('@jupyter-widgets/base');
+var _ = require('lodash');
+
+import ParaViewWebClient from 'paraviewweb/src/IO/WebSocket/ParaViewWebClient';
+import RemoteRenderer from 'paraviewweb/src/NativeUI/Canvas/RemoteRenderer';
+import SizeHelper from "paraviewweb/src/Common/Misc/SizeHelper";
+import SmartConnect from 'wslink/src/SmartConnect';
+
+export var RemoteRendererModel = widgets.DOMWidgetModel.extend({
+  defaults: _.extend(widgets.DOMWidgetModel.prototype.defaults(), {
+    _model_name: 'RemoteRendererModel',
+    _view_name: 'RemoteRendererView',
+    _model_module: 'pvlink',
+    _view_module: 'pvlink',
+    _model_module_version: '0.1.0',
+    _view_module_version: '0.1.0',
+  })
+});
+
+export var RemoteRendererView = widgets.DOMWidgetView.extend({
+  render: function () {
+    var that = this;
+    
+    // div to hold the canvas of the RemoteRenderer.
+    var render_div = document.createElement('div');
+    render_div.style.height = '100%';
+    render_div.style.width = '100%';
+    this.el.appendChild(render_div);
+
+    /* Get configuration for SmartConnect.
+    *  SmartConnect will establish a direct
+    *  WebSocket connection using Autobahn. 
+    */
+    var config = {
+      sessionURL: this.model.get('sessionURL'),
+      secret: this.model.get('authKey')
+    };
+    var smartConnect = SmartConnect.newInstance({ config: config });
+
+    smartConnect.onConnectionReady(function (connection) {
+      // Create the RemoteRenderer
+      var pvwClient = ParaViewWebClient.createClient(connection, [
+        'MouseHandler',
+        'ViewPort',
+        'ViewPortImageDelivery']
+      );
+      var renderer = new RemoteRenderer(pvwClient);
+      renderer.setContainer(render_div);
+      renderer.setView(that.model.get('viewID'));
+      renderer.onImageReady(function () {
+        // Resize when the renderer is placed within a widget.
+        if (that.el.style.width != '100%') {
+          that.el.style.width = '100%';
+          renderer.resize();
+        }
+        console.log("We are good.");
+      });
+
+      // Handle size changes when the entire window is resized.
+      SizeHelper.onSizeChange(function () {
+        renderer.resize();
+      });
+      SizeHelper.startListening();
+
+      // Explicit render called from python side.
+      that.model.on('change:_update', function () {
+        renderer.render(true);
+      }, that);
+    });
+
+    smartConnect.connect();
+  },
+});
diff --git a/docker/pvlink/pvlink/js/package.json b/docker/pvlink/pvlink/js/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..1fab78cf58533be0fcbb6446ddc67ec1d888ced6
--- /dev/null
+++ b/docker/pvlink/pvlink/js/package.json
@@ -0,0 +1,44 @@
+{
+  "name": "pvlink",
+  "version": "0.1.0",
+  "description": "Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook",
+  "author": "Alice Grosch",
+  "main": "lib/index.js",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com//pvlink.git"
+  },
+  "keywords": [
+    "jupyter",
+    "widgets",
+    "ipython",
+    "ipywidgets",
+    "jupyterlab-extension"
+  ],
+  "files": [
+    "lib/**/*.js",
+    "dist/*.js"
+  ],
+  "scripts": {
+    "clean": "rimraf dist/",
+    "prepublish": "webpack",
+    "build": "webpack",
+    "watch": "webpack --watch --mode=development",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "devDependencies": {
+    "webpack": "^3.5.5",
+    "rimraf": "^2.6.1",
+    "hammerjs": "^2.0.8",
+    "monologue.js": "^0.3.5"
+  },
+  "dependencies": {
+    "@jupyter-widgets/base": "^1.1 || ^2",
+    "lodash": "^4.17.4",
+    "paraviewweb": "^3.2.12",
+    "wslink": "^0.1.12"
+  },
+  "jupyterlab": {
+    "extension": "lib/labplugin"
+  }
+}
diff --git a/docker/pvlink/pvlink/js/webpack.config.js b/docker/pvlink/pvlink/js/webpack.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..372bbe234cdc00cb05c77ebb80b94cdc5597b084
--- /dev/null
+++ b/docker/pvlink/pvlink/js/webpack.config.js
@@ -0,0 +1,72 @@
+var path = require('path');
+var version = require('./package.json').version;
+
+// Custom webpack rules are generally the same for all webpack bundles, hence
+// stored in a separate local variable.
+var rules = [
+    { test: /\.css$/, use: ['style-loader', 'css-loader']}
+]
+
+
+module.exports = [
+    {// Notebook extension
+     //
+     // This bundle only contains the part of the JavaScript that is run on
+     // load of the notebook. This section generally only performs
+     // some configuration for requirejs, and provides the legacy
+     // "load_ipython_extension" function which is required for any notebook
+     // extension.
+     //
+        entry: './lib/extension.js',
+        output: {
+            filename: 'extension.js',
+            path: path.resolve(__dirname, '..', 'pvlink', 'static'),
+            libraryTarget: 'amd'
+        }
+    },
+    {// Bundle for the notebook containing the custom widget views and models
+     //
+     // This bundle contains the implementation for the custom widget views and
+     // custom widget.
+     // It must be an amd module
+     //
+        entry: './lib/index.js',
+        output: {
+            filename: 'index.js',
+            path: path.resolve(__dirname, '..', 'pvlink', 'static'),
+            libraryTarget: 'amd'
+        },
+        devtool: 'source-map',
+        module: {
+            rules: rules
+        },
+        externals: ['@jupyter-widgets/base']
+    },
+    {// Embeddable pvlink bundle
+     //
+     // This bundle is generally almost identical to the notebook bundle
+     // containing the custom widget views and models.
+     //
+     // The only difference is in the configuration of the webpack public path
+     // for the static assets.
+     //
+     // It will be automatically distributed by unpkg to work with the static
+     // widget embedder.
+     //
+     // The target bundle is always `dist/index.js`, which is the path required
+     // by the custom widget embedder.
+     //
+        entry: './lib/embed.js',
+        output: {
+            filename: 'index.js',
+            path: path.resolve(__dirname, 'dist'),
+            libraryTarget: 'amd',
+            publicPath: 'https://unpkg.com/pvlink@' + version + '/dist/'
+        },
+        devtool: 'source-map',
+        module: {
+            rules: rules
+        },
+        externals: ['@jupyter-widgets/base']
+    }
+];
diff --git a/docker/pvlink/pvlink/pv_server.py b/docker/pvlink/pvlink/pv_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..d429c47016b11519b113d35c138c9fbc2be45744
--- /dev/null
+++ b/docker/pvlink/pvlink/pv_server.py
@@ -0,0 +1,62 @@
+# add paraview modules
+import sys
+sys.path.append('/home/grosch/Devel/install/ParaView-v5.6.0/build/lib/python3.6/site-packages/')
+
+# import to process args
+import os
+
+# import paraview modules.
+from paraview.web import pv_wslink
+from paraview.web import protocols as pv_protocols
+
+from paraview import simple
+from wslink import server
+
+try:
+    import argparse
+except ImportError:
+    # since  Python 2.6 and earlier don't have argparse, we simply provide
+    # the source for the same as _argparse and we use it instead.
+    from vtk.util import _argparse as argparse
+
+# =============================================================================
+# Create custom PVServerProtocol class to handle clients requests
+# =============================================================================
+
+class _DemoServer(pv_wslink.PVServerProtocol):
+    authKey = "wslink-secret"
+    def initialize(self):
+        # Bring used components
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebMouseHandler())
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPort())
+        self.registerVtkWebProtocol(pv_protocols.ParaViewWebViewPortImageDelivery())
+        self.updateSecret(_DemoServer.authKey)
+
+        # Disable interactor-based render calls
+        simple.GetRenderView().EnableRenderOnInteraction = 0
+        simple.GetRenderView().Background = [0,0,0]
+        cone = simple.Cone()
+        simple.Show(cone)
+        simple.Render()
+
+        # Update interaction mode
+        pxm = simple.servermanager.ProxyManager()
+        interactionProxy = pxm.GetProxy('settings', 'RenderViewInteractionSettings')
+        interactionProxy.Camera3DManipulators = ['Rotate', 'Pan', 'Zoom', 'Pan', 'Roll', 'Pan', 'Zoom', 'Rotate', 'Zoom']
+
+# =============================================================================
+# Main: Parse args and start server
+# =============================================================================
+
+if __name__ == "__main__":
+    # Create argument parser
+    parser = argparse.ArgumentParser(description="ParaViewWeb Demo")
+
+    # Add default arguments
+    server.add_arguments(parser)
+
+    # Extract arguments
+    args = parser.parse_args()
+
+    # Start server
+    server.start_webserver(options=args, protocol=_DemoServer)
\ No newline at end of file
diff --git a/docker/pvlink/pvlink/pvlink.json b/docker/pvlink/pvlink/pvlink.json
new file mode 100644
index 0000000000000000000000000000000000000000..ce45f56f6b937a77442d9c1530747c16508750b3
--- /dev/null
+++ b/docker/pvlink/pvlink/pvlink.json
@@ -0,0 +1,5 @@
+{
+  "load_extensions": {
+    "pvlink/extension": true
+  }
+}
diff --git a/docker/pvlink/pvlink/pvlink/__init__.py b/docker/pvlink/pvlink/pvlink/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..cb1c0b2cc962d83068190085d8d66ba7e11f4d72
--- /dev/null
+++ b/docker/pvlink/pvlink/pvlink/__init__.py
@@ -0,0 +1,11 @@
+from ._version import version_info, __version__
+
+from .remoterenderer import *
+
+def _jupyter_nbextension_paths():
+    return [{
+        'section': 'notebook',
+        'src': 'static',
+        'dest': 'pvlink',
+        'require': 'pvlink/extension'
+    }]
diff --git a/docker/pvlink/pvlink/pvlink/_version.py b/docker/pvlink/pvlink/pvlink/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed74cad81423128d33c830d30531df907ca88a33
--- /dev/null
+++ b/docker/pvlink/pvlink/pvlink/_version.py
@@ -0,0 +1,6 @@
+version_info = (0, 1, 0, 'alpha', 0)
+
+_specifier_ = {'alpha': 'a', 'beta': 'b', 'candidate': 'rc', 'final': ''}
+
+__version__ = '%s.%s.%s%s'%(version_info[0], version_info[1], version_info[2],
+  '' if version_info[3]=='final' else _specifier_[version_info[3]]+str(version_info[4]))
diff --git a/docker/pvlink/pvlink/pvlink/remoterenderer.py b/docker/pvlink/pvlink/pvlink/remoterenderer.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f5937846cf4015962adf10f0de88493531dd6f7
--- /dev/null
+++ b/docker/pvlink/pvlink/pvlink/remoterenderer.py
@@ -0,0 +1,34 @@
+import ipywidgets as widgets
+from traitlets import Unicode, Int
+
+@widgets.register
+class RemoteRenderer(widgets.DOMWidget):
+    """ParaviewWeb RemoteRenderer for the Jupyter Notebook."""
+    _view_name = Unicode('RemoteRendererView').tag(sync=True)
+    _model_name = Unicode('RemoteRendererModel').tag(sync=True)
+    _view_module = Unicode('pvlink').tag(sync=True)
+    _model_module = Unicode('pvlink').tag(sync=True)
+    _view_module_version = Unicode('^0.1.0').tag(sync=True)
+    _model_module_version = Unicode('^0.1.0').tag(sync=True)
+
+    # URL to establish a websocket connection to.
+    sessionURL = Unicode('ws://localhost:8080/ws').tag(sync=True)
+    # Authentication key for clients to connect to the WebSocket.
+    authKey = Unicode('wslink-secret').tag(sync=True)
+    # ViewID of the view to connect to (only relevant 
+    # if multiple views exist on the server side).
+    viewID = Unicode("-1").tag(sync=True)
+    # Placeholder to force rendering updates on change.
+    _update = Int(0).tag(sync=True)
+
+
+    def __init__(self, sessionURL='ws://localhost:8080/ws', authKey='wslink-secret', viewID='-1', *args, **kwargs):
+        super(RemoteRenderer, self).__init__(*args, **kwargs)
+        self.sessionURL = sessionURL
+        self.authKey = authKey
+        self.viewID = viewID
+
+
+    def update_render(self):
+        """Explicit call for the renderer on the javascript side to render."""
+        self._update += 1
\ No newline at end of file
diff --git a/docker/pvlink/pvlink/remoterenderer.py b/docker/pvlink/pvlink/remoterenderer.py
new file mode 100644
index 0000000000000000000000000000000000000000..0f5937846cf4015962adf10f0de88493531dd6f7
--- /dev/null
+++ b/docker/pvlink/pvlink/remoterenderer.py
@@ -0,0 +1,34 @@
+import ipywidgets as widgets
+from traitlets import Unicode, Int
+
+@widgets.register
+class RemoteRenderer(widgets.DOMWidget):
+    """ParaviewWeb RemoteRenderer for the Jupyter Notebook."""
+    _view_name = Unicode('RemoteRendererView').tag(sync=True)
+    _model_name = Unicode('RemoteRendererModel').tag(sync=True)
+    _view_module = Unicode('pvlink').tag(sync=True)
+    _model_module = Unicode('pvlink').tag(sync=True)
+    _view_module_version = Unicode('^0.1.0').tag(sync=True)
+    _model_module_version = Unicode('^0.1.0').tag(sync=True)
+
+    # URL to establish a websocket connection to.
+    sessionURL = Unicode('ws://localhost:8080/ws').tag(sync=True)
+    # Authentication key for clients to connect to the WebSocket.
+    authKey = Unicode('wslink-secret').tag(sync=True)
+    # ViewID of the view to connect to (only relevant 
+    # if multiple views exist on the server side).
+    viewID = Unicode("-1").tag(sync=True)
+    # Placeholder to force rendering updates on change.
+    _update = Int(0).tag(sync=True)
+
+
+    def __init__(self, sessionURL='ws://localhost:8080/ws', authKey='wslink-secret', viewID='-1', *args, **kwargs):
+        super(RemoteRenderer, self).__init__(*args, **kwargs)
+        self.sessionURL = sessionURL
+        self.authKey = authKey
+        self.viewID = viewID
+
+
+    def update_render(self):
+        """Explicit call for the renderer on the javascript side to render."""
+        self._update += 1
\ No newline at end of file
diff --git a/docker/pvlink/pvlink/remoterenderer_test.ipynb b/docker/pvlink/pvlink/remoterenderer_test.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..420e318f3369a9a75e225a34af4a54f9d6bef3c8
--- /dev/null
+++ b/docker/pvlink/pvlink/remoterenderer_test.ipynb
@@ -0,0 +1,103 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pvlink import RemoteRenderer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "b3d897da337443239c15f5346e046e55",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "RemoteRenderer()"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "renderer = RemoteRenderer()\n",
+    "# renderer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from ipywidgets import Box"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {
+    "scrolled": false
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "4428cc6539af4c0eb23f822bd10873df",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Box(children=(RemoteRenderer(),), layout=Layout(height='500px'))"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "out = Box(children=[renderer])\n",
+    "out.layout.height = '500px'\n",
+    "out"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docker/pvlink/pvlink/setup.cfg b/docker/pvlink/pvlink/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..3c6e79cf31da1c0433d2fa666bf50b53f6359f26
--- /dev/null
+++ b/docker/pvlink/pvlink/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/docker/pvlink/pvlink/setup.py b/docker/pvlink/pvlink/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd55fd4ce7de4856a85fed780ad87b9d488e6958
--- /dev/null
+++ b/docker/pvlink/pvlink/setup.py
@@ -0,0 +1,180 @@
+from __future__ import print_function
+from setuptools import setup, find_packages, Command
+from setuptools.command.sdist import sdist
+from setuptools.command.build_py import build_py
+from setuptools.command.egg_info import egg_info
+from subprocess import check_call
+import os
+import sys
+import platform
+
+here = os.path.dirname(os.path.abspath(__file__))
+node_root = os.path.join(here, 'js')
+is_repo = os.path.exists(os.path.join(here, '.git'))
+
+npm_path = os.pathsep.join([
+    os.path.join(node_root, 'node_modules', '.bin'),
+                os.environ.get('PATH', os.defpath),
+])
+
+from distutils import log
+log.set_verbosity(log.DEBUG)
+log.info('setup.py entered')
+log.info('$PATH=%s' % os.environ['PATH'])
+
+LONG_DESCRIPTION = 'Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook'
+
+def js_prerelease(command, strict=False):
+    """decorator for building minified js/css prior to another command"""
+    class DecoratedCommand(command):
+        def run(self):
+            jsdeps = self.distribution.get_command_obj('jsdeps')
+            if not is_repo and all(os.path.exists(t) for t in jsdeps.targets):
+                # sdist, nothing to do
+                command.run(self)
+                return
+
+            try:
+                self.distribution.run_command('jsdeps')
+            except Exception as e:
+                missing = [t for t in jsdeps.targets if not os.path.exists(t)]
+                if strict or missing:
+                    log.warn('rebuilding js and css failed')
+                    if missing:
+                        log.error('missing files: %s' % missing)
+                    raise e
+                else:
+                    log.warn('rebuilding js and css failed (not a problem)')
+                    log.warn(str(e))
+            command.run(self)
+            update_package_data(self.distribution)
+    return DecoratedCommand
+
+def update_package_data(distribution):
+    """update package_data to catch changes during setup"""
+    build_py = distribution.get_command_obj('build_py')
+    # distribution.package_data = find_package_data()
+    # re-init build_py options which load package_data
+    build_py.finalize_options()
+
+
+class NPM(Command):
+    description = 'install package.json dependencies using npm'
+
+    user_options = []
+
+    node_modules = os.path.join(node_root, 'node_modules')
+
+    targets = [
+        os.path.join(here, 'pvlink', 'static', 'extension.js'),
+        os.path.join(here, 'pvlink', 'static', 'index.js')
+    ]
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def get_npm_name(self):
+        npmName = 'npm';
+        if platform.system() == 'Windows':
+            npmName = 'npm.cmd';
+            
+        return npmName;
+    
+    def has_npm(self):
+        npmName = self.get_npm_name();
+        try:
+            check_call([npmName, '--version'])
+            return True
+        except:
+            return False
+
+    def should_run_npm_install(self):
+        package_json = os.path.join(node_root, 'package.json')
+        node_modules_exists = os.path.exists(self.node_modules)
+        return self.has_npm()
+
+    def run(self):
+        has_npm = self.has_npm()
+        if not has_npm:
+            log.error("`npm` unavailable.  If you're running this command using sudo, make sure `npm` is available to sudo")
+
+        env = os.environ.copy()
+        env['PATH'] = npm_path
+
+        if self.should_run_npm_install():
+            log.info("Installing build dependencies with npm.  This may take a while...")
+            npmName = self.get_npm_name();
+            check_call([npmName, 'install'], cwd=node_root, stdout=sys.stdout, stderr=sys.stderr)
+            os.utime(self.node_modules, None)
+
+        for t in self.targets:
+            if not os.path.exists(t):
+                msg = 'Missing file: %s' % t
+                if not has_npm:
+                    msg += '\nnpm is required to build a development version of a widget extension'
+                raise ValueError(msg)
+
+        # update package data in case this created new files
+        update_package_data(self.distribution)
+
+version_ns = {}
+with open(os.path.join(here, 'pvlink', '_version.py')) as f:
+    exec(f.read(), {}, version_ns)
+
+setup_args = {
+    'name': 'pvlink',
+    'version': version_ns['__version__'],
+    'description': 'Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook',
+    'long_description': LONG_DESCRIPTION,
+    'include_package_data': True,
+    'data_files': [
+        ('share/jupyter/nbextensions/pvlink', [
+            'pvlink/static/extension.js',
+            'pvlink/static/index.js',
+            'pvlink/static/index.js.map',
+        ],),
+        ('etc/jupyter/nbconfig/notebook.d' ,['pvlink.json'])
+    ],
+    'install_requires': [
+        'ipywidgets>=7.0.0',
+        'wslink>=0.1.11',
+        'twisted>=19.2.1',
+    ],
+    'packages': find_packages(),
+    'zip_safe': False,
+    'cmdclass': {
+        'build_py': js_prerelease(build_py),
+        'egg_info': js_prerelease(egg_info),
+        'sdist': js_prerelease(sdist, strict=True),
+        'jsdeps': NPM,
+    },
+
+    'author': 'Alice Grosch',
+    'author_email': 'a.grosch@fz-juelich.de',
+    'url': 'https://github.com//pvlink',
+    'keywords': [
+        'ipython',
+        'jupyter',
+        'widgets',
+    ],
+    'classifiers': [
+        'Development Status :: 4 - Beta',
+        'Framework :: IPython',
+        'Intended Audience :: Developers',
+        'Intended Audience :: Science/Research',
+        'Topic :: Multimedia :: Graphics',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+    ],
+}
+
+setup(**setup_args)
diff --git a/docker/pvlink/setup.cfg b/docker/pvlink/setup.cfg
new file mode 100644
index 0000000000000000000000000000000000000000..3c6e79cf31da1c0433d2fa666bf50b53f6359f26
--- /dev/null
+++ b/docker/pvlink/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/docker/pvlink/setup.py b/docker/pvlink/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd55fd4ce7de4856a85fed780ad87b9d488e6958
--- /dev/null
+++ b/docker/pvlink/setup.py
@@ -0,0 +1,180 @@
+from __future__ import print_function
+from setuptools import setup, find_packages, Command
+from setuptools.command.sdist import sdist
+from setuptools.command.build_py import build_py
+from setuptools.command.egg_info import egg_info
+from subprocess import check_call
+import os
+import sys
+import platform
+
+here = os.path.dirname(os.path.abspath(__file__))
+node_root = os.path.join(here, 'js')
+is_repo = os.path.exists(os.path.join(here, '.git'))
+
+npm_path = os.pathsep.join([
+    os.path.join(node_root, 'node_modules', '.bin'),
+                os.environ.get('PATH', os.defpath),
+])
+
+from distutils import log
+log.set_verbosity(log.DEBUG)
+log.info('setup.py entered')
+log.info('$PATH=%s' % os.environ['PATH'])
+
+LONG_DESCRIPTION = 'Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook'
+
+def js_prerelease(command, strict=False):
+    """decorator for building minified js/css prior to another command"""
+    class DecoratedCommand(command):
+        def run(self):
+            jsdeps = self.distribution.get_command_obj('jsdeps')
+            if not is_repo and all(os.path.exists(t) for t in jsdeps.targets):
+                # sdist, nothing to do
+                command.run(self)
+                return
+
+            try:
+                self.distribution.run_command('jsdeps')
+            except Exception as e:
+                missing = [t for t in jsdeps.targets if not os.path.exists(t)]
+                if strict or missing:
+                    log.warn('rebuilding js and css failed')
+                    if missing:
+                        log.error('missing files: %s' % missing)
+                    raise e
+                else:
+                    log.warn('rebuilding js and css failed (not a problem)')
+                    log.warn(str(e))
+            command.run(self)
+            update_package_data(self.distribution)
+    return DecoratedCommand
+
+def update_package_data(distribution):
+    """update package_data to catch changes during setup"""
+    build_py = distribution.get_command_obj('build_py')
+    # distribution.package_data = find_package_data()
+    # re-init build_py options which load package_data
+    build_py.finalize_options()
+
+
+class NPM(Command):
+    description = 'install package.json dependencies using npm'
+
+    user_options = []
+
+    node_modules = os.path.join(node_root, 'node_modules')
+
+    targets = [
+        os.path.join(here, 'pvlink', 'static', 'extension.js'),
+        os.path.join(here, 'pvlink', 'static', 'index.js')
+    ]
+
+    def initialize_options(self):
+        pass
+
+    def finalize_options(self):
+        pass
+
+    def get_npm_name(self):
+        npmName = 'npm';
+        if platform.system() == 'Windows':
+            npmName = 'npm.cmd';
+            
+        return npmName;
+    
+    def has_npm(self):
+        npmName = self.get_npm_name();
+        try:
+            check_call([npmName, '--version'])
+            return True
+        except:
+            return False
+
+    def should_run_npm_install(self):
+        package_json = os.path.join(node_root, 'package.json')
+        node_modules_exists = os.path.exists(self.node_modules)
+        return self.has_npm()
+
+    def run(self):
+        has_npm = self.has_npm()
+        if not has_npm:
+            log.error("`npm` unavailable.  If you're running this command using sudo, make sure `npm` is available to sudo")
+
+        env = os.environ.copy()
+        env['PATH'] = npm_path
+
+        if self.should_run_npm_install():
+            log.info("Installing build dependencies with npm.  This may take a while...")
+            npmName = self.get_npm_name();
+            check_call([npmName, 'install'], cwd=node_root, stdout=sys.stdout, stderr=sys.stderr)
+            os.utime(self.node_modules, None)
+
+        for t in self.targets:
+            if not os.path.exists(t):
+                msg = 'Missing file: %s' % t
+                if not has_npm:
+                    msg += '\nnpm is required to build a development version of a widget extension'
+                raise ValueError(msg)
+
+        # update package data in case this created new files
+        update_package_data(self.distribution)
+
+version_ns = {}
+with open(os.path.join(here, 'pvlink', '_version.py')) as f:
+    exec(f.read(), {}, version_ns)
+
+setup_args = {
+    'name': 'pvlink',
+    'version': version_ns['__version__'],
+    'description': 'Displays the ParaviewWeb RemoteRenderer in a Jupyter Notebook',
+    'long_description': LONG_DESCRIPTION,
+    'include_package_data': True,
+    'data_files': [
+        ('share/jupyter/nbextensions/pvlink', [
+            'pvlink/static/extension.js',
+            'pvlink/static/index.js',
+            'pvlink/static/index.js.map',
+        ],),
+        ('etc/jupyter/nbconfig/notebook.d' ,['pvlink.json'])
+    ],
+    'install_requires': [
+        'ipywidgets>=7.0.0',
+        'wslink>=0.1.11',
+        'twisted>=19.2.1',
+    ],
+    'packages': find_packages(),
+    'zip_safe': False,
+    'cmdclass': {
+        'build_py': js_prerelease(build_py),
+        'egg_info': js_prerelease(egg_info),
+        'sdist': js_prerelease(sdist, strict=True),
+        'jsdeps': NPM,
+    },
+
+    'author': 'Alice Grosch',
+    'author_email': 'a.grosch@fz-juelich.de',
+    'url': 'https://github.com//pvlink',
+    'keywords': [
+        'ipython',
+        'jupyter',
+        'widgets',
+    ],
+    'classifiers': [
+        'Development Status :: 4 - Beta',
+        'Framework :: IPython',
+        'Intended Audience :: Developers',
+        'Intended Audience :: Science/Research',
+        'Topic :: Multimedia :: Graphics',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.7',
+        'Programming Language :: Python :: 3',
+        'Programming Language :: Python :: 3.3',
+        'Programming Language :: Python :: 3.4',
+        'Programming Language :: Python :: 3.5',
+        'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+    ],
+}
+
+setup(**setup_args)
diff --git a/docker/remoterenderer_test.ipynb b/docker/remoterenderer_test.ipynb
new file mode 100644
index 0000000000000000000000000000000000000000..e6d7171c7f03e3c0e881562c592393e4b470df47
--- /dev/null
+++ b/docker/remoterenderer_test.ipynb
@@ -0,0 +1,86 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pvlink import RemoteRenderer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "renderer = RemoteRenderer()\n",
+    "# renderer"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from ipywidgets import Box"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "36e64b966b4d47e49e00bb93c60714aa",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Box(children=(RemoteRenderer(),), layout=Layout(height='500px'))"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "out = Box(children=[renderer])\n",
+    "out.layout.height = '500px'\n",
+    "out"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.9"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}