Files
home-infra/stacks/nextcloud/docker-compose.yml
T
a.zubarev d1a25f43a2
Nextcloud CI/CD / Deploy Nextcloud (push) Failing after 1s
Обновить stacks/nextcloud/docker-compose.yml
2026-09-30 14:24:44 +03:00

1204 lines
26 KiB
YAML

#!/usr/bin/env bash
set -Eeuo pipefail
umask 077
# ============================================================
# Configuration
# ============================================================
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SRC="${ROOT}/stacks/nextcloud"
LIVE="/srv/nfs/infra/nextcloud"
ENV_FILE="${LIVE}/.env"
BACKUP_ROOT="/srv/nfs/infra/.deploy-backups/nextcloud"
BACKUP_KEEP=3
STAMP="$(date '+%Y%m%d-%H%M%S')"
BACKUP_DIR="${BACKUP_ROOT}/${STAMP}"
DB_BACKUP_DIR="${BACKUP_DIR}/database"
DB_BACKUP_FILE="${DB_BACKUP_DIR}/nextcloud.dump"
PREVIOUS_COMPOSE="/tmp/nextcloud-compose-${STAMP}-$$.yml"
INIT_TIMEOUT=900
INIT_INTERVAL=5
OCC_TIMEOUT=300
OCC_INTERVAL=5
# ============================================================
# Runtime state
# ============================================================
VERSION_CHANGED=false
PRODUCTION_CHANGED=false
MIGRATION_STARTED=false
BACKUP_COMPLETE=false
UPGRADE_STARTED=""
# ============================================================
# Helpers
# ============================================================
log()
{
echo
echo "============================================================"
echo "$*"
echo "============================================================"
}
cleanup_temp()
{
rm -f -- "${PREVIOUS_COMPOSE}" 2>/dev/null || true
}
restore_previous_compose()
{
if [[ ! -f "${PREVIOUS_COMPOSE}" ]]; then
echo "Previous Compose file is unavailable; automatic restore skipped."
return 0
fi
echo
echo "--- Restore previous docker-compose.yml ---"
cp \
"${PREVIOUS_COMPOSE}" \
"${LIVE}/docker-compose.yml"
(
cd "${LIVE}"
docker compose \
--env-file "${ENV_FILE}" \
up -d \
--remove-orphans
) || true
}
print_failure_context()
{
local RC="$1"
local FAILED_LINE="$2"
local FAILED_COMMAND="$3"
echo
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo " Nextcloud deployment FAILED"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo
echo "Exit code:"
echo " ${RC}"
echo
echo "Failed at line:"
echo " ${FAILED_LINE}"
echo
echo "Failed command:"
echo " ${FAILED_COMMAND}"
if [[ "${MIGRATION_STARTED}" == "true" ]]; then
echo
echo "Database migration MAY already have started."
echo "Automatic rollback will NOT be performed."
if [[ "${BACKUP_COMPLETE}" == "true" ]]; then
echo
echo "Pre-upgrade backup:"
echo " ${BACKUP_DIR}"
fi
echo
echo "For rollback restore the PostgreSQL dump first."
elif [[ "${PRODUCTION_CHANGED}" == "true" ]]; then
echo
echo "Database migration was NOT started."
echo "Restoring previous Compose configuration."
restore_previous_compose
else
echo
echo "Production was NOT modified."
echo "Database migration was NOT started."
echo "No service rollback is required."
fi
if [[ -d "${BACKUP_DIR}" ]] &&
[[ "${BACKUP_COMPLETE}" != "true" ]]
then
echo
echo "Removing incomplete backup directory:"
echo " ${BACKUP_DIR}"
rm -rf -- "${BACKUP_DIR}" || true
fi
}
on_error()
{
local RC=$?
local FAILED_COMMAND="${BASH_COMMAND}"
local FAILED_LINE="${BASH_LINENO[0]:-unknown}"
trap - ERR
print_failure_context \
"${RC}" \
"${FAILED_LINE}" \
"${FAILED_COMMAND}"
exit "${RC}"
}
fail()
{
local MESSAGE="$*"
echo
echo "ERROR: ${MESSAGE}" >&2
return 1
}
cleanup_old_backups()
{
echo
echo "--- Backup retention ---"
echo "Keeping last ${BACKUP_KEEP} complete pre-upgrade backups."
mkdir -p "${BACKUP_ROOT}"
mapfile -t BACKUPS < <(
find "${BACKUP_ROOT}" \
-mindepth 1 \
-maxdepth 1 \
-type f \
-name '.complete' \
-printf '%h\n' |
sed 's#.*/##' |
grep -E '^[0-9]{8}-[0-9]{6}$' |
sort -r
)
if (( ${#BACKUPS[@]} <= BACKUP_KEEP )); then
echo "Current complete backup count: ${#BACKUPS[@]}"
echo "Nothing to delete."
return 0
fi
for BACKUP_NAME in "${BACKUPS[@]:${BACKUP_KEEP}}"
do
OLD_BACKUP="${BACKUP_ROOT}/${BACKUP_NAME}"
echo
echo "Removing old backup:"
echo " ${OLD_BACKUP}"
rm -rf -- "${OLD_BACKUP}"
done
}
version_less_than()
{
local A="$1"
local B="$2"
[[ "${A}" == "${B}" ]] && return 1
[[ "$(
printf '%s\n%s\n' "${A}" "${B}" |
sort -V |
head -n1
)" == "${A}" ]]
}
json_string_value()
{
local KEY="$1"
sed -nE \
"s/.*\"${KEY}\"[[:space:]]*:[[:space:]]*\"([^\"]+)\".*/\1/p" |
head -n1
}
get_occ_status_json()
{
local OUTPUT
local RC
set +e
OUTPUT="$(
docker exec \
-u www-data \
nextcloud \
php occ status \
--output=json \
2>&1
)"
RC=$?
set -e
if [[ "${RC}" -ne 0 ]]; then
printf '%s\n' "${OUTPUT}" >&2
return "${RC}"
fi
printf '%s\n' "${OUTPUT}"
}
validate_occ_status_json()
{
local STATUS_JSON="$1"
echo "${STATUS_JSON}" |
grep -Eq '"installed"[[:space:]]*:[[:space:]]*true' ||
fail "Nextcloud is not reported as installed"
echo "${STATUS_JSON}" |
grep -Eq '"needsDbUpgrade"[[:space:]]*:[[:space:]]*false' ||
fail "Nextcloud requires a database upgrade"
echo "${STATUS_JSON}" |
grep -Eq '"maintenance"[[:space:]]*:[[:space:]]*false' ||
fail "Nextcloud is in maintenance mode"
}
wait_for_occ_normal()
{
local ELAPSED=0
local STATUS_JSON=""
local RC=0
echo
echo "Waiting for Nextcloud OCC status..."
while (( ELAPSED < OCC_TIMEOUT ))
do
set +e
STATUS_JSON="$(get_occ_status_json 2>&1)"
RC=$?
set -e
if [[ "${RC}" -eq 0 ]] &&
echo "${STATUS_JSON}" | grep -Eq '"needsDbUpgrade"[[:space:]]*:[[:space:]]*false' &&
echo "${STATUS_JSON}" | grep -Eq '"maintenance"[[:space:]]*:[[:space:]]*false'
then
echo "Nextcloud OCC status is normal."
return 0
fi
sleep "${OCC_INTERVAL}"
ELAPSED=$((ELAPSED + OCC_INTERVAL))
echo "Waiting for OCC... ${ELAPSED}s"
done
echo
echo "Last OCC output:"
echo "${STATUS_JSON}"
fail "Timeout waiting for normal Nextcloud OCC status"
}
trap on_error ERR
trap cleanup_temp EXIT
# ============================================================
# Required commands
# ============================================================
log "Pre-flight checks"
for CMD in \
docker \
psql \
pg_dump \
pg_restore \
sha256sum \
sort \
grep \
sed \
find \
awk \
du \
diff \
seq
do
command -v "${CMD}" >/dev/null 2>&1 || \
fail "Required command not found: ${CMD}"
done
docker compose version >/dev/null 2>&1 || \
fail "Docker Compose plugin is not available"
# ============================================================
# Required files
# ============================================================
[[ -f "${SRC}/docker-compose.yml" ]] || \
fail "${SRC}/docker-compose.yml not found"
[[ -f "${LIVE}/docker-compose.yml" ]] || \
fail "${LIVE}/docker-compose.yml not found"
[[ -f "${ENV_FILE}" ]] || \
fail "${ENV_FILE} not found"
# Preserve production Compose before any change.
cp \
"${LIVE}/docker-compose.yml" \
"${PREVIOUS_COMPOSE}"
# ============================================================
# Backup directory access
# ============================================================
mkdir -p "${BACKUP_ROOT}"
[[ -w "${BACKUP_ROOT}" ]] || \
fail "${BACKUP_ROOT} is not writable"
# ============================================================
# Load PostgreSQL settings
# ============================================================
set -a
# shellcheck disable=SC1090
source "${ENV_FILE}"
set +a
: "${POSTGRES_HOST:?POSTGRES_HOST not defined}"
: "${POSTGRES_DB:?POSTGRES_DB not defined}"
: "${POSTGRES_USER:?POSTGRES_USER not defined}"
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD not defined}"
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
export PGPASSWORD="${POSTGRES_PASSWORD}"
export PGCONNECT_TIMEOUT=10
# ============================================================
# Validate requested Compose
# ============================================================
log "Validate Docker Compose"
docker compose \
--env-file "${ENV_FILE}" \
-f "${SRC}/docker-compose.yml" \
config >/dev/null
echo "Docker Compose configuration is valid."
# ============================================================
# Show requested Compose changes
# ============================================================
log "Docker Compose changes"
if diff -u \
"${LIVE}/docker-compose.yml" \
"${SRC}/docker-compose.yml"
then
echo "No docker-compose.yml text changes detected."
fi
# ============================================================
# Detect requested Nextcloud image
# ============================================================
log "Detect requested Nextcloud image"
mapfile -t NEXTCLOUD_IMAGES < <(
docker compose \
--env-file "${ENV_FILE}" \
-f "${SRC}/docker-compose.yml" \
config --images |
grep '^nextcloud:' |
sort -u
)
if [[ "${#NEXTCLOUD_IMAGES[@]}" -ne 1 ]]; then
echo "Detected Nextcloud images:"
if (( ${#NEXTCLOUD_IMAGES[@]} > 0 )); then
printf ' %s\n' "${NEXTCLOUD_IMAGES[@]}"
else
echo " none"
fi
fail "Services nextcloud and cron must use exactly the same Nextcloud image"
fi
NEW_IMAGE="${NEXTCLOUD_IMAGES[0]}"
echo "Requested image:"
echo " ${NEW_IMAGE}"
# ============================================================
# Current Nextcloud container
# ============================================================
docker inspect nextcloud >/dev/null 2>&1 || \
fail "Current Nextcloud container does not exist"
CURRENT_RUNNING="$(
docker inspect \
-f '{{.State.Running}}' \
nextcloud
)"
[[ "${CURRENT_RUNNING}" == "true" ]] || \
fail "Current Nextcloud container is not running"
OLD_IMAGE="$(
docker inspect \
-f '{{.Config.Image}}' \
nextcloud
)"
# ============================================================
# Detect current Nextcloud status/version
#
# IMPORTANT:
# Compare internal "version" (e.g. 35.0.1.1) with the target
# image's $OC_Version. Do NOT compare "versionstring" (35.0.1)
# against $OC_Version, otherwise an unchanged image looks like
# a version upgrade.
# ============================================================
log "Detect current Nextcloud version"
set +e
CURRENT_STATUS_JSON="$(get_occ_status_json 2>&1)"
CURRENT_STATUS_RC=$?
set -e
if [[ "${CURRENT_STATUS_RC}" -ne 0 ]]; then
echo
echo "--- OCC status output ---"
echo "${CURRENT_STATUS_JSON}"
fail \
"Could not get current Nextcloud status (exit code ${CURRENT_STATUS_RC})"
fi
CURRENT_NC_VERSION="$(
printf '%s\n' "${CURRENT_STATUS_JSON}" |
json_string_value version
)"
CURRENT_NC_VERSIONSTRING="$(
printf '%s\n' "${CURRENT_STATUS_JSON}" |
json_string_value versionstring
)"
[[ -n "${CURRENT_NC_VERSION}" ]] || \
fail "Could not determine current internal Nextcloud version"
validate_occ_status_json "${CURRENT_STATUS_JSON}"
docker exec \
-u www-data \
nextcloud \
php occ status -e
echo "Current image:"
echo " ${OLD_IMAGE}"
echo "Current Nextcloud internal version:"
echo " ${CURRENT_NC_VERSION}"
if [[ -n "${CURRENT_NC_VERSIONSTRING}" ]]; then
echo "Current Nextcloud display version:"
echo " ${CURRENT_NC_VERSIONSTRING}"
fi
# ============================================================
# Pull target Nextcloud image
# ============================================================
log "Pull target Nextcloud image"
docker pull "${NEW_IMAGE}"
# ============================================================
# Determine exact target Nextcloud version
# ============================================================
log "Detect target Nextcloud version"
TARGET_NC_VERSION="$(
docker run \
--rm \
--entrypoint php \
"${NEW_IMAGE}" \
-r '
require "/usr/src/nextcloud/version.php";
echo implode(".", $OC_Version);
'
)"
TARGET_NC_VERSIONSTRING="$(
docker run \
--rm \
--entrypoint php \
"${NEW_IMAGE}" \
-r '
require "/usr/src/nextcloud/version.php";
echo isset($OC_VersionString) ? $OC_VersionString : implode(".", $OC_Version);
'
)"
[[ -n "${TARGET_NC_VERSION}" ]] || \
fail "Could not determine target Nextcloud version"
echo "Target Docker image:"
echo " ${NEW_IMAGE}"
echo "Target Nextcloud internal version:"
echo " ${TARGET_NC_VERSION}"
echo "Target Nextcloud display version:"
echo " ${TARGET_NC_VERSIONSTRING}"
# ============================================================
# Version safety checks
# ============================================================
log "Validate version transition"
if version_less_than \
"${TARGET_NC_VERSION}" \
"${CURRENT_NC_VERSION}"
then
fail \
"Nextcloud downgrade is not allowed: ${CURRENT_NC_VERSION} -> ${TARGET_NC_VERSION}"
fi
CURRENT_MAJOR="${CURRENT_NC_VERSION%%.*}"
TARGET_MAJOR="${TARGET_NC_VERSION%%.*}"
if (( TARGET_MAJOR > CURRENT_MAJOR + 1 )); then
fail \
"Skipping Nextcloud major versions is forbidden: ${CURRENT_MAJOR} -> ${TARGET_MAJOR}"
fi
if [[ "${CURRENT_NC_VERSION}" != "${TARGET_NC_VERSION}" ]]; then
VERSION_CHANGED=true
fi
echo "Current internal version: ${CURRENT_NC_VERSION}"
echo "Target internal version: ${TARGET_NC_VERSION}"
echo "Version changed: ${VERSION_CHANGED}"
# ============================================================
# PRE-UPGRADE BACKUP
#
# Only when the actual internal Nextcloud version changes.
# ============================================================
if [[ "${VERSION_CHANGED}" == "true" ]]; then
log "Prepare Nextcloud pre-upgrade backup"
mkdir -p "${DB_BACKUP_DIR}"
echo
echo "--- Backup current docker-compose.yml ---"
cp \
"${LIVE}/docker-compose.yml" \
"${BACKUP_DIR}/docker-compose.yml"
printf '%s\n' "${CURRENT_STATUS_JSON}" \
> "${BACKUP_DIR}/nextcloud-status-before.json"
cat > "${BACKUP_DIR}/metadata.txt" <<META_EOF
timestamp=${STAMP}
current_image=${OLD_IMAGE}
target_image=${NEW_IMAGE}
current_nextcloud_version=${CURRENT_NC_VERSION}
target_nextcloud_version=${TARGET_NC_VERSION}
postgres_host=${POSTGRES_HOST}
postgres_port=${POSTGRES_PORT}
postgres_database=${POSTGRES_DB}
postgres_user=${POSTGRES_USER}
META_EOF
echo
echo "--- Test PostgreSQL connection ---"
DB_TEST="$(
psql \
-h "${POSTGRES_HOST}" \
-p "${POSTGRES_PORT}" \
-U "${POSTGRES_USER}" \
-d "${POSTGRES_DB}" \
-Atqc 'SELECT 1;'
)"
[[ "${DB_TEST}" == "1" ]] || \
fail "Cannot connect to PostgreSQL"
echo "PostgreSQL connection successful."
echo
echo "--- PostgreSQL version check ---"
SERVER_VERSION="$(
psql \
-h "${POSTGRES_HOST}" \
-p "${POSTGRES_PORT}" \
-U "${POSTGRES_USER}" \
-d "${POSTGRES_DB}" \
-Atqc 'SHOW server_version;'
)"
[[ -n "${SERVER_VERSION}" ]] || \
fail "Could not determine PostgreSQL server version"
printf '%s\n' "${SERVER_VERSION}" \
> "${DB_BACKUP_DIR}/postgresql-version.txt"
SERVER_MAJOR="${SERVER_VERSION%%.*}"
PG_DUMP_VERSION="$(
pg_dump --version |
sed -E 's/.* ([0-9]+(\.[0-9]+)?).*/\1/'
)"
PG_DUMP_MAJOR="${PG_DUMP_VERSION%%.*}"
echo "PostgreSQL server:"
echo " ${SERVER_VERSION}"
echo "pg_dump client:"
echo " ${PG_DUMP_VERSION}"
if (( PG_DUMP_MAJOR < SERVER_MAJOR )); then
fail \
"pg_dump ${PG_DUMP_VERSION} is older than PostgreSQL server ${SERVER_VERSION}"
fi
echo
echo "--- Database size ---"
DB_SIZE="$(
psql \
-h "${POSTGRES_HOST}" \
-p "${POSTGRES_PORT}" \
-U "${POSTGRES_USER}" \
-d "${POSTGRES_DB}" \
-Atqc \
'SELECT pg_size_pretty(pg_database_size(current_database()));'
)"
echo "Database:"
echo " ${POSTGRES_DB}"
echo "Size:"
echo " ${DB_SIZE}"
log "Create PostgreSQL backup"
pg_dump \
-h "${POSTGRES_HOST}" \
-p "${POSTGRES_PORT}" \
-U "${POSTGRES_USER}" \
-d "${POSTGRES_DB}" \
--format=custom \
--no-owner \
--no-privileges \
--file="${DB_BACKUP_FILE}"
[[ -s "${DB_BACKUP_FILE}" ]] || \
fail "PostgreSQL dump does not exist or is empty"
echo
echo "--- Validate PostgreSQL dump ---"
pg_restore \
--list \
"${DB_BACKUP_FILE}" \
> "${DB_BACKUP_DIR}/nextcloud.dump.list"
[[ -s "${DB_BACKUP_DIR}/nextcloud.dump.list" ]] || \
fail "pg_restore could not validate PostgreSQL dump"
echo
echo "--- Generate and verify SHA256 ---"
(
cd "${DB_BACKUP_DIR}"
sha256sum \
nextcloud.dump \
> nextcloud.dump.sha256
# The checksum contains the relative name "nextcloud.dump",
# therefore validation MUST run from the same directory.
sha256sum \
-c nextcloud.dump.sha256
)
DB_BACKUP_SIZE="$(
du -h "${DB_BACKUP_FILE}" |
awk '{print $1}'
)"
touch "${BACKUP_DIR}/.complete"
BACKUP_COMPLETE=true
echo
echo "PostgreSQL backup successful."
echo
echo "Backup directory:"
echo " ${BACKUP_DIR}"
echo
echo "Database dump:"
echo " ${DB_BACKUP_FILE}"
echo
echo "Dump size:"
echo " ${DB_BACKUP_SIZE}"
echo
echo "SHA256:"
cat "${DB_BACKUP_DIR}/nextcloud.dump.sha256"
cleanup_old_backups
fi
# ============================================================
# Deploy requested Compose
# ============================================================
log "Deploy docker-compose.yml"
cp \
"${SRC}/docker-compose.yml" \
"${LIVE}/docker-compose.yml"
PRODUCTION_CHANGED=true
cd "${LIVE}"
# ============================================================
# Pull all Compose images
# ============================================================
log "Pull Docker images"
docker compose \
--env-file "${ENV_FILE}" \
pull
# ============================================================
# NEXTCLOUD VERSION UPGRADE
# ============================================================
if [[ "${VERSION_CHANGED}" == "true" ]]; then
log "Nextcloud upgrade ${CURRENT_NC_VERSION} -> ${TARGET_NC_VERSION}"
echo
echo "--- Stop Nextcloud cron ---"
docker compose \
--env-file "${ENV_FILE}" \
stop cron || true
UPGRADE_STARTED="$(
date -u '+%Y-%m-%dT%H:%M:%SZ'
)"
echo
echo "--- Recreate Nextcloud container ---"
# From this point the new image can modify the DB schema.
MIGRATION_STARTED=true
docker compose \
--env-file "${ENV_FILE}" \
up -d \
--no-deps \
nextcloud
log "Wait for Nextcloud initialization"
INITIALIZED=false
for _ in $(seq 1 $((INIT_TIMEOUT / INIT_INTERVAL)))
do
RUNNING="$(
docker inspect \
-f '{{.State.Running}}' \
nextcloud 2>/dev/null ||
echo false
)"
if [[ "${RUNNING}" != "true" ]]; then
echo
echo "--- Nextcloud container log ---"
docker logs \
--since "${UPGRADE_STARTED}" \
nextcloud || true
fail "Nextcloud container stopped during upgrade"
fi
STARTUP_LOG="$(
docker logs \
--since "${UPGRADE_STARTED}" \
nextcloud 2>&1 || true
)"
if echo "${STARTUP_LOG}" |
grep -q 'Update failed'
then
echo "${STARTUP_LOG}"
fail "Nextcloud automatic upgrade failed"
fi
if echo "${STARTUP_LOG}" |
grep -q 'Initializing finished'
then
INITIALIZED=true
break
fi
sleep "${INIT_INTERVAL}"
done
if [[ "${INITIALIZED}" != "true" ]]; then
echo
echo "--- Nextcloud container log ---"
docker logs \
--since "${UPGRADE_STARTED}" \
nextcloud || true
fail "Timeout waiting for Nextcloud initialization"
fi
log "Nextcloud startup upgrade output"
STARTUP_LOG="$(
docker logs \
--since "${UPGRADE_STARTED}" \
nextcloud 2>&1
)"
echo "${STARTUP_LOG}"
STARTUP_UPDATE_SUCCESS=false
if echo "${STARTUP_LOG}" |
grep -q 'Update successful'
then
STARTUP_UPDATE_SUCCESS=true
fi
echo
echo "Startup log contains 'Update successful':"
echo " ${STARTUP_UPDATE_SUCCESS}"
log "Run explicit occ upgrade"
set +e
OCC_UPGRADE_OUTPUT="$(
docker exec \
-u www-data \
nextcloud \
php occ upgrade 2>&1
)"
OCC_UPGRADE_RC=$?
set -e
echo "${OCC_UPGRADE_OUTPUT}"
if [[ "${OCC_UPGRADE_RC}" -ne 0 ]]; then
fail \
"Explicit occ upgrade returned exit code ${OCC_UPGRADE_RC}"
fi
EXPLICIT_UPDATE_SUCCESS=false
EXPLICIT_ALREADY_CURRENT=false
if echo "${OCC_UPGRADE_OUTPUT}" |
grep -q 'Update successful'
then
EXPLICIT_UPDATE_SUCCESS=true
fi
if echo "${OCC_UPGRADE_OUTPUT}" |
grep -Eqi \
'already.*(latest|up.?to.?date)|(is|already).*up.?to.?date'
then
EXPLICIT_ALREADY_CURRENT=true
fi
echo
echo "Explicit occ upgrade contains 'Update successful':"
echo " ${EXPLICIT_UPDATE_SUCCESS}"
echo "Explicit occ upgrade reports already current:"
echo " ${EXPLICIT_ALREADY_CURRENT}"
if [[ "${STARTUP_UPDATE_SUCCESS}" != "true" ]] &&
[[ "${EXPLICIT_UPDATE_SUCCESS}" != "true" ]]
then
if [[ "${EXPLICIT_ALREADY_CURRENT}" == "true" ]]; then
echo
echo "occ reports that Nextcloud is already current,"
echo "but 'Update successful' was not found in either"
echo "the startup log or explicit occ upgrade output."
fi
fail "Could not confirm 'Update successful'"
fi
log "Validate Nextcloud status after upgrade"
wait_for_occ_normal
FINAL_STATUS_JSON="$(get_occ_status_json)"
echo "${FINAL_STATUS_JSON}"
validate_occ_status_json "${FINAL_STATUS_JSON}"
docker exec \
-u www-data \
nextcloud \
php occ status -e
echo
echo "Nextcloud database state validated successfully."
log "Start Nextcloud cron"
docker compose \
--env-file "${ENV_FILE}" \
up -d \
--no-deps \
cron
log "Apply remaining Compose changes"
docker compose \
--env-file "${ENV_FILE}" \
up -d \
--remove-orphans
# ============================================================
# NO NEXTCLOUD VERSION CHANGE
# ============================================================
else
log "No Nextcloud version change"
echo "Current internal version:"
echo " ${CURRENT_NC_VERSION}"
# Compose-only change, for example bind mount changes.
# No DB backup/occ upgrade is performed.
docker compose \
--env-file "${ENV_FILE}" \
up -d \
--remove-orphans
wait_for_occ_normal
fi
# ============================================================
# Final container checks
# ============================================================
log "Final container checks"
for CONTAINER in \
nextcloud \
nextcloud-cron \
nextcloud-redis
do
RUNNING="$(
docker inspect \
-f '{{.State.Running}}' \
"${CONTAINER}" 2>/dev/null ||
echo false
)"
if [[ "${RUNNING}" != "true" ]]; then
fail "${CONTAINER} is not running"
fi
echo "${CONTAINER}: running"
done
# ============================================================
# Final OCC status/version
# ============================================================
log "Final OCC status"
wait_for_occ_normal
FINAL_STATUS_JSON="$(get_occ_status_json)"
echo "${FINAL_STATUS_JSON}"
validate_occ_status_json "${FINAL_STATUS_JSON}"
docker exec \
-u www-data \
nextcloud \
php occ status -e
RUNNING_NC_VERSION="$(
printf '%s\n' "${FINAL_STATUS_JSON}" |
json_string_value version
)"
RUNNING_NC_VERSIONSTRING="$(
printf '%s\n' "${FINAL_STATUS_JSON}" |
json_string_value versionstring
)"
[[ -n "${RUNNING_NC_VERSION}" ]] || \
fail "Could not determine final Nextcloud version"
[[ "${RUNNING_NC_VERSION}" == "${TARGET_NC_VERSION}" ]] || \
fail \
"Running Nextcloud version ${RUNNING_NC_VERSION} does not match target ${TARGET_NC_VERSION}"
RUNNING_IMAGE="$(
docker inspect \
-f '{{.Config.Image}}' \
nextcloud
)"
# ============================================================
# Success
# ============================================================
log "Nextcloud deployment successful"
echo "Previous Nextcloud internal version:"
echo " ${CURRENT_NC_VERSION}"
echo
echo "Running Nextcloud internal version:"
echo " ${RUNNING_NC_VERSION}"
if [[ -n "${RUNNING_NC_VERSIONSTRING}" ]]; then
echo
echo "Running Nextcloud display version:"
echo " ${RUNNING_NC_VERSIONSTRING}"
fi
echo
echo "Previous Docker image:"
echo " ${OLD_IMAGE}"
echo
echo "Running Docker image:"
echo " ${RUNNING_IMAGE}"
if [[ "${VERSION_CHANGED}" == "true" ]]; then
echo
echo "Pre-upgrade backup:"
echo " ${BACKUP_DIR}"
echo
echo "PostgreSQL dump:"
echo " ${DB_BACKUP_FILE}"
echo
echo "Backup retention:"
echo " last ${BACKUP_KEEP} complete backups"
fi
# ============================================================
# Clear credentials
# ============================================================
unset PGPASSWORD
unset POSTGRES_PASSWORD
PRODUCTION_CHANGED=false
exit 0