#!/usr/bin/env bash set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" env_file="${repo_root}/.env.devprod" source "${repo_root}/scripts/lib-compose-project.sh" mode="ssl" nextcloud_image_override="" nextcloud_major_shortcut="" pull_image="1" do_backup="1" skip_precheck="0" skip_image_check="0" backup_dir="" db_engine_override="" usage() { cat <<'USAGE' Usage: ./upgrade-devprod-nextcloud.sh [major] [options] Safely upgrades the Nextcloud app/cron containers in devprod mode. Options: major Resolve latest official nextcloud:.*-apache image --ssl Use docker-compose.devprod.yml (default) --nossl Use docker-compose.devprod.nossl.yml --env-file Override env file (default: ./.env.devprod) --nc34 Shortcut for --nextcloud-image nextcloud:34-apache --nc33 Legacy shortcut for --nextcloud-image nextcloud:33-apache --nextcloud-image Target image; persisted to env after success --backup-dir Backup output dir --db-engine Force DB engine for backup (default: auto-detect from env) --skip-backup Skip backup step --skip-precheck Skip status precheck (not recommended) --skip-image-check Skip Docker Hub/running image update notice --no-pull Skip docker pull for selected image -h, --help Show this help Examples: ./upgrade-devprod-nextcloud.sh 34 ./upgrade-devprod-nextcloud.sh --nc34 ./upgrade-devprod-nextcloud.sh --nossl --nextcloud-image nextcloud:34.0.0-apache USAGE } read_kv() { local key="$1" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } set_kv() { local key="$1" local value="$2" local tmp_file="" tmp_file="$(mktemp)" awk -v key="${key}" -v value="${value}" ' BEGIN { found = 0 } $0 ~ "^" key "=" { print key "=" value found = 1 next } { print } END { if (!found) { print key "=" value } } ' "${env_file}" > "${tmp_file}" mv "${tmp_file}" "${env_file}" } resolve_path_from_env() { local key="$1" local default_path="$2" local value value="$(read_kv "${key}" || true)" value="${value:-${default_path}}" if [[ "${value}" == /* ]]; then echo "${value}" else echo "${repo_root}/${value}" fi } compose() { docker compose -f "${compose_file}" --env-file "${env_file}" "$@" } resolve_latest_nextcloud_major_image() { local major="$1" local timeout_seconds="${2:-8}" if ! command -v python3 >/dev/null 2>&1; then echo "python3 is required to resolve the latest Nextcloud major image." >&2 return 1 fi python3 - "${major}" "${timeout_seconds}" <<'PY' import json import re import sys import urllib.error import urllib.request major = int(sys.argv[1]) timeout = int(sys.argv[2]) best = None best_tag = None url = "https://hub.docker.com/v2/repositories/library/nextcloud/tags?page_size=100" try: while url: with urllib.request.urlopen(url, timeout=timeout) as response: payload = json.load(response) for entry in payload.get("results", []): name = str(entry.get("name", "")).strip() match = re.fullmatch(r"(\d+)(?:\.(\d+))?(?:\.(\d+))?-apache", name) if not match or int(match.group(1)) != major: continue minor = int(match.group(2)) if match.group(2) is not None else -1 patch = int(match.group(3)) if match.group(3) is not None else -1 version_key = (major, minor, patch) if best is None or version_key > best: best = version_key best_tag = name url = payload.get("next") except urllib.error.URLError as exc: print(f"Docker Hub lookup failed: {getattr(exc, 'reason', exc)}", file=sys.stderr) sys.exit(1) except Exception as exc: print(f"Docker Hub lookup failed: {exc}", file=sys.stderr) sys.exit(1) if not best_tag: print(f"No official nextcloud:{major}.*-apache image tag found on Docker Hub", file=sys.stderr) sys.exit(1) print(f"nextcloud:{best_tag}") PY } ensure_target_image_ready() { if [[ "${pull_image}" == "1" ]]; then echo "Pulling image: ${NEXTCLOUD_IMAGE}" docker pull "${NEXTCLOUD_IMAGE}" return 0 fi if docker image inspect "${NEXTCLOUD_IMAGE}" >/dev/null 2>&1; then echo "Using existing local image: ${NEXTCLOUD_IMAGE}" return 0 fi echo "Target image is not present locally and --no-pull was requested: ${NEXTCLOUD_IMAGE}" echo "Either remove --no-pull or pre-pull the image first." exit 1 } occ_exec() { compose exec -T --user www-data app php occ "$@" } dump_mariadb() { local dump_user="$1" local dump_password="$2" local dump_db="$3" local dump_out="$4" local dump_err="$5" local dump_cmd='if command -v mysqldump >/dev/null 2>&1; then exec mysqldump --single-transaction --skip-lock-tables -u"$DUMP_USER" "$DUMP_DB" elif command -v mariadb-dump >/dev/null 2>&1; then exec mariadb-dump --single-transaction --skip-lock-tables -u"$DUMP_USER" "$DUMP_DB" else echo "Neither mysqldump nor mariadb-dump is available in db container." >&2 exit 127 fi' compose exec -T \ -e DUMP_USER="${dump_user}" \ -e DUMP_DB="${dump_db}" \ -e MYSQL_PWD="${dump_password}" \ db sh -lc "${dump_cmd}" \ > "${dump_out}" 2> "${dump_err}" } dump_postgres() { local dump_user="$1" local dump_password="$2" local dump_db="$3" local dump_out="$4" local dump_err="$5" local dump_cmd='if command -v pg_dump >/dev/null 2>&1; then exec pg_dump -U "$DUMP_USER" -d "$DUMP_DB" else echo "pg_dump is not available in db container." >&2 exit 127 fi' compose exec -T \ -e DUMP_USER="${dump_user}" \ -e DUMP_DB="${dump_db}" \ -e PGPASSWORD="${dump_password}" \ db sh -lc "${dump_cmd}" \ > "${dump_out}" 2> "${dump_err}" } create_backup_archive() { local archive_file="$1" local base_dir="$2" local source_path="$3" local error_file="${archive_file}.stderr.log" local tar_rc="0" set +e tar -czf "${archive_file}" -C "${base_dir}" "${source_path}" 2> "${error_file}" tar_rc="$?" set -e if [[ "${tar_rc}" -eq 0 ]]; then rm -f "${error_file}" return 0 fi if [[ "${tar_rc}" -eq 1 ]] && grep -q "file changed as we read it" "${error_file}"; then echo "Archive completed with live-file warnings: ${archive_file}" echo "See: ${error_file}" return 0 fi echo "Archive failed: ${archive_file}" echo "See: ${error_file}" return "${tar_rc}" } status_has_true() { local status_json="$1" local key="$2" echo "${status_json}" | grep -q "\"${key}\":[[:space:]]*true" } refresh_bundled_app_from_image() { local app_id="$1" if [[ ! "${app_id}" =~ ^[A-Za-z0-9_-]+$ ]]; then echo "Refusing to refresh invalid bundled app id: ${app_id}" return 1 fi compose exec -T app sh -lc ' set -eu app_id="$1" src="/usr/src/nextcloud/apps/${app_id}" dst="/var/www/html/apps/${app_id}" if [ ! -d "${src}" ]; then echo "Bundled app source not found in target image: ${src}" >&2 exit 1 fi rm -rf "${dst}" mkdir -p /var/www/html/apps cp -a "${src}" "${dst}" chown -R www-data:www-data "${dst}" ' sh "${app_id}" } replace_custom_app_with_bundled_app_if_available() { local app_id="$1" if [[ ! "${app_id}" =~ ^[A-Za-z0-9_-]+$ ]]; then echo "Refusing to replace invalid app id: ${app_id}" return 1 fi compose exec -T app sh -lc ' set -eu app_id="$1" src="/usr/src/nextcloud/apps/${app_id}" custom="/var/www/html/custom_apps/${app_id}" bundled="/var/www/html/apps/${app_id}" backup_dir="/var/www/html/data/app-backups/stale-custom-apps" if [ ! -d "${src}" ] || [ ! -d "${custom}" ]; then exit 0 fi stamp="$(date +%Y%m%d-%H%M%S)" mkdir -p "${backup_dir}" mv "${custom}" "${backup_dir}/${app_id}-${stamp}" rm -rf "${bundled}" cp -a "${src}" "${bundled}" chown -R www-data:www-data "${bundled}" "${backup_dir}/${app_id}-${stamp}" echo "Replaced stale custom app ${app_id} with bundled target-image copy." echo "Stale custom copy moved to data/app-backups/stale-custom-apps/${app_id}-${stamp}" ' sh "${app_id}" } refresh_nextcloud_core_from_image() { compose exec -T app sh -lc ' set -eu cd /var/www/html for path in 3rdparty apps core lib ocs ocs-provider resources; do rm -rf "${path}" cp -a "/usr/src/nextcloud/${path}" "${path}" done find /usr/src/nextcloud -maxdepth 1 -type f -exec cp -a {} /var/www/html/ \; chown -R www-data:www-data 3rdparty apps core lib ocs ocs-provider resources find /var/www/html -maxdepth 1 -type f -exec chown www-data:www-data {} \; ' } run_occ_upgrade_with_bundled_app_repair() { local upgrade_output="" local upgrade_rc="0" local app_id="" set +e upgrade_output="$(occ_exec upgrade 2>&1)" upgrade_rc="$?" set -e printf '%s\n' "${upgrade_output}" if [[ "${upgrade_rc}" -eq 0 ]]; then return 0 fi if printf '%s\n' "${upgrade_output}" | grep -q 'CleanPreviews::__construct().*IAppConfig.*OC\\AllConfig'; then echo "Detected mixed Nextcloud core files; refreshing image-owned core files from target image." refresh_nextcloud_core_from_image echo "Retrying Nextcloud upgrade after core refresh..." occ_exec upgrade return fi app_id="$(printf '%s\n' "${upgrade_output}" | sed -nE 's/.*app "([^"]+)".*not correctly replaced.*/\1/p' | head -n1)" if [[ -z "${app_id}" ]]; then return "${upgrade_rc}" fi echo "Refreshing bundled app files from target image: ${app_id}" refresh_bundled_app_from_image "${app_id}" echo "Retrying Nextcloud upgrade after bundled app refresh..." occ_exec upgrade } detect_db_engine() { if [[ -n "${db_engine_override}" ]]; then echo "${db_engine_override}" return 0 fi local nc_db_engine nc_db_engine="$(read_kv "NEXTCLOUD_DB_ENGINE" || true)" case "${nc_db_engine}" in mysql|mariadb) echo "mysql" return 0 ;; postgres|postgresql) echo "postgres" return 0 ;; esac local pg_db pg_user pg_db="$(read_kv "POSTGRES_DB" || true)" pg_user="$(read_kv "POSTGRES_USER" || true)" if [[ -n "${pg_db}" && -n "${pg_user}" ]]; then echo "postgres" return 0 fi echo "mysql" } wait_occ_ready() { local attempts="${1:-24}" local sleep_seconds="${2:-5}" local i for i in $(seq 1 "${attempts}"); do if occ_exec status >/dev/null 2>&1; then return 0 fi sleep "${sleep_seconds}" done return 1 } read_status_json() { occ_exec status --output=json 2>/dev/null || true } ensure_bundled_custom_app_compatibility() { local compat_script="${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" if [[ ! -x "${compat_script}" ]]; then return 0 fi "${compat_script}" --image "${NEXTCLOUD_IMAGE}" --tracked-only } update_appstore_app_compatibility() { local compat_script="${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" local incompatible_apps=() local app_id="" if [[ ! -x "${compat_script}" ]]; then return 0 fi if "${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only; then return 0 fi echo "Some app-store custom apps do not currently declare compatibility with ${NEXTCLOUD_IMAGE}." echo "Attempting app-store updates for the blocking app(s), then re-checking compatibility..." mapfile -t incompatible_apps < <("${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only --list-blocking-apps || true) if [[ "${#incompatible_apps[@]}" -eq 0 ]]; then echo "Unable to determine blocking app IDs." return 1 fi for app_id in "${incompatible_apps[@]}"; do if [[ -z "${app_id}" ]]; then continue fi echo "Updating app after core upgrade: ${app_id}" if ! occ_exec app:update "${app_id}"; then echo "App update did not complete for ${app_id}; continuing to final compatibility check." fi replace_custom_app_with_bundled_app_if_available "${app_id}" occ_exec app:enable "${app_id}" >/dev/null 2>&1 || true done "${compat_script}" --image "${NEXTCLOUD_IMAGE}" --untracked-only } next_arg_is_value="" for arg in "$@"; do if [[ -n "${next_arg_is_value}" ]]; then case "${next_arg_is_value}" in env_file) env_file="${arg}" ;; nextcloud_image_override) nextcloud_image_override="${arg}" ;; backup_dir) backup_dir="${arg}" ;; esac next_arg_is_value="" continue fi case "${arg}" in --ssl) mode="ssl" ;; --nossl) mode="nossl" ;; --env-file) next_arg_is_value="env_file" ;; --env-file=*) env_file="${arg#*=}" ;; --nc34) nextcloud_image_override="nextcloud:34-apache" ;; --nc33) nextcloud_image_override="nextcloud:33-apache" ;; --nextcloud-image) next_arg_is_value="nextcloud_image_override" ;; --nextcloud-image=*) nextcloud_image_override="${arg#*=}" ;; --backup-dir) next_arg_is_value="backup_dir" ;; --backup-dir=*) backup_dir="${arg#*=}" ;; --db-engine) next_arg_is_value="db_engine_override" ;; --db-engine=*) db_engine_override="${arg#*=}" ;; --skip-backup) do_backup="0" ;; --skip-precheck) skip_precheck="1" ;; --skip-image-check) skip_image_check="1" ;; --no-pull) pull_image="0" ;; -h|--help) usage exit 0 ;; *) if [[ "${arg}" =~ ^[0-9]+$ ]]; then if [[ -n "${nextcloud_major_shortcut}" ]]; then echo "Only one Nextcloud major shortcut may be provided." usage exit 1 fi nextcloud_major_shortcut="${arg}" else echo "Unknown option: ${arg}" usage exit 1 fi ;; esac done if [[ -n "${next_arg_is_value}" ]]; then echo "Missing value for option: --${next_arg_is_value//_/-}" exit 1 fi if [[ -n "${nextcloud_major_shortcut}" && -n "${nextcloud_image_override}" ]]; then echo "Use either a major shortcut or --nextcloud-image, not both." exit 1 fi if [[ "${env_file}" != /* ]]; then env_file="${repo_root}/${env_file}" fi if [[ -f "${env_file}" ]]; then if ! export_compose_project_name_from_env "${env_file}"; then warn_missing_compose_project_name "${env_file}" fi fi if [[ -n "${db_engine_override}" ]]; then case "${db_engine_override}" in mysql|postgres) ;; *) echo "Invalid --db-engine value: ${db_engine_override} (expected mysql or postgres)" exit 1 ;; esac fi if [[ ! -f "${env_file}" ]]; then echo "Missing env file: ${env_file}" exit 1 fi if [[ "${mode}" == "nossl" ]]; then compose_file="${repo_root}/docker-compose.devprod.nossl.yml" else compose_file="${repo_root}/docker-compose.devprod.yml" fi target_image="${nextcloud_image_override}" if [[ -n "${nextcloud_major_shortcut}" ]]; then target_image="$(resolve_latest_nextcloud_major_image "${nextcloud_major_shortcut}")" echo "Resolved Nextcloud ${nextcloud_major_shortcut} shortcut to ${target_image}" fi if [[ -z "${target_image}" ]]; then target_image="$(read_kv "NEXTCLOUD_IMAGE" || true)" fi target_image="${target_image:-nextcloud:34-apache}" export NEXTCLOUD_IMAGE="${target_image}" echo "Using compose file: ${compose_file}" echo "Using env file: ${env_file}" echo "Target Nextcloud image: ${NEXTCLOUD_IMAGE}" if [[ "${skip_image_check}" != "1" && -x "${repo_root}/scripts/check-nextcloud-image-update.sh" ]]; then "${repo_root}/scripts/check-nextcloud-image-update.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --image "${NEXTCLOUD_IMAGE}" \ --include-newer-major || true fi if ! wait_occ_ready 24 5; then echo "Nextcloud OCC is not ready; aborting." exit 1 fi if [[ "${skip_precheck}" != "1" ]]; then echo "Running pre-upgrade status check..." status_json="$(read_status_json)" if [[ -z "${status_json}" ]]; then echo "Unable to read OCC status in JSON form. Run manually: docker compose ... exec -T --user www-data app php occ status" exit 1 fi if status_has_true "${status_json}" "maintenance"; then echo "Instance is already in maintenance mode; continuing so this script can finish or settle the upgrade." fi if status_has_true "${status_json}" "needsDbUpgrade"; then echo "Instance already reports a pending database/core upgrade; continuing so this script can finish it." fi fi if [[ "${do_backup}" == "1" ]]; then timestamp="$(date +%Y%m%d-%H%M%S)" if [[ -z "${backup_dir}" ]]; then backup_dir="${repo_root}/backups/nextcloud-upgrade-${timestamp}" fi mkdir -p "${backup_dir}" echo "Creating backup in: ${backup_dir}" cp -f "${env_file}" "${backup_dir}/env.snapshot" cp -f "${compose_file}" "${backup_dir}/compose.snapshot.yml" db_dump_file="${backup_dir}/nextcloud-db.sql" db_dump_err_file="${backup_dir}/nextcloud-db.stderr.log" db_engine="$(detect_db_engine)" echo "Detected DB engine for backup: ${db_engine}" db_dump_ok="0" if [[ "${db_engine}" == "postgres" ]]; then db_name="$(read_kv "POSTGRES_DB" || true)" db_user="$(read_kv "POSTGRES_USER" || true)" db_password="$(read_kv "POSTGRES_PASSWORD" || true)" db_name="${db_name:-nextcloud}" if [[ -n "${db_user}" && -n "${db_password}" ]]; then if dump_postgres "${db_user}" "${db_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then db_dump_ok="1" fi fi if [[ "${db_dump_ok}" != "1" ]]; then echo "PostgreSQL backup failed." echo "See: ${db_dump_err_file}" echo "Quick check:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T db sh -lc 'command -v pg_dump || true'" exit 1 fi else db_name="$(read_kv "MYSQL_DATABASE" || true)" db_user="$(read_kv "MYSQL_USER" || true)" db_password="$(read_kv "MYSQL_PASSWORD" || true)" db_root_password="$(read_kv "MYSQL_ROOT_PASSWORD" || true)" db_name="${db_name:-nextcloud}" if [[ -n "${db_user}" && -n "${db_password}" ]]; then if dump_mariadb "${db_user}" "${db_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then db_dump_ok="1" fi fi if [[ "${db_dump_ok}" != "1" && -n "${db_root_password}" ]]; then if dump_mariadb "root" "${db_root_password}" "${db_name}" "${db_dump_file}" "${db_dump_err_file}"; then db_dump_ok="1" fi fi if [[ "${db_dump_ok}" != "1" ]]; then echo "MySQL/MariaDB backup failed." echo "See: ${db_dump_err_file}" echo "Quick check:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T db sh -lc 'command -v mysqldump || command -v mariadb-dump || true'" exit 1 fi fi create_backup_archive "${backup_dir}/nextcloud-html.tar.gz" "${repo_root}" nextcloud/html create_backup_archive "${backup_dir}/nextcloud-data.tar.gz" "${repo_root}" nextcloud/data custom_apps_path="$(resolve_path_from_env "NEXTCLOUD_CUSTOM_APPS_PATH" "./nextcloud/custom_apps")" if [[ -d "${custom_apps_path}" ]]; then create_backup_archive "${backup_dir}/nextcloud-custom-apps.tar.gz" "$(dirname "${custom_apps_path}")" "$(basename "${custom_apps_path}")" fi fi ensure_bundled_custom_app_compatibility ensure_target_image_ready echo "Recreating app/cron containers on target image..." compose up -d --force-recreate --no-deps app cron if ! wait_occ_ready 36 5; then echo "Nextcloud OCC did not come back after container recreate." echo "Fix container health and rerun:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade" exit 1 fi echo "Checking post-recreate upgrade status..." status_json="$(read_status_json)" if [[ -z "${status_json}" ]]; then echo "Unable to read OCC status after container recreate." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi if status_has_true "${status_json}" "maintenance"; then echo "Instance is in maintenance mode after recreate; continuing to the upgrade/settle step." fi echo "Running Nextcloud upgrade/settle step..." run_occ_upgrade_with_bundled_app_repair status_json="$(read_status_json)" if [[ -z "${status_json}" ]]; then echo "Unable to read OCC status after upgrade step." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi if echo "${status_json}" | grep -q '"needsDbUpgrade":[[:space:]]*true'; then echo "Nextcloud still requires a database upgrade after occ upgrade." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi update_appstore_app_compatibility if [[ -x "${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" ]]; then "${repo_root}/scripts/check-nextcloud-custom-app-compat.sh" --image "${NEXTCLOUD_IMAGE}" fi echo "Running post-upgrade repair tasks..." occ_exec maintenance:repair occ_exec db:add-missing-indices || true occ_exec maintenance:mimetype:update-js || true echo "Reapplying Nextcloud defaults and NuQloud skeleton..." "${repo_root}/scripts/configure-nextcloud-defaults.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --restore-core-skeleton echo "Restarting app container to clear stale opcache..." compose restart app if ! wait_occ_ready 36 5; then echo "Nextcloud OCC did not come back after post-upgrade restart." echo "Fix container health and rerun:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi final_status_json="$(read_status_json)" if [[ -z "${final_status_json}" ]]; then echo "Unable to read OCC status after post-upgrade restart." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi if echo "${final_status_json}" | grep -q '"needsDbUpgrade":[[:space:]]*true'; then echo "Nextcloud still requires a database upgrade after the upgrade run." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ upgrade" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi if echo "${final_status_json}" | grep -q '"maintenance":[[:space:]]*true'; then echo "Maintenance mode is still enabled after upgrade; disabling it..." if ! occ_exec maintenance:mode --off >/dev/null 2>&1; then echo "Unable to disable maintenance mode automatically." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:mode --off" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi final_status_json="$(read_status_json)" if [[ -z "${final_status_json}" ]]; then echo "Unable to re-read OCC status after disabling maintenance mode." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi if echo "${final_status_json}" | grep -q '"maintenance":[[:space:]]*true'; then echo "Maintenance mode is still enabled after attempting to disable it." echo "Run manually:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ maintenance:mode --off" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T --user www-data app php occ status" exit 1 fi fi echo "Final status:" occ_exec status persisted_nextcloud_image="$(read_kv "NEXTCLOUD_IMAGE" || true)" if [[ "${persisted_nextcloud_image}" != "${NEXTCLOUD_IMAGE}" ]]; then set_kv "NEXTCLOUD_IMAGE" "${NEXTCLOUD_IMAGE}" echo "Persisted NEXTCLOUD_IMAGE=${NEXTCLOUD_IMAGE} to ${env_file}" fi echo "Upgrade complete."