Migrate VMware VMs to STACKIT with Coriolis
STACKIT
Last updated on
Migrate VMware VMs to STACKIT with Coriolis: prepare endpoints, synchronize target disks, deploy and validate a new VM, then complete cutover and operating handoff.
STACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repositorySTACKIT
The sovereign European cloud provider behind the framework, delivering IaaS and PaaS from German and Austrian data centers with full digital independence.
Use Coriolis to copy VMware VM disks to STACKIT, adapt the guest operating system to the target virtual hardware, and start the migrated VM in a STACKIT project. The application and database remain inside the VM. This is a Relocate migration, not an application rebuild or a move to a managed database.
Follow the sequence in this guide: prepare source and target, connect Coriolis to both platforms,
transfer disks, test an isolated target, synchronize changes, and perform the final cutover.
The API examples use curl and jq; they do not require the example repository’s Python helpers.
Coriolis separates moving disk data from creating a destination VM. A Transfer defines the source and target settings. Each Execution writes the initial disk contents or subsequent changed blocks to destination volumes. Completing that copy does not start an application VM.
A Deployment consumes the transferred disk state, clones it for this guide’s rehearsal/final servers, adapts the guest OS and creates a new STACKIT server. It recreates the workload on new virtual hardware; it does not move the original ESXi VM object or copy its CPU/RAM hardware unchanged. Source sizing is the input to an explicit STACKIT machine-type mapping.
| Phase | Input | Result | What remains separate |
|---|---|---|---|
| Transfer Execution | Source VM disks and the Transfer definition. | Copied/synchronized destination volumes and integrity evidence. | Final VM creation, guest acceptance and traffic switch. |
| Deployment | Completed transferred disk state and approved target mappings. | A new server with attached deployment volumes, target NICs and adapted guest OS. | Application/data acceptance and production cutover. |
An existing cloned rehearsal is a point-in-time test VM. Later Executions update the transferred
volumes, not that rehearsal’s disks. Create a fresh Deployment to test a newer synchronization.
With auto_deploy: false, request each disk synchronization and VM creation explicitly.
| Term | Meaning |
|---|---|
| Coriolis appliance | The installed Coriolis control plane, including its web interface, identity service, migration API and workers. |
| Provider | The Coriolis adapter for a platform: vmware_vsphere for VMware and stackit for STACKIT. |
| Endpoint | A saved connection to a source or destination platform, including credentials and provider settings. |
| Coriolis project | The Coriolis authorization scope containing endpoints and migration jobs. It is not a STACKIT project. |
| Keystone | The appliance’s identity API. It issues a token scoped to the selected Coriolis project. |
| Coriolis worker region | A logical group of Coriolis workers selected for endpoint access. It is separate from the STACKIT cloud region. |
| STACKIT project | The cloud resource scope containing the destination network, servers and volumes. |
| Transfer | The migration definition: source VM IDs, destination endpoint, network mapping and disk-transfer settings. |
| Execution | One run of a Transfer. The initial run copies the disks; later runs synchronize changed blocks. |
| Deployment | An operation that creates a destination server from transferred disks. |
| Migration worker | A Coriolis process or temporary VM that reads, transfers or adapts disks. It is not the migrated application server. |
| OS morphing | Adaptation of the guest’s drivers, boot and network configuration to the destination platform. |
| CBT | VMware Changed Block Tracking, used to identify disk blocks modified since an earlier synchronization. |
| NFC/NBD | VMware’s network disk-access path used during export; the Coriolis worker must reach the serving ESXi hosts. |
| Rehearsal | An isolated test deployment before production cutover. |
| Cutover | The final write stop, disk synchronization, target start, validation and traffic switch. |
The worked example uses scf-relocate-app: Ubuntu 24.04, Spring Boot and PostgreSQL 16 on one VM,
with a 12-GiB system disk and an 8-GiB database disk. Use the same Coriolis operations for other
supported VMs; select their own OS, capacity, networks and application acceptance tests.
The requests below use the Transfer/Deployment API profile of the Coriolis 2608.2 appliance and its VMware/STACKIT providers. Retrieve the installed provider schemas before creating requests; they define the accepted fields. This guide uses Transfers, not the separate Replica/DR workflow.
For Server Agent checks, activate STACKIT Agent Service once per destination project, then install and provision the agent on the target guest. Project activation and guest provisioning are separate operations. SSH-based validation does not require Server Agent.
| Connection | Required access | Purpose |
|---|---|---|
| Operator to appliance | HTTPS on the configured web/API port | Authentication, configuration and task status. |
| Coriolis worker to vCenter/ESXi | TCP/443 | VMware inventory, snapshots and API operations. |
| Coriolis worker to source ESXi hosts | TCP/902 | NFC/NBD disk export; include every host that can serve the selected VM’s disks. |
| Appliance/workers to STACKIT APIs | HTTPS/TCP/443 | Authenticate and create destination resources. |
| Coriolis worker to temporary STACKIT workers | TCP/22 and TCP/5566 for this HTTPS-transfer configuration | Worker management and disk transfer. |
| Target guest to platform services | Metadata, DNS and approved package/agent services | Guest initialization and operations. |
Route the Coriolis worker to the private destination migration network. For an appliance in the same STACKIT project, use an attached network or approved routed connection. For an on-premises appliance, provide a site-to-site VPN or private interconnect. A VPN supplies network reachability; it is not a Coriolis migration mechanism. No specific VPN product or intermediate host is required.
Keep temporary workers private. Restrict worker access to Coriolis and operator access to approved management sources. Do not connect rehearsal VMs to production traffic or identity-sensitive services.
Use Bash, curl, jq and trusted TLS certificates on the operator workstation. Install the
official STACKIT CLI for the optional Server Agent commands. Do not disable certificate checking.
Keep authentication files and API responses in a private directory, outside a Git repository.
Use a dedicated Bash session for operator commands; stop on a failed request rather than continuing
with an empty or stale identifier.
set -euo pipefailumask 077WORK="$HOME/coriolis-migration"mkdir -p "$WORK"chmod 700 "$WORK"CORIOLIS_URL='https://coriolis.example.com'CORIOLIS_USER='migration-operator'CORIOLIS_PROJECT='admin'CORIOLIS_PASSWORD_FILE="$HOME/.config/coriolis/password"VMWARE_HOST='vcenter.example.com'VMWARE_USER='migration-user@vsphere.local'VMWARE_PASSWORD_FILE="$HOME/.config/coriolis/vmware-password"STACKIT_KEY_FILE="$HOME/.config/stackit/service-account.json"STACKIT_ORGANIZATION_ID='replace-with-organization-uuid'STACKIT_PROJECT_ID='replace-with-project-uuid'STACKIT_REGION='eu01'STACKIT_AVAILABILITY_ZONE='eu01-1'MIGRATION_NETWORK_ID='replace-with-migration-network-uuid'TARGET_NETWORK_ID='replace-with-application-network-uuid'TARGET_SECURITY_GROUP_ID='replace-with-application-security-group-uuid'WORKER_IMAGE_ID='replace-with-ubuntu-worker-image-uuid'WORKER_MACHINE_TYPE='c3i.2'TARGET_MACHINE_TYPE='c3i.2'SOURCE_NETWORK='VM Network'Replace every example hostname, username and replace-with-... value before execution:
| Input | Value to supply | Where to obtain it |
|---|---|---|
CORIOLIS_URL | HTTPS appliance origin, without a trailing slash | The appliance deployment’s web address. |
CORIOLIS_USER, CORIOLIS_PROJECT | Coriolis login and authorized project name | Appliance administrator; admin is the example scope, not a required name. |
CORIOLIS_PASSWORD_FILE | File containing the Coriolis password | Your secret store; create the local file with mode 600. |
VMWARE_HOST, VMWARE_USER, VMWARE_PASSWORD_FILE | vCenter/ESXi hostname, migration account and mode-600 password file | VMware administrator and secret store. |
STACKIT_KEY_FILE | Original STACKIT service-account key JSON, mode 600 | STACKIT service-account key creation or your secret store. |
STACKIT_ORGANIZATION_ID, STACKIT_PROJECT_ID | Organization and destination project UUIDs | STACKIT Portal organization/project details. |
STACKIT_REGION, STACKIT_AVAILABILITY_ZONE | Placement region and zone | Destination project configuration; this example uses eu01 / eu01-1. |
MIGRATION_NETWORK_ID | Network UUID for temporary workers | Destination project’s Networks view or API. It must be reachable from Coriolis. |
TARGET_NETWORK_ID, TARGET_SECURITY_GROUP_ID | Application network and security-group UUIDs | Destination project’s Networks and Security Groups views or API. |
WORKER_IMAGE_ID | Ubuntu worker image UUID with cloud-init | STACKIT image catalog; choose an image available to the destination project. |
WORKER_MACHINE_TYPE, TARGET_MACHINE_TYPE | Worker and final server machine types | STACKIT machine-type catalog and sizing plan. c3i.2 is the small example’s choice. |
SOURCE_NETWORK | Source VM’s exact port-group/network identifier | VMware VM network-adapter configuration; VM Network is the example port group. |
The API calls below return CORIOLIS_PROJECT_ID, WORKER_REGION_ID, SOURCE_ENDPOINT_ID,
TARGET_ENDPOINT_ID, VM_ID, TRANSFER_ID, EXECUTION_ID and DEPLOYMENT_ID. Do not invent
these IDs or substitute the VM’s display name. TARGET_SERVER_ID is the resulting STACKIT server UUID.
The example uses one destination network for workers and the isolated application; use distinct
network IDs when the landing-zone design separates them. Keep shell tracing and verbose HTTP
logging disabled around authentication.
Migrate the existing guest; do not reinstall its OS, application or database as a preparation step. Record VM identifiers, vCPU/RAM, firmware, disks, mount points, network adapters, guest accounts, application services and data dependencies. Take and verify an application-consistent backup.
For the worked example, prepare this source inventory:
| Item | Example configuration |
|---|---|
| VM | scf-relocate-app, Ubuntu 24.04, 2 vCPU, 3 GiB RAM, EFI boot. |
| Application | Spring Boot on Java 21, systemd unit relocate-demo.service, HTTP on 127.0.0.1:8080. |
| Database | PostgreSQL 16, cluster relocate, database relocate, port 5432. |
| System disk | 12 GiB, OS and application binaries. |
| Data disk | 8 GiB, UUID-mounted at /srv/relocate-data; PostgreSQL data at /srv/relocate-data/postgresql. |
| Test data | migration_records table; /api/evidence returns record counts and a content digest; POST /api/writes inserts one synthetic record. |
| Background writer | relocate-writer.timer; stop it during fixed-boundary comparisons. |
These service names, paths and application endpoints belong to this example. For another workload, record its actual equivalents and use its business-level acceptance tests. Coriolis does not require Spring Boot, PostgreSQL, a blank data disk or a particular VM name.
Record source application health and a consistency checkpoint before the initial copy. For the dedicated example, stop its synthetic writer and capture independent SQL/HTTP evidence. Run these commands inside the source guest, not on the appliance or operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo systemctl stop relocate-writer.timer relocate-writer.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'GUEST_EVIDENCE is a private output directory on the guest. Preserve these source records without
overwriting them during target acceptance. Keep the example writer stopped through initial copy
and rehearsal. For production applications, define the backup/checkpoint and comparison method
with the application owner; account for writes made while an online copy runs.
Use the VMware client with the dedicated migration account to create and remove a test snapshot
on the selected VM. Verify datastore headroom, healthy snapshot consolidation and running VMware
Tools. Enable Changed Block Tracking (CBT) for the selected VM/disks through the approved VMware
procedure before the first Transfer. This guide sets automatically_enable_cbt: false because CBT
is prepared explicitly.
Confirm that the source VM has a stable identifier and appears in Coriolis inventory. Resolve missing identifiers, unsupported versions or failed disk-export access before starting a copy. Do not modify VM identity or patch provider libraries as a general migration step.
Prepare the destination project with the networks and security groups from the input table. Reserve capacity for the transferred volumes, temporary workers and rehearsal/final servers. Create a worker image with cloud-init, and confirm the chosen machine types and zone are available.
Complete the routing and firewall connections from the architecture table before configuring endpoints. Validate the paths from the Coriolis worker host, not only from the browser workstation. The appliance web interface is a control-plane connection; it does not carry all disk traffic.
Discover the appliance’s identity and migration API paths. The configured origin is the same HTTPS origin used for the web interface:
curl --fail --silent --show-error "$CORIOLIS_URL/api/config" > "$WORK/config.json"IDENTITY_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.keystone' "$WORK/config.json")"CORIOLIS_API_URL="$CORIOLIS_URL$(jq -er '.config.servicesUrls.coriolis' "$WORK/config.json")"USER_DOMAIN=$(jq -er '.config.defaultUserDomain' "$WORK/config.json")Build the password-authentication request from the protected password file and request an
unscoped Keystone token. X-Subject-Token is the response header containing the token:
jq -n --arg username "$CORIOLIS_USER" --arg domain "$USER_DOMAIN" \ --rawfile password "$CORIOLIS_PASSWORD_FILE" \ '{auth:{identity:{methods:["password"],password:{user:{name:$username, password:($password|rtrimstr("\n")),domain:{name:$domain}}}},scope:"unscoped"}}' \ > "$WORK/login.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/login.json" \ -D "$WORK/unscoped.headers" -o "$WORK/unscoped.json"UNSCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/unscoped.headers")printf 'X-Auth-Token: %s\n' "$UNSCOPED_TOKEN" > "$WORK/unscoped-request.headers"curl --fail --silent --show-error -H @"$WORK/unscoped-request.headers" \ "$IDENTITY_URL/auth/projects" > "$WORK/projects.json"CORIOLIS_PROJECT_ID=$(jq -er --arg project "$CORIOLIS_PROJECT" \ '[.projects[]|select(.name==$project)]|if length==1 then .[0].id else error("Select one authorized Coriolis project") end' \ "$WORK/projects.json")Scope the token to that Coriolis project. Store the resulting header privately and use it for every migration API request. The token in the JSON request is read from a file, not a command-line argument:
jq -n --rawfile token "$WORK/unscoped-request.headers" --arg project "$CORIOLIS_PROJECT_ID" \ '{auth:{identity:{methods:["token"],token:{id:($token|sub("^X-Auth-Token: ";"")|rtrimstr("\n"))}}, scope:{project:{id:$project}}}}' > "$WORK/scope.json"curl --fail --silent --show-error -X POST "$IDENTITY_URL/auth/tokens" \ -H 'Content-Type: application/json' --data-binary @"$WORK/scope.json" \ -D "$WORK/scoped.headers" -o "$WORK/scoped.json"SCOPED_TOKEN=$(awk 'tolower($1)=="x-subject-token:" {gsub("\r","",$2); print $2}' "$WORK/scoped.headers")printf 'X-Auth-Token: %s\n' "$SCOPED_TOKEN" > "$WORK/coriolis.headers"API="$CORIOLIS_API_URL/$CORIOLIS_PROJECT_ID"unset UNSCOPED_TOKEN SCOPED_TOKENAPI is the project-scoped migration API base, for example
https://coriolis.example.com/coriolis/<coriolis-project-id>. A 401 response requires renewed
authentication; verify the existing operation’s status before resubmitting a request.
curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/providers" > "$WORK/providers.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" "$API/regions" > "$WORK/regions.json"jq '.regions[]|{id,name,enabled}' "$WORK/regions.json"WORKER_REGION_ID='replace-with-enabled-coriolis-worker-region-id'curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/16" > "$WORK/vmware-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/16" > "$WORK/stackit-connection-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/vmware_vsphere/schemas/8" > "$WORK/source-environment-schema.json"curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/providers/stackit/schemas/4" > "$WORK/destination-environment-schema.json"Select the enabled Coriolis region whose workers have the required source/destination access,
and replace WORKER_REGION_ID with its returned ID. A region named Public is a logical Coriolis
worker group; the name does not enable public IPs. STACKIT_REGION still selects the destination
cloud region. In this API, schema selector 16 returns connection settings, 8 the VMware source
environment, and 4 the STACKIT destination environment. Validate request files against their
returned JSON Schemas before submitting them.
Create the VMware endpoint. host is the vCenter or supported standalone ESXi address, not the
guest IP. The account must see every selected VM and the associated datastores:
jq -n --arg host "$VMWARE_HOST" --arg username "$VMWARE_USER" \ --rawfile password "$VMWARE_PASSWORD_FILE" --arg worker "$WORKER_REGION_ID" \ '{endpoint:{name:"vmware-source",type:"vmware_vsphere",mapped_regions:[$worker], connection_info:{host:$host,port:443,username:$username, password:($password|rtrimstr("\n")),allow_untrusted:false}}}' > "$WORK/vmware-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/vmware-endpoint-request.json" \ "$API/endpoints" > "$WORK/vmware-endpoint.json"SOURCE_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/vmware-endpoint.json")Create the STACKIT endpoint. The provider’s service_account_key field contains the base64
representation of the original key JSON. Base64 is encoding, not encryption; protect this request file:
jq -n --arg organization "$STACKIT_ORGANIZATION_ID" --arg project "$STACKIT_PROJECT_ID" \ --arg region "$STACKIT_REGION" --arg worker "$WORKER_REGION_ID" --rawfile key "$STACKIT_KEY_FILE" \ '{endpoint:{name:"stackit-destination",type:"stackit",mapped_regions:[$worker], connection_info:{organization_id:$organization,project_id:$project, region_name:$region,service_account_key:($key|@base64)}}}' > "$WORK/stackit-endpoint-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/stackit-endpoint-request.json" \ "$API/endpoints" > "$WORK/stackit-endpoint.json"TARGET_ENDPOINT_ID=$(jq -er '.endpoint.id' "$WORK/stackit-endpoint.json")Validate both connections and require valid: true. Then refresh source inventory:
for ENDPOINT_ID in "$SOURCE_ENDPOINT_ID" "$TARGET_ENDPOINT_ID"; do curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary '{"validate-connection":null}' \ "$API/endpoints/$ENDPOINT_ID/actions" > "$WORK/validate-$ENDPOINT_ID.json" jq -e '.["validate-connection"].valid==true' "$WORK/validate-$ENDPOINT_ID.json"donecurl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/endpoints/$SOURCE_ENDPOINT_ID/instances?refresh=true&limit=100" > "$WORK/source-instances.json"jq '.instances[]|{id,name,os_type,power_state}' "$WORK/source-instances.json"VM_ID='replace-with-selected-instance-id-from-coriolis'Select the exact returned VM ID, cross-check it against the source inventory, and replace VM_ID.
For an estate larger than this inventory page, retrieve all pages before selection. Save every
returned resource ID; after an interrupted POST, inspect the API’s existing endpoints/jobs instead
of creating duplicates. Endpoint validation confirms credentials/API access, not complete disk-path reachability.
Create one Transfer definition for the selected VM. The source VM remains running during the initial copy; the maintenance window belongs to the final synchronization and traffic switch. Set the disk-transfer policy and the later Deployment’s target mapping now. These settings belong to one migration definition, but a Transfer Execution and a Deployment remain separate operations. The expected result of this phase is synchronized target volumes, not a booted application server.
jq -n --arg source "$SOURCE_ENDPOINT_ID" --arg destination "$TARGET_ENDPOINT_ID" --arg vm "$VM_ID" \ --arg project "$STACKIT_PROJECT_ID" --arg source_network "$SOURCE_NETWORK" \ --arg target_network "$TARGET_NETWORK_ID" --arg migration_network "$MIGRATION_NETWORK_ID" \ --arg security_group "$TARGET_SECURITY_GROUP_ID" --arg image "$WORKER_IMAGE_ID" \ --arg worker_type "$WORKER_MACHINE_TYPE" --arg target_type "$TARGET_MACHINE_TYPE" \ --arg zone "$STACKIT_AVAILABILITY_ZONE" \ '{transfer:{scenario:"live_migration",origin_endpoint_id:$source,destination_endpoint_id:$destination, instances:[$vm],source_environment:{export_transfer_mechanism:"openvixdisklib", automatically_enable_cbt:false,verify_disk_integrity:true,skip_nfc_validation:false}, destination_environment:{project:$project,network_map:{($source_network):$target_network}, migr_network:$migration_network,migr_machine_type:$worker_type,machine_type:$target_type, availability_zone:$zone,migr_image_map:{linux:$image},set_dhcp:true, migr_worker_use_public_ip:false,use_public_ip:false,preserve_fixed_ips:false, retain_user_credentials:true,security_groups:[$security_group],data_transfer_mechanism:"HTTPS", volumes_are_zeroed:false,delete_disks_on_server_termination:false}, network_map:{($source_network):$target_network},clone_disks:true,skip_os_morphing:false}}' \ > "$WORK/transfer-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/transfer-request.json" \ "$API/transfers" > "$WORK/transfer.json"TRANSFER_ID=$(jq -er '.transfer.id' "$WORK/transfer.json")| Setting | Effect |
|---|---|
instances | Exact VM IDs from the source endpoint’s inventory. Start with one representative VM. |
network_map | Source port group to destination network mapping. Add a mapping for every source network used by the selected VMs. |
migr_network, migr_machine_type, migr_image_map | Placement and image of temporary workers, not of the final application server. |
machine_type, availability_zone, security_groups | Final server sizing, zone and application security groups. |
openvixdisklib | The VMware disk-export mechanism used in this example. Select an export mechanism supported by the installed provider and source platform. |
verify_disk_integrity: true | Enables end-to-end disk checksum verification. |
skip_nfc_validation: false | Keeps ESXi disk-path connectivity validation enabled. |
migr_worker_use_public_ip: false, use_public_ip: false | Keeps workers and final servers private. |
set_dhcp: true, preserve_fixed_ips: false | Configures target DHCP; it does not retain the source address or MAC. |
retain_user_credentials: true | Retains existing guest users/credentials; enforce the target SSH policy explicitly during acceptance. |
clone_disks: true, skip_os_morphing: false | Uses cloned disks for deployment and adapts the guest to the destination hardware. |
This request configures Linux guests. For Windows, configure the provider’s Windows worker/image
and VirtIO-driver settings and use Windows guest checks. Use separate Transfers for VMs needing
different sizing or guest policies. For a migration wave, include all selected IDs in instances
and coordinate the application dependency group’s final write stop.
Create an Execution of that Transfer. shutdown_instances: false leaves source power under your
control; auto_deploy: false separates disk copy from target startup:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/initial-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/initial-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/initial-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/initial-status.json"Repeat the GET status request until the Execution is terminal. Proceed only when the execution,
disk replication and both DELETE_TRANSFER_SOURCE_RESOURCES / DELETE_TRANSFER_TARGET_RESOURCES
tasks are COMPLETED. These cleanup tasks remove temporary transfer resources, not the source VM.
Resolve an ERROR and its cleanup before scheduling another Execution of the same Transfer.
For a lost POST response, list the Transfer’s executions and recover the returned ID; do not repeat
the POST without reconciling the result.
Turn the completed transferred disk state into a new STACKIT server. The Deployment creates server/NIC/volume resources, prepares boot and guest drivers through OS morphing, and starts the guest. The source ESXi VM remains a separate object with its own identity and power state.
Use the source inventory as the starting point, then approve the mapping before Deployment:
| Source input | STACKIT mapping | Example |
|---|---|---|
| vCPU and RAM | Select a machine type with the approved capacity; do not assume identical size names or memory ratios. | Source: 2 vCPU / 3 GiB RAM. Target: the selected c3i.2 catalog configuration. Verify its actual CPU/RAM capacity. |
| System and data disks | Map each disk to a destination volume with adequate capacity and storage performance. | Preserve separate 12-GiB system and 8-GiB data source disks; accept and record actual target volume sizes. |
| BIOS/EFI and guest OS | Use a supported target boot configuration and perform OS morphing. | EFI Ubuntu guest with VirtIO disks/networking. |
| Source network adapters | Create target NICs on the mapped networks with the approved security groups. | VM Network maps to TARGET_NETWORK_ID; this guide uses DHCP rather than the source IP/MAC. |
| Existing users and services | Apply the access/identity policy and validate the relocated guest workload. | Keep required users and Spring Boot/PostgreSQL services; enforce target key-only SSH. |
machine_type specifies the final server, while migr_machine_type sizes only temporary workers.
When machine_type is omitted, the STACKIT provider selects a minimum viable type based on the
source resource requirements. This guide supplies TARGET_MACHINE_TYPE explicitly so the
deployment uses a reviewed mapping. Moving the disks does not by itself approve a target size.
Before the copy, record an application data baseline on the source. Keep the example’s synthetic writer stopped until this rehearsal is validated. A continuously written production database requires its own consistency/rehearsal procedure; an online disk copy is not a replacement for application-consistent backup or coordinated final quiescence.
Use clone_disks: true to create a test server from separate deployment volumes while retaining
the transferred volumes for subsequent synchronization. Keep the server isolated from production
traffic. Create the Deployment only after the disk-transfer and cleanup checks have passed:
jq -n --arg transfer "$TRANSFER_ID" \ '{deployment:{transfer_id:$transfer,clone_disks:true,force:false,skip_os_morphing:false}}' \ > "$WORK/deployment-request.json"curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/rehearsal-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/rehearsal-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/rehearsal-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/rehearsal-status.json"Require last_execution_status: COMPLETED and completed morphing, finalization and cleanup tasks.
force: false prevents forced deployment; cloned disks keep transferred disk state available for
later synchronization. The rehearsal is a separate target, not the production traffic switch.
Retrieve the created NIC/volume identifiers:
jq --arg vm "$VM_ID" \ '.deployment.info[$vm].instance_deployment_info|{instance_name,nic_ids,volumes_info}' \ "$WORK/rehearsal-status.json"In STACKIT, match those NIC and volume IDs to the resulting server and record its UUID as
TARGET_SERVER_ID. Names are not unique across source, rehearsal and final deployments; always
match resource IDs. Verify its private address, network, security groups and disks before login.
Obtain its SSH host-key fingerprint through a trusted management channel; the source VM’s
fingerprint is not a target host-key pin.
Compare the source inventory with the exact target. Run guest commands through the approved private SSH/console path or through a provisioned STACKIT Server Agent. Keep the rehearsal isolated from production clients, scheduled writers and identity-sensitive integrations.
For Linux guests, start with these standard checks:
systemd-detect-virtuname -rtest -d /sys/firmware/efi && printf 'EFI boot\n'lsblk -o NAME,SIZE,FSTYPE,UUID,MOUNTPOINTSfindmntip -br addressip routesystemctl --failed --no-pagergetent passwdgetent groupls -l /sys/class/block/vd*/device/driver /sys/class/net/*/device/driver| Check | Acceptance criterion |
|---|---|
| Virtual hardware | Target runs on KVM; active disk/NIC bindings use the target’s VirtIO drivers, not VMware devices. Inspect bindings, not only the list of loaded modules. |
| Firmware and storage | Boot mode matches the planned target; all disks, UUID mounts and application data directories are present. Reconcile actual target sizes with the sizing plan. |
| VMware Tools | VMware Tools binaries/services are removed from the target. Package configuration remnants are not running tools; do not purge unrelated kernel modules. |
| Network | Target has the intended DHCP address, route, DNS and security groups; no stale VMware interface configuration breaks connectivity. |
| Users and access | Required usernames, numeric UIDs/GIDs, groups, sudo policy and authorized-key fingerprints match the access plan. Do not collect password hashes or private keys. |
| Identity | Document host-key changes and machine/monitoring identity. Keep parallel rehearsal identities isolated; regenerate identity only through the workload’s approved procedure. |
| Services | Required application/database services are healthy; no unexplained failed units remain. |
Enforce the approved SSH policy after retain_user_credentials processing. For the Ubuntu example,
keep the current administrative session open, apply a key-only policy and validate before reload:
printf 'PasswordAuthentication no\n' | sudo tee /etc/ssh/sshd_config.d/00-migration-ssh.conf >/dev/nullprintf 'ssh_pwauth: false\n' | sudo tee /etc/cloud/cloud.cfg.d/99-migration-ssh.cfg >/dev/nullsudo systemctl daemon-reloadsudo systemctl start sshsudo /usr/sbin/sshd -tsudo /usr/sbin/sshd -T | grep -E '^(passwordauthentication|pubkeyauthentication) 'sudo systemctl reload sshRequire passwordauthentication no and pubkeyauthentication yes, then test a fresh key-based
login from the approved operator path. Resolve any conflicting effective policy before proceeding.
For the Java example’s unit, configure SuccessExitStatus=143 so a normal SIGTERM stop is treated
as successful; do not restart the application just to clear an unexplained failure.
For every application, verify its service health, read/write behavior, data consistency and external dependencies. Use the source’s recorded data boundary, not the rehearsal’s own data as its expected result. Keep acceptance writes on the isolated rehearsal separate from the source-of-truth comparison.
On the example’s target guest, repeat the SQL, HTTP and mount checks from the recorded source baseline. These commands run inside the target guest, not on the appliance/operator workstation:
umask 077GUEST_EVIDENCE="$HOME/migration-evidence"mkdir -p "$GUEST_EVIDENCE"sudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), count(*) FILTER (WHERE kind='seed'), count(*) FILTER (WHERE kind='write'), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;" \ > "$GUEST_EVIDENCE/data.txt"curl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -S \ > "$GUEST_EVIDENCE/api.json"findmnt -n -o UUID --target /srv/relocate-data > "$GUEST_EVIDENCE/data-uuid.txt"sudo -u postgres psql -d relocate -Atc 'SHOW data_directory;'Collect the source/target records into separate operator evidence folders; do not overwrite the baseline.
Require identical record/seed/write counts, content digest and data mount UUID. Confirm PostgreSQL
uses /srv/relocate-data/postgresql, and verify relocate-demo.service and
postgresql@16-relocate.service are active. For example, ten additional source writes change a
1,004-record baseline to 1,014; use your captured counts rather than a fixed number from this guide.
Verify Server Agent separately from guest package presence. In the operator shell, authenticate
the official CLI with STACKIT_KEY_FILE, set TARGET_SERVER_ID to the matched server UUID,
and submit a read-only command:
TARGET_SERVER_ID='replace-with-matched-stackit-server-uuid'stackit auth activate-service-account --service-account-key-path "$STACKIT_KEY_FILE"stackit server command create --server-id "$TARGET_SERVER_ID" --project-id "$STACKIT_PROJECT_ID" \ --region "$STACKIT_REGION" --template-name RunShellScript \ --params 'script=id -u; uname -r; systemd-detect-virt' \ --assume-yes --output-format json > "$WORK/agent-command.json"AGENT_COMMAND_ID=$(jq -er '.id' "$WORK/agent-command.json")stackit server command describe "$AGENT_COMMAND_ID" --server-id "$TARGET_SERVER_ID" \ --project-id "$STACKIT_PROJECT_ID" --region "$STACKIT_REGION" --output-format jsonRequire completed status and exit code 0. AGENT_COMMAND_ID is the returned command ID, not a
server identifier. Confirm monitoring ingestion, backup scope and a recovery test before handoff.
Use another Execution of the same Transfer to synchronize source changes. Complete all earlier copy/deployment tasks before starting it. A new Transfer is not required for every delta. An existing cloned rehearsal does not receive these changes; deploy fresh cloned disks to test the new boundary.
For the dedicated example VM, prove the delta with one batch of ten synthetic writes. First
confirm the console/session belongs to the recorded VMware source, not a same-name STACKIT clone;
systemd-detect-virt must identify VMware. Keep the background writer stopped. Do not insert
synthetic records into a production application:
for WRITE_NUMBER in {1..10}; do curl --fail --silent --show-error -X POST http://127.0.0.1:8080/api/writes || exit 1donecurl --fail --silent --show-error http://127.0.0.1:8080/api/evidence | jq -SVerify exactly ten extra records, unchanged seed data and SQL/API agreement. Record the new source count/digest. Do not repeat an uncertain write batch; compare counts first. For a production VM, use its normal workload changes and application consistency checks instead.
In the operator shell, synchronize the disks and inspect that exact new Execution:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/delta-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/delta-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/delta-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/delta-status.json"Require completed replication/checksums and both resource-cleanup tasks. Repeat the Deployment
request with clone_disks: true, save its new DEPLOYMENT_ID, and match its new STACKIT resource
IDs. Repeat guest/data acceptance against the updated source boundary. Retain or remove older
rehearsals according to the test plan; never use their stale data as the delta result.
The initial copy and test Deployment take place before the maintenance window. Final copy uses the frozen source boundary; production traffic moves only after the final new VM passes acceptance.
Execute the approved maintenance plan in this order:
poweredOff in vCenter/ESXi.For the Ubuntu/PostgreSQL example, perform the source write stop inside the verified source guest:
sudo systemctl stop relocate-writer.timer relocate-writer.service relocate-demo.servicesudo -u postgres psql -d relocate -At -F '|' -c \ "SELECT count(*), md5(string_agg(record_id || ':' || payload, E'\n' ORDER BY record_id)) FROM migration_records;"sudo pg_ctlcluster --mode fast 16 relocate stopsudo syncCopy the final count/digest into the acceptance record before shutting down the source through VMware’s normal guest shutdown action. Verify the VM is off; a sent shutdown request is not proof. Do not force power-off to skip a failed application/database stop.
In the operator shell, the final disk synchronization uses the same API operation as the earlier delta, but the source is now off and the expected data boundary is fixed:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' \ --data-binary '{"execution":{"shutdown_instances":false,"auto_deploy":false}}' \ "$API/transfers/$TRANSFER_ID/executions" > "$WORK/final-execution.json"EXECUTION_ID=$(jq -er '.execution.id' "$WORK/final-execution.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/transfers/$TRANSFER_ID/executions/$EXECUTION_ID?include_task_info=true" \ > "$WORK/final-status.json"jq '.execution|{id,status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-status.json"After final copy and cleanup are COMPLETED, repeat the Deployment POST with the same
deployment-request.json. Record and inspect the new final Deployment:
curl --fail --silent --show-error -X POST -H @"$WORK/coriolis.headers" \ -H 'Content-Type: application/json' --data-binary @"$WORK/deployment-request.json" \ "$API/deployments" > "$WORK/final-deployment.json"DEPLOYMENT_ID=$(jq -er '.deployment.id' "$WORK/final-deployment.json")curl --fail --silent --show-error -H @"$WORK/coriolis.headers" \ "$API/deployments/$DEPLOYMENT_ID?include_info=true&include_task_info=true" \ > "$WORK/final-deployment-status.json"jq '.deployment|{id,last_execution_status,tasks:[.tasks[]|{task_type,status}]}' "$WORK/final-deployment-status.json"Require completed Deployment tasks. Validate this final server against the frozen source count, digest and mount UUID, not against an earlier rehearsal. Do not run concurrent copy/deployment operations on the same Transfer.
Before enabling target writes, rollback means isolating/stopping the target, keeping traffic away from it and resuming the retained source through the approved rollback plan. After target writes, reconcile new target data before reverting; merely restarting the old VM loses those writes. Delete the source only after the retention period and the application owner’s release.
Hand operations the source/target inventory, endpoint/Transfer/Execution/Deployment IDs, final server/NIC/volume IDs, completed task records, application/data acceptance, SSH/identity policy, monitoring and backup/recovery configuration. Protect credential-bearing files and apply the secret-retention policy; do not publish raw endpoint requests or authentication headers.
Remove temporary workers left by failed jobs only after correlating their ownership and task state. Review security groups and resource costs. Record actual final volume capacity rather than assuming it equals the source. For additional VMs, repeat the preparation/mapping/acceptance process and migrate dependency-aligned waves through the same Coriolis API sequence.
Cloudbase Coriolis Open external site Leads off the trail Spring Boot and PostgreSQL example workload Open the repository