diff --git a/Dockerfile b/Dockerfile
index 6bdbe7d9257bf9650d2be99cbe1a36e8797dea8a..3e4b0242a62982a8b07a28fb4a6d60f3812493ce 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ LABEL maintainer="Sebastian Höffner <shoeffner@tzi.de>"
 LABEL description="A small webapp to parse sentences using the DiaSpace grammar (University of Bremen) with OpenCCG."
 LABEL version="2.1"
 
-EXPOSE 8080
+EXPOSE 5000 8080
 
 ENV OPENCCG_HOME /openccg
 ENV PATH "$OPENCCG_HOME/bin:$PATH"
@@ -18,6 +18,11 @@ RUN curl -o openccg-0.9.5.tgz https://datapacket.dl.sourceforge.net/project/open
     && curl -O http://www.diaspace.uni-bremen.de/twiki/pub/DiaSpace/ReSources/english.zip \
     && unzip -d /english english.zip \
     && rm english.zip \
+# Download viz.js
+    && mkdir -p /app/webopenccg/static \
+    && curl -o /app/webopenccg/static/viz.js https://github.com/mdaines/viz.js/releases/download/v2.0.0/viz.js \
+    && curl -o /app/webopenccg/static/viz.js https://github.com/mdaines/viz.js/releases/download/v2.0.0/lite.render.js \
+# Install libraries etc.
     && apt-get update \
     && apt-get install -y python3 python3-pip graphviz libgraphviz-dev python-tk \
     && pip3 install flask \
@@ -26,19 +31,15 @@ RUN curl -o openccg-0.9.5.tgz https://datapacket.dl.sourceforge.net/project/open
                     pygraphviz \
     && (cd /openccg && ccg-build)
 
-COPY webopenccg /app
+COPY setup.py requirements.txt README.md /app/
+COPY webopenccg /app/webopenccg/
 COPY tests /tests
 
-ADD https://github.com/mdaines/viz.js/releases/download/v2.0.0/viz.js \
-    https://github.com/mdaines/viz.js/releases/download/v2.0.0/lite.render.js \
-    /app/static/
+RUN pip3 install /app
 
-RUN chmod +r /app/static/viz.js /app/static/lite.render.js
-
-WORKDIR /app
 CMD uwsgi --http :8080 \
           --uid www-data \
           --manage-script-name \
-          --module webopenccg \
+          --module webopenccg.webapp \
           --callable app \
           --master
diff --git a/Makefile b/Makefile
index 63e6674e4a35ff452d98e69cb352b5d4a1ea6f49..47b6b3d1e0c9af5940b373e0b90866fe4f0429ca 100644
--- a/Makefile
+++ b/Makefile
@@ -1,18 +1,21 @@
+CONTAINER_PATH=/app
+
 webopenccg/generated_openccg_parser.py: OpenCCG.ebnf
-	docker run --rm --name web-openccg -v $$(pwd):/tmp web-openccg tatsu --generate-parser /tmp/$< --outfile /tmp/$@
+	docker run --rm --name web-openccg-make -v $$(pwd):/tmp web-openccg tatsu --generate-parser /tmp/$< --outfile /tmp/$@
 
 .PHONY: build
 build:
 	docker build . -t web-openccg
-	# Copy over the viz.js files for the local development server (mounting would otherwise overwrite it)
-	docker run --rm --detach --name web-openccg web-openccg
-	docker cp web-openccg:/webopenccg/static ./webopenccg/static
-	docker stop web-openccg
+
+webopenccg/static/%.js: | build
+	docker run --rm --detach --name web-openccg-build-$(notdir $@) web-openccg
+	docker cp web-openccg-build-$(notdir $@):${CONTAINER_PATH}/$@ $$(pwd)/$@
+	docker stop web-openccg-build-$(notdir $@)
 
 .PHONY: run
-run:
-	docker run --rm -p 5000:5000 -v $$(pwd)/webopenccg:/app:ro web-openccg python3 /app/webopenccg.py
+run: webopenccg/static/viz.js webopenccg/static/lite.render.js
+	docker run --rm -p 5000:5000 -v $$(pwd)/webopenccg:${CONTAINER_PATH}/webopenccg:ro --name web-openccg web-openccg python3 -m webopenccg.webapp
 
 .PHONY: test
 test:
-	docker run --rm -v $$(pwd)/webopenccg:/app -v $$(pwd)/tests:/tests:ro web-openccg python3 -m unittest discover /tests
+	docker run --rm -v $$(pwd)/webopenccg:${CONTAINER_PATH}/webopenccg:ro -v $$(pwd)/tests:/tests:ro --name web-openccg-test web-openccg python3 -m unittest discover /tests
diff --git a/docker-compose.yml b/docker-compose.yml
index 5c98e1b7778fef09c28e5126ab6fa21cf9562d0d..625c8ef9efa01e4ea42f6a76d7fc60e4e917eda3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,6 +5,7 @@ services:
 
     openccg:
         build: .
+        image: web-openccg
         ports:
             - "80:8080"
         environment:
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b4f66071a774bf5a993bdc03b00d30fdcabffee9
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,4 @@
+requests
+tatsu
+pygraphviz
+flask
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..11fa05d01536da146df60bd85477b6923e90d4d4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+import re
+from pathlib import Path
+
+from setuptools import setup, find_packages
+
+
+REPOSITORY = 'https://github.com/shoeffner/web-openccg'
+README = re.sub(r' _(.+): ([^(http)].+)',
+                r' _\1: {}/blob/master/\2'.format(REPOSITORY),
+                Path('README.md').read_text())
+
+setup(
+    name='WebOpenCCG',
+    version='1.0.0',
+
+    description="A thin web wrapper around OpenCCG's wccg.",
+    long_description=README,
+    author='Sebastian Höffner',
+    author_email='shoeffner@tzi.de',
+    url='https://litmus.informatik.uni-bremen.de/openccg',
+    project_urls={
+        'Bug Tracker': f'{REPOSITORY}/issues',
+        'Documentation': f'{REPOSITORY}/tree/master/README.md',
+        'Source Code': f'{REPOSITORY}/tree/master',
+    },
+
+    install_requires=Path('requirements.txt').read_text(),
+
+    packages=find_packages(),
+    package_data={
+        'webopenccg': ['static/*', 'templates/*']
+    },
+
+    entry_points={
+        'console_scripts': [
+            'webopenccg = webopenccg.webapp:app.run'
+        ]
+    },
+
+    test_suite="tests",
+
+    classifiers=[
+        'Development Status :: 3 - Alpha',
+        'Intended Audience :: Science/Research',
+        'Natural Language :: English',
+        'Programming Language :: Python :: 3.7',
+    ],
+    license='MIT'
+)
diff --git a/tests/test_wccg.py b/tests/test_wccg.py
index 4f7cd46eaa30c2fe63801746f3b3243218b48a6b..d908989829fb56487d13acad6bec8bbea46cb979 100644
--- a/tests/test_wccg.py
+++ b/tests/test_wccg.py
@@ -1,15 +1,10 @@
 import json
+import os
 import unittest
 
 from pathlib import Path
 
-# Shortcut to discover files inside the app instead of making it an installable
-# package.
-import os
-os.sys.path.append(Path(__file__).parent / '..' / 'app')
-
-
-import wccg  # noqa
+from webopenccg import wccg
 
 
 class CCGtoJSONTestCase(unittest.TestCase):
diff --git a/webopenccg/__init__.py b/webopenccg/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/webopenccg/wccg.py b/webopenccg/wccg.py
index 8703e9f0f71f4c755071c1d888ed36c8eb844686..c209a0a9dcab4ed0b6de3539919249ae0153c98f 100644
--- a/webopenccg/wccg.py
+++ b/webopenccg/wccg.py
@@ -6,7 +6,7 @@ import subprocess
 from tatsu.util import asjson
 from tatsu.model import ModelBuilderSemantics
 
-from generated_openccg_parser import OpenCCGParser
+from webopenccg.generated_openccg_parser import OpenCCGParser
 
 
 def parse(sentence):
diff --git a/webopenccg/webopenccg.py b/webopenccg/webapp.py
similarity index 97%
rename from webopenccg/webopenccg.py
rename to webopenccg/webapp.py
index 0af770083497953d6b618f42470f561104de5963..45aff47c8b6ab547795c621cba54a763cf8f7917 100644
--- a/webopenccg/webopenccg.py
+++ b/webopenccg/webapp.py
@@ -8,8 +8,8 @@ from flask import (Blueprint,
                    request,
                    url_for)
 
-import graphs
-import wccg
+from webopenccg import graphs
+from webopenccg import wccg
 
 
 bp = Blueprint('openccg', __name__,