Skip to main content
A Fly.io app moves to Sevalla as one application: each process group in fly.toml becomes a process, release_command becomes a job, the volume becomes a disk, and the secrets and [env] entries become environment variables. A Sevalla PostgreSQL database replaces Fly Managed Postgres or Fly Postgres. You recreate the setup while the Fly.io app keeps running, copy the data during a short maintenance window, then point the domains at Sevalla.

At a glance

Environment variables

Fly.io doesn’t pass secrets or [env] entries to builds, so make them available during runtime only. With the Dockerfile build strategy, every variable that’s available during build is passed to the build as a build argument.

How Fly.io maps to Sevalla

Before you begin

  • flyctl, signed in to the organization that owns the app.
  • The app’s code in a repository on GitHub, GitLab or Bitbucket. Sevalla doesn’t deploy from a local folder like fly deploy does. See Git providers.
  • A Sevalla company with a payment method.
  • The PostgreSQL client tools, version 17 or newer: the dump leaves out Fly Managed Postgres extensions with pg_dump --exclude-extension, which version 17 added. See Import and export PostgreSQL.
  • A maintenance window: the Fly.io app takes no requests while you copy the data.

Take stock of the Fly.io app

Collect what you’ll recreate, with your app’s name in place of storefront:
  • config show prints the deployed fly.toml as JSON: the [processes], the release_command, the [http_service] with its checks, the [[mounts]] and the [env] entries.
  • status and scale show list the Machines of each process group, their size and their regions. Fly.io’s fra region is in Frankfurt and iad in Ashburn, like the Sevalla locations Frankfurt, Germany and Ashburn, USA. See Locations.
  • volumes list shows each volume’s size and the Machine it’s attached to.
  • secrets list shows each secret’s name and a digest, never its value. You read the values in Move the secrets and environment variables.
  • mpg list shows the Managed Postgres clusters and their IDs; for unmanaged Fly Postgres, run fly postgres list. Create the Sevalla database on the same PostgreSQL major version or a newer one: Fly Managed Postgres offers 16 and 17, and an unmanaged Fly Postgres cluster created today runs 18.
  • certs list shows the custom domains.

Create the database

In the sidebar, click Databases, then click Create database. Under Database, choose PostgreSQL and a version at least as new as Fly.io’s. Enter orders-db as the Display name, choose the Data center closest to the Fly.io app’s primary region and a Size with room for the data, then click Create database. See Create a database.

Create the application

1

Create it from the repository

In the sidebar, click Applications, then click Create > Application. Under Source, click Private repository, then choose the Git provider, the Repository and the Branch. Under Location, choose the same Data center as the database, choose a Size, and click Create application. The application opens on its Overview tab with the status Not deployed yet: creating an application doesn’t deploy it. See Create an application.
2

Build from the Dockerfile

Fly.io builds the Dockerfile in your repository, and Sevalla can build the same file. On the Settings tab, in the Build strategy section, click Update build strategy. Click Dockerfile, keep the Dockerfile path Dockerfile and the Docker context ., then click Update build strategy. If fly.toml sets [build] dockerfile, enter that path instead. See Dockerfile.
A web process on H1 can’t have custom domains. If you’ll move a domain, choose a larger size, such as S1. Sevalla builds the Dockerfile for linux/amd64 and passes no --target, so the image is the last stage, as with a Fly.io deploy without build-target. To drop the Dockerfile later, see Build with Ingot instead.

Connect the database

Click the Networking tab. Under Connected services, click Add internal connection. Choose orders-db as the Service and turn on Add environment variables to the application. Change the key DB_URL to DATABASE_URL, the name that fly mpg attach and fly postgres attach use, clear Available during build, and click Add internal connection. See Connect an application.

Move the secrets and environment variables

1

Read the secret values

fly secrets list shows a digest of each secret, not its value: Fly.io makes values available only to the app. Take them from where you keep them, such as a password manager, or read each one from a running Machine:
Skip DATABASE_URL: the internal connection replaces it.
2

Write a .env file

Put each secret and each [env] entry from fly.toml in a file as KEY=value. Replace the values that point at Fly.io: a .internal or .flycast address of the app’s own web process becomes http://<internal hostname>:8080, with the hostname from the Internal hostnames section of the Networking tab, and a fly.dev domain becomes the application’s sevalla.app domain under its name in the page header. Don’t commit the file.
3

Import the file

Click the Environment variables tab, then click Add environment variables. Click Import .env and choose the file. Keep Available during runtime selected, clear Available during buildtime, and click Add environment variables.
Don’t add PORT: Sevalla sets it to the web process’s port. Fly.io’s own variables, such as FLY_APP_NAME, FLY_REGION and PRIMARY_REGION, don’t exist on Sevalla: remove code that depends on them. See Environment variables.

Recreate the process groups

1

Update the web process

On the Processes tab, click ⋯ on the web process, then click Update process. Enter the Custom start command. Under Health checks, turn on Readiness probe and enter the check’s path, such as /healthz, as the Path, then the Initial delay, Period and Timeout: 10, 30 and 5 for grace_period = "10s", interval = "30s" and timeout = "5s". Click Update process.
2

Create the background workers and the job

Click Create process for each other group and for release_command. Choose Background worker or Job, enter a Name and the Custom start command, set the Start policy to Before deployment for the job, choose a Resource, and click Create process.
3

Create the disk

Click the Disks tab, then click Create disk. In Process, pick the process of the group the volume is mounted for. Enter the volume’s destination, such as /data, as the Path, pick a Size (10 GB is the smallest), and click Create disk. See Persistent storage.
If your server listens on internal_port instead of reading PORT, set the web process’s port to that number on the Networking tab. See Web process port. Fly.io sends SIGINT when it stops a Machine, unless kill_signal says otherwise. Sevalla sends SIGTERM: make sure your servers and workers shut down cleanly on it. See Processes.

Copy the PostgreSQL data

1

Stop writes on Fly.io

Stop the background workers, then cordon the web Machines: Fly Proxy stops sending them requests, and fly ssh still reaches them. Take the Machine IDs from fly status:
Fly.io has no maintenance page: visitors get no response until you point the domain at Sevalla. To undo, run fly machine uncordon <machine id> and scale the workers back up.
2

Open a proxy to the Fly.io database

In a second terminal, keep a proxy running. For Fly Managed Postgres, use the cluster ID from fly mpg list; the proxy listens on localhost:16380:
For unmanaged Fly Postgres, proxy to the Postgres app on localhost:15432:
3

Dump the database

Use the user, password and database from the app’s DATABASE_URL, which fly ssh console --app storefront --command "printenv DATABASE_URL" prints, with localhost and the proxy’s port as the host. For Fly Managed Postgres, the user is fly-user and the database fly-db:
Fly Managed Postgres installs the pg_stat_monitor and pgaudit extensions in every database. A Sevalla database user can’t create them, so the dump leaves them out. For unmanaged Fly Postgres, dump through port 15432, without the two options:
4

Open the Sevalla database to your computer

In the sidebar, click Databases, then click orders-db. Click the Networking tab, click Enable under External connection, then click Enable in the dialog. Copy the External hostname, External port and password. See Enable the external connection.
5

Restore the dump

--clean --if-exists drops the tables of an earlier attempt first. Use the user and database name of orders-db, from its Overview tab.
To rehearse, run the dump and restore once without stopping writes, deploy, and test on the sevalla.app domain. The real cutover then replaces the rehearsal’s data. With the data in place before the first deployment, the release job runs its migrations against it, like release_command does on Fly.io.

Deploy and verify

1

Deploy

On the application’s Overview tab, click Deploy now under Recent deployments, check the Branch, and click Deploy.
2

Check the deployment

On the Deployments tab, wait for the status Deployed. The build log shows Building and pushing docker image using Dockerfile... and the job’s result, such as release has been successful.With the API, Get deployment (getDeploymentV3) returns the status, success for Deployed, and Get application deployment logs (getApplicationDeploymentLogsV3) finds the job’s result. APP_ID and DEPLOYMENT_ID come from Deploy with the API, where the job is named release. The messages carry terminal color codes, which gsub removes:
3

Compare the data

Run the same query against the Fly.io database, through the proxy, and the Sevalla one, through its external connection. Each table’s row count and checksum must match. A worker that’s already running on Sevalla can change rows the moment the data arrives, so leave out the columns it writes: the example’s worker sets status and processed_at on the orders it processes.
PGTZ=UTC makes both servers print times the same way. These are the example’s values: for your app, what counts is that both databases print the same lines.
4

Test the application

Click the domain under the application’s name and test the critical paths: sign-in, a write to the database, a background task. On the Processes tab, the web process and the background workers show Running.With the API, Get application runtime logs (getApplicationRuntimeLogsV3) with search finds a line that a worker logs. The example worker logs each order it processes and each time it reaches the web process over the internal hostname:
CLI: sevalla apps logs runtime "$APP_ID" --search reached. MCP: search_runtime_logs.
5

Turn on auto-deploy

To deploy every push to the branch automatically, as a fly deploy in your CI did, click the Settings tab, then click Update auto-deploy in the Auto-deploy section. Turn on Auto-deploy and click Update auto-deploy. Remove the fly deploy step from your CI. See Deployments.

Copy the volume files

The disk is mounted from the first deployment on, so copy the volume’s files once the application runs. fly ssh needs the Fly.io Machine with the volume to be running. With auto_stop_machines on, Fly Proxy stops it after a few minutes without requests, cordoned or not: start it again with fly machine start <machine id> --app storefront, and it stays cordoned.
1

Archive the volume on Fly.io

Pack the volume’s destination, here /data, into an archive outside it, and download it:
--exclude=lost+found leaves out the folder that every volume’s file system has. fly ssh sftp get doesn’t overwrite a local file: delete an earlier volume.tar.gz first.
2

List the files with their checksums

3

Upload the archive

On the application’s Web terminal tab, wait for the status Live: the first tab opens sh in the web process. If the disk is on a background worker, click Web terminal on that process in the Overview tab instead. Click Files > Upload files…, choose volume.tar.gz, set the Destination to the disk’s path, /data, and click Upload. Each file can be up to 100 MB. For a larger archive, split it first with split -b 90m volume.tar.gz volume.tar.gz.part-, upload the parts one at a time, and join them in the terminal with cat /data/volume.tar.gz.part-* > /data/volume.tar.gz && rm /data/volume.tar.gz.part-*. See Copy files to and from a process.
4

Unpack it

In the terminal:
The list must match fly-files.txt line for line.
A script or an AI agent can run the two commands of the last step with Run a command in a process (execProcessCommandV3), and compare the list with fly-files.txt. WEB_ID comes from Deploy with the API:
CLI: sevalla apps processes exec "$WEB_ID" --app-id "$APP_ID" -- sh -c 'cd /data && find . -type f -exec sha256sum {} + | sort -k 2' | jq -j .stdout, which prints the same JSON. MCP: sevalla_api_update with execProcessCommandV3.

Build with Ingot instead

Sevalla’s default build strategy, Ingot, detects Node.js, Python, PHP, Ruby, Go, Rust, Java, .NET, Elixir and more, and writes the Dockerfile for each build, so you don’t maintain one. It reads the language version from your project files, such as engines.node in package.json, not from an ARG NODE_VERSION in your Dockerfile, and the processes keep their start commands. To try it, click the Settings tab, then click Update build strategy in the Build strategy section. Click Ingot, click Update build strategy, and deploy. If the deployment or the checks in Deploy and verify fail, switch back to Dockerfile. Ingot doesn’t read your Dockerfile, but it adds the patterns of .dockerignore to its own, so you can keep both files while you compare.

Run in more than one location

A Fly.io app with Machines in more than one region becomes one Sevalla application per location, with a load balancer in front:
  1. Deploy the application in the first location as this page describes.
  2. Copy it to each other location with Clone application (cloneApplicationV3) and the location’s cluster_id, or with sevalla apps clone <app id> --display-name "storefront (Ashburn)" --cluster <cluster id>. The dashboard has no clone action: there, create the application again as this page describes. A clone gets the processes, the disk settings with an empty disk, the environment variables, the build settings and the auto-deploy setting, and isn’t deployed.
  3. In each copy, change the variables that point at the first location: DATABASE_URL becomes the database’s external connection URL, because internal connections work only within one location, and internal hostnames become the copy’s own. Then deploy it.
  4. Create a load balancer with Geo routing and add each application as an App destination. Visitors reach the closest one.
  5. Add the custom domains to the load balancer instead of the applications, as in Move the custom domains.
A disk belongs to one application, so files on it aren’t shared between locations: keep files that every location serves in object storage.

Move the custom domains

The switch has no downtime if you add Sevalla’s verification records while the domain still points at Fly.io, and change the records that point it only once the certificate is ready.
1

Add each domain

On the application’s Domains tab, click Add custom domain, enter the domain in Domain name, for example shop.example.com, and click Add custom domain. Its DNS records sheet opens.
2

Add the verification records

At your DNS provider, add the _cf-custom-hostname and _acme-challenge records that the sheet shows. Keep the A and AAAA records, or the CNAME, that point the domain at Fly.io: Fly.io keeps serving it. If _acme-challenge already has a CNAME for Fly.io’s certificate, replace it with Sevalla’s record: Fly.io’s current certificate stays in use. When the domain’s status changes to Pointing, its certificate is ready. See Verify the domain.
3

Point it at Sevalla

Delete the A and AAAA records that point at Fly.io’s addresses, or the CNAME to storefront.fly.dev, and add the pointing record that the sheet shows. Delete the AAAA record too: visitors on IPv6 would otherwise still reach Fly.io. The status changes to Active when Sevalla sees the record. See Point the domain.
Lower the TTL of the records that point at Fly.io to a few minutes a day before the switch, so resolvers pick up the change quickly.

Shut down the Fly.io app

When the domains are Active and traffic reaches Sevalla:
  1. Disable the external connection you opened: on the database’s Networking tab, click Disable under External connection, then click Disable in the dialog.
  2. Remove the domains from Fly.io with fly certs remove shop.example.com --app storefront.
  3. Keep storefront.dump and volume.tar.gz until you no longer need them.
  4. Delete the app with fly apps destroy storefront, which deletes its Machines and volumes. Delete a Managed Postgres cluster with fly mpg destroy <cluster id>, and an unmanaged Fly Postgres app with fly apps destroy storefront-db. You can’t undo these.

Troubleshoot

Deploy with the API

This sequence does what Create the database to Deploy and verify do in the dashboard, with the Sevalla API and the Dockerfile build strategy. It reads the app’s settings with fly config show, so it runs from any folder: it creates the database and the application, adds the secrets and [env] entries, turns each process group into a process, attaches the disk, restores the dump, and deploys. A script or an AI agent runs it as is, in one shell. Each step names the CLI command and the Sevalla MCP server tool that do the same. You need:
  • An API key with the Company developer role, in SEVALLA_API_KEY.
  • The repository on GitHub, and a GitHub account with access to it connected to Sevalla. The API has no call that reports the connection: without access, the step that creates the application fails with 400 and Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to connect a Git account (My account in the dashboard) and, for GitHub, to give Sevalla’s GitHub App access to the repository.
  • flyctl, signed in, plus curl, jq, openssl and the PostgreSQL client tools.
  • storefront.dump in the current folder, from Copy the PostgreSQL data.
Export the app’s settings, its secrets except DATABASE_URL, and its [env] entries:
secrets.jsonl holds the secret values: don’t commit it. fly ssh console needs a running Machine; < /dev/null keeps it from reading the list of names.
1

Choose the location

List clusters (listClustersV3) returns every location with its region ID in location. Save the one closest to the Fly.io app’s primary region, here eu-frankfurt-1 (Frankfurt, Germany) for Fly.io’s fra:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the sizes

List process resource types (listProcessResourceTypesV3), List disk resource types (listDiskResourceTypesV3) and List database resource types (listDatabaseResourceTypesV3). Save H1 for the background workers and the job, the 10 GB disk and DB1, the smallest database. The web process keeps S1, which can have custom domains:
CLI: sevalla resources process-types, sevalla resources disk-types and sevalla resources db-types. MCP: get_platform_options for the process and database sizes, and sevalla_api_read with listDiskResourceTypesV3.
3

Look up the PostgreSQL version

List database versions (listDatabaseVersionsV3) returns the versions the location offers, oldest first. Save the newest:
CLI: sevalla resources db-versions --cluster "$CLUSTER_ID". MCP: not needed, create_database takes the newest version by default.
4

Create the database

Create database (createDatabaseV3). You choose the password, and the API never returns it, so keep it for the connection URL and the restore:
CLI: sevalla databases create --name orders-db --type postgresql --db-version "$PG_VERSION" --cluster "$CLUSTER_ID" --resource-type "$DB_SIZE_ID" --db-name storefront --db-user storefront --db-password "$PG_PASSWORD". MCP: create_database.
5

Create the application

Create application (createApplicationV3) creates the application with a web process at S1, and doesn’t deploy it:
CLI: sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch main. MCP: create_application with deploy: false.
6

Build from the Dockerfile

Update application (updateApplicationV3) switches to the Dockerfile build strategy:
CLI: sevalla apps update "$APP_ID" --data '{"build_type": "dockerfile", "dockerfile_path": "Dockerfile", "docker_context": "."}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
7

Wait for the database and connect it

Get database (getDatabaseV3) shows status ready when the database is up, with its internal hostname and port. Then Create internal connection (createApplicationInternalConnectionV3):
The API adds no environment variables; the next steps add DATABASE_URL. CLI: sevalla databases get "$PG_ID" and sevalla apps internal-connections create "$APP_ID" --target-id "$PG_ID" --target-type database. MCP: get_database, and sevalla_api_create with createApplicationInternalConnectionV3 and the same body.
8

Replace the Fly.io addresses

List processes (listProcessesV3) returns the web process with its internal_hostname. Print it, and the entries that still point at Fly.io:
In env.jsonl and secrets.jsonl, change each printed value: an address of the app’s own web process becomes http://<internal hostname>:8080, and a fly.dev domain becomes the application’s sevalla.app domain. For the example’s APP_INTERNAL_URL:
CLI: sevalla apps processes list --app-id "$APP_ID". MCP: sevalla_api_read with listProcessesV3.
9

Add the environment variables

Create environment variable (createEnvironmentVariableV3), once per variable, available during runtime only: DATABASE_URL in the format the internal connection uses, then every secret and [env] entry:
It prints each key as it’s added. The CLI’s sevalla apps env-vars create makes every variable available during build too, which passes it to the Dockerfile build: use the API or MCP for this step. MCP: set_environment_variables with buildtime: false on each variable.
10

Set up the web process

Update process (updateProcessV3) sets the start command of the group that [http_service] serves, after exec, and the readiness probe. Use the path, grace_period, interval and timeout of your [[http_service.checks]], in seconds; always send timeoutSeconds, which is 120 when left out:
CLI: sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --command "exec node server.js" sets the command; the CLI can’t set probes. MCP: sevalla_api_update with updateProcessV3 and the same body.
11

Create the background workers and the job

Create process (createProcessV3): a background worker for every other process group, its command after exec, then a job with the start policy beforeDeployment for release_command:
Skip the job if fly.toml has no release_command. CLI: sevalla apps processes create "$APP_ID" --name worker --type worker --resource-type "$H1_ID" --command "exec node worker.js", and for the job --type job --job-start-policy beforeDeployment. MCP: sevalla_api_create with createProcessV3 and the same body.
12

Attach the disk

Attach or update process disk (updateProcessDiskV3) attaches the disk at the volume’s destination. This attaches it to the web process; for a volume mounted for another group, use that process’s ID:
CLI: sevalla apps processes disk update "$WEB_ID" --app-id "$APP_ID" --resource-type "$DISK_ID" --mount-path /data. MCP: sevalla_api_update with updateProcessDiskV3 and the same body.
13

Restore the data

Toggle external connection (toggleDatabaseExternalConnectionV3) opens the database to your computer. Wait until Get database returns its external_hostname, then restore the dump from Copy the PostgreSQL data:
The first command prints External connection toggle started. For a minute or two after the hostname appears, connections can fail with server closed the connection unexpectedly, so the loop runs pg_restore again until it succeeds; --clean --if-exists makes each attempt start from the dump. CLI: sevalla databases connections external-toggle "$PG_ID". MCP: sevalla_api_create with toggleDatabaseExternalConnectionV3.
14

Deploy

Trigger deployment (triggerDeploymentV3) deploys the latest commit of the branch:
CLI: sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.
15

Wait for the deployment

Get deployment (getDeploymentV3) reports the status, which ends as success, failed, cancelled or skipped. The loop keeps polling through a failed request, for up to 30 minutes:
CLI: sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.
16

Turn on auto-deploy

Update application (updateApplicationV3):
CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
Then copy the files as in Copy the volume files and run the checks in Deploy and verify. To build with Ingot instead of the Dockerfile, update the application with {"build_type": "ingot"} and deploy again. Add application domain (addApplicationDomainV3) adds each custom domain; Get application domain (getApplicationDomainV3) then returns the records to add in dns_records, under verification, ssl and origin. CLI: sevalla apps domains add "$APP_ID" --name shop.example.com. MCP: add_domain.