diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d70b33035c9a08176a558b9996e0c1ae2c29730d
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,38 @@
+stages:
+  - build_image
+  - test_image
+#  - deploy_image
+  - deploy_application
+  - registry_cleanup
+image: "${CI_REGISTRY_IMAGE}/cibuild:19.03.1"
+variables:
+  DOCKER_TLS_CERTDIR: "/certs"
+  IMAGES_TO_BUILD: "cspp_geo_grb cspp_geo_grb_notify tests/cspp_geo_grb_sender"
+services:
+  - docker:19.03.1-dind
+workflow:
+  rules:
+    # don't build tags right now
+    - if: $CI_COMMIT_TAG
+      when: never
+    - if: $CI_MERGE_REQUEST_ID
+      when: never
+    - when: always
+
+before_script:
+  - docker info
+  - docker login -u ${CI_REGISTRY_USER} -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
+
+build_cspp_geo_grb:
+  stage: build_image
+  tags:
+    - docker
+  script:
+    - ci/build_docker_image.sh
+
+registry_cleanup:
+  stage: registry_cleanup
+  tags:
+    - docker
+  script:
+    - ci/registry_cleanup.sh
diff --git a/ci/build_docker_image.sh b/ci/build_docker_image.sh
new file mode 100755
index 0000000000000000000000000000000000000000..efcb78f5cfff56bc664fd89d0378d92c9a353921
--- /dev/null
+++ b/ci/build_docker_image.sh
@@ -0,0 +1,34 @@
+#!/usr/bin/env bash
+
+docker_id() {
+    # Get docker ID for the specified image
+    docker inspect --format {{.Id}} $1 || echo ""
+}
+
+set -ex
+DOCKER_TAG="dev_$(date -u +%Y%m%d)_${CI_COMMIT_SHORT_SHA}"
+echo ${DOCKER_TAG}
+
+for image_dir in $IMAGES_TO_BUILD; do
+    # remove sub-directories (tests/)
+    image_name=$(basename $image_dir)
+    image_url="${CI_REGISTRY_IMAGE}/${image_name}"
+    echo "Building $image_dir"
+    docker pull ${image_url}:latest || true
+    pre_id=$(docker_id "${image_url}:latest")
+    docker build --cache-from ${image_url}:latest --tag "${image_url}:${DOCKER_TAG}" "${image_dir}"
+    post_id=$(docker_id "${image_url}:${DOCKER_TAG}")
+    if [ $pre_id == $post_id ]; then
+        echo "Image \"${image_name}\" has not been updated. Will not reupload."
+        # delete this tag, it isn't going to be used
+        docker image rm "${image_url}:${DOCKER_TAG}"
+    else
+        # tag this build as latest and upload both tags
+        echo "Image \"${image_name}\" has been updated. Tagging as latest and uploading..."
+        docker tag "${image_url}:${DOCKER_TAG}" "${image_url}:latest"
+        docker push ${image_url}:$DOCKER_TAG
+        docker push ${image_url}:latest
+    fi
+done
+
+echo "Done"
\ No newline at end of file
diff --git a/ci/cibuild/Dockerfile b/ci/cibuild/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..4a8d1eefe289ded0e9252dd902a88f2737f6c183
--- /dev/null
+++ b/ci/cibuild/Dockerfile
@@ -0,0 +1,5 @@
+# Custom docker-based build image with a few nice things added
+FROM docker:19.03.1
+RUN apk update && apk upgrade && \
+    apk add findutils bash curl jq && \
+    rm -rf /var/cache/apk/*
diff --git a/ci/registry_cleanup.sh b/ci/registry_cleanup.sh
new file mode 100755
index 0000000000000000000000000000000000000000..c54b00078cfcbe321446a999c5d2fc292ea01d58
--- /dev/null
+++ b/ci/registry_cleanup.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+PROJECT_NAME="cspp_geo%2Fcspp-geo-web-viewer"
+PROJ_URL="${CI_API_V4_URL}/projects/${PROJECT_NAME}"
+registries_json=$(curl -H "PRIVATE_TOKEN: ${API_TOKEN}" "${PROJ_URL}/registry/repositories")
+if [[ $? -ne 0 ]]; then
+    echo "Could not download list of container registries"
+    exit 1
+fi
+
+registry_id() {
+    echo $registries_json | name="$1" jq '.[] | select(.name == $ENV.name) | .id'
+}
+
+set -ex
+for image_dir in $IMAGES_TO_BUILD; do
+    image_name=$(basename $image_dir)
+    docker_reg_id=$(registry_id "$image_name")
+    # use Personal Access Token created by David Hoese and stored in CI environment variables
+    resp=$(curl -XDELETE -H "PRIVATE-TOKEN: ${API_TOKEN}" -d 'name_regex=dev_.*' -d "keep_n=5" "${PROJ_URL}/registry/repositories/${docker_reg_id}/tags")
+    if [[ $resp =~ "Unauthorized" ]]; then
+        echo $resp
+        exit 1
+    fi
+done
+
diff --git a/tests/cspp_geo_grb_sender/cadu_sender.py b/tests/cspp_geo_grb_sender/cadu_sender.py
index c7f06bd768ba88b470c9082e9027c5c97aa1889c..b84e2611fd40672216c43c45db6c9923549dc6d4 100644
--- a/tests/cspp_geo_grb_sender/cadu_sender.py
+++ b/tests/cspp_geo_grb_sender/cadu_sender.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
-"""
-This script feeds raw GRB CADUs to a receiving ingestor.
+"""Script to send raw GRB CADUs to a receiving ingestor.
+
 The CADUs are assumed to be grouped in files with naming
 convention CADU_5_010XXXX