Skip to main content
A DigitalOcean App Platform app moves to Sevalla as an application with one process per service, worker and job component, a static site for each static site component, and a Sevalla database in place of each managed database. You read the app spec with doctl, recreate each component while the App Platform app keeps running, copy the data during a short maintenance window, then point the domains at Sevalla.

At a glance

Environment variables

How DigitalOcean App Platform maps to Sevalla

Before you begin

  • doctl, signed in with doctl auth init to the team that owns the app, with a token that can read and update apps and databases.
  • The app’s code in a repository on GitHub, GitLab or Bitbucket. See Git providers.
  • A Sevalla company with a payment method.
  • The PostgreSQL client tools, at the same major version as the managed database or newer, and jq. See Import and export PostgreSQL.
  • A maintenance window: the App Platform app is offline while you copy the data.

Take stock of the App Platform app

The app spec holds every setting you recreate. Find the app’s ID, then save its spec and list the database clusters:
app.json contains the encrypted values of secret variables and your settings: don’t commit it. List the components to recreate:
The example app has a service, a static site, a worker, three jobs and a managed PostgreSQL database. Every component becomes a process or a service on Sevalla:
  • The spec’s region is where the app runs. Create the Sevalla services in the closest location:
  • A database with production: true is a managed database cluster. Its cluster_name is the Name in doctl databases list, and Version is the PostgreSQL version: create the Sevalla database with the same version or a newer one.
  • domains lists the custom domains to move. The starter domain on ondigitalocean.app isn’t listed and doesn’t move: Sevalla gives the application a sevalla.app domain and the static site a sevalla.page domain.
List the variables that don’t have a plain value: the encrypted ones and the ones that use bindable variables.
The spec holds encrypted values as EV[1:...], which only App Platform can decrypt. Read each one in a console session of the component that has it, here the api service, and save it in a file named secrets.env as KEY=value, one per line:
In the console, run printenv SESSION_SECRET, then exit. secrets.env contains secrets: don’t commit it either.

Create the database

In the sidebar, click Databases, then click Create database. Under Database, choose PostgreSQL and a version at least as new as the managed database’s. Enter orders-db as the Display name, choose the Data center closest to the app’s region and a Size with room for the data, then click Create database. The database’s status changes to Ready when it’s up. 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 of the service. 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

Check the build

Sevalla builds with Ingot, which detects the language and the start command without configuration, as App Platform’s buildpacks do. If the service has a source_dir, click the Settings tab, click Update build strategy under Build strategy, set the Build path to that folder, and click Update build strategy. If the service builds from a dockerfile_path, choose Dockerfile there instead and enter the path. See Build strategies.
A web process on H1 can’t have custom domains. If you’ll move a domain, choose a larger size, such as S1.

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 your code already reads, and click Add internal connection. See Connect an application. The URL starts with postgres:// and has no sslmode: the internal connection stays on Sevalla’s private network. If your code connects with TLS and the managed database’s CA certificate (${db.CA_CERT}), make it connect without them when that variable isn’t set, as the example app does: DigitalOcean’s certificate doesn’t validate a Sevalla database.

Move the environment variables

App Platform has app-level variables, which every component gets, and component-level variables, which win over them. On Sevalla, every process of the application shares the application’s variables, so the app-level and the component-level variables of the service, the workers and the jobs all go to the application. If two components set the same key to different values, set the value in the process’s start command instead, for example WORKER_INTERVAL_SECONDS=5 exec node worker.js. A static site’s variables go to the static site.
1

Export the plain variables

Write the variables with plain values into one file per scope:
2

Import them

On the application, click the Environment variables tab, then click Add environment variables. Click Import .env and choose RUN_AND_BUILD_TIME.env, keep Available during runtime and Available during buildtime selected, and click Add environment variables. Import RUN_TIME.env the same way with only Available during runtime selected, and BUILD_TIME.env with only Available during buildtime. Skip empty files.
3

Add the secrets

Import secrets.env with the availability of each secret’s scope.
4

Replace the bindable variables

Add each variable that used a bindable variable with its Sevalla value from Replace the bindable variables. DATABASE_URL is already there, from the internal connection.
The Add environment variables sheet with key and value rows, Add another, Import .env and the availability optionsThe Add environment variables sheet with key and value rows, Add another, Import .env and the availability options
Don’t add PORT: Sevalla sets it to the web process’s port. See Environment variables.

Replace the bindable variables

App Platform fills in bindable variables (${...}) when it deploys. Sevalla has no bindable variables with these names, so each one becomes a variable from the internal connection, a variable Sevalla sets, or a value you type: For the example app, PUBLIC_URL is the application’s URL and CORS_ORIGIN is the static site’s URL. DATABASE_CA_CERT and COMMIT_SHA go away: the jobs read SVL_DEPLOYMENT_COMMIT_SHA when COMMIT_SHA isn’t set. On the static site, API_URL becomes the application’s URL followed by /api.

Recreate the components

1

Create the processes

On the Processes tab, click Create process for each worker and job. Choose Background worker, Cron job or Job, enter the component’s name as the Name and the Custom start command, set the Start policy (jobs) or the Schedule (cron jobs), choose a Resource, and click Create process.
2

Add the health check

If the service has a health_check with an http_path, click ⋯ on the Web process, then click Update process. Under Health checks, turn on Readiness probe, set the Path to the http_path, and copy any timings the spec sets. App Platform marks an instance unhealthy after 9 failed checks by default, Sevalla after 3. Set a liveness_health_check as the Liveness probe the same way, then click Update process.
A service without http_path has a TCP health check on App Platform. Sevalla’s probes are HTTP only: add a path that answers 200, or leave the probes off. Choose each process’s size and instance count from the spec’s instance_size_slug and instance_count, with at least as much memory: S1 has 1 GB, against 512 MB for apps-s-1vcpu-0.5gb. See Processes and Scaling.

Create the static site

1

Open the create form

In the sidebar, click Static sites, then click Create static site. Under Source, click Private repository and choose the Git provider, the Repository and the Branch of the static site component.
2

Copy the build settings

Under Build settings, set the Build command to the component’s build_command, the Root directory to its source_dir, and the Publish directory to its output_dir. App Platform searches _static, dist, public and build when output_dir isn’t set: enter the folder your build writes.
3

Add its variables

Under Environment variables, add the component’s envs, with bindable values replaced. For the example app, API_URL is the application’s URL followed by /api.
4

Create it

Click Create static site. The first deployment starts at once. See Create a static site.

Replace the ingress rules

App Platform serves every component of an app on the same domains and routes requests by path, with ingress rules such as /api to the service and / to the static site. On Sevalla, each application and each static site has domains of its own, and no setting routes paths between them. Load balancers route by location, not by path, and a static site’s proxy rules cache responses, so they don’t fit an API either.
  • Give each service its own domain. For example, keep shop.example.com for the static site and move the service to api.example.com. Point the static site’s API address (API_URL in the example app) at the application’s domain, and let the application send CORS headers for the static site’s domain (CORS_ORIGIN in the example app).
  • Keep the service’s paths. A rule with preserve_path_prefix: true passes /api/products to the service as it is: the service then answers on https://api.example.com/api/products. Without it, App Platform strips the prefix, and the service answers on https://api.example.com/products.
  • Or serve everything from the application. If the static files can be built into the application, serve them from the web process and keep one domain.
  • Redirect rules become a _redirects file on the static site, or redirects in the application. CORS rules become a _headers file on the static site, or headers that the application sets.

Copy the PostgreSQL data

1

Stop writes on App Platform

Archive the app: App Platform stops every component, including workers and scheduled jobs, and answers every request with an offline page and status 503. Archiving is an update of the spec, with maintenance.archive:
To bring the app back, set archive to false the same way. App Platform then deploys it again and runs its POST_DEPLOY jobs.
2

Allow your computer in the trusted sources

A database cluster with trusted sources accepts connections only from them, such as the app. Add your IP address:
Without it, pg_dump waits and then fails with Operation timed out.
3

Dump the database

doctl databases connection returns the cluster’s connection URL for the defaultdb database and the doadmin user. --show puts the real password in it: without it, doctl prints ********. If the spec’s database has a db_name, put that name in place of defaultdb:
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

With the external connection’s details in the URL:
See Import a database for large databases and errors.
6

Compare the data

Count the rows and take a checksum of every table on both sides, and compare the results:
sevalla.txt lists each table with its row count and checksum, for example orders|1000|ed8a6c61bbd5d6e738497c271125ce7b.
A Sevalla PostgreSQL database sorts text with the C collation, where a DigitalOcean managed database uses en_US.UTF-8: an ORDER BY on a text column can return rows in another order, which is why the comparison sorts with COLLATE "C". If your app depends on that order, test those queries on Sevalla before the cutover. To rehearse, run these steps once without archiving the app, deploy, and test on the sevalla.app domain. For the real cutover, empty the database first, for example with pg_restore’s --clean --if-exists options, so the restore doesn’t collide with the rehearsal’s tables.

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 each job’s result, such as migrate has been successful.With the API, Get deployment (getDeploymentV3) returns the status, success for Deployed, and Get application deployment logs (getApplicationDeploymentLogsV3) finds the jobs’ results. APP_ID and DEPLOYMENT_ID come from Deploy with the API:
3

Test the application

Click the domain under the application’s name and test the critical paths. On the Processes tab, the web process and the background workers show Running. The example app answers with the copied data:
With the API, Get application runtime logs (getApplicationRuntimeLogsV3) with search finds a line that a worker logs, for example the example worker’s start line:
CLI: sevalla apps logs runtime "$APP_ID" --search "Worker ready". MCP: search_runtime_logs.
4

Test the static site

Open the static site’s sevalla.page domain. The example site’s build wrote the application’s URL into config.json:
5

Check the cron jobs

On the Processes tab, click ⋯ on a cron job, click Run now, then click Run now in the dialog. Its status changes to Succeeded. See Check cron runs.With the API, Trigger cron job (triggerProcessCronV3) starts a run and List cron runs (listProcessCronRunsV3) returns its status, newest first:
CLI: sevalla apps processes trigger "$CRON_ID" --app-id "$APP_ID", then sevalla apps processes runs --app-id "$APP_ID" --process-id "$CRON_ID". MCP: sevalla_api_create with triggerProcessCronV3, then sevalla_api_read with listProcessCronRunsV3.
6

Turn on auto-deploy

To deploy every push to the branch automatically, as deploy_on_push did, click the Settings tab, then click Update auto-deploy in the Auto-deploy section. Turn on Auto-deploy and click Update auto-deploy. The static site has Auto-deploy on from the start. See Deployments.

Move the custom domains

The app’s domains serve every component on App Platform. On Sevalla, add each domain to the service that answers it, for example shop.example.com to the static site and api.example.com to the application (see Replace the ingress rules). App Platform keeps serving each domain until you change its pointing record, so the switch has no downtime:
1

Add each domain

On the application’s or the static site’s Domains tab, click Add custom domain, enter the domain in Domain name, and click Add custom domain.
2

Verify it

Add the ownership and certificate records that Sevalla shows at your DNS provider, next to the record that points at App Platform. When the domain’s status changes to Pointing, its certificate is ready. See Verify the domain.
3

Point it at Sevalla

Replace the record that points at App Platform (a CNAME to the app’s ondigitalocean.app domain, or A records to App Platform’s ingress IP addresses) with the pointing record that Sevalla shows. The status changes to Active when Sevalla sees it. See Point the domain.
4

Remove it from App Platform

Once the domain is Active on Sevalla, delete it from the spec’s domains and update the app.
If DigitalOcean manages the domain’s DNS (We manage your domain, a zone in the spec’s domains), App Platform keeps the domain’s records itself. First remove zone from the domain in the spec and update the app, as DigitalOcean describes, then change the records in DigitalOcean’s DNS or at the DNS provider you move to. After the move, update the values that held the domain, such as CORS_ORIGIN and the static site’s API_URL in the example app, then deploy both.

Shut down the App Platform 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 your IP address from the cluster’s trusted sources: find its UUID with doctl databases firewalls list "$DO_DB_ID", then run doctl databases firewalls remove "$DO_DB_ID" --uuid <rule UUID>.
  3. Keep storefront.dump until you no longer need it.
  4. Delete the App Platform app with doctl apps delete "$DO_APP_ID" --force. The managed database cluster stays, and keeps billing, but the app is no longer one of its trusted sources: a cluster without trusted sources accepts connections from any address.
  5. Delete the cluster with doctl databases delete "$DO_DB_ID" --force once you’re sure. Deleting it deletes its data and its backups.
To repeat the setup for staging, put the applications in a pipeline.

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 Ingot: it reads the app spec, creates the database, the application and the static site, turns each component into a process, copies the data and deploys. A script or an AI agent runs it as is, in one shell, from the folder with secrets.env from Take stock of the App Platform app. 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.
  • doctl, signed in, plus curl, jq, openssl and the PostgreSQL client tools.
The API allows 20 service creations per hour per company and operation; this sequence creates three services.
1

Choose the location

List clusters (listClustersV3) returns every location with its region ID in location. Save the one closest to the app’s region, here us-ashburn-1 (Ashburn, USA) for nyc:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the process size

List process resource types (listProcessResourceTypesV3). Save S1 for the processes this sequence creates:
CLI: sevalla resources process-types. MCP: get_platform_options.
3

Look up the database size and version

List database resource types (listDatabaseResourceTypesV3) and List database versions (listDatabaseVersionsV3). Save DB1, the smallest size, or one with room for the data, and the newest PostgreSQL version, which must be at least the managed database’s:
CLI: sevalla resources db-types and sevalla resources db-versions --cluster "$CLUSTER_ID". MCP: get_platform_options.
4

Create the PostgreSQL database

Create database (createDatabaseV3). You choose the password, and the API never returns it, so keep it for the connection URL:
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 for the api service with a web process at S1, which can have custom domains, 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

Create the static site

Create static site (createStaticSiteV3) with the static site component’s source_dir, build_command and output_dir. It doesn’t deploy the site:
CLI: sevalla static-sites create --name storefront-web --repository "$REPO" --branch main --build-command "npm --prefix web run build" --publish-directory web/dist --node-version lts; the CLI has no root directory option, so the build command and publish directory include the folder. MCP: create_static_site.
7

Wait for the database

Get database (getDatabaseV3) shows status ready when the database is up, with its internal hostname and port:
CLI: sevalla databases get "$PG_ID". MCP: get_database.
8

Connect the database

Create internal connection (createApplicationInternalConnectionV3):
The API adds no environment variables; the next step adds DATABASE_URL. CLI: sevalla apps internal-connections create "$APP_ID" --target-id "$PG_ID" --target-type database. MCP: sevalla_api_create with createApplicationInternalConnectionV3 and the same body.
9

Add the environment variables

List application domains (listApplicationDomainsV3) and List static site domains (listStaticSiteDomainsV3) return the two URLs. Then Create environment variable (createEnvironmentVariableV3) adds, once per variable: DATABASE_URL in the internal connection’s format, the spec’s plain app-level and component-level variables with the availability of their scope, the secrets from secrets.env at runtime only, and the replacements for the example app’s bindable variables:
CLI: sevalla apps env-vars create "$APP_ID" --key <key> --value <value>, once per variable; it makes every variable available during the build and at runtime. MCP: set_environment_variables with all the variables in one call.
10

Add the static site's variables

Create static site environment variable (createStaticSiteEnvVarV3), once per variable of the static site component. The example site’s API_URL points at the application:
CLI: sevalla static-sites env-vars create "$SITE_ID" --key API_URL --value "$APP_URL/api". MCP: sevalla_api_create with createStaticSiteEnvVarV3 and the same body.
11

Add the health check

List processes (listProcessesV3) finds the web process, and Update process (updateProcessV3) turns the service’s health_check into a readiness probe, with App Platform’s defaults for the timings the spec leaves out. Always send timeoutSeconds: a probe without it gets 120 seconds.
CLI: not available, the CLI can’t set probes. MCP: sevalla_api_update with updateProcessV3 and the same body.
12

Create the workers, jobs and cron jobs

Create process (createProcessV3), once per worker and job in the spec: a background worker with exec before the run_command, a job with the start policy that matches its kind, and a cron job with the schedule and time zone of a SCHEDULED job:
CLI: sevalla apps processes create "$APP_ID" --name worker --type worker --resource-type "$S1_ID" --command "exec node worker.js"; for a job add --type job --job-start-policy afterSuccessDeployment, for a cron job --type cron --schedule "*/15 * * * *" (the CLI can’t set a time zone). MCP: sevalla_api_create with createProcessV3 and the same body.
13

Stop writes on App Platform

Archive the App Platform app, as in Copy the PostgreSQL data, and let your IP address through the database cluster’s trusted sources:
14

Open the database to your computer

Toggle external connection (toggleDatabaseExternalConnectionV3) turns the external connection on, and Get database (getDatabaseV3) returns its hostname and port once it’s up:
CLI: sevalla databases connections external-toggle "$PG_ID". MCP: sevalla_api_create with toggleDatabaseExternalConnectionV3.
15

Copy and compare the data

Dump the managed database, restore it, and compare the row counts and checksums of every table:
16

Deploy the application

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

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.
18

Deploy the static site

Deploy static site (deployStaticSiteV3), then Get static site deployment (getStaticSiteDeploymentV3) until it ends:
CLI: sevalla static-sites deployments trigger "$SITE_ID", then sevalla static-sites deployments get "$SITE_DEPLOYMENT_ID" --site-id "$SITE_ID". MCP: sevalla_api_create with deployStaticSiteV3, then sevalla_api_read with getStaticSiteDeploymentV3.
19

Turn on auto-deploy

Update application (updateApplicationV3) and Update static site (updateStaticSiteV3). Both start with auto-deploy off when the API creates them:
CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and updateStaticSiteV3.
Then run the checks in Deploy and verify, and turn the external connection off again with the same toggle call. Add application domain (addApplicationDomainV3) and Add static site domain (addStaticSiteDomainV3) add each custom domain; read its DNS records in dns_records with List application domains a few seconds later.