Skip to content

Server Migration

Move a running subscription — database, filestore and public routing — from one server to another without changing its domain.

Migration is available for both deployment strategies:

Strategy Moves the instance between
Docker Portainer environments (a new stack is created on the target, the old one is deleted after the grace period)
DBFilter DBFilter servers (the database is restored on the target Odoo, then dropped from the source)

The customer keeps the same domain throughout. Only the Nginx proxy target changes, in a single step near the end of the pipeline.

This is not a zero-downtime operation

The data is copied with a backup / restore cycle. Anything written to the instance after the backup starts and before traffic is switched is lost. Plan migrations inside a maintenance window.


1. Where to Find It

Entry point Location
Migrate Server button Subscription form header — visible when the subscription is Running, has a deployment type, and has no migration already in flight
Migrations stat button Subscription form button box — opens the migrations of that subscription
Migrations menu Sadeem SAAS → Migrations (sequence 35)
Group Access on sadeem.subscription.migration
User read
Operator read, write
Manager read, write, create, delete

The Migrate Server button itself is restricted to Manager; the migration wizard (sadeem.subscription.migration.wizard) is manager-only as well.


2. Preconditions

_migration_validate() refuses to start unless all of the following hold:

  • The subscription has a deployment type (not none)
  • The subscription state is Running
  • The subscription has no active stages — cancel them first
  • No other migration for this subscription is in an active state
  • The subscription is registered on a backup server (both backup_server_id and backup_uuid are set) — the backup and the restore both run on the backup server

Then the deployment module validates the chosen target:

Docker (`sadeem_saas_docker`)

  • A target environment is selected and differs from the current one
  • The subscription's Docker image is available in the target environment (the environment must be listed on the image)
  • The target environment has available container capacity
  • The subscription has no code server — remove it first; it can be recreated on the new server afterwards
  • Odoo version mismatch between the subscription and the target environment's images is a warning only — it is posted in the chatter and the migration continues

DBFilter (`sadeem_saas_dbfilter`)

  • A target DBFilter server is selected, is in state Confirmed, and differs from the current one
  • The target server's Odoo version matches the subscription's — cross-version migration is blocked
  • The target server has available capacity

3. The Pipeline

flowchart TD
    A[Draft] --> B[Queued]
    B --> C[Backing Up]
    C -->|backup server line ready| D[Provisioning Target]
    D --> E[Restoring]
    E -->|restore callback| F[Switching Traffic]
    F --> G[Verifying]
    G --> H[Done]
    C -.-> X[Failed]
    D -.-> X
    E -.-> X
    F -.-> X
    G -.-> X
    X -->|Retry / Start Over| B
    X -->|Rollback| R[Rolled Back]
    H -->|Rollback within grace period| R
    H -->|grace period elapsed| Z[Source cleaned up]

Each step is guarded by its own boolean flag and committed independently, exactly like the subscription build pipeline. A retry therefore resumes at the failed step instead of redoing completed work.

State Step flag set on success What happens
Queued — Job enqueued on the Subscription Migrations queue channel
Backing Up step_backup Requests a zip backup of the source on its backup server, tagged with a unique backup_request_ref, then parks
Provisioning Target step_provision Prepares the target so it can accept a restore, and records new_server_ip / new_http_port / new_websocket_port
Restoring step_restore Asks the backup server to stream the backup line onto the target, then parks until the completion callback arrives
Switching Traffic step_switch Points the subscription at the target, re-points the Nginx proxy host, updates web.base.url on the instance (SaaS Slave)
Verifying step_verify Polls the target directly (ip:port with a Host header) for up to 180 s, then checks the public URL
Done step_backup_server, later step_cleanup Backup registration moved to the target's backup server; cleanup_after is set

Parked steps are normal

Backing Up and Restoring hand their work to the backup server and exit cleanly (MigrationPending). The migration sits in that state — with an explanatory chatter message — until the watcher cron or the completion callback resumes it. A migration that stays in one of these states for a few minutes is not stuck.

Provisioning, per strategy

Docker DBFilter
Target IP / port Next free port on the target environment (next_port + 3) The target server's IP and Odoo HTTP / WebSocket ports
Work done Repoints the subscription's environment / root server / PostgreSQL fields, creates the stack via Portainer, creates the addons directory on the target root server, waits up to 300 s for the container Waits up to 60 s for the target Odoo to answer
Before restore Drops the empty database the fresh stack auto-created, otherwise the restore fails with Database already exists —

Custom addons are not copied

On Docker, only the directory is created on the target root server. When the source and target root servers differ, the migration posts "Action required: custom addons … must be copied manually". Copy the addons before the restored database tries to load those modules.


4. Fields

Identification

  1. Name - MIG00001, from the sadeem.subscription.migration sequence (readonly)
  2. Subscription - Migrated subscription (required)
  3. Domain, Subscription Type, Company - Related to the subscription, stored
  4. State - See the pipeline table above (tracked)
  5. Failure Message - Populated with <step>: <error> when a step raises

Source Snapshot (taken at creation, used by rollback)

  1. Source Server IP, Source HTTP Port, Source Websocket Port
  2. Source Backup Server, Source Backup UUID
  3. Docker only: Source Environment, Source Stack ID, Source Root Server, Source Addons Dir, Source External PG, Source PG Server
  4. DBFilter only: Source DBFilter Server

Target

  1. Target Environment (Docker) / Target DBFilter Server (DBFilter)
  2. Target Server IP, Target HTTP Port, Target Websocket Port - Filled by the provisioning step

Remote Operation Tracking

  1. Backup Request Ref - Unique comment sent with the backup request; the watcher cron matches the backup server's line by it
  2. Backup Requested At - Start of the remote backup timeout window
  3. Backup Line UUID - Completed backup line used for the restore
  4. Restore Requested At - Start of the restore callback timeout window
  5. Restore Callback Token - Secret in the callback URL, rotated on every restore request
  6. Job UUID - Queue job running the migration; used to detect interrupted jobs
  7. Backup File - Deprecated, only set on migrations that ran before the backup moved to the backup server

Cleanup

  1. Cleanup Grace Period (days) - How long the old instance is kept after success (default: 7). 0 cleans up immediately
  2. Cleanup After - Computed deadline; the cleanup cron acts when it passes
  3. Can Rollback - True while the state is Failed or Done and the source has not been cleaned up yet

5. Actions

Action Available when Effect
Start Migration Draft (from the wizard) Validates, enqueues the job, posts to the subscription chatter
Retry Failed Clears the failure and re-enqueues; completed steps are skipped
Start Over Failed Tears down everything created on the target, restores the source fields, clears the backup/restore tracking and step flags, then re-runs from the backup step
Mark Failed Active states, job not running Recovery for a job that died without reaching the exception handler (worker kill, time limit, server restart) so the record can be retried or rolled back
Mark Done Active states or Failed, job not running Manual completion after verifying the target by hand; starts the grace period
Rollback Can Rollback Removes the target artifacts, restores the source IP/port (and strategy fields), re-points Nginx, restores the backup registration
Clean Up Now Done, not yet cleaned up Destroys the source instance immediately instead of waiting for the grace period

Cleanup is the point of no return

Once the source is cleaned up (step_cleanup), rollback is no longer possible.

  • Docker — the old stack is deleted from the source environment. Its volumes and the old custom-addons directory are removed only when the old root server differs from the new one; when both stacks live on the same root server, the new stack is still mounting them, so they are left alone.
  • DBFilter — the old database is dropped from the source server. If the drop fails, the cleanup raises and can be retried with Clean Up Now.

Rollback details

  • The subscription is pointed back at old_server_ip / old_http_port / old_websocket_port
  • If traffic had already been switched, the Nginx proxy host is re-pointed and web.base.url is restored on the instance
  • If the backup registration had been moved, it is moved back to the source backup server
  • Docker: the stack created on the target is deleted. DBFilter: the restored database is dropped from the target
  • Failures in these teardown steps do not abort the rollback — they are posted as warnings for manual follow-up

6. Automation

Cron Interval Purpose
SAAS Package Migration: check remote backup/restore operations 2 minutes Resumes migrations parked in Backing Up once the backup line is complete; fails them if the backup line failed or timed out; reaps Restoring migrations whose callback never arrived
SAAS Package Migration: cleanup old instances 1 hour Destroys sources of Done migrations whose Cleanup After has passed
Timeout Value Applies to
Remote backup 8 hours Backing Up — the backup server's own async poll cap is 6 h
Remote restore callback 12 hours Restoring — only a reaper for a lost callback or a dead restore job
Target Odoo after restore 180 s (poll every 10 s) Verifying
Target container after stack creation 300 s (Docker) / 60 s (DBFilter) Provisioning Target

Jobs run on the dedicated Subscription Migrations queue channel with max_retries=1, so a failing migration surfaces as a Failed record instead of being retried blindly.

Backup line retention

Migration backup lines inherit the backup server's default retention. If the line is rotated away before the restore is requested, the backup server answers HTTP 404 — the migration resets its backup step and reports that Retry will create a fresh backup.


7. Restore Completion Callback

The backup server reports the restore result to the master:

POST https://your-master.sadeem.cloud/sadeem/migration/restore/<token>
Content-Type: application/json

{"success": true, "message": "restored on target"}
  • <token> is the migration's single-use restore_callback_token — same trust model as /sadeem/webhook/<token>
  • Only a migration in state Restoring whose restore step is still open accepts the callback; anything else answers 404 Unknown or stale callback token
  • success: false fails the migration with the reported message

See Webhooks for the other public callback routes.


8. Effect on the Subscription

  • A warning banner is shown on the subscription form while a migration is active — avoid start / stop / cancel actions until it completes
  • has_active_migration and migration_count are computed on the subscription
  • Successful migration, rollback and manual completion are all posted to the subscription chatter with a link to the migration record
  • On Docker, the backup server registration follows the target environment's default backup server; on DBFilter, the target server's. If it changes, the old registration is cancelled and a new one is created