Migrate Nextcloud to a new server with the same Fully Qualified Domain Name
Introduction
This document describes a migration path for a Nextcloud instance from one VPS to another. The migration path relates to a Nextcloud instance running in docker-compose, along with a few other services that are not relevant to this tutorial.
The starting docker-compose.yml is:
$ cat nextcloud/docker-compose.yml
version: '3'
services:
proxy:
image: nginxproxy/nginx-proxy:alpine
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"
container_name: nextcloud-proxy
networks:
- nextcloud_network
ports:
- 80:80
- 443:443
volumes:
- ./proxy/conf.d:/etc/nginx/conf.d:rw
- ./proxy/vhost.d:/etc/nginx/vhost.d:rw
- ./proxy/html:/usr/share/nginx/html:rw
- ./proxy/certs:/etc/nginx/certs:ro
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
restart: unless-stopped
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: nextcloud-letsencrypt
depends_on:
- proxy
networks:
- nextcloud_network
volumes:
- ./proxy/certs:/etc/nginx/certs:rw
- ./proxy/vhost.d:/etc/nginx/vhost.d:rw
- ./proxy/html:/usr/share/nginx/html:rw
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
db:
image: mariadb:10.6
command:
- "--innodb-read-only-compressed=OFF"
container_name: nextcloud-mariadb
networks:
- nextcloud_network
volumes:
- db:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
environment:
- MYSQL_ROOT_PASSWORD=<SOME_GREAT_PASS>
- MYSQL_PASSWORD=<ANOTHER_GREAT_PASS>
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
restart: unless-stopped
app:
image: nextcloud:27-apache
container_name: nextcloud-app
networks:
- nextcloud_network
depends_on:
- letsencrypt
- proxy
- db
volumes:
- nextcloud:/var/www/html
- ./app/config:/var/www/html/config
- ./app/custom_apps:/var/www/html/custom_apps
- ./app/data:/var/www/html/data
- ./app/themes:/var/www/html/themes
- /etc/localtime:/etc/localtime:ro
environment:
- VIRTUAL_HOST=<FQDN>
- LETSENCRYPT_HOST=<FQDN>
- LETSENCRYPT_EMAIL=<EMAIL>
- OVERWRITEPROTOCOL=https
restart: unless-stopped
volumes:
nextcloud:
db:
networks:
nextcloud_network:
Planning ahead the migration
A successful migration requires planning ahead. The rough outline of the steps is:
- Switch the current instance of the Nextcloud to the maintenance mode
- Perform backup of the current instance
- Install prerequisites on the new VPS
- Create a necessary folder structure on the new server
- Copy backup to the new VPS
- List all exact version of images of the running instance
- Set up a docker-compose.yml on the new instance to create all necessary docker volumes
- Restore the content from the backup to the newly created docker volumes while making sure that ownership and permissions are preserved
- Start the services with the fixed versions without
letsencrypt - Check the logs of all services and verify that all of them started without issues
- Switch the DNS record to point to the new VPS
- Wait until the old TTL expires and verify that you can access the new instance
- When the TTL has expired, start the remaining
letsencryptservice - If everything is OK, switch off the maintenance mode on the new instance
- The old instance can be safely removed now
Execution of the migration plan
1. Switch the current instance to the maintenance mode
On the old VPS execute the following command:
docker exec -it -u www-data nextcloud-app /var/www/html/occ maintenance:mode --on
Wait a while and verify that the instance is in the maintenance mode.
$ curl https://<FQDN>
...
<div class="icon-big icon-error"></div>
<h2>Maintenance mode</h2>
<p>This Nextcloud instance is currently in maintenance mode, which may take a while. This page will refresh itself when the instance is available again.</p>
<p>Contact your system administrator if this message persists or appeared unexpectedly.</p>
</div>
...
2. Backup of the current instance
$ cat >>backup_nextcloud.sh<<EOT
#!/bin/bash
set -e
cd ~/nextcloud/
docker-compose down
[ -d backup/backup_$(date +"%Y%m%d") ] || mkdir -p backup/backup_$(date +"%Y%m%d")
docker run --rm --volume nextcloud_db:/db --volume $(pwd)/backup/backup_$(date +"%Y%m%d"):/backup ubuntu tar cvfz /backup/db.tar.gz /db
docker run --rm --volume nextcloud_nextcloud:/nextcloud --volume $(pwd)/backup/backup_$(date +"%Y%m%d"):/backup ubuntu tar cvfz /backup/nextcloud.tar.gz /nextcloud
sudo tar -I pigz -cvf $(pwd)/backup/backup_$(date +"%Y%m%d")/app_data.tar.gz app/ # This can take a lot of time
sudo tar -czvf $(pwd)/backup/backup_$(date +"%Y%m%d")/proxy.tar.gz proxy/
cd ~/nextcloud/
docker-compose up -d
EOT
3. Setting up the prerequisites on the new instance
Since docker-compose will be used to run the nextcloud on the new instance too we need to install it. To do so we will follow the official documentation from docs.docker.com.
Besides the required software, we need to enable communication between the new and old VPS. The easiest way is to set up ssh with keys.
3.1 Set up the hostname
sudo hostnamectl set-hostname <FQDN>
3.2 Set up communication from the new to the old VPS
First, generate the ssh keys.
ssh-keygen -t ed25519
Now that the ssh keys are created, add the public key to authorized_keys on the old VPS.
$ ssh-copy-id -i $HOME/.ssh/id_ed25519.pub tihomir@<OLD_VPS_IP>
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/tihomir/.ssh/id_ed25519.pub"
The authenticity of host '<OLD_VPS_IP> (<OLD_VPS_IP>)' can't be established.
ED25519 key fingerprint is SHA256:<MASKED>.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
tihomir@<OLD_VPS_IP>'s password:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'tihomir@<OLD_VPS_IP>'"
and check to make sure that only the key(s) you wanted were added.
Add ssh-agent to startup
cat >>.bashrc<<EOT
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
eval `ssh-agent`
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
fi
export SSH_AUTH_SOCK=~/.ssh/ssh_auth_sock
EOT
Start the ssh-agent
$ eval `ssh-agent`
Agent pid 21835
Add the private ssh key to the ssh-agent
$ ssh-add /home/tihomir/.ssh/id_ed25519
Identity added: /home/tihomir/.ssh/id_ed25519 (tihomir@<FQDN>)
Test the connection to the old VPS.
ssh tihomir@<OLD_VPS_IP>
3.3 Install the prerequisites
First, upgrade Ubuntu to the latest version.
sudo apt update
sudo apt upgrade
Restart if necessary.
Install everyday utilities
sudo apt install vim git screen unzip jq gcc npm python3-venv
Install the docker engine on the new VPS (running Ubuntu). The commands are copied from the original site to make the steps easier to follow.
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-**plugin**
Install docker-compose standalone.
sudo curl -SL https://github.com/docker/compose/releases/download/v2.23.3/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose
sudo chmod a+x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
To avoid using sudo when running docker command add the user to the docker group.
sudo usermod -aG docker tihomir
4. Create necessary directory structure on the new VPS
mkdir -p nextcloud/backup
5. Copy the backup from the old VPS to the new one
Start a new screen session
screen
rsync -a tihomir@<OLD_VPS_IP>:/home/tihomir/nextcloud/backup/backup_20231126 /home/tihomir/nextcloud/backup/
If using IPv6, enable it and make sure that you have connectivity.
6. Find out exact versions running on the current instance
Nextcloud
$ docker exec -it -u www-data nextcloud-app /var/www/html/occ -V
Nextcloud is in maintenance mode, no apps are loaded.
Commands provided by apps are unavailable.
Nextcloud 27.1.2
Based on our docker-compose.yml and the version of the running Nextcloud, we need the docker image nextcloud:27.1.2-apache for the app service.
MariaDB
$ docker exec -it nextcloud-mariadb mariadb --version
mariadb Ver 15.1 Distrib 10.6.15-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2
Based on our docker-compose.yml and the version of the running MariaDB we need the docker image mariadb:10.6.15 for the db service.
7. Setup docker-compose.yml to create necessary volumes
Switch to the working directory
cd $HOME/nextcloud
cat >>docker-compose.yml<<EOT
version: '3'
services:
dummy:
image: alpine:latest
container_name: dummy
restart: unless-stopped
networks:
- nextcloud_network
volumes:
- nextcloud:/nc
- db:/db
volumes:
nextcloud:
db:
networks:
nextcloud_network:
EOT
Run docker-compose to create the necessary volumes
$ docker-compose up -d
[+] Running 3/3
✔ Volume "nextcloud_db" Created 0.0s
✔ Volume "nextcloud_nextcloud" Created 0.0s
✔ Container dummy Started 0.0s
8. Restore backup
Now that the backups are transferred from the old VPS to the new one and the necessary docker volumes are created, we can start restoring files from the backup.
One small remark: the username under which we work and the path to docker-compose.yml must be the same on both VMs. Otherwise, we have to move the directories created below manually.
cd $HONE/nextcloud
sudo tar -C / -xpvzf backup/backup_20231126/proxy.tar.gz
docker run --rm --volume nextcloud_db:/db --volume $(pwd)/backup:/backup ubuntu tar -C / -xpvzf /backup/backup_20241101/db.tar.gz
docker run --rm --volume nextcloud_nextcloud:/nextcloud --volume $(pwd)/backup:/backup ubuntu tar -C / -xpvzf /backup/backup_20241101/nextcloud.tar.gz
sudo tar -C / -xpvzf backup/backup_20241101/app_data.tar.gz # This can take a lot of time, depending on the size of the backup and on the performance of the VPS
9. Start the services with fixed versions and without letsencrypt
The docker-compose.yml used in this step is:
$ cat docker-compose.yml
version: '3'
services:
proxy:
image: nginxproxy/nginx-proxy:alpine
labels:
- "com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=true"
container_name: nextcloud-proxy
networks:
- nextcloud_network
ports:
- 80:80
- 443:443
volumes:
- ./proxy/conf.d:/etc/nginx/conf.d:rw
- ./proxy/vhost.d:/etc/nginx/vhost.d:rw
- ./proxy/html:/usr/share/nginx/html:rw
- ./proxy/certs:/etc/nginx/certs:ro
- /etc/localtime:/etc/localtime:ro
- /var/run/docker.sock:/tmp/docker.sock:ro
restart: unless-stopped
# letsencrypt:
# image: jrcs/letsencrypt-nginx-proxy-companion
# container_name: nextcloud-letsencrypt
# depends_on:
# - proxy
# networks:
# - nextcloud_network
# volumes:
# - ./proxy/certs:/etc/nginx/certs:rw
# - ./proxy/vhost.d:/etc/nginx/vhost.d:rw
# - ./proxy/html:/usr/share/nginx/html:rw
# - /etc/localtime:/etc/localtime:ro
# - /var/run/docker.sock:/var/run/docker.sock:ro
# restart: unless-stopped
#
db:
image: mariadb:10.6.15
command:
- "--innodb-read-only-compressed=OFF"
container_name: nextcloud-mariadb
networks:
- nextcloud_network
volumes:
- db:/var/lib/mysql
- /etc/localtime:/etc/localtime:ro
environment:
- MYSQL_ROOT_PASSWORD=<SOME_GREAT_PASS>
- MYSQL_PASSWORD=<ANOTHER_GREAT_PASS>
- MYSQL_DATABASE=nextcloud
- MYSQL_USER=nextcloud
restart: unless-stopped
app:
image: nextcloud:27.1.2-apache
container_name: nextcloud-app
networks:
- nextcloud_network
depends_on:
# - letsencrypt
- proxy
- db
volumes:
- nextcloud:/var/www/html
- ./app/config:/var/www/html/config
- ./app/custom_apps:/var/www/html/custom_apps
- ./app/data:/var/www/html/data
- ./app/themes:/var/www/html/themes
- /etc/localtime:/etc/localtime:ro
environment:
- VIRTUAL_HOST=<FQDN>
- LETSENCRYPT_HOST=<FQDN>
- LETSENCRYPT_EMAIL=<EMAIL>
- OVERWRITEPROTOCOL=https
restart: unless-stopped
volumes:
nextcloud:
db:
networks:
nextcloud_network:
Start the docker-compose
$ docker-compose up -d
...
✔ b49eecef6de7 Pull complete 10.1s
[+] Running 4/4
✔ Network nextcloud_nextcloud_network Created 0.1s
✔ Container nextcloud-mariadb Started 1.0s
✔ Container nextcloud-proxy Started 1.0s
✔ Container nextcloud-app Started
10. Check the logs of all services and verify that all of them started without issues
docker-compose logs --tail 30 app
docker-compose logs --tail 30 db
Check if the server is reachable and in maintenance mode:
$ curl -k -H 'host: <FQDN>' https://<NEW_VPS_IP>
...
<main>
<h1 class="hidden-visually">
Nextcloud </h1>
<div class="guest-box">
<div class="icon-big icon-error"></div>
<h2>Maintenance mode</h2>
<p>This Nextcloud instance is currently in maintenance mode, which may take a while. This page will refresh itself when the instance is available again.</p>
<p>Contact your system administrator if this message persists or appeared unexpectedly.</p>
</div>
</main>
...
11. Switch the DNS record to point to the new VPS
This is out of scope of this tutorial. Check with your DNS provider on how to do it.
12. Wait till old TTL expires and verify that you can access the new instance
This is also out of scope for this tutorial. The DNS TTL can be set to anything between 1 minute and a few hours.
13. If everything OK switch off the maintenance mode on the new instance
After uncommenting the letsencrypt service section in docker-compose.yml, stop all running services in docker-compose and start them all again with the letsencrypt service enabled.
docker-compose stop
docker-compose up -d
Now the new instance should be accessible and with a valid TLS certificate. Verify the connection and if everything is OK, proceed to the last step that involves the configuration of the new instance.
14. If everything so far was OK, switch off the maintenance mode on the new instance
docker exec -it -u www-data nextcloud-app /var/www/html/occ maintenance:mode --off
Give it a bit of time and connect to the new instance.
15. The old instance can be safely removed now
The old instance can now be recycled. As usual, make a good backup before decommissioning anything.
Conclusion
The migration required some work but was not too troublesome. The advantage was that the FQDN did not change, so we did not have to reconfigure the clients, and we could simply restore the backup from the old instance without touching the configuration.