--- # Nextcloud Major Version Upgrade Playbook (Fixed Version) # Created: 2026-01-23 # Purpose: Safely upgrade Nextcloud from v30 to v32 via v31 (staged upgrade) # # Usage: # cd ansible/ # HCLOUD_TOKEN="..." ansible-playbook -i hcloud.yml \ # playbooks/260123-upgrade-nextcloud-v2.yml --limit \ # --private-key "../keys/ssh/" # # Requirements: # - HCLOUD_TOKEN environment variable set # - SSH access to target server # - Sufficient disk space for backups # # Improvements over v1: # - Idempotent: can be re-run safely after failures # - Better version state tracking (reads actual running version) # - Proper maintenance mode handling # - Stage skipping if already on target version # - Better error messages and rollback instructions - name: Upgrade Nextcloud from v30 to v32 (staged) hosts: all become: true gather_facts: true vars: nextcloud_base_dir: "/opt/nextcloud" backup_dir: "/root/nextcloud-backup-{{ ansible_date_time.iso8601_basic_short }}" target_version: "32" tasks: # ============================================================ # PRE-UPGRADE CHECKS # ============================================================ - name: Display upgrade plan debug: msg: | ============================================================ Nextcloud Upgrade Plan - {{ inventory_hostname }} ============================================================ Target: Nextcloud v{{ target_version }} Backup: {{ backup_dir }} This playbook will: 1. Detect current version 2. Create backup if needed 3. Upgrade through required stages (v30→v31→v32) 4. Skip stages already completed 5. Re-enable apps and disable maintenance mode Estimated time: 10-20 minutes ============================================================ - name: Check if Nextcloud is installed shell: docker ps --filter "name=^nextcloud$" --format "{{ '{{' }}.Names{{ '}}' }}" register: nextcloud_running changed_when: false failed_when: false - name: Fail if Nextcloud is not running fail: msg: "Nextcloud container is not running on {{ inventory_hostname }}" when: "'nextcloud' not in nextcloud_running.stdout" - name: Get current Nextcloud version shell: docker exec -u www-data nextcloud php occ status --output=json register: nextcloud_status changed_when: false failed_when: false - name: Parse Nextcloud status set_fact: nc_status: "{{ nextcloud_status.stdout | from_json }}" when: nextcloud_status.rc == 0 - name: Handle Nextcloud in maintenance mode block: - name: Display maintenance mode warning debug: msg: "⚠ Nextcloud is in maintenance mode. Attempting to disable it..." - name: Disable maintenance mode if enabled shell: docker exec -u www-data nextcloud php occ maintenance:mode --off register: maint_off changed_when: "'disabled' in maint_off.stdout" - name: Wait a moment for mode change pause: seconds: 2 - name: Re-check status after disabling maintenance mode shell: docker exec -u www-data nextcloud php occ status --output=json register: nextcloud_status_retry changed_when: false - name: Update status set_fact: nc_status: "{{ nextcloud_status_retry.stdout | from_json }}" when: nextcloud_status.rc != 0 or (nc_status is defined and nc_status.maintenance | bool) - name: Display current version debug: msg: | Current: v{{ nc_status.versionstring }} Target: v{{ target_version }} Maintenance mode: {{ nc_status.maintenance }} - name: Check if already on target version debug: msg: "✓ Nextcloud is already on v{{ nc_status.versionstring }} - nothing to do" when: nc_status.versionstring is version(target_version, '>=') - name: End play if already upgraded meta: end_host when: nc_status.versionstring is version(target_version, '>=') - name: Check disk space shell: df -BG {{ nextcloud_base_dir }} | tail -1 | awk '{print $4}' | sed 's/G//' register: disk_space_gb changed_when: false - name: Verify sufficient disk space fail: msg: "Insufficient disk space: {{ disk_space_gb.stdout }}GB available, need at least 5GB" when: disk_space_gb.stdout | int < 5 - name: Display available disk space debug: msg: "Available disk space: {{ disk_space_gb.stdout }}GB" # ============================================================ # BACKUP PHASE (only if not already backed up) # ============================================================ - name: Check if backup already exists stat: path: "{{ backup_dir }}" register: backup_exists - name: Skip backup if already exists debug: msg: "✓ Backup already exists at {{ backup_dir }} - skipping backup phase" when: backup_exists.stat.exists - name: Create backup block: - name: Create backup directory file: path: "{{ backup_dir }}" state: directory mode: '0700' - name: Enable maintenance mode for backup shell: docker exec -u www-data nextcloud php occ maintenance:mode --on register: maintenance_on changed_when: "'enabled' in maintenance_on.stdout" - name: Backup Nextcloud database shell: | docker exec nextcloud-db pg_dump -U nextcloud nextcloud | gzip > {{ backup_dir }}/database.sql.gz args: creates: "{{ backup_dir }}/database.sql.gz" - name: Get database backup size stat: path: "{{ backup_dir }}/database.sql.gz" register: db_backup - name: Display database backup info debug: msg: "Database backup: {{ (db_backup.stat.size / 1024 / 1024) | round(2) }} MB" - name: Stop Nextcloud containers for volume backup community.docker.docker_compose_v2: project_src: "{{ nextcloud_base_dir }}" state: stopped - name: Backup Nextcloud app volume shell: | tar -czf {{ backup_dir }}/nextcloud-app-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-app/_data . args: creates: "{{ backup_dir }}/nextcloud-app-volume.tar.gz" - name: Backup Nextcloud database volume shell: | tar -czf {{ backup_dir }}/nextcloud-db-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-db-data/_data . args: creates: "{{ backup_dir }}/nextcloud-db-volume.tar.gz" - name: Copy current docker-compose.yml to backup copy: src: "{{ nextcloud_base_dir }}/docker-compose.yml" dest: "{{ backup_dir }}/docker-compose.yml.backup" remote_src: true - name: Display backup summary debug: msg: | ============================================================ ✓ Backup completed: {{ backup_dir }} ============================================================ To restore from backup if needed: 1. cd {{ nextcloud_base_dir }} && docker compose down 2. tar -xzf {{ backup_dir }}/nextcloud-app-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-app/_data 3. tar -xzf {{ backup_dir }}/nextcloud-db-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-db-data/_data 4. cp {{ backup_dir }}/docker-compose.yml.backup {{ nextcloud_base_dir }}/docker-compose.yml 5. cd {{ nextcloud_base_dir }} && docker compose up -d ============================================================ - name: Restart containers after backup community.docker.docker_compose_v2: project_src: "{{ nextcloud_base_dir }}" state: present - name: Wait for Nextcloud to be ready shell: | for i in {1..24}; do if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then exit 0 fi sleep 5 done exit 1 register: nextcloud_ready changed_when: false - name: Disable maintenance mode after backup shell: docker exec -u www-data nextcloud php occ maintenance:mode --off register: maint_off_backup changed_when: "'disabled' in maint_off_backup.stdout" when: not backup_exists.stat.exists # ============================================================ # DETERMINE UPGRADE PATH # ============================================================ - name: Get current version for upgrade planning shell: docker exec -u www-data nextcloud php occ status --output=json register: current_version_check changed_when: false - name: Parse current version set_fact: current_version: "{{ (current_version_check.stdout | from_json).versionstring }}" - name: Determine required upgrade stages set_fact: required_stages: "{{ [] }}" - name: Add v30→v31 stage if needed set_fact: required_stages: "{{ required_stages + [{'from': '30', 'to': '31', 'stage': 1}] }}" when: current_version is version('30', '>=') and current_version is version('31', '<') - name: Add v31→v32 stage if needed set_fact: required_stages: "{{ required_stages + [{'from': '31', 'to': '32', 'stage': 2}] }}" when: current_version is version('31', '>=') and current_version is version('32', '<') - name: Display upgrade stages debug: msg: | Current version: v{{ current_version }} Required stages: {{ required_stages | length }} {% if required_stages | length > 0 %} Will upgrade: {% for stage in required_stages %}v{{ stage.from }}→v{{ stage.to }}{{ ' ' if not loop.last else '' }}{% endfor %} {% else %} No upgrade stages needed {% endif %} - name: Skip upgrade if no stages needed meta: end_host when: required_stages | length == 0 # ============================================================ # STAGED UPGRADE LOOP # ============================================================ - name: Perform staged upgrades include_tasks: "{{ playbook_dir }}/260123-upgrade-nextcloud-stage-v2.yml" loop: "{{ required_stages }}" loop_control: loop_var: stage # ============================================================ # POST-UPGRADE # ============================================================ - name: Get final version shell: docker exec -u www-data nextcloud php occ status --output=json register: final_status changed_when: false - name: Parse final version set_fact: final_version: "{{ (final_status.stdout | from_json).versionstring }}" - name: Verify upgrade to target version fail: msg: "Upgrade incomplete - on v{{ final_version }}, expected v{{ target_version }}.x" when: final_version is version(target_version, '<') - name: Run database optimizations shell: docker exec -u www-data nextcloud php occ db:add-missing-indices register: db_indices changed_when: false failed_when: false - name: Run bigint conversion shell: docker exec -u www-data nextcloud php occ db:convert-filecache-bigint --no-interaction register: db_bigint changed_when: false failed_when: false timeout: 600 - name: Re-enable critical apps shell: | docker exec -u www-data nextcloud php occ app:enable user_oidc || true docker exec -u www-data nextcloud php occ app:enable richdocuments || true register: apps_enabled changed_when: false - name: Ensure maintenance mode is disabled shell: docker exec -u www-data nextcloud php occ maintenance:mode --off register: final_maint_off changed_when: "'disabled' in final_maint_off.stdout" failed_when: false - name: Update docker-compose.yml to use latest tag replace: path: "{{ nextcloud_base_dir }}/docker-compose.yml" regexp: 'image:\s*nextcloud:\d+' replace: 'image: nextcloud:latest' - name: Display success message debug: msg: | ============================================================ ✓ UPGRADE SUCCESSFUL! ============================================================ Server: {{ inventory_hostname }} From: v30.x To: v{{ final_version }} Backup: {{ backup_dir }} Next steps: 1. Test login: https://nextcloud.{{ client_domain }} 2. Test OIDC: Click "Login with Authentik" 3. Test file operations 4. Test Collabora Office If all tests pass, remove backup: rm -rf {{ backup_dir }} docker-compose.yml now uses 'nextcloud:latest' tag ============================================================