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_idandbackup_uuidare 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
- Name -
MIG00001, from thesadeem.subscription.migrationsequence (readonly) - Subscription - Migrated subscription (required)
- Domain, Subscription Type, Company - Related to the subscription, stored
- State - See the pipeline table above (tracked)
- Failure Message - Populated with
<step>: <error>when a step raises
Source Snapshot (taken at creation, used by rollback)
- Source Server IP, Source HTTP Port, Source Websocket Port
- Source Backup Server, Source Backup UUID
- Docker only: Source Environment, Source Stack ID, Source Root Server, Source Addons Dir, Source External PG, Source PG Server
- DBFilter only: Source DBFilter Server
Target
- Target Environment (Docker) / Target DBFilter Server (DBFilter)
- Target Server IP, Target HTTP Port, Target Websocket Port - Filled by the provisioning step
Remote Operation Tracking
- Backup Request Ref - Unique comment sent with the backup request; the watcher cron matches the backup server's line by it
- Backup Requested At - Start of the remote backup timeout window
- Backup Line UUID - Completed backup line used for the restore
- Restore Requested At - Start of the restore callback timeout window
- Restore Callback Token - Secret in the callback URL, rotated on every restore request
- Job UUID - Queue job running the migration; used to detect interrupted jobs
- Backup File - Deprecated, only set on migrations that ran before the backup moved to the backup server
Cleanup
- Cleanup Grace Period (days) - How long the old instance is kept after success (default: 7).
0cleans up immediately - Cleanup After - Computed deadline; the cleanup cron acts when it passes
- Can Rollback - True while the state is
FailedorDoneand 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.urlis 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-userestore_callback_token— same trust model as/sadeem/webhook/<token>- Only a migration in state
Restoringwhose restore step is still open accepts the callback; anything else answers404 Unknown or stale callback token success: falsefails 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_migrationandmigration_countare 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