feat: Add Nextcloud major version upgrade playbook (v30→v32)

Created: 2026-01-23

Add automated playbook to safely upgrade Nextcloud from v30 (EOL) to v32
through staged upgrades, respecting Nextcloud's no-version-skip policy.

Features:
- Pre-upgrade validation (version, disk space, maintenance mode)
- Automatic full backup (database + volumes)
- Staged upgrades: v30 → v31 → v32
- Per-stage app disabling/enabling
- Database migrations (indices, bigint conversion)
- Post-upgrade validation and system checks
- Rollback instructions in case of failure
- Updates docker-compose.yml to 'latest' tag after success

Files:
- playbooks/260123-upgrade-nextcloud.yml (main playbook)
- playbooks/260123-upgrade-nextcloud-stage.yml (stage tasks)

Usage:
  cd ansible/
  HCLOUD_TOKEN="..." ansible-playbook -i hcloud.yml \
    playbooks/260123-upgrade-nextcloud.yml --limit kikker

Safety:
- Creates timestamped backup before any changes
- Stops containers during volume backup
- Verifies version after each stage
- Provides rollback commands in output

Ready to upgrade kikker from v30.0.17 to v32.x

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pieter 2026-01-23 20:58:25 +01:00
parent 27d59e4cd3
commit 14256bcbce
2 changed files with 388 additions and 0 deletions

View file

@ -0,0 +1,118 @@
---
# Nextcloud Upgrade Stage Task File
# This file is included by 260123-upgrade-nextcloud.yml for each upgrade stage
# Do not run directly
- name: "Stage {{ stage.stage }}: Upgrade from v{{ stage.from }} to v{{ stage.to }}"
debug:
msg: |
============================================================
Starting Stage {{ stage.stage }}: v{{ stage.from }} → v{{ stage.to }}
============================================================
- name: "Stage {{ stage.stage }}: Verify current version is v{{ stage.from }}"
shell: docker exec -u www-data nextcloud php occ status --output=json
register: stage_version_check
changed_when: false
- name: "Stage {{ stage.stage }}: Parse current version"
set_fact:
stage_current: "{{ (stage_version_check.stdout | from_json).versionstring }}"
- name: "Stage {{ stage.stage }}: Check version compatibility"
fail:
msg: "Expected v{{ stage.from }}.x but found v{{ stage_current }}"
when: stage_current is version(stage.from, '<') or stage_current is version(stage.to, '>=')
- name: "Stage {{ stage.stage }}: Disable non-essential apps"
shell: |
docker exec -u www-data nextcloud php occ app:list --output=json | \
jq -r '.enabled | keys[]' | \
grep -Ev '^(files|dav|federatedfilesharing|settings|provisioning_api|files_sharing|files_trashbin|files_versions|comments|contactsinteraction|dashboard|activity|notifications|user_status|weather_status|workflowengine)$' | \
while read app; do
echo "Disabling $app"
docker exec -u www-data nextcloud php occ app:disable "$app" || true
done
register: apps_disabled
changed_when: "'Disabling' in apps_disabled.stdout"
- name: "Stage {{ stage.stage }}: Update docker-compose.yml to v{{ stage.to }}"
replace:
path: "{{ nextcloud_base_dir }}/docker-compose.yml"
regexp: 'image:\s*nextcloud:{{ stage.from }}'
replace: 'image: nextcloud:{{ stage.to }}'
- name: "Stage {{ stage.stage }}: Pull Nextcloud v{{ stage.to }} image"
shell: docker pull nextcloud:{{ stage.to }}
register: image_pull
changed_when: "'Downloaded' in image_pull.stdout or 'Pulling' in image_pull.stdout"
- name: "Stage {{ stage.stage }}: Start Nextcloud with new version"
community.docker.docker_compose_v2:
project_src: "{{ nextcloud_base_dir }}"
state: present
pull: policy=always
- name: "Stage {{ stage.stage }}: Wait for container to be ready"
shell: |
timeout=300
elapsed=0
while [ $elapsed -lt $timeout ]; do
if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then
echo "Container ready"
exit 0
fi
sleep 5
elapsed=$((elapsed + 5))
done
echo "Timeout waiting for container"
exit 1
register: container_ready
changed_when: false
- name: "Stage {{ stage.stage }}: Run occ upgrade"
shell: docker exec -u www-data nextcloud php occ upgrade --no-interaction
register: occ_upgrade
changed_when: "'Update successful' in occ_upgrade.stdout or 'upgraded' in occ_upgrade.stdout"
failed_when:
- occ_upgrade.rc != 0
- "'already latest version' not in occ_upgrade.stdout"
- name: "Stage {{ stage.stage }}: Display upgrade output"
debug:
msg: "{{ occ_upgrade.stdout_lines }}"
- name: "Stage {{ stage.stage }}: Verify upgrade succeeded"
shell: docker exec -u www-data nextcloud php occ status --output=json
register: stage_verify
changed_when: false
- name: "Stage {{ stage.stage }}: Parse upgraded version"
set_fact:
stage_upgraded: "{{ (stage_verify.stdout | from_json).versionstring }}"
- name: "Stage {{ stage.stage }}: Check upgrade was successful"
fail:
msg: "Upgrade to v{{ stage.to }} failed - still on v{{ stage_upgraded }}"
when: stage_upgraded is version(stage.to, '<')
- name: "Stage {{ stage.stage }}: Run database migrations"
shell: docker exec -u www-data nextcloud php occ db:add-missing-indices
register: db_indices
changed_when: "'indices added' in db_indices.stdout"
failed_when: false
- name: "Stage {{ stage.stage }}: Run database column conversions"
shell: docker exec -u www-data nextcloud php occ db:convert-filecache-bigint --no-interaction
register: db_bigint
changed_when: "'converted' in db_bigint.stdout"
failed_when: false
timeout: 600
- name: "Stage {{ stage.stage }}: Success"
debug:
msg: |
============================================================
✓ Stage {{ stage.stage }} completed successfully
Upgraded from v{{ stage.from }} to v{{ stage_upgraded }}
============================================================

View file

@ -0,0 +1,270 @@
---
# 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.
============================================================