Zero down-time deploy and server checks

This change makes Dokku start up the new container, run a set of checks
against it, and only switch traffic over to the new containers if all
checks complete successfully. No requests are dropped during the switch
over.

To specify checks, add a CHECKS file to the root of your project
directory. This is a text file with one line per check. Empty lines and
lines starting with # are ignored.

A check is a relative URL and may be followed by expected content from
the page, for example:

  /about     Our Amazing Team

Even if you don’t use any checks, this change will prevent downtime
during switching from old to new container.

See: https://labnotes.org/zero-downtime-deploy-with-dokku/
This commit is contained in:
Assaf Arkin
2014-04-28 21:28:50 -07:00
parent 42fee25f2f
commit 00ec004871
3 changed files with 100 additions and 5 deletions

74
plugins/checks/check-deploy Executable file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Hook to check server against list of checks specified in CHECKS file. Each
# check is a relative path and, optionally, expected content.
#
# For example:
# / My Amazing App
# /stylesheets/index.css .body
# /scripts/index.js $(function()
# /images/logo.png
#
# Waits 5 seconds, giving server time to start, before running the checks. For
# shorter/longer wait, change the DOKKU_CHECKS_WAIT environment variable (value
# in seconds).
set -eo pipefail; [[ $DOKKU_TRACE ]] && set -x
CONTAINERID="$1"; APP="$2"; PORT="$3" ; HOSTNAME="${4:-localhost}"
# source in app env to get DOKKU_CHECKS_WAIT and any other necessary vars
[[ -f "$DOKKU_ROOT/$APP/ENV" ]] && source $DOKKU_ROOT/$APP/ENV
# echo "DOKKU_CHECKS_WAIT is $DOKKU_CHECKS_WAIT"
FILENAME="$DOKKU_ROOT/$APP/CHECKS"
WAIT="${DOKKU_CHECKS_WAIT:-5}"
# try to copy CHECKS from container if not in APP dir & quit gracefully if it doesn't exist
# docker cp exits with status 1 when run as non-root user when it tries to chown the file
# after successfully copying the file. Thus, we suppress stderr.
# ref: https://github.com/dotcloud/docker/issues/3986
if [[ ! -f "$FILENAME" ]] ; then
echo " check-deploy: $FILENAME not found. attempting to retrieve it from container ..."
TMPDIR=$(mktemp -d /tmp/CHECKS.XXXXX)
docker cp $CONTAINERID:/app/CHECKS $TMPDIR 2> /dev/null || true
if [[ ! -s "${TMPDIR}/CHECKS" ]] ; then
echo " CHECKS file not found in container. skipping checks."
rm -rf $TMPDIR
exit 0
else
echo " CHECKS file found in container"
FILENAME=${TMPDIR}/CHECKS
function cleanup() {
echo " removing CHECKS file copied from container"
rm -rf $TMPDIR
}
trap cleanup EXIT
fi
fi
echo "Waiting $WAIT seconds ..."
sleep $WAIT
# -q do not use .curlrc (must come first)
# --compressed Test compression handled correctly
# --fail Fail on server errors (4xx, 5xx)
# --location Follow redirects
CURL_OPTIONS="-q --compressed --fail --location --max-time 30"
cat "$FILENAME" | while read PATHNAME EXPECTED ; do
# Ignore empty lines and lines starting with #
[[ -z "$PATHNAME" || "$PATHNAME" =~ ^\# ]] && continue
URL="http://$HOSTNAME:$PORT$PATHNAME"
echo "checking with: curl $CURL_OPTIONS $URL"
HTML=$(curl $CURL_OPTIONS $URL)
if [[ -n "$EXPECTED" && ! "$HTML" =~ "$EXPECTED" ]] ; then
echo -e "\033[31m\033[1m$URL: expected to but did not find: \"$EXPECTED\"\033[0m"
exit 1
else
echo -e "\033[32m\033[1m$URL => \"$EXPECTED\"\033[0m"
fi
done
echo -e "\033[32m\033[1mAll checks successful!\033[0m"

View File

@@ -84,6 +84,8 @@ EOF
echo "http://$hostname" > "$DOKKU_ROOT/$APP/URL"
fi
pluginhook nginx-pre-reload $APP
echo "-----> Running nginx-pre-reload"
pluginhook nginx-pre-reload $APP $PORT
echo " Reloading nginx"
sudo /etc/init.d/nginx reload > /dev/null
fi