diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..651e4560b9b8c04fbaceedab05dc6c93868f7ead
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,5 @@
+root=true
+
+[*.{yml,yaml}]
+indent_size=2
+indent_style=space
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a9c6a834d8dd3f3482c5758fa50d6573c86af0bb
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,132 @@
+# CI configuration which tests ucamlookup against supported Django and Python
+# versions.
+#
+# GitLab CI does nto support Matrix builds in the traditional sense. Instead we
+# build up a matrix of test jobs using inheritance via "extends".
+#
+# See also: https://gitlab.com/gitlab-org/gitlab-ce/issues/19199
+
+# Test code coverage
+coverage:
+  extends: .test
+  variables:
+    TOX_ENVLIST: coverage
+    PYTHON_VERSION: "3.7"
+
+  # Look for the summary line output from coverage's text report. The
+  # parentheses are used to indicate which portion of the report contains the
+  # coverage percentage.
+  coverage: '/^TOTAL\s+\d+\s+\d+\s+(\d+)%$/'
+
+# Check for PEP8 violations
+flake8:
+  extends: .test
+  variables:
+    TOX_ENVLIST: flake8
+    PYTHON_VERSION: "3.7"
+
+# Run test suite against supported Python/Django combinations.
+python27-django111:
+  extends: .py27
+  variables:
+    TOX_DJANGO_FRAGMENT: "django111"
+
+python34-django111:
+  extends: .py34
+  variables:
+    TOX_DJANGO_FRAGMENT: "django111"
+
+python35-django111:
+  extends: .py35
+  variables:
+    TOX_DJANGO_FRAGMENT: "django111"
+
+python36-django111:
+  extends: .py36
+  variables:
+    TOX_DJANGO_FRAGMENT: "django111"
+
+python37-django111:
+  extends: .py37
+  variables:
+    TOX_DJANGO_FRAGMENT: "django111"
+
+python34-django20:
+  extends: .py34
+  variables:
+    TOX_DJANGO_FRAGMENT: "django20"
+
+python35-django20:
+  extends: .py35
+  variables:
+    TOX_DJANGO_FRAGMENT: "django20"
+
+python36-django20:
+  extends: .py36
+  variables:
+    TOX_DJANGO_FRAGMENT: "django20"
+
+python37-django20:
+  extends: .py37
+  variables:
+    TOX_DJANGO_FRAGMENT: "django20"
+
+python35-django21:
+  extends: .py35
+  variables:
+    TOX_DJANGO_FRAGMENT: "django21"
+
+python36-django21:
+  extends: .py36
+  variables:
+    TOX_DJANGO_FRAGMENT: "django21"
+
+python37-django21:
+  extends: .py37
+  variables:
+    TOX_DJANGO_FRAGMENT: "django21"
+
+# Template jobs which run tests in various Python versions.
+.py27:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "2.7"
+    TOX_PY_FRAGMENT: "py27"
+
+.py34:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.4"
+    TOX_PY_FRAGMENT: "py34"
+
+.py35:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.5"
+    TOX_PY_FRAGMENT: "py35"
+
+.py36:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.6"
+    TOX_PY_FRAGMENT: "py36"
+
+.py37:
+  extends: .test
+  variables:
+    PYTHON_VERSION: "3.7"
+    TOX_PY_FRAGMENT: "py37"
+
+# Base test template job.
+.test:
+  image: python:${PYTHON_VERSION}
+
+  script:
+    - pip install tox
+    - tox -e ${TOX_ENVLIST}
+
+  variables:
+    PYTHON_VERSION: replace-with-python-version
+    TOX_PY_FRAGMENT: replace-with-pyXY
+    TOX_DJANGO_FRAGMENT: replace-with-djangoXY
+    TOX_ENVLIST: "$TOX_PY_FRAGMENT-$TOX_DJANGO_FRAGMENT"
diff --git a/tox.ini b/tox.ini
index e443d5f914ab17cb82a1f578a5fbec0c47c81e68..1e75cd30a57b16dc2fc7566c607d703e8d5ad3d8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,8 +11,8 @@
 # Envs which should be run by default. This will execute a matrix of tests
 envlist =
     py{27,34,35,36}-django111
-    py{34,35,36, 37}-django20
-    py{35,36, 37}-django21
+    py{34,35,36,37}-django20
+    py{35,36,37}-django21
     coverage
     flake8
 # Allow overriding toxworkdir via environment variable