#!/usr/bin/env bash # # Pull the latest production Postgres dump off the DB server and restore # it into the local `prop_dev` database, replacing whatever is there. # # The production server runs scripts/backup_prop_db.sh on a daily cron, # which writes pg_dump custom-format dumps to /backups/prop/ as # `prop_YYYYMMDD_HHMMSS.dump`. We SSH in, pick the newest one, scp it # locally, then drop/recreate prop_dev and restore into it. # # Usage: # # scripts/restore_prod_to_local.sh # REMOTE_HOST=my-db-host REMOTE_USER=graham scripts/restore_prod_to_local.sh # # Before running: stop `mix phx.server` so Ecto doesn't hold connections. set -euo pipefail REMOTE_HOST="${REMOTE_HOST:-db-prop}" REMOTE_USER="${REMOTE_USER:-$USER}" REMOTE_BACKUP_DIR="${REMOTE_BACKUP_DIR:-/backups/prop}" LOCAL_DB="${LOCAL_DB:-prop_dev}" LOCAL_USER="${LOCAL_USER:-postgres}" LOCAL_HOST="${LOCAL_HOST:-localhost}" LOCAL_TMP="${LOCAL_TMP:-/tmp}" ssh_target="${REMOTE_USER}@${REMOTE_HOST}" log() { printf '%s restore_prod_to_local: %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$*" } die() { log "ERROR: $*" exit 1 } command -v ssh >/dev/null || die "ssh not on PATH" command -v scp >/dev/null || die "scp not on PATH" command -v pg_restore >/dev/null || die "pg_restore not on PATH (install postgresql client tools)" command -v psql >/dev/null || die "psql not on PATH" log "looking up latest dump on ${ssh_target}:${REMOTE_BACKUP_DIR}" remote_file=$(ssh "$ssh_target" "ls -1t ${REMOTE_BACKUP_DIR}/prop_*.dump 2>/dev/null | head -n1") [[ -n "$remote_file" ]] || die "no prop_*.dump files found in ${REMOTE_BACKUP_DIR} on ${REMOTE_HOST}" filename=$(basename "$remote_file") local_file="${LOCAL_TMP}/${filename}" log "latest dump: ${remote_file}" # Always remove the downloaded dump, even on mid-run failure. Saves disk # on laptops where dumps are multi-GB. cleanup() { [[ -n "${local_file:-}" && -f "$local_file" ]] && rm -f "$local_file" } trap cleanup EXIT log "downloading to ${local_file}" scp "${ssh_target}:${remote_file}" "$local_file" # Kick any existing connections off the DB before dropping it — otherwise # `DROP DATABASE` fails with "is being accessed by other users". This is # aggressive, but it's a dev box and the user was told to stop the server. log "terminating existing connections to ${LOCAL_DB}" psql -h "$LOCAL_HOST" -U "$LOCAL_USER" -d postgres -v ON_ERROR_STOP=1 </dev/null SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${LOCAL_DB}' AND pid <> pg_backend_pid(); SQL log "dropping and recreating ${LOCAL_DB}" psql -h "$LOCAL_HOST" -U "$LOCAL_USER" -d postgres -v ON_ERROR_STOP=1 </dev/null DROP DATABASE IF EXISTS ${LOCAL_DB}; CREATE DATABASE ${LOCAL_DB} OWNER ${LOCAL_USER}; SQL # --no-owner / --no-acl so ownership in the prod dump doesn't cause role # errors locally. -j 4 parallelises table data restore. log "restoring dump into ${LOCAL_DB}" pg_restore \ -h "$LOCAL_HOST" \ -U "$LOCAL_USER" \ -d "$LOCAL_DB" \ --no-owner \ --no-acl \ -j 4 \ "$local_file" log "done — ${LOCAL_DB} is a copy of prod as of $(basename "$filename" .dump)"