diff --git a/Dockerfile b/Dockerfile
index eee6c126..6fdf32bd 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -3,9 +3,12 @@ FROM centos:centos7 as installing
 RUN yum update -y \
     && yum install -y wget tar unzip dos2unix \
     && yum clean all
+    
+ARG GROUPER_CONTAINER_VERSION
 
 ENV GROUPER_VERSION=2.4.0 \
-     JAVA_HOME=/usr/lib/jvm/zulu-8/
+     JAVA_HOME=/usr/lib/jvm/zulu-8/ \
+     GROUPER_CONTAINER_VERSION=$GROUPER_CONTAINER_VERSION
 
 # use Zulu package
 RUN rpm --import http://repos.azulsystems.com/RPM-GPG-KEY-azulsystems \
@@ -36,6 +39,9 @@ RUN echo 'Downloading Grouper Installer...' \
     && wget -q -O /opt/grouper/$GROUPER_VERSION/grouperInstaller.jar http://software.internet2.edu/grouper/release/$GROUPER_VERSION/grouperInstaller.jar
 
 COPY container_files/grouper.installer.properties /opt/grouper/$GROUPER_VERSION
+# Temporary morphString file used for building, not used in production
+COPY container_files/morphString.properties /opt/grouper/$GROUPER_VERSION
+
 
 RUN echo 'Installing Grouper'; \
     PATH=$PATH:$JAVA_HOME/bin; \
@@ -47,7 +53,7 @@ RUN echo 'Installing Grouper'; \
 FROM centos:centos7 as cleanup
 
 ENV GROUPER_VERSION=2.4.0 \
-    TOMCAT_VERSION=8.5.12 \    
+    TOMCAT_VERSION=8.5.42 \    
     TOMEE_VERSION=7.0.0
 
 COPY --from=installing /opt/grouper/$GROUPER_VERSION/grouperInstaller.jar /opt/grouper/
@@ -60,20 +66,20 @@ COPY --from=installing /opt/grouper/$GROUPER_VERSION/apache-tomcat-$TOMCAT_VERSI
 COPY --from=installing /opt/grouper/$GROUPER_VERSION/apache-tomee-webprofile-$TOMEE_VERSION/ /opt/tomee/
 COPY --from=installing /etc/alternatives/java /etc/alternatives/java
 
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.11.0/log4j-core-2.11.0.jar /opt/tomcat/bin
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.11.0/log4j-api-2.11.0.jar /opt/tomcat/bin
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.11.0/log4j-jul-2.11.0.jar /opt/tomcat/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.11.0/log4j-core-2.11.0.jar /opt/tomcat/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.11.0/log4j-api-2.11.0.jar /opt/tomcat/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.11.0/log4j-jul-2.11.0.jar /opt/tomcat/bin
 
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.11.0/log4j-core-2.11.0.jar /opt/tomee/bin
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.11.0/log4j-api-2.11.0.jar /opt/tomee/bin
-ADD http://central.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.11.0/log4j-jul-2.11.0.jar /opt/tomee/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-core/2.11.0/log4j-core-2.11.0.jar /opt/tomee/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-api/2.11.0/log4j-api-2.11.0.jar /opt/tomee/bin
+ADD https://repo1.maven.org/maven2/org/apache/logging/log4j/log4j-jul/2.11.0/log4j-jul-2.11.0.jar /opt/tomee/bin
 
 RUN cd /opt/grouper/grouper.apiBinary/; \
-    rm -fr ddlScripts/ grouper.lck grouper.log grouper.script grouper.tmp/ gshAddGrouperSystemWsGroup.gsh logs/
+    rm -fr ddlScripts/ grouper.properties grouper.lck grouper.log grouper.script grouper.tmp/ gshAddGrouperSystemWsGroup.gsh logs/
 
 RUN cd /opt/tomcat/; \
     chmod +r bin/log4j-*.jar; \
-    rm -fr webapps/docs/ webapps/examples/ webapps/host-manager/ webapps/manager/ logs/* temp/* work/* conf/logging.properties
+    rm -fr webapps/docs/ webapps/examples/ webapps/host-manager/ webapps/manager/ webapps/ROOT/ logs/* temp/* work/* conf/logging.properties
 
 RUN cd /opt/tomee/; \
     chmod +r bin/log4j-*.jar; \
@@ -86,17 +92,20 @@ COPY container_files/tomcat/ /opt/tomcat/
 COPY container_files/tomee/ /opt/tomee/
 
 
-FROM tier/shibboleth_sp
+FROM tier/shibboleth_sp:3.0.4_03122019
 
 LABEL author="tier-packaging@internet2.edu <tier-packaging@internet2.edu>" \
       Vendor="TIER" \
       ImageType="Grouper" \
       ImageName=$imagename \
       ImageOS=centos7
+      
+ARG GROUPER_CONTAINER_VERSION
 
 ENV JAVA_HOME=/usr/lib/jvm/zulu-8/ \
     PATH=$PATH:$JAVA_HOME/bin \
-    GROUPER_HOME=/opt/grouper/grouper.apiBinary
+    GROUPER_HOME=/opt/grouper/grouper.apiBinary \
+    GROUPER_CONTAINER_VERSION=$GROUPER_CONTAINER_VERSION
 
 RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime
 
@@ -138,6 +147,8 @@ WORKDIR /opt/grouper/grouper.apiBinary/
 
 EXPOSE 80 443
 
+HEALTHCHECK NONE
+
 ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
 
 CMD ["bin/gsh", "-loader"]
diff --git a/Jenkinsfile b/Jenkinsfile
index 8cb90349..3ed439e5 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -21,13 +21,15 @@ pipeline {
                         echo "You must define an imagename in common.bash"
                         currentBuild.result = 'FAILURE'
                      }
-                    sh 'mkdir -p bin'
-                    sh 'mkdir -p tmp'
+                    sh 'mkdir -p tmp && mkdir -p bin'
                     dir('tmp'){
                       git([ url: "https://github.internet2.edu/docker/util.git", credentialsId: "jenkins-github-access-token" ])
-                      sh 'ls'
-                      sh 'mv bin/* ../bin/.'
+                      sh 'rm -rf ../bin/*'
+                      sh 'mv ./bin/* ../bin/.'
                     }
+                    // Build and test scripts expect that 'tag' is present in common.bash. This is necessary for both Jenkins and standalone testing.
+                    // We don't care if there are more 'tag' assignments there. The latest one wins.
+                    sh "echo >> common.bash ; echo \"tag=\\\"${tag}\\\"\" >> common.bash ; echo common.bash ; cat common.bash"
                 }  
              }
         }    
@@ -48,15 +50,55 @@ pipeline {
         stage('Build') {
             steps {
                 script {
-                   docker.withRegistry('https://registry.hub.docker.com/',   "dockerhub-$maintainer") {
-                      def baseImg = docker.build("$maintainer/$imagename", "--no-cache .")
-                      // test the environment 
-                      sh 'cd test-compose && ./compose.sh'
-                      // bring down after testing
-                      sh 'cd test-compose && docker-compose down'
-                      baseImg.push("$tag")
-                   }
-               }
+                  try{
+                      docker.withRegistry('https://registry.hub.docker.com/',   "dockerhub-$maintainer") {
+                        baseImg = docker.build("$maintainer/$imagename", "--build-arg GROUPER_CONTAINER_VERSION=$tag --no-cache .")
+                      }
+                  } catch(error) {
+                     def error_details = readFile('./debug');
+                     def message = "BUILD ERROR: There was a problem building ${imagename}:${tag}. \n\n ${error_details}"
+                     sh "rm -f ./debug"
+                     handleError(message)
+                  }
+                }
+            }
+        }
+        stage('Test') {
+            steps {
+                script {
+                   try {
+                     sh 'bin/test.sh 2>&1 | tee debug ; test ${PIPESTATUS[0]} -eq 0'
+                   } catch (error) {
+                     def error_details = readFile('./debug')
+                     def message = "BUILD ERROR: There was a problem testing ${imagename}:${tag}. \n\n ${error_details}"
+                     sh "rm -f ./debug"
+                     handleError(message)
+                   } 
+                }    
+             }
+        }
+        
+        stage('Push') {
+            steps {
+                script {
+                      //// scan the image with clair
+                      // sh 'docker run -p 5432:5432 -d --name clairdb arminc/clair-db:latest'
+                      // sh 'docker run -p 6060:6060 --link clairdb:postgres -d --name clair arminc/clair-local-scan:v2.0.5'
+                      // sh 'curl -L -o clair-scanner https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64'
+                      // sh 'chmod 755 clair-scanner'
+                      // sh "./clair-scanner --ip 172.17.0.1 -r test.out $maintainer/$imagename:latest"
+                      //// test the environment
+                      // sh 'docker kill clairdb'
+                      // sh 'docker rm clairdb'
+                      // sh 'docker kill clair'
+                      // sh 'docker rm clair'
+                      // sh 'cd test-compose && ./compose.sh'
+                      //// bring down after testing
+                      //sh 'cd test-compose && docker-compose down'
+                      docker.withRegistry('https://registry.hub.docker.com/',   "dockerhub-$maintainer") {
+                        baseImg.push("$tag")
+                      }
+                  }
             }
         }
         stage('Notify') {
diff --git a/README.md b/README.md
index edd47bb5..691033e4 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,10 @@
-[![Build Status](https://jenkins.testbed.tier.internet2.edu/job/docker/job/grouper/job/master/badge/icon)](https://jenkins.testbed.tier.internet2.edu/job/docker/job/grouper/job/master/)
+[![Build Status](https://jenkins.testbed.tier.internet2.edu/buildStatus/icon?job=docker/grouper/master)](https://jenkins.testbed.tier.internet2.edu/buildStatus/icon?job=docker/grouper/master)
+
+
+
+This repository contains the source code used to create the InCommon Trusted Access Platform Grouper container. This standalone container is pushed to Dockerhub, various tags are available at the following URL: https://hub.docker.com/r/tier/grouper/tags. This repo can also be cloned and the container built locally. 
+
+The test-compose directory contains an example Grouper environment that starts up the various Grouper components. This example demonstrates how one might go about customizing and deploying their Grouper containers, using the TIER Grouper image as a base image. If evaluating Grouper, this is a good place to start. 
 
 
 # Upgrading from 2.3 to 2.4
@@ -6,12 +12,16 @@
 If upgrading from Grouper version 2.3 to 2.4 and using LDAP, modifications will be needed in subject.properties and grouper-loaders.proprties. Further details about this can be found at the following URL:
 https://spaces.at.internet2.edu/display/Grouper/vt-ldap+to+ldaptive+migration+for+LDAP+access
 
+In particular, in subject.properties, *.param.base.value should be adjusted to only contain the RDN (Relative Distinguished Name), not the full DN.  For example, "OU=People", not "OU=People,DC=domain,DC=edu"
+
 Additional upgrade information can be found at the following URL: https://spaces.at.internet2.edu/display/Grouper/v2.4+Upgrade+Instructions+from+v2.3
 
+
+
 # Supported tags
 
 -	latest
--   patch specific tags* (i.e. 2.3.0-a97-u41-w11-p16)
+-   patch specific tags with date timestamp* (i.e. 2.4.0-80-u51-w10-p11-20191118)
 
 \* Patch builds are routinely produced, but not necessarily for each patch release. The following monikers are used to construct the tag name:
  
@@ -19,6 +29,7 @@ Additional upgrade information can be found at the following URL: https://spaces
 - u = ui patch number
 - w = ws patch number
 - p = pspng patch number
+- last field = the year, month and day the image was built
 
 # Quick reference
 
@@ -165,7 +176,7 @@ For passing full files into the container, this container will make any secrets
 Docker Secrets can also be used to pass in strings, such as a database connection string password, into the component config. To pass in the Grouper database connection string, one might set the property and value as such:
 
 ```text
-hibernate.connection.password.elConfig = ${java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD_FILE') != null ? org.apache.commons.io.FileUtils.readFileToString(java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD_FILE'), "utf-8") : java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD') }
+hibernate.connection.password.elConfig = ${java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD_FILE') != null ? org.apache.commons.io.FileUtils.readFileToString(new("java.io.File", java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD_FILE')), "utf-8") : java.lang.System.getenv().get('GROUPER_DATABASE_PASSWORD') }
 ```
 
 Note that the default property name has been changed by appending `.elConfig`. (This causes Grouper to evaluate the string before saving the value.) The expression allows deployers to use a file containing only the database password as a Docker Secret and reference the file name via the `GROUPER_DATABASE_PASSWORD_FILE` environment property. This allows the config files to be baked into the image, if desired. Also, but not recommended, the database password could just be set in the Docker Service definition as an environment variable, `GROUPER_DATABASE_PASSWORD`. (Technically the expression can be broken up and just the desired functionality used.) Of course, using Grouper's MorphString functionality is supported and likely is the best option, but does require more effort in setting it up.
diff --git a/container_files/grouper.installer.properties b/container_files/grouper.installer.properties
index 3d2b2f6d..4e018728 100644
--- a/container_files/grouper.installer.properties
+++ b/container_files/grouper.installer.properties
@@ -14,9 +14,12 @@ grouperInstaller.default.installOrUpgrade = install
 ##
 ##############################
 
+grouperInstaller.autorun.forceInstallPatch = t
 grouperInstaller.autorun.installAllPatches = false
 grouperInstaller.autorun.installPatchesUpToACertainPatchLevel = true
-grouperInstaller.autorun.installPatchesUpToThesePatchLevels = grouper_v2_4_0_api_patch_1,grouper_v2_4_0_ui_patch_0,grouper_v2_4_0_ws_patch_0,grouper_v2_4_0_pspng_patch_0
+# 2.4.0-a93-u56-w11-p12-20200214-rc1
+grouperInstaller.autorun.installPatchesUpToThesePatchLevels = grouper_v2_4_0_api_patch_93,grouper_v2_4_0_ui_patch_56,grouper_v2_4_0_ws_patch_11,grouper_v2_4_0_pspng_patch_12
+
 
 #### set this to true to try to use defaults for everything.  Only things without default values will need to be set
 grouperInstaller.autorun.useDefaultsAsMuchAsAvailable = true
diff --git a/container_files/httpd/grouper-www.conf b/container_files/httpd/grouper-www.conf
index 368f7f6c..562e47c6 100644
--- a/container_files/httpd/grouper-www.conf
+++ b/container_files/httpd/grouper-www.conf
@@ -7,6 +7,10 @@ ProxyPass /grouper ajp://localhost:8009/grouper  timeout=2400
 ProxyPass /grouper-ws ajp://localhost:8009/grouper-ws  timeout=2400
 ProxyPass /grouper-ws-scim ajp://localhost:8009/grouper-ws-scim  timeout=2400
 
+RewriteEngine on
+RewriteCond %{REQUEST_URI} "^/$"
+RewriteRule . %{REQUEST_SCHEME}://%{HTTP_HOST}/grouper/ [R=301,L]
+
 <Location /grouper>
   AuthType shibboleth
   ShibRequestSetting requireSession 1
diff --git a/container_files/morphString.properties b/container_files/morphString.properties
new file mode 100644
index 00000000..52479216
--- /dev/null
+++ b/container_files/morphString.properties
@@ -0,0 +1 @@
+encrypt.key=fh43IRJ4Nf5
diff --git a/test-compose/data/Dockerfile b/test-compose/data/Dockerfile
index cbb3ca2d..e4ffb7f8 100644
--- a/test-compose/data/Dockerfile
+++ b/test-compose/data/Dockerfile
@@ -11,7 +11,7 @@ RUN yum install -y epel-release \
     && yum clean all \
     && rm -rf /var/cache/yum
 
-RUN mysql_install_db \
+RUN mysql_install_db --force \
     && chown -R mysql:mysql /var/lib/mysql/ \
     && sed -i 's/^\(bind-address\s.*\)/# \1/' /etc/my.cnf \
     && sed -i 's/^\(log_error\s.*\)/# \1/' /etc/my.cnf \
diff --git a/tests/clairscan.sh b/tests/clairscan.sh
new file mode 100755
index 00000000..99e36141
--- /dev/null
+++ b/tests/clairscan.sh
@@ -0,0 +1,72 @@
+#!/bin/bash
+
+startsecs=$(date +'%s')
+starttime=$(date +%H:%M:%S)
+
+echo 'starting:' ${starttime}
+
+#ensure clair-scanner
+if [ ! -s ./clair-scanner ]; then
+  echo 'downloading curl-scanner...'
+  curl -s -L -o ./clair-scanner https://github.com/arminc/clair-scanner/releases/download/v8/clair-scanner_linux_amd64
+  chmod 755 clair-scanner
+else
+  echo 'using existing clair-scanner...'
+fi
+
+#ensure DB container
+echo 'ensuring a fresh clair-db container...'
+docker ps | grep clair-db &>/dev/null
+if [ $? == "0" ]; then
+  echo 'removing existing clair-db container...'
+  docker kill db &>/dev/null
+  docker rm db &>/dev/null
+  docker run -p 5432:5432 -d --name db arminc/clair-db:latest &>/dev/null
+else
+  docker run -p 5432:5432 -d --name db arminc/clair-db:latest &>/dev/null
+fi
+sleep 30
+
+#ensure clair-scan container
+echo 'ensuring a fresh clair-scan container...'
+docker ps | grep clair-local-scan &>/dev/null
+if [ $? == "0" ]; then
+  echo 'removing existing clair-scan container...'
+  docker kill clair &>/dev/null
+  docker rm clair &>/dev/null
+  docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.5 &>/dev/null
+else
+  docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.5 &>/dev/null
+fi
+sleep 30
+
+#get ip where clair-scanner will listen
+clairip=$(/sbin/ifconfig docker0 | grep 'inet ' | sed 's/^[[:space:]]*//g' | cut -f 2 -d ' ' | sed 's/^[[:space:]]*//g')
+echo 'sending ip addr' ${clairip} 'to clair-scan server...'
+
+#run scan
+echo 'running scan...'
+./clair-scanner --ip ${clairip} $1
+retcode=$?
+
+#eval results
+if [ $retcode == '0' ]; then
+  echo 'scan found nothing.'
+else
+  echo 'scan found issues.'
+fi
+
+#cleanup
+echo 'removing temporary containers...'
+docker kill clair &>/dev/null
+docker rm clair &>/dev/null
+docker kill db &>/dev/null
+docker rm db &>/dev/null
+
+endsecs=$(date +'%s')
+endtime=$(date +%H:%M:%S)
+echo 'finished:' $endtime '  ('$((endsecs - startsecs)) 'seconds)'
+echo ""
+
+#pass along return code from scan
+exit $retcode
diff --git a/tests/main.bats b/tests/main.bats
new file mode 100644
index 00000000..0c18d122
--- /dev/null
+++ b/tests/main.bats
@@ -0,0 +1,16 @@
+#!/usr/bin/env bats
+
+load ../common
+
+@test "010 Image is present and healthy" {
+    docker image inspect ${maintainer}/${imagename}
+}
+
+@test "030 Test Compose the environment" {
+    cd test-compose && ./compose.sh && docker-compose down
+}
+
+
+@test "070 There are no known security vulnerabilities" {
+    ./tests/clairscan.sh ${maintainer}/${imagename}:latest
+}