mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-14 03:19:51 +00:00
08735e6cc8
Publish upgrade-postgres.sh with install and upgrade flows, include the PostgreSQL compose override when present, and sync the script to BunnyCDN.
382 lines
12 KiB
Bash
Executable File
382 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
## Explicit Coolify internal PostgreSQL major-version migrator.
|
|
## This script is intentionally not run by upgrade.sh automatically.
|
|
|
|
set -Eeuo pipefail
|
|
|
|
SOURCE_DIR="/data/coolify/source"
|
|
ENV_FILE="${SOURCE_DIR}/.env"
|
|
BACKUP_DIR="/data/coolify/backups/internal-postgres"
|
|
OVERRIDE_FILE="${SOURCE_DIR}/docker-compose.postgres-upgrade.yml"
|
|
ROLLBACK_FILE="${SOURCE_DIR}/postgres-upgrade-rollback.env"
|
|
DATE=$(date +%Y-%m-%d-%H-%M-%S)
|
|
LOGFILE="${SOURCE_DIR}/postgres-upgrade-${DATE}.log"
|
|
COMMAND="${1:-upgrade}"
|
|
TARGET_MAJOR="${1:-18}"
|
|
|
|
if [ "$COMMAND" = "rollback" ]; then
|
|
TARGET_MAJOR=""
|
|
else
|
|
COMMAND="upgrade"
|
|
fi
|
|
|
|
TARGET_IMAGE="${COOLIFY_POSTGRES_TARGET_IMAGE:-postgres:${TARGET_MAJOR}-alpine}"
|
|
TARGET_VOLUME="${COOLIFY_POSTGRES_TARGET_VOLUME:-coolify-db-pg${TARGET_MAJOR}}"
|
|
TEMP_CONTAINER="coolify-db-pg${TARGET_MAJOR:-rollback}-restore-${DATE}"
|
|
DUMP_FILE="${BACKUP_DIR}/postgres-upgrade-${DATE}.sql.gz"
|
|
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOGFILE"
|
|
}
|
|
|
|
fail() {
|
|
log "ERROR: $1"
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage:
|
|
$0 <target-major>
|
|
$0 rollback
|
|
|
|
Examples:
|
|
$0 18
|
|
$0 rollback
|
|
|
|
Environment overrides:
|
|
COOLIFY_POSTGRES_TARGET_IMAGE=postgres:18-alpine
|
|
COOLIFY_POSTGRES_TARGET_VOLUME=coolify-db-pg18
|
|
EOF
|
|
}
|
|
|
|
cleanup() {
|
|
docker rm -f "$TEMP_CONTAINER" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
get_env_var() {
|
|
local key="$1"
|
|
local fallback="${2:-}"
|
|
local value
|
|
|
|
value=$(grep -E "^${key}=" "$ENV_FILE" 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true)
|
|
value="${value%\"}"
|
|
value="${value#\"}"
|
|
value="${value%\'}"
|
|
value="${value#\'}"
|
|
|
|
if [ -z "$value" ]; then
|
|
printf '%s' "$fallback"
|
|
else
|
|
printf '%s' "$value"
|
|
fi
|
|
}
|
|
|
|
wait_for_postgres() {
|
|
local container="$1"
|
|
local user="$2"
|
|
local database="$3"
|
|
local attempts=60
|
|
|
|
for _ in $(seq 1 "$attempts"); do
|
|
if docker exec "$container" pg_isready -U "$user" -d "$database" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
compose_files() {
|
|
printf -- '-f %s/docker-compose.yml -f %s/docker-compose.prod.yml ' "$SOURCE_DIR" "$SOURCE_DIR"
|
|
|
|
if [ -f "${SOURCE_DIR}/docker-compose.custom.yml" ]; then
|
|
printf -- '-f %s/docker-compose.custom.yml ' "$SOURCE_DIR"
|
|
fi
|
|
|
|
if [ -f "$OVERRIDE_FILE" ]; then
|
|
printf -- '-f %s ' "$OVERRIDE_FILE"
|
|
fi
|
|
}
|
|
|
|
validate_target_major() {
|
|
case "$TARGET_MAJOR" in
|
|
''|*[!0-9]*)
|
|
usage
|
|
fail "Target major version must be numeric. Example: $0 18"
|
|
;;
|
|
esac
|
|
|
|
if [ "$TARGET_MAJOR" -lt 10 ]; then
|
|
fail "Target major version must be 10 or higher."
|
|
fi
|
|
}
|
|
|
|
mount_path_for_major() {
|
|
local major="$1"
|
|
|
|
if [ "$major" -ge 18 ]; then
|
|
printf '%s' '/var/lib/postgresql'
|
|
else
|
|
printf '%s' '/var/lib/postgresql/data'
|
|
fi
|
|
}
|
|
|
|
current_postgres_mount_name() {
|
|
docker inspect coolify-db --format '{{range .Mounts}}{{if or (eq .Destination "/var/lib/postgresql/data") (eq .Destination "/var/lib/postgresql")}}{{.Name}}{{end}}{{end}}' 2>/dev/null
|
|
}
|
|
|
|
current_postgres_mount_path() {
|
|
docker inspect coolify-db --format '{{range .Mounts}}{{if or (eq .Destination "/var/lib/postgresql/data") (eq .Destination "/var/lib/postgresql")}}{{.Destination}}{{end}}{{end}}' 2>/dev/null
|
|
}
|
|
|
|
current_postgres_image() {
|
|
docker inspect coolify-db --format '{{.Config.Image}}' 2>/dev/null
|
|
}
|
|
|
|
write_override_file() {
|
|
local image="$1"
|
|
local volume="$2"
|
|
local mount_path="$3"
|
|
|
|
cat > "$OVERRIDE_FILE" <<YAML
|
|
services:
|
|
postgres:
|
|
image: "${image}"
|
|
volumes:
|
|
- coolify-db:${mount_path}
|
|
volumes:
|
|
coolify-db:
|
|
name: "${volume}"
|
|
external: true
|
|
YAML
|
|
}
|
|
|
|
write_rollback_file() {
|
|
local previous_image="$1"
|
|
local previous_volume="$2"
|
|
local previous_mount_path="$3"
|
|
local previous_override_present="$4"
|
|
local upgraded_image="$5"
|
|
local upgraded_volume="$6"
|
|
|
|
cat > "$ROLLBACK_FILE" <<EOF
|
|
PREVIOUS_IMAGE='${previous_image}'
|
|
PREVIOUS_VOLUME='${previous_volume}'
|
|
PREVIOUS_MOUNT_PATH='${previous_mount_path}'
|
|
PREVIOUS_OVERRIDE_PRESENT='${previous_override_present}'
|
|
UPGRADED_IMAGE='${upgraded_image}'
|
|
UPGRADED_VOLUME='${upgraded_volume}'
|
|
CREATED_AT='${DATE}'
|
|
EOF
|
|
chmod 600 "$ROLLBACK_FILE"
|
|
}
|
|
|
|
start_stack() {
|
|
local files
|
|
files=$(compose_files)
|
|
|
|
# shellcheck disable=SC2086
|
|
LATEST_IMAGE="${LATEST_IMAGE:-latest}" docker compose --env-file "$ENV_FILE" $files up -d --remove-orphans --wait --wait-timeout 120
|
|
}
|
|
|
|
print_rollback_instructions() {
|
|
cat <<EOF | tee -a "$LOGFILE"
|
|
|
|
Rollback command if the upgraded database does not work:
|
|
${SOURCE_DIR}/upgrade-postgres.sh rollback
|
|
|
|
Rollback metadata was saved to:
|
|
${ROLLBACK_FILE}
|
|
|
|
The previous active Docker volume was '${PREVIOUS_VOLUME}'.
|
|
The new Docker volume is '${TARGET_VOLUME}'.
|
|
The dump file is '${DUMP_FILE}'.
|
|
EOF
|
|
}
|
|
|
|
validate_common_requirements() {
|
|
mkdir -p "$BACKUP_DIR"
|
|
touch "$LOGFILE"
|
|
chmod 700 "$BACKUP_DIR"
|
|
|
|
[ -f "$ENV_FILE" ] || fail "Missing ${ENV_FILE}. Run this on a self-hosted Coolify server."
|
|
command -v docker >/dev/null 2>&1 || fail "Docker is required."
|
|
docker info >/dev/null 2>&1 || fail "Docker daemon is not reachable."
|
|
}
|
|
|
|
rollback_postgres() {
|
|
validate_common_requirements
|
|
|
|
[ -f "$ROLLBACK_FILE" ] || fail "Missing rollback metadata file: ${ROLLBACK_FILE}"
|
|
|
|
# shellcheck disable=SC1090
|
|
. "$ROLLBACK_FILE"
|
|
|
|
[ -n "${PREVIOUS_IMAGE:-}" ] || fail "Rollback metadata is missing PREVIOUS_IMAGE."
|
|
[ -n "${PREVIOUS_VOLUME:-}" ] || fail "Rollback metadata is missing PREVIOUS_VOLUME."
|
|
[ -n "${PREVIOUS_MOUNT_PATH:-}" ] || fail "Rollback metadata is missing PREVIOUS_MOUNT_PATH."
|
|
[ -n "${PREVIOUS_OVERRIDE_PRESENT:-}" ] || fail "Rollback metadata is missing PREVIOUS_OVERRIDE_PRESENT."
|
|
|
|
log "Rolling back Coolify internal PostgreSQL."
|
|
log "Previous image: ${PREVIOUS_IMAGE}"
|
|
log "Previous volume: ${PREVIOUS_VOLUME}"
|
|
log "Previous mount path: ${PREVIOUS_MOUNT_PATH}"
|
|
|
|
docker volume inspect "$PREVIOUS_VOLUME" >/dev/null 2>&1 || fail "Previous volume '${PREVIOUS_VOLUME}' does not exist."
|
|
|
|
log "Stopping Coolify application container before rollback."
|
|
docker stop coolify >>"$LOGFILE" 2>&1 || true
|
|
|
|
log "Removing current coolify-db container. Current upgraded volume is kept untouched."
|
|
docker rm -f coolify-db >>"$LOGFILE" 2>&1 || true
|
|
|
|
if [ "$PREVIOUS_OVERRIDE_PRESENT" = "true" ]; then
|
|
log "Restoring previous PostgreSQL compose override."
|
|
write_override_file "$PREVIOUS_IMAGE" "$PREVIOUS_VOLUME" "$PREVIOUS_MOUNT_PATH"
|
|
else
|
|
log "Removing PostgreSQL compose override to restore base compose configuration."
|
|
rm -f "$OVERRIDE_FILE"
|
|
fi
|
|
|
|
log "Starting Coolify stack with rollback database volume."
|
|
start_stack >>"$LOGFILE" 2>&1 || fail "Could not start Coolify stack after rollback. See ${LOGFILE}."
|
|
|
|
log "Rollback completed successfully."
|
|
cat <<EOF | tee -a "$LOGFILE"
|
|
|
|
Rollback completed.
|
|
Current active PostgreSQL volume should be '${PREVIOUS_VOLUME}'.
|
|
The upgraded volume '${UPGRADED_VOLUME:-unknown}' was left untouched for inspection or manual cleanup.
|
|
EOF
|
|
}
|
|
|
|
upgrade_postgres() {
|
|
validate_target_major
|
|
TARGET_MOUNT_PATH=$(mount_path_for_major "$TARGET_MAJOR")
|
|
validate_common_requirements
|
|
|
|
log "Starting Coolify internal PostgreSQL major upgrade."
|
|
log "Target major: ${TARGET_MAJOR}"
|
|
log "Target image: ${TARGET_IMAGE}"
|
|
log "Target volume: ${TARGET_VOLUME}"
|
|
log "Target mount path: ${TARGET_MOUNT_PATH}"
|
|
|
|
DB_USERNAME=$(get_env_var DB_USERNAME coolify)
|
|
DB_DATABASE=$(get_env_var DB_DATABASE coolify)
|
|
|
|
if ! docker ps -a --format '{{.Names}}' | grep -qx 'coolify-db'; then
|
|
fail "Container 'coolify-db' was not found. Start Coolify before running this script."
|
|
fi
|
|
|
|
if ! docker ps --format '{{.Names}}' | grep -qx 'coolify-db'; then
|
|
log "Starting existing coolify-db container for version detection and dump."
|
|
docker start coolify-db >>"$LOGFILE" 2>&1 || fail "Could not start coolify-db."
|
|
fi
|
|
|
|
wait_for_postgres coolify-db "$DB_USERNAME" "$DB_DATABASE" || fail "Existing coolify-db is not ready."
|
|
|
|
SERVER_VERSION_NUM=$(docker exec coolify-db psql -U "$DB_USERNAME" -d "$DB_DATABASE" -Atc 'SHOW server_version_num;' | tr -d '[:space:]')
|
|
CURRENT_MAJOR=$((SERVER_VERSION_NUM / 10000))
|
|
PREVIOUS_VOLUME=$(current_postgres_mount_name)
|
|
PREVIOUS_MOUNT_PATH=$(current_postgres_mount_path)
|
|
PREVIOUS_IMAGE=$(current_postgres_image)
|
|
|
|
[ -n "$PREVIOUS_VOLUME" ] || fail "Could not detect current PostgreSQL Docker volume."
|
|
[ -n "$PREVIOUS_MOUNT_PATH" ] || fail "Could not detect current PostgreSQL mount path."
|
|
[ -n "$PREVIOUS_IMAGE" ] || fail "Could not detect current PostgreSQL image."
|
|
|
|
if [ -f "$OVERRIDE_FILE" ]; then
|
|
PREVIOUS_OVERRIDE_PRESENT=true
|
|
else
|
|
PREVIOUS_OVERRIDE_PRESENT=false
|
|
fi
|
|
|
|
log "Current PostgreSQL major: ${CURRENT_MAJOR}"
|
|
log "Current active volume: ${PREVIOUS_VOLUME}"
|
|
log "Current image: ${PREVIOUS_IMAGE}"
|
|
log "Current mount path: ${PREVIOUS_MOUNT_PATH}"
|
|
|
|
if [ "$CURRENT_MAJOR" -eq "$TARGET_MAJOR" ]; then
|
|
log "PostgreSQL is already on major ${TARGET_MAJOR}. Nothing to do."
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$CURRENT_MAJOR" -gt "$TARGET_MAJOR" ]; then
|
|
fail "Downgrade from ${CURRENT_MAJOR} to ${TARGET_MAJOR} is not supported. Use '$0 rollback' to restore the previous upgrade state."
|
|
fi
|
|
|
|
if docker volume inspect "$TARGET_VOLUME" >/dev/null 2>&1; then
|
|
fail "Target volume '${TARGET_VOLUME}' already exists. Set COOLIFY_POSTGRES_TARGET_VOLUME to a new name or remove the old failed target volume."
|
|
fi
|
|
|
|
log "Stopping Coolify application container to prevent writes during dump."
|
|
docker stop coolify >>"$LOGFILE" 2>&1 || true
|
|
|
|
log "Creating compressed dump at ${DUMP_FILE}."
|
|
docker exec coolify-db pg_dumpall -U "$DB_USERNAME" | gzip -c > "$DUMP_FILE"
|
|
chmod 600 "$DUMP_FILE"
|
|
|
|
if [ ! -s "$DUMP_FILE" ]; then
|
|
fail "Dump file is empty. Aborting."
|
|
fi
|
|
|
|
log "Creating target Docker volume '${TARGET_VOLUME}'."
|
|
docker volume create "$TARGET_VOLUME" >>"$LOGFILE" 2>&1
|
|
|
|
log "Pulling ${TARGET_IMAGE}."
|
|
docker pull "$TARGET_IMAGE" >>"$LOGFILE" 2>&1
|
|
|
|
log "Starting temporary PostgreSQL ${TARGET_MAJOR} container."
|
|
docker run -d \
|
|
--name "$TEMP_CONTAINER" \
|
|
--network coolify \
|
|
-e POSTGRES_HOST_AUTH_METHOD=trust \
|
|
-v "${TARGET_VOLUME}:${TARGET_MOUNT_PATH}" \
|
|
"$TARGET_IMAGE" >>"$LOGFILE" 2>&1
|
|
|
|
wait_for_postgres "$TEMP_CONTAINER" postgres postgres || fail "Temporary PostgreSQL ${TARGET_MAJOR} container did not become ready."
|
|
|
|
log "Restoring dump into target volume."
|
|
gunzip -c "$DUMP_FILE" | docker exec -i "$TEMP_CONTAINER" psql -U postgres -d postgres >>"$LOGFILE" 2>&1
|
|
|
|
log "Smoke-checking restored Coolify database."
|
|
docker exec "$TEMP_CONTAINER" psql -U "$DB_USERNAME" -d "$DB_DATABASE" -Atc 'SELECT 1;' | grep -qx '1' || fail "Restored database smoke check failed."
|
|
|
|
log "Saving rollback metadata to ${ROLLBACK_FILE}."
|
|
write_rollback_file "$PREVIOUS_IMAGE" "$PREVIOUS_VOLUME" "$PREVIOUS_MOUNT_PATH" "$PREVIOUS_OVERRIDE_PRESENT" "$TARGET_IMAGE" "$TARGET_VOLUME"
|
|
|
|
log "Writing Docker Compose override to ${OVERRIDE_FILE}."
|
|
write_override_file "$TARGET_IMAGE" "$TARGET_VOLUME" "$TARGET_MOUNT_PATH"
|
|
|
|
log "Stopping temporary restore container."
|
|
docker rm -f "$TEMP_CONTAINER" >>"$LOGFILE" 2>&1 || true
|
|
|
|
log "Stopping old coolify-db container. Previous volume '${PREVIOUS_VOLUME}' will be kept for rollback."
|
|
docker rm -f coolify-db >>"$LOGFILE" 2>&1 || true
|
|
|
|
log "Starting Coolify stack with PostgreSQL ${TARGET_MAJOR}."
|
|
start_stack >>"$LOGFILE" 2>&1 || fail "Could not start Coolify stack with upgraded PostgreSQL. See ${LOGFILE}."
|
|
|
|
log "Coolify internal PostgreSQL upgrade completed successfully."
|
|
print_rollback_instructions
|
|
}
|
|
|
|
case "$COMMAND" in
|
|
rollback)
|
|
rollback_postgres
|
|
;;
|
|
upgrade)
|
|
upgrade_postgres
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
*)
|
|
usage
|
|
fail "Unknown command: ${COMMAND}"
|
|
;;
|
|
esac
|