diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000000000000000000000000000000000000..448277025707d290ca92a8b83e17707cc2f73b78
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,3 @@
+-# Remove the `/arbor-source` prefix such that codecov.io can map the files
+-fixes:
+-  - "/arbor-source/::"
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6e6c5b9659a82c7a2f53a70cb884c9f28d65c30e..554ac2ea41ba6b424f31d49c09a1991f77f61444 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -5,29 +5,39 @@ stages:
   - build
   - allocate
   - test
+  - upload_reports
   - cleanup
 
 # Builds a docker image on kubernetes
-build:
+.build_docker_images:
   extends: .dind
   stage: build
   only: ['master', 'staging', 'trying']
   variables:
     GIT_SUBMODULE_STRATEGY: recursive
-    BUILD_DOCKERFILE: docker/build-env/Dockerfile
-    BUILD_IMAGE: $CI_REGISTRY_IMAGE/build-env:latest
-    DEPLOY_DOCKERFILE: docker/deploy/Dockerfile
-    DEPLOY_IMAGE: $CI_REGISTRY_IMAGE/deploy:$CI_COMMIT_SHA
-  before_script:
-    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
   script:
+    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
     - docker build -f $BUILD_DOCKERFILE --network=host --cache-from $BUILD_IMAGE --build-arg BUILDKIT_INLINE_CACHE=1 -t $BUILD_IMAGE .
     - docker push $BUILD_IMAGE
     - docker build -f $DEPLOY_DOCKERFILE --network=host --build-arg BUILD_ENV=$BUILD_IMAGE -t $DEPLOY_IMAGE .
     - docker push $DEPLOY_IMAGE
 
-# Executes the docker image on Daint via Sarus
-image: $CI_REGISTRY_IMAGE/deploy:$CI_COMMIT_SHA
+build release:
+  extends: .build_docker_images
+  variables:
+    BUILD_DOCKERFILE: ci/release/build.Dockerfile
+    BUILD_IMAGE: $CI_REGISTRY_IMAGE/release/build:latest
+    DEPLOY_DOCKERFILE: ci/release/deploy.Dockerfile
+    DEPLOY_IMAGE: $CI_REGISTRY_IMAGE/release/deploy:$CI_COMMIT_SHA
+
+build codecov:
+  extends: .build_docker_images
+  variables:
+    BUILD_DOCKERFILE: ci/codecov/build.Dockerfile
+    BUILD_IMAGE: $CI_REGISTRY_IMAGE/codecov/build:latest
+    DEPLOY_DOCKERFILE: ci/codecov/deploy.Dockerfile
+    DEPLOY_IMAGE: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
+
 
 # Some variables used for running on daint
 variables:
@@ -35,43 +45,121 @@ variables:
   USE_MPI: 'YES'
   DISABLE_AFTER_SCRIPT: 'YES'
   PULL_IMAGE: 'NO'
-  ALLOCATION_NAME: arbor-ci-$CI_PIPELINE_ID
   SLURM_CONSTRAINT: gpu
   SLURM_JOB_NUM_NODES: 2
   SLURM_PARTITION: normal
+  SLURM_TIMELIMIT: '15:00'
+
+### Release tests ###
+allocate release:
+  stage: allocate
+  image: $CI_REGISTRY_IMAGE/release/deploy:$CI_COMMIT_SHA
+  only: ['master', 'staging', 'trying']
+  extends: .daint_alloc
+  variables:
+    PULL_IMAGE: 'YES'
+    ALLOCATION_NAME: arbor-ci-release-$CI_PIPELINE_ID
+
+single node release:
+  extends: .daint
+  image: $CI_REGISTRY_IMAGE/release/deploy:$CI_COMMIT_SHA
+  only: ['master', 'staging', 'trying']
+  stage: test
+  resource_group: daint-job
+  script:
+    - unit
+    - unit-local
+    - unit-modcc
+  variables:
+    SLURM_JOB_NUM_NODES: 1
+    SLURM_NTASKS: 1
+    ALLOCATION_NAME: arbor-ci-release-$CI_PIPELINE_ID
+
+multi node release:
+  extends: .daint
+  image: $CI_REGISTRY_IMAGE/release/deploy:$CI_COMMIT_SHA
+  only: ['master', 'staging', 'trying']
+  stage: test
+  resource_group: daint-job
+  script:
+    - unit-mpi
+  variables:
+    SLURM_JOB_NUM_NODES: 2
+    SLURM_NTASKS: 2
+    ALLOCATION_NAME: arbor-ci-release-$CI_PIPELINE_ID
 
-allocate:
+deallocate release:
+  only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/release/deploy:$CI_COMMIT_SHA
+  stage: cleanup
+  extends: .daint_dealloc
+  variables:
+    ALLOCATION_NAME: arbor-ci-release-$CI_PIPELINE_ID
+
+### Codecov tests ###
+allocate codecov:
   stage: allocate
   only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
   extends: .daint_alloc
   variables:
     PULL_IMAGE: 'YES'
+    ALLOCATION_NAME: arbor-ci-codecov-$CI_PIPELINE_ID
 
-single node:
+single node codecov:
   extends: .daint
   only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
   stage: test
   resource_group: daint-job
   script:
+    - codecov_pre
     - unit
     - unit-local
     - unit-modcc
+    - codecov_post
   variables:
     SLURM_JOB_NUM_NODES: 1
     SLURM_NTASKS: 1
+    ALLOCATION_NAME: arbor-ci-codecov-$CI_PIPELINE_ID
+  artifacts:
+    paths:
+      - codecov-reports/
 
-multi node:
+multi node codecov:
   extends: .daint
   only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
   stage: test
   resource_group: daint-job
   script:
+    - codecov_pre
     - unit-mpi
+    - codecov_post
   variables:
     SLURM_JOB_NUM_NODES: 2
     SLURM_NTASKS: 2
+    ALLOCATION_NAME: arbor-ci-codecov-$CI_PIPELINE_ID
+  artifacts:
+    paths:
+      - codecov-reports/
 
-deallocate:
+upload codecov reports:
+  extends: .daint
   only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
+  stage: upload_reports
+  variables:
+    SLURM_JOB_NUM_NODES: 1
+    SLURM_NTASKS: 1
+    ALLOCATION_NAME: arbor-ci-codecov-$CI_PIPELINE_ID
+  script: upload_codecov
+  resource_group: daint-job
+
+deallocate codecov:
+  only: ['master', 'staging', 'trying']
+  image: $CI_REGISTRY_IMAGE/codecov/deploy:$CI_COMMIT_SHA
   stage: cleanup
-  extends: .daint_dealloc
\ No newline at end of file
+  extends: .daint_dealloc
+  variables:
+    ALLOCATION_NAME: arbor-ci-codecov-$CI_PIPELINE_ID
\ No newline at end of file
diff --git a/README.md b/README.md
index 71eea5c42ebfe510afa6a3f3cf69548255677bff..59fd442a878e26264a05f941a3a874de78e21f21 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![CI status](https://gitlab.com/cscs-ci/arbor-sim/arbor/badges/master/pipeline.svg)](https://gitlab.com/cscs-ci/arbor-sim/arbor/-/commits/master) [![Build Status](https://travis-ci.org/arbor-sim/arbor.svg?branch=master)](https://travis-ci.org/arbor-sim/arbor) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/arbor-sim/arbor) 
+[![CI status](https://gitlab.com/cscs-ci/arbor-sim/arbor/badges/master/pipeline.svg)](https://gitlab.com/cscs-ci/arbor-sim/arbor/-/commits/master) [![Build Status](https://travis-ci.org/arbor-sim/arbor.svg?branch=master)](https://travis-ci.org/arbor-sim/arbor) [![codecov](https://codecov.io/gl/cscs-ci:arbor-sim/arbor/branch/master/graph/badge.svg)](https://codecov.io/gl/cscs-ci:arbor-sim/arbor) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/arbor-sim/arbor) 
 
 # Arbor Library
 
diff --git a/ci/codecov/build.Dockerfile b/ci/codecov/build.Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..1cb095e3e1c544bfc44c04ac0f8e6ef8582397da
--- /dev/null
+++ b/ci/codecov/build.Dockerfile
@@ -0,0 +1,33 @@
+FROM nvidia/cuda:10.1-devel-ubuntu18.04
+
+WORKDIR /root
+
+ARG MPICH_VERSION=3.3.2
+
+ENV DEBIAN_FRONTEND noninteractive
+ENV FORCE_UNSAFE_CONFIGURE 1
+ENV MPICH_VERSION ${MPICH_VERSION}
+
+# Install basic tools
+RUN apt-get update -qq && apt-get install -qq -y --no-install-recommends \
+    build-essential lcov \
+    python \
+    git tar wget curl && \
+    rm -rf /var/lib/apt/lists/*
+
+# Install cmake
+RUN wget -qO- "https://cmake.org/files/v3.17/cmake-3.17.0-Linux-x86_64.tar.gz" | tar --strip-components=1 -xz -C /usr/local
+
+# Install MPICH ABI compatible with Cray's lib on Piz Daint
+RUN wget -q https://www.mpich.org/static/downloads/${MPICH_VERSION}/mpich-${MPICH_VERSION}.tar.gz && \
+    tar -xzf mpich-${MPICH_VERSION}.tar.gz && \
+    cd mpich-${MPICH_VERSION} && \
+    ./configure --disable-fortran && \
+    make install -j$(nproc) && \
+    rm -rf mpich-${MPICH_VERSION}.tar.gz mpich-${MPICH_VERSION}
+
+# Install bundle tooling for creating small Docker images
+RUN wget -q https://github.com/haampie/libtree/releases/download/v1.1.3/libtree_x86_64.tar.gz && \
+    tar -xzf libtree_x86_64.tar.gz && \
+    rm libtree_x86_64.tar.gz && \
+    ln -s /root/libtree/libtree /usr/local/bin/libtree
\ No newline at end of file
diff --git a/ci/codecov/deploy.Dockerfile b/ci/codecov/deploy.Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e2ba4d7bffc081199e25e64a41b51a7691a4e2d1
--- /dev/null
+++ b/ci/codecov/deploy.Dockerfile
@@ -0,0 +1,91 @@
+# Multistage build: here we import the current source code
+# into build environment image, build the project, bundle it
+# and then extract it into a small image that just contains
+# the binaries we need to run
+
+ARG BUILD_ENV
+
+ARG SOURCE_DIR=/arbor-source
+ARG BUILD_DIR=/arbor-build
+ARG BUNDLE_DIR=/root/arbor.bundle
+
+FROM $BUILD_ENV as builder
+
+ARG SOURCE_DIR
+ARG BUILD_DIR
+ARG BUNDLE_DIR
+
+# Build arbor
+COPY . ${SOURCE_DIR}
+
+# Build and bundle binaries
+RUN mkdir ${BUILD_DIR} && cd ${BUILD_DIR} && \
+    CC=mpicc CXX=mpicxx cmake ${SOURCE_DIR} \
+      -DARB_VECTORIZE=ON \
+      -DARB_ARCH=broadwell \
+      -DARB_WITH_PYTHON=OFF \
+      -DARB_WITH_MPI=ON \
+      -DARB_GPU=cuda \
+      -DCMAKE_BUILD_TYPE=Debug \
+      -DCMAKE_CXX_FLAGS="-g -O0 -fprofile-arcs -ftest-coverage" \
+      -DCMAKE_EXE_LINKER_FLAGS="-fprofile-arcs -ftest-coverage" \
+      -DCMAKE_BUILD_TYPE=Release \
+      -DCMAKE_INSTALL_PREFIX=/usr && \
+    make -j$(nproc) tests && \
+    libtree --chrpath \
+      -d ${BUNDLE_DIR} \
+      ${BUILD_DIR}/bin/modcc \
+      ${BUILD_DIR}/bin/unit \
+      ${BUILD_DIR}/bin/unit-local \
+      ${BUILD_DIR}/bin/unit-modcc \
+      ${BUILD_DIR}/bin/unit-mpi
+
+# Install some code cov related executables
+RUN libtree -d ${BUNDLE_DIR} $(which gcov) && \
+    cp -L ${SOURCE_DIR}/ci/codecov_pre ${SOURCE_DIR}/ci/codecov_post ${SOURCE_DIR}/ci/upload_codecov ${BUNDLE_DIR}/usr/bin && \
+    cp -L $(which lcov geninfo) ${BUNDLE_DIR}/usr/bin
+
+# In the build dir, remove everything except for gcno coverage files
+RUN mv ${BUILD_DIR} ${BUILD_DIR}-tmp && \
+  mkdir ${BUILD_DIR} && \
+  cd ${BUILD_DIR}-tmp && \
+  find -iname "*.gcno" -exec cp --parent \{\} ${BUILD_DIR} \; && \
+  rm -rf ${BUILD_DIR}-tmp
+
+# Only keep the sources for tests, not the git history
+RUN rm -rf ${SOURCE_DIR}/.git
+
+FROM ubuntu:18.04
+
+ARG SOURCE_DIR
+ARG BUILD_DIR
+ARG BUNDLE_DIR
+
+ENV SOURCE_DIR=$SOURCE_DIR
+ENV BUILD_DIR=$BUILD_DIR
+ENV BUNDLE_DIR=$BUNDLE_DIR
+
+# This is the only thing necessary really from nvidia/cuda's ubuntu18.04 runtime image
+ENV NVIDIA_VISIBLE_DEVICES all
+ENV NVIDIA_DRIVER_CAPABILITIES compute,utility
+ENV NVIDIA_REQUIRE_CUDA "cuda>=10.1 brand=tesla,driver>=384,driver<385 brand=tesla,driver>=396,driver<397 brand=tesla,driver>=410,driver<411"
+
+# Install perl to make lcov happy
+RUN apt-get update -qq && \
+    apt-get install --no-install-recommends -qq perl curl ca-certificates && \
+    rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder ${BUNDLE_DIR} ${BUNDLE_DIR}
+COPY --from=builder ${SOURCE_DIR} ${SOURCE_DIR}
+COPY --from=builder ${BUILD_DIR} ${BUILD_DIR}
+
+# Make it easy to call our binaries.
+ENV PATH="${BUNDLE_DIR}/usr/bin:$PATH"
+
+# Automatically print stacktraces on segfault
+ENV LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so
+
+RUN echo "${BUNDLE_DIR}/usr/lib/" > /etc/ld.so.conf.d/arbor.conf && ldconfig
+
+WORKDIR ${BUNDLE_DIR}/usr/bin
+
diff --git a/ci/codecov_post b/ci/codecov_post
new file mode 100755
index 0000000000000000000000000000000000000000..dafc9376d4577cae5c0b35cdbf82fe3eb9a333fe
--- /dev/null
+++ b/ci/codecov_post
@@ -0,0 +1,22 @@
+#!/bin/bash
+
+# In case of MPI tests running on a shared file system, we run into race conditions writing files
+# so here we generate some unique names for the codecov files.
+
+LOCAL_REPORTS="/codecov-reports"
+SHARED_REPORTS="$CI_PROJECT_DIR/codecov-reports"
+REPORT_NAME=`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1`
+mkdir -p "$SHARED_REPORTS"
+
+# Create coverage reports for code run
+echo "Combining reports"
+lcov --no-external --capture --base-directory $SOURCE_DIR --directory $BUILD_DIR --output-file "$LOCAL_REPORTS/run.info" &> /dev/null
+lcov --add-tracefile "$LOCAL_REPORTS/baseline-codecov.info" --add-tracefile "$LOCAL_REPORTS/run.info" --output-file "$LOCAL_REPORTS/combined.info" &> /dev/null
+lcov --remove "$LOCAL_REPORTS/combined.info" "$SOURCE_DIR/test/*" --output-file "$LOCAL_REPORTS/combined.info" &> /dev/null
+lcov --remove "$LOCAL_REPORTS/combined.info" "$SOURCE_DIR/CMakeCXXCompilerId.cpp" --output-file "$LOCAL_REPORTS/combined.info" &> /dev/null
+lcov --remove "$LOCAL_REPORTS/combined.info" "$SOURCE_DIR/ext/*" --output-file "$LOCAL_REPORTS/combined.info" &> /dev/null
+
+# Only keep our own source
+lcov --extract "$LOCAL_REPORTS/combined.info" "$SOURCE_DIR/*" --output-file "$LOCAL_REPORTS/combined.info" &> /dev/null
+
+cp "$LOCAL_REPORTS/combined.info" "$SHARED_REPORTS/codecov-$REPORT_NAME.info"
diff --git a/ci/codecov_pre b/ci/codecov_pre
new file mode 100755
index 0000000000000000000000000000000000000000..f6584d4ef7f16e0fdee87200178fb7df2e672fbb
--- /dev/null
+++ b/ci/codecov_pre
@@ -0,0 +1,10 @@
+#!/bin/bash
+
+# In case of MPI tests running on a shared file system, we run into race conditions writing files
+# so here we generate some unique names for the codecov files.
+
+LOCAL_REPORTS="/codecov-reports"
+mkdir -p "$LOCAL_REPORTS"
+
+echo "Generating baseline codecov report"
+lcov --no-external --capture --initial --base-directory $SOURCE_DIR --directory $BUILD_DIR --output-file "$LOCAL_REPORTS/baseline-codecov.info" &> /dev/null
diff --git a/docker/build-env/Dockerfile b/ci/release/build.Dockerfile
similarity index 90%
rename from docker/build-env/Dockerfile
rename to ci/release/build.Dockerfile
index 70b1ba5f772c704ce475760afe232bf500312401..67ba8be8aa21edc61f52bdbd9659f2675714be1e 100644
--- a/docker/build-env/Dockerfile
+++ b/ci/release/build.Dockerfile
@@ -2,7 +2,7 @@ FROM nvidia/cuda:10.1-devel-ubuntu18.04
 
 WORKDIR /root
 
-ARG MPICH_VERSION=3.1.4
+ARG MPICH_VERSION=3.3.2
 
 ENV DEBIAN_FRONTEND noninteractive
 ENV FORCE_UNSAFE_CONFIGURE 1
@@ -29,5 +29,5 @@ RUN wget -q https://www.mpich.org/static/downloads/${MPICH_VERSION}/mpich-${MPIC
 # Install bundle tooling for creating small Docker images
 RUN wget -q https://github.com/haampie/libtree/releases/download/v1.1.3/libtree_x86_64.tar.gz && \
     tar -xzf libtree_x86_64.tar.gz && \
-    rm libtree_x86_64.tar.gz  && \
-    ln -s /root/libtree/libtree /usr/local/bin/libtree
+    rm libtree_x86_64.tar.gz && \
+    ln -s /root/libtree/libtree /usr/local/bin/libtree
\ No newline at end of file
diff --git a/docker/deploy/Dockerfile b/ci/release/deploy.Dockerfile
similarity index 93%
rename from docker/deploy/Dockerfile
rename to ci/release/deploy.Dockerfile
index 443427df2bd93984fc9ec40d127c06f9487d65cb..69a65ff96e3b2107d27ca6c7f4f29339f7666505 100644
--- a/docker/deploy/Dockerfile
+++ b/ci/release/deploy.Dockerfile
@@ -16,7 +16,7 @@ ARG BUILD_DIR
 ARG BUNDLE_DIR
 
 # Build arbor
-COPY . /arbor
+COPY . $SOURCE_DIR
 
 # Build and bundle binaries
 RUN mkdir ${BUILD_DIR} && cd ${BUILD_DIR} && \
@@ -57,6 +57,9 @@ COPY --from=builder ${SOURCE_DIR} ${SOURCE_DIR}
 # Make it easy to call our binaries.
 ENV PATH="${BUNDLE_DIR}/usr/bin:$PATH"
 
+# Automatically print stacktraces on segfault
+ENV LD_PRELOAD=/lib/x86_64-linux-gnu/libSegFault.so
+
 RUN echo "${BUNDLE_DIR}/usr/lib/" > /etc/ld.so.conf.d/arbor.conf && ldconfig
 
 WORKDIR ${BUNDLE_DIR}/usr/bin
diff --git a/ci/upload_codecov b/ci/upload_codecov
new file mode 100755
index 0000000000000000000000000000000000000000..080d06e2916cf13d0e370ac546f4c9debbf85101
--- /dev/null
+++ b/ci/upload_codecov
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+# Combine all reports into a single one
+SHARED_REPORTS="$CI_PROJECT_DIR/codecov-reports"
+TRACE_FILES_ARGS=`find "$SHARED_REPORTS" -type f -iname '*.info' -exec sh -c "echo --add-tracefile {}" \;`
+lcov ${TRACE_FILES_ARGS} --output-file "$SHARED_REPORTS/combined.info"
+
+pushd $SOURCE_DIR
+bash <(curl -s https://codecov.io/bash) -f "$SHARED_REPORTS/combined.info" -t $CODECOV_TOKEN_GITHUB
+bash <(curl -s https://codecov.io/bash) -f "$SHARED_REPORTS/combined.info" -t $CODECOV_TOKEN_GITLAB
+popd
\ No newline at end of file