Migration toolkit: Nextcloud bare/Hansson install → Nextcloud All-in-One (AIO).
Tested path: NC v30 (Debian 12, Apache + PHP-FPM + PostgreSQL) → AIO v13 (NC v33) on Debian 13. Works for any NC version that can be upgraded sequentially to the AIO target version.
- Transfer — rsync files + database dump from the old server to the new VM
- Build staging — minimal Apache + PHP + DB stack to run the upgrade chain
- Upgrade — sequential major-version upgrades (e.g. v30 → v31 → v32 → v33)
- Install AIO — Docker + AIO mastercontainer; guided wizard
- Import — restore the upgraded DB into AIO, carry over instanceid/passwordsalt/secret
The data directory (/mnt/ncdata) is rsynced once upfront and delta-synced at cutover.
Downtime window is only the final delta-sync + DNS cutover — typically under an hour.
- New VM with Debian 12/13, SSH root access, enough disk for data
- Old server accessible via SSH from the new VM (key-based)
- DNS control for your NC domain
~/.ssh/configaliases set up for all hosts
Optional:
- Proxmox for live snapshots between upgrade steps (
USE_PROXMOX=true) - OPNSense with public VIP routing (
proxmox/build-vm.sh)
cp config.sh config.local.sh # never commit this file
# edit config.local.sh — fill in all variables
source config.local.sh
./01-transfer.sh # start background rsync (can take hours)
./02-build-staging.sh # build Apache+PHP+DB on target
./03-upgrade.sh # sequential NC upgrade chain
./04-install-aio.sh test # AIO on staging domain first
# → complete wizard in browser at https://<target>:8080
./05-import.sh # import DB + data into AIO
# After verification:
./04-install-aio.sh production # switch to production domain + LE cert
./05-import.sh # re-run import on production AIO| Variable | Description |
|---|---|
SOURCE_HOST |
IP/hostname of the existing NC server |
SOURCE_NC_PATH |
Path to NC app directory (default /var/www/nextcloud) |
SOURCE_DATA_PATH |
Path to ncdata (default /mnt/ncdata) |
SOURCE_DB |
Database name on source |
SOURCE_DB_TYPE |
postgres or mysql |
SOURCE_DB_PASS |
DB password — only needed for MySQL |
TARGET_SSH |
SSH target for new VM (root@10.x.x.x or alias) |
TARGET_DATA_PATH |
Where ncdata lives on target |
NC_DOMAIN |
Production domain for AIO + Let's Encrypt |
NC_STAGING_DOMAIN |
Staging domain (HTTP only, internal) |
STAGING_DB_PASS |
Password for staging DB (created in step 02) |
NC_UPGRADE_VERSIONS |
Array of NC versions to upgrade through |
USE_PROXMOX |
true/false — enable snapshot after each upgrade step |
PROXMOX_HOST |
SSH alias for PVE node |
PROXMOX_VMID |
VM ID for snapshots |
AIO uses PostgreSQL exclusively. If your source uses MySQL/MariaDB, you need one
extra step after the upgrade chain and before 05-import.sh:
# On the target VM, after 03-upgrade.sh completes:
sudo -u www-data php8.3 /var/www/nextcloud/occ \
db:convert-type --all-apps pgsql \
<staging_db_user> <staging_db_pass> <staging_db_name>This converts the staging MariaDB to PostgreSQL in-place. After conversion,
05-import.sh takes a pg_dump of that PostgreSQL DB and imports it into AIO.
NC requires sequential major-version upgrades — you cannot skip a major.
Set NC_UPGRADE_VERSIONS in config.sh based on your source version:
| Source | Example path |
|---|---|
| v28/v29 | ("29.0.x" "30.0.x" "31.0.x" "32.0.x" "33.0.x") |
| v30 | ("31.0.14" "32.0.12" "33.0.6") |
| v32 | ("33.0.6") |
Check the NC release archive for the latest patch version per major. AIO ships a specific NC version — check the AIO release notes to know which final version to target.
If you use Proxmox, proxmox/build-vm.sh creates the target VM automatically:
Debian 13 cloud image, separate LVM data disk, loopback VIP (optional), OPNSense routing (optional).
Fill in the Proxmox VM section of config.sh, then:
./proxmox/build-vm.shAfter the VM is up, set TARGET_SSH to match VM_PRIVATE_IP and continue with 01-transfer.sh.
These caused real failures during development. Read before running.
Debian 13 ships PHP 8.4. NC v30 requires PHP ≤ 8.3.
02-build-staging.sh adds the sury.org repo for PHP 8.3 automatically.
If your source is NC v32+, you can remove the sury.org step and use the system PHP.
--exclude='config/' matches any directory named config in the tree,
including apps/someapp/config/. Use --exclude='/config/' (leading slash) to
anchor it to the root of the source — this is what the scripts use.
APCu is disabled in CLI by default (apc.enable_cli=0). occ commands that touch
APCu caches silently fail or produce wrong results. 02-build-staging.sh sets
apc.enable_cli=1 in /etc/php/8.3/cli/php.ini automatically.
NC's config.php defines $CONFIG but does not return it.
include('/path/to/config.php') returns 1 (bool), not the config array.
Use require instead — the scripts do this.
Third-party apps that are present in /var/www/nextcloud/apps/ but lack
appinfo/info.xml (abandoned, partially deleted, or leftover) cause occ upgrade
to crash. 03-upgrade.sh removes them automatically before each upgrade step.
The AIO mastercontainer must not bind ports 80, 443, or 3478.
AIO's apache sub-container manages those ports itself.
Binding them on the mastercontainer causes the domain check to fail with a
conflict — two processes listening on port 443.
04-install-aio.sh does not bind those ports.
Older AIO docs refer to nextcloud-aio-postgresql — this was renamed.
Current names used by 05-import.sh:
| Role | Container/resource name |
|---|---|
| DB container | nextcloud-aio-database |
| Database | nextcloud_database |
| NC DB user | oc_nextcloud |
| DB superuser | nextcloud |
When AIO starts for the first time with existing data, Elasticsearch indexes all files, ClamAV downloads its virus database, and Recognize scans all photos. On a large installation (500K+ files) this can take 1–2 hours at high CPU load.
Plan this for late evening — do not run the first AIO start during business hours or when the server is serving other workloads.
These three values in config.php are the identity of the NC instance. They must
match what is in the database. 05-import.sh reads them from the staging stack
and writes them into the AIO container's config. If the staging stack has been
removed before running 05-import.sh, note these values from the source
config.php and set them manually.
On Debian 12/13, systemd-resolved can intercept DNS and cause resolution
failures inside Docker containers. If containers cannot resolve hostnames:
systemctl disable --now systemd-resolved
echo "nameserver 1.1.1.1" > /etc/resolv.confDocker caches the upstream DNS from /etc/resolv.conf at daemon start.
After a VM IP change or network reconfiguration, containers may get DNS
timeouts even though the host resolves correctly. Fix:
systemctl restart docker
docker start $(docker ps -aq)05-import.sh stops nextcloud-aio-notify-push to release DB connections.
After import, restart it via the AIO admin UI or:
docker start nextcloud-aio-notify-pushIf you use OPNSense, its root shell is csh. Shell redirects like 2>/dev/null
are interpreted differently by csh and can corrupt commands.
Always wrap non-trivial commands in sh -c '...' when SSHing to OPNSense.
proxmox/build-vm.sh does this for all fw01 commands.
The imported database contains Talk settings from the old installation — signaling server URL, STUN/TURN addresses and TURN secret. After import these are all wrong for AIO.
AIO generates its own secrets at first start. Retrieve and apply them:
# Get AIO's TURN secret and signaling secret
docker exec nextcloud-aio-talk env | grep -E 'TURN_SECRET|SIGNALING_SECRET'
# Remove old signaling servers and set only the AIO one
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:set spreed \
signaling_servers \
--value='{"servers":[{"server":"https://<NC_DOMAIN>/standalone-signaling/","verify":true}],"secret":"<SIGNALING_SECRET>"}'
# Fix STUN server (AIO Talk handles STUN on port 3478)
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:set spreed \
stun_servers --value='["<NC_DOMAIN>:3478"]'
# Fix TURN server with AIO's secret
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:set spreed \
turn_servers \
--value='[{"schemes":"turn,turns","server":"<NC_DOMAIN>:3478","secret":"<TURN_SECRET>","protocols":"udp,tcp"}]'Make sure port 3478 TCP+UDP is open in your firewall for the server's public IP.
Symptom if missed: "Error while creating the conversation" in Talk; NC log shows
Failed to send message to signaling server and 404 Not Found on /api/v1/room/.
If the old installation had an office app (OnlyOffice, Collabora), its config is in the imported database. AIO comes with its own office app (EuroOffice by default). Two office apps enabled simultaneously conflict, and the old app's URL will point nowhere.
After import, disable the old app and clean up its config:
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:disable onlyoffice
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:delete onlyoffice DocumentServerUrl
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:delete onlyoffice DocumentServerInternalUrl
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:app:delete onlyoffice settings_errorAIO's EuroOffice is served at https://<NC_DOMAIN>/eurooffice via the built-in Caddy proxy.
If the source NC sent mail via a local MTA (sendmail, msmtp, postfix), that config does not survive migration — AIO containers have no local mail daemon.
Configure NC to use an external SMTP relay:
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtpmode --value='smtp'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtphost --value='<smtp-relay-host>'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtpport --value=587 --type=integer
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtpsecure --value='tls'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtpauth --value=1 --type=integer
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtpname --value='<smtp-user>'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_smtppassword --value='<smtp-password>'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_from_address --value='noreply'
docker exec nextcloud-aio-nextcloud php /var/www/html/occ config:system:set mail_domain --value='<your-domain>'Use a dedicated sending account (e.g. next@yourdomain.com) so the noreply@ alias
stays shared and inbound bounces go somewhere monitored.
03-upgrade.sh prints a list of apps that need manual reinstall after AIO import.
These are apps that were enabled in your source but are not included in AIO.
Common examples:
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:install occweb
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:install drawio
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:install maps
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:install extract
docker exec nextcloud-aio-nextcloud php /var/www/html/occ app:enable notesAIO includes natively (no action needed): fulltextsearch, notify_push, Talk/spreed, whiteboard, recognize, files_antivirus (ClamAV), imaginary.