diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile index 84164d0b5..9eee61bcc 100644 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/Dockerfile @@ -1,10 +1,6 @@ -FROM postgres:{{ cookiecutter.postgresql_version }} +FROM postgres:10.3 -COPY ./compose/production/postgres/backup.sh /usr/local/bin/backup -RUN chmod +x /usr/local/bin/backup - -COPY ./compose/production/postgres/restore.sh /usr/local/bin/restore -RUN chmod +x /usr/local/bin/restore - -COPY ./compose/production/postgres/list-backups.sh /usr/local/bin/list-backups -RUN chmod +x /usr/local/bin/list-backups +COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance +RUN chmod +x /usr/local/bin/maintenance/* +RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ + && rmdir /usr/local/bin/maintenance diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/backup.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/backup.sh deleted file mode 100644 index 46438011c..000000000 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/backup.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - - -# we might run into trouble when using the default `postgres` user, e.g. when dropping the postgres -# database in restore.sh. Check that something else is used here -if [ "$POSTGRES_USER" == "postgres" ] -then - echo "creating a backup as the postgres user is not supported, make sure to set the POSTGRES_USER environment variable" - exit 1 -fi - -# export the postgres password so that subsequent commands don't ask for it -export PGPASSWORD=$POSTGRES_PASSWORD - -echo "creating backup" -echo "---------------" - -FILENAME=backup_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz -pg_dump -h postgres -U $POSTGRES_USER | gzip > /backups/$FILENAME - -echo "successfully created backup $FILENAME" diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/list-backups.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/list-backups.sh deleted file mode 100644 index 2be3d1d61..000000000 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/list-backups.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - - -echo "listing available backups" -echo "-------------------------" -ls /backups/ diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/constants.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/constants.sh new file mode 100644 index 000000000..6ca4f0ca9 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/constants.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + + +BACKUP_DIR_PATH='/backups' +BACKUP_FILE_PREFIX='backup' diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/countdown.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/countdown.sh new file mode 100644 index 000000000..e6cbfb6ff --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/countdown.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + + +countdown() { + declare desc="A simple countdown. Source: https://superuser.com/a/611582" + local seconds="${1}" + local d=$(($(date +%s) + "${seconds}")) + while [ "$d" -ge `date +%s` ]; do + echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; + sleep 0.1 + done +} diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/messages.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/messages.sh new file mode 100644 index 000000000..f6be756e9 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/messages.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + + +message_newline() { + echo +} + +message_debug() +{ + echo -e "DEBUG: ${@}" +} + +message_welcome() +{ + echo -e "\e[1m${@}\e[0m" +} + +message_warning() +{ + echo -e "\e[33mWARNING\e[0m: ${@}" +} + +message_error() +{ + echo -e "\e[31mERROR\e[0m: ${@}" +} + +message_info() +{ + echo -e "\e[37mINFO\e[0m: ${@}" +} + +message_suggestion() +{ + echo -e "\e[33mSUGGESTION\e[0m: ${@}" +} + +message_success() +{ + echo -e "\e[32mSUCCESS\e[0m: ${@}" +} diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/yes_no.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/yes_no.sh new file mode 100644 index 000000000..fd9cae161 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/_sourced/yes_no.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + + +yes_no() { + declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." + local arg1="${1}" + + local response= + read -r -p "${arg1} (y/[n])? " response + if [[ "${response}" =~ ^[Yy]$ ]] + then + exit 0 + else + exit 1 + fi +} diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backup b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backup new file mode 100644 index 000000000..8da45561b --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backup @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + + +### Create a database backup. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres backup + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "Backing up the '${POSTGRES_DB}' database..." + + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with something else and try again." + exit 1 +fi + +export PGPASSWORD="${POSTGRES_PASSWORD}" + +backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" +pg_dump \ + --host=postgres \ + --dbname="${POSTGRES_DB}" \ + --username="${POSTGRES_USER}" \ + | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" + + +message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backups b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backups new file mode 100644 index 000000000..0484ccff5 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/backups @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + + +### View backups. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres backups + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "These are the backups you have got:" + +ls -lht "${BACKUP_DIR_PATH}" diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/restore b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/restore new file mode 100644 index 000000000..e13796e04 --- /dev/null +++ b/{{cookiecutter.project_slug}}/compose/production/postgres/maintenance/restore @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + + +### Restore database from a backup. +### +### Parameters: +### <1> filename of an existing backup. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +if [[ -z ${1+x} ]]; then + message_error "Backup filename is not specified." + exit 1 +fi +backup_filename="${BACKUP_DIR_PATH}/${1}" +if [[ ! -f "${backup_filename}" ]]; then + message_error "No backup with the specified filename found." + exit 1 +fi + +message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Restoring as 'postgres' user is not supported." + exit 1 +fi + +export PGPASSWORD="${POSTGRES_PASSWORD}" + +message_info "Dropping all connections to the database..." +# Source: http://dba.stackexchange.com/a/11895 +drop_postgres_connections_sql='UPDATE pg_database' +drop_postgres_connections_sql+=" SET datallowconn = 'false'" +drop_postgres_connections_sql+=" WHERE datname = '${POSTGRES_DB}';" +drop_postgres_connections_sql+='SELECT pg_terminate_backend(pid)' +drop_postgres_connections_sql+=' FROM pg_stat_activity' +drop_postgres_connections_sql+=" WHERE datname = '${POSTGRES_DB}';" +psql \ + --host=localhost \ + --username=postgres \ + --dbname=postgres \ + --command="${drop_postgres_connections_sql}" + +message_info "Dropping the database..." +dropdb \ + --host=postgres \ + --username="${POSTGRES_USER}" \ + "${POSTGRES_DB}" + +message_info "Creating a new database..." +createdb \ + --host=postgres \ + --username="${POSTGRES_USER}" \ + --owner="${POSTGRES_USER}" \ + "${POSTGRES_DB}" + +message_info "Applying the backup to the new database..." +gunzip -c "${backup_filename}" \ + | psql \ + --host=postgres \ + --username="${POSTGRES_USER}" \ + "${POSTGRES_DB}" + +message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." diff --git a/{{cookiecutter.project_slug}}/compose/production/postgres/restore.sh b/{{cookiecutter.project_slug}}/compose/production/postgres/restore.sh deleted file mode 100644 index e7358949e..000000000 --- a/{{cookiecutter.project_slug}}/compose/production/postgres/restore.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -o errexit -set -o pipefail -set -o nounset - - -# we might run into trouble when using the default `postgres` user, e.g. when dropping the postgres -# database in restore.sh. Check that something else is used here -if [ "$POSTGRES_USER" == "postgres" ] -then - echo "restoring as the postgres user is not supported, make sure to set the POSTGRES_USER environment variable" - exit 1 -fi - -# export the postgres password so that subsequent commands don't ask for it -export PGPASSWORD=$POSTGRES_PASSWORD - -# check that we have an argument for a filename candidate -if [[ $# -eq 0 ]] ; then - echo 'usage:' - echo ' docker-compose -f production.yml run postgres restore ' - echo '' - echo 'to get a list of available backups, run:' - echo ' docker-compose -f production.yml run postgres list-backups' - exit 1 -fi - -# set the backupfile variable -BACKUPFILE=/backups/$1 - -# check that the file exists -if ! [ -f $BACKUPFILE ]; then - echo "backup file not found" - echo 'to get a list of available backups, run:' - echo ' docker-compose -f production.yml run postgres list-backups' - exit 1 -fi - -echo "beginning restore from $1" -echo "-------------------------" - -# delete the db -# deleting the db can fail. Spit out a comment if this happens but continue since the db -# is created in the next step -echo "deleting old database $POSTGRES_USER" -if dropdb -h postgres -U $POSTGRES_USER $POSTGRES_USER -then echo "deleted $POSTGRES_USER database" -else echo "database $POSTGRES_USER does not exist, continue" -fi - -# create a new database -echo "creating new database $POSTGRES_USER" -createdb -h postgres -U $POSTGRES_USER $POSTGRES_USER -O $POSTGRES_USER - -# restore the database -echo "restoring database $POSTGRES_USER" -gunzip -c $BACKUPFILE | psql -h postgres -U $POSTGRES_USER