diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ef2aaba5b689f70c326690ba7e1f0d0e2635f750..6a1f24a6dec8bb47b9944e7956836fe74c475b4a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,13 +1,18 @@
 variables:
+  DOCKER_DIR: docker-images
+  DOCKER_TAR: ${DOCKER_DIR}/app.tar
+  IMAGE_NAME: ${CI_PROJECT_NAME}:${CI_COMMIT_REF_SLUG}
   NSHMP_LIB: nshmp-lib
   NSHMP_LIB_GIT: https://gitlab-ci-token:${CI_JOB_TOKEN}@code.usgs.gov/ghsc/nshmp/${NSHMP_LIB}.git
 
 stages:
   - test
+  - publish
 
 ####
 # Templates:
 #   - nshmp-lib: Download nshmp-lib repo
+#   - adjust-image-names: Update container image names
 ####
 .templates:
   nshmp-lib: &nshmp-lib |-
@@ -15,15 +20,24 @@ stages:
     rm -rf ${NSHMP_LIB};
     git clone ${NSHMP_LIB_GIT};
     cd ${CI_PROJECT_NAME};
+  adjust-image-names: &adjust-image-names
+    IMAGE_NAME=${IMAGE_NAME/:master/:latest};
+    INTERNAL_IMAGE_NAME=${CI_REGISTRY_IMAGE}/${IMAGE_NAME};
+
+####
+# Template: Devlopment tags
+####
+.dev-tags:
+  tags:
+    - development
 
 ####
 # Template: Common Gradle test
 ####
 .gradle:
+  extends: .dev-tags
   stage: test
   image: gradle:jdk11
-  tags:
-    - development
   only:
     - merge_request
     - master@ghsc/nshmp/nshmp-netcdf
@@ -58,3 +72,57 @@ Spotbugs Main:
     - .gradle
   script:
     - ./gradlew spotbugsMain;
+
+####
+# Build Docker image.
+# Globals:
+#   DOCKER_DIR - The directory to put Docker image
+#   DOCKER_TAR - The path to the Docker image tar
+#   IMAGE_NAME - Docker image name
+####
+Build Image:
+  stage: test
+  image: docker:stable
+  extends: .dev-tags
+  only:
+    - merge_request
+    - master@ghsc/nshmp/nshmp-netcdf
+    - tags@ghsc/nshmp/nshmp-netcdf
+  before_script:
+    - *adjust-image-names
+    - rm -rf ${DOCKER_DIR}
+    - apk add git;
+    - *nshmp-lib
+    - mv ../${NSHMP_LIB} .
+  script:
+    - mkdir ${DOCKER_DIR}
+    - docker build -t local/${IMAGE_NAME} .
+    - docker save local/${IMAGE_NAME} > ${DOCKER_TAR}
+  artifacts:
+    paths:
+      - ${DOCKER_DIR}
+
+####
+# Publish Docker image to GitLab registry.
+# Globals:
+#   DOCKER_TAR - The path to the Docker image tar
+#   INTERNAL_IMAGE_NAME - GitLab registry Docker image name
+#   IMAGE_NAME - Docker image nam
+####
+Publish Image:
+  stage: publish
+  image: docker:stable
+  extends: .dev-tags
+  only:
+    - master@ghsc/nshmp/nshmp-netcdf
+    - tags@ghsc/nshmp/nshmp-netcdf
+  before_script:
+    - *adjust-image-names
+  script:
+    - echo "${CHS_PASSWORD}" | docker login --username ${CHS_USERNAME} --password-stdin ${CODE_REGISTRY}
+    - docker load -i ${DOCKER_TAR}
+    - docker tag local/${IMAGE_NAME} ${INTERNAL_IMAGE_NAME}
+    - docker push ${INTERNAL_IMAGE_NAME}
+    - docker image rm local/${IMAGE_NAME}
+    - docker image rm ${INTERNAL_IMAGE_NAME}
+    - rm -rf /root/.docker/config.json