2026-01-23 21:25:44 +01:00
|
|
|
---
|
|
|
|
|
# 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 <server> \
|
|
|
|
|
# --private-key "../keys/ssh/<server>"
|
|
|
|
|
#
|
|
|
|
|
# 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: |
|
2026-01-23 21:41:14 +01:00
|
|
|
count=0
|
|
|
|
|
max_attempts=24
|
|
|
|
|
while [ $count -lt $max_attempts ]; do
|
2026-01-23 21:25:44 +01:00
|
|
|
if docker exec nextcloud curl -f http://localhost:80/status.php 2>/dev/null; then
|
2026-01-23 21:41:14 +01:00
|
|
|
echo "Ready after $count attempts"
|
2026-01-23 21:25:44 +01:00
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
sleep 5
|
2026-01-23 21:41:14 +01:00
|
|
|
count=$((count + 1))
|
2026-01-23 21:25:44 +01:00
|
|
|
done
|
2026-01-23 21:41:14 +01:00
|
|
|
echo "Timeout after $max_attempts attempts"
|
2026-01-23 21:25:44 +01:00
|
|
|
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
|
|
|
|
|
============================================================
|