Cloudron Migration Tipps

Since my current Cloudron installation was both running out of disk space and running a soon unsupported version of Ubuntu, it was time to move it to a new and bigger server. Luckily the way Cloudron is setup makes this already very easy, but since I am using some of its apps in a production manner (like for example its mail server) I cannot just tell everybody, that would connect to the server to please not do it for a given timeframe. Hence I was looking into ways to automate as much of the migration as possible. Below is a small protocol of the steps I have taken.

Get your auth token

To keep the time between the start of the backup and the shutdown of the old system as short as possible I wanted to trigger a full system backup via script, wait for it to complete and then poweroff the machine. To give me a signal that I can start the restore on the new machine I also added a telegram notification to the script. But in order for the script to be able to trigger the backup it needs a token. The token can be retrieved with the following steps.

  1. Open your Cloudron dashboard in Google Chrome
  2. Open the “Developer Tools” (located below “More Tools”)
  3. Switch to the “Network” tab and locate the “status” request
  4. Select “Headers” and then “Request Headers”
  5. Copy the value of the “authorisation” header

The value of it (but without the Bearer part) then needs to be placed into the below script where it currently says authToken=XXX. And while you’re there also put your domain where it currently says cloudronDomain=my.cloudron.domain.

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

authToken=XXX
cloudronDomain=my.cloudron.domain

getInstalledApps(){
  curl -s "https://$cloudronDomain/api/v1/apps" \
  -H "authorization: Bearer $authToken" \
  --compressed | jq -r '.apps[].id'
}

# trigger backup and get ID of task
createAppBackup(){
  local appId=$1
  curl -s "https://$cloudronDomain/api/v1/apps/$appId/backup" \
    -H "authority: $cloudronDomain" \
    -H "accept: application/json, text/plain, */*" \
    -H "authorization: Bearer $authToken" \
    -H "content-type: application/json;charset=UTF-8" \
    -H "origin: https://$cloudronDomain" \
    -H "referer: https://$cloudronDomain/" \
    --data-raw "{}" \
    --compressed | jq -r .taskId
}

createBoxBackup() {
  curl -s "https://$cloudronDomain/api/v1/backups/create" \
  -H "authority: $cloudronDomain" \
  -H "accept: application/json, text/plain, */*" \
  -H "authorization: Bearer $authToken" \
  -H "content-type: application/json;charset=UTF-8" \
  -H "origin: https://$cloudronDomain" \
  -H "referer: https://$cloudronDomain/" \
  --data-raw '{}' \
  --compressed | jq -r .taskId
}

getTaskStatus(){
  # when called without a specific id it will list all current tasks
  local taskId=${1:-""}
  curl -s "https://$cloudronDomain/api/v1/tasks/$taskId" \
  -H "authority: $cloudronDomain" \
  -H "accept: application/json, text/plain, */*" \
  -H "authorization: Bearer $authToken" \
  -H "referer: https://$cloudronDomain/" \
  --compressed
}

waitForTask(){
  local taskId=$1
  if [ "$taskId" == "null" ]; then
    exit 1
  fi
  while true; do
    taskState=$(getTaskStatus "$taskId")
    # TODO also check for failure? exit script with error (and restart stopped apps)
    # TODO when checking multiple conditions a case would be better
    if [ "$taskState" == *"Something has gone wrong"* ]; then
      continue
    fi
    if [ "$(echo "$taskState" | jq -r .status)" == "Not found" ]; then
      exit 1
    fi
    if [ "$(echo "$taskState" | jq -r .success)" == "true" ]; then
      break
    fi

    progressMessage=$(echo "$taskState" | jq -r .message)
    if [ "$progressMessage" != ${previousMessage:-""} ]; then
      echo $progressMessage
      previousMessage=$progressMessage
      sleep 1
    fi
    #echo "$taskState" | jq -r .message
    #sleep 5
  done
}

stopApp(){
  local appId=$1
  curl "https://$cloudronDomain/api/v1/apps/$appId/stop" \
  -H "authority: $cloudronDomain" \
  -H "accept: application/json, text/plain, */*" \
  -H "authorization: Bearer $authToken" \
  -H "content-type: application/json;charset=UTF-8" \
  -H "origin: https://$cloudronDomain" \
  -H "referer: https://$cloudronDomain/" \
  --data-raw '{}' \
  --compressed | jq -r .taskId
}

startApp(){
  local appId=$1
  curl "https://$cloudronDomain/api/v1/apps/$appId/start" \
  -H "authority: $cloudronDomain" \
  -H "accept: application/json, text/plain, */*" \
  -H "authorization: Bearer $authToken" \
  -H "content-type: application/json;charset=UTF-8" \
  -H "origin: https://$cloudronDomain" \
  -H "referer: https://$cloudronDomain/" \
  --data-raw '{}' \
  --compressed | jq -r .taskId
}

getAppStatus(){
  local appId=$1
  curl "https://$cloudronDomain/api/v1/apps/$appId" \
  -H "authority: $cloudronDomain" \
  -H "accept: application/json, text/plain, */*" \
  -H "authorization: Bearer $authToken" \
  -H "referer: https://$cloudronDomain/" \
  --compressed
}

# app id for testing
#appId=ZZZ

# backup box
taskId=$(createBoxBackup)
# wait for job completion
#taskId=7365
waitForTask "$taskId"

# send notification
telegramApi="XXXXXX"
telegramChat="YYY"
curl -s -X POST https://api.telegram.org/bot$telegramApi/sendMessage -d chat_id=$telegramChat -d text="Backup has completed"

# comment the below line once you know the backup and notification succeed
exit 0

# poweroff
poweroff

The script should be placed directly on your old Cloudron machine and be run with a user that can directly run the poweroff command to stop it once the backup has completed.

The way the script works is that it creates a “task” for a full system backup. We then have to repeatedly ask the Cloudron API for the status of our backup task, and once the API confirmed the successful backup we can trigger a direct poweroff to turn off the old machine.

To have an easy and non-Cloudron dependent way I decided to send myself a notification via Telegram about the successful backup. For this both an api key and a channel id need to retrieved following these instructions.

Since its good practice to test a backup before the final migration the above script does not actually shut down the old machine until the line exit 0 has been commented.

Hint: The full system only includes apps that are running. Apps that exist in a stopped state will need to be restored manually from their last backup.

Preparing the restore

I’m storing my Cloudron backups in a self-hosted Minio instance. The general restore procedure is again very nicely documented for Cloudron already. But what it does not explicitly say is that you can already start the new installation, while the old is still backing up. I used the following steps. Commands on the old server are prefixed with [old] and commands on the new one with [new].

  1. [old] run script
  2. [old] download an old backup config from the old system
  3. [new] run the Cloudron install procedure and wait for the following prompt:
After reboot, visit https://123.123.123.123 and accept the self-signed certificate to finish setup.

The server has to be rebooted to apply all the settings. Reboot now ? [Y/n]
  1. [old] wait until the script has completed and server is shut down
  2. [new] confirm the above prompt and open the address in a browser
  3. get the id from the last box backup and replace the value in the previously downloaded backup config
  4. [new] follow the restore instructions in the Cloudron web ui

Finalizing the Migration

And that was basically the whole restore process. The Cloudron system will now first download and restore the box backup. Afterwards you can already navigate to your dashboard domain and login with your old user. The Cloudron system will take care of automatically restoring all previously running apps and their data. Just don’t forget to actually cancel the old server.