--- # Nextcloud Major Version Upgrade Playbook # Created: 2026-01-23 # Purpose: Safely upgrade Nextcloud from v30 to v32 via v31 (staged upgrade) # # Usage: # ansible-playbook -i hcloud.yml playbooks/260123-upgrade-nextcloud.yml --limit kikker # # Requirements: # - HCLOUD_TOKEN environment variable set # - SSH access to target server # - Sufficient disk space for backups # # Notes: # - Nextcloud does NOT support skipping major versions # - This playbook performs: v30 → v31 → v32 # - Creates backups before each stage # - Automatic rollback on failure - 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 }}" upgrade_stages: - { from: "30", to: "31", stage: 1 } - { from: "31", to: "32", stage: 2 } tasks: # ============================================================ # PRE-UPGRADE CHECKS AND PREPARATION # ============================================================ - name: Display upgrade plan debug: msg: | ============================================================ Nextcloud Upgrade Plan - {{ inventory_hostname }} ============================================================ Upgrade Path: v30 → v31 → v32 This playbook will: 1. Check current Nextcloud version 2. Create full backup of volumes and database 3. Disable all apps except core ones 4. Upgrade to v31 (Stage 1) 5. Verify v31 is working 6. Upgrade to v32 (Stage 2) 7. Verify v32 is working 8. Re-enable apps Backup location: {{ backup_dir }} Estimated time: 15-25 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 - name: Parse Nextcloud status set_fact: nc_status: "{{ nextcloud_status.stdout | from_json }}" - name: Display current version debug: msg: | Current version: {{ nc_status.versionstring }} Installed: {{ nc_status.installed }} Maintenance mode: {{ nc_status.maintenance }} Needs DB upgrade: {{ nc_status.needsDbUpgrade }} - name: Check if already on target version debug: msg: "Nextcloud is already on v32.x - skipping upgrade" when: nc_status.versionstring is version('32', '>=') - name: End play if already upgraded meta: end_host when: nc_status.versionstring is version('32', '>=') - name: Verify starting version is v30.x fail: msg: "This playbook only upgrades from v30. Current version: {{ nc_status.versionstring }}" when: nc_status.versionstring is version('30', '<') or nc_status.versionstring is version('31', '>=') - name: Check disk space shell: df -h {{ nextcloud_base_dir }} | tail -1 | awk '{print $4}' register: disk_space changed_when: false - name: Display available disk space debug: msg: "Available disk space: {{ disk_space.stdout }}" - name: Check if maintenance mode is enabled fail: msg: "Nextcloud is in maintenance mode. Please investigate before upgrading." when: nc_status.maintenance | bool # ============================================================ # BACKUP PHASE # ============================================================ - name: Create backup directory file: path: "{{ backup_dir }}" state: directory mode: '0700' - name: Enable Nextcloud maintenance mode shell: docker exec -u www-data nextcloud php occ maintenance:mode --on register: maintenance_on changed_when: "'Maintenance mode 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. Stop containers: cd {{ nextcloud_base_dir }} && docker compose down 2. Restore app volume: tar -xzf {{ backup_dir }}/nextcloud-app-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-app/_data 3. Restore DB volume: tar -xzf {{ backup_dir }}/nextcloud-db-volume.tar.gz -C /var/lib/docker/volumes/nextcloud-db-data/_data 4. Restore compose file: cp {{ backup_dir }}/docker-compose.yml.backup {{ nextcloud_base_dir }}/docker-compose.yml 5. Start containers: cd {{ nextcloud_base_dir }} && docker compose up -d ============================================================ # ============================================================ # STAGED UPGRADE LOOP # ============================================================ - name: Perform staged upgrades include_tasks: "{{ playbook_dir }}/260123-upgrade-nextcloud-stage.yml" loop: "{{ upgrade_stages }}" loop_control: loop_var: stage # ============================================================ # POST-UPGRADE VALIDATION # ============================================================ - name: Get final Nextcloud version shell: docker exec -u www-data nextcloud php occ status --output=json register: final_status changed_when: false - name: Parse final status set_fact: final_nc_status: "{{ final_status.stdout | from_json }}" - name: Verify upgrade to v32 fail: msg: "Upgrade failed - still on v{{ final_nc_status.versionstring }}" when: final_nc_status.versionstring is version('32', '<') - name: Run Nextcloud system check shell: docker exec -u www-data nextcloud php occ check register: system_check changed_when: false failed_when: false - name: Display system check results debug: msg: "{{ system_check.stdout_lines }}" - name: Re-enable user_oidc app shell: docker exec -u www-data nextcloud php occ app:enable user_oidc register: oidc_enable changed_when: "'enabled' in oidc_enable.stdout" failed_when: false - name: Re-enable richdocuments (Collabora) shell: docker exec -u www-data nextcloud php occ app:enable richdocuments register: collabora_enable changed_when: "'enabled' in collabora_enable.stdout" failed_when: false - name: Disable maintenance mode shell: docker exec -u www-data nextcloud php occ maintenance:mode --off register: maintenance_off changed_when: "'Maintenance mode disabled' in maintenance_off.stdout" - name: Update docker-compose.yml to use 'latest' tag lineinfile: path: "{{ nextcloud_base_dir }}/docker-compose.yml" regexp: '^\s*image:\s*nextcloud:32\s*$' line: ' image: nextcloud:latest' state: present - name: Display upgrade success message debug: msg: | ============================================================ ✓ Nextcloud Upgrade SUCCESSFUL! ============================================================ Server: {{ inventory_hostname }} Previous version: v30.x New version: v{{ final_nc_status.versionstring }} Backup location: {{ backup_dir }} Next steps: 1. Test login at: https://nextcloud.{{ client_domain }} 2. Test OIDC login (Login with Authentik) 3. Test file upload/download 4. Test Collabora Office integration If everything works, you can remove the backup: rm -rf {{ backup_dir }} The docker-compose.yml has been updated to use 'latest' tag for future automatic updates. ============================================================