This guide explains how to migrate an existing application from another hosting provider to Upsun.
It focuses on the technical steps required to make your application deployable on Upsun, including
configuration, services, environment variables, and data migration.
No prior experience with Upsun is needed — each step is explained simply, with examples and troubleshooting tips.
Before you begin
You need:
- An understanding of the main Upsun concepts
- An Upsun account.
If you don’t already have one, register for a trial account.
You can sign up with an email address or an existing GitHub, Bitbucket, or Google account.
If you choose one of these accounts, you can set a password for your Upsun account later.
- The Upsun CLI installed locally
Follow the steps below to begin the migration of your project.
1. Audit your data from your current project
Before migrating, make sure you have access to:
- Your source code
- Your database
- Your assets and uploaded files
From Self-Managed
From Heroku
From Vercel
From Railway
Runtime & build
- Runtime version: language and version in use (Node.js, Python, Ruby, PHP, etc.)
- Package manager: npm, yarn, pnpm, pip, composer, bundler, etc.
- System dependencies: packages installed via
apt, yum, or similar
- Build process: how assets are compiled, if any (webpack, vite, sass, etc.)
- How the app starts: systemd unit, supervisor config, pm2, Docker entrypoint, etc.
- Reverse proxy: nginx, Apache, or Caddy config (rewrites, headers, timeouts, SSL termination)
Processes & scheduling
- Background workers: how they are started and supervised
- Cron jobs: run
crontab -l to list all scheduled tasks
Environment & secrets
- Environment variables: run
env on your server to list them
.env files: check for any local env files used at runtime
- SSL certificates: Let’s Encrypt, manual certs, or managed by proxy
Data & storage
- Database engine and version: PostgreSQL, MySQL, MongoDB, etc.
- Writable folders: directories your app writes to at runtime (uploads, cache, logs)
- Backup strategy: how and where backups are currently stored
Access
- Users and teams: who currently has access to the server and application
- SSH access: confirm you can connect to export data
App/runtime/build
- Stack & runtime versions: current stack (
heroku stack), Node/Python/Ruby/etc versions, package manager (npm/yarn/pnpm), locked deps.
- Slug/build behavior: anything relying on Heroku’s slug compilation quirks,
postinstall, release phase, heroku-postbuild.
- Build vs run separation: what gets generated at build time (assets, SSR bundles) vs needs runtime write access.
Processes, jobs, scheduling
- Release phase scripts (Procfile
release): DB migrations, cache warmups, etc.
- Heroku Scheduler jobs (often forgotten) + any custom cron-like jobs.
- Workers/queues: what queue system (Redis/Rabbit/etc), concurrency settings, retry policies.
Networking, routing, and platform features
- Routes/HTTP behavior: any reliance on Heroku router timeouts, request body size limits, websockets, sticky sessions (rare), streaming.
- Outbound networking: external integrations with IP allowlists; need for stable egress IPs.
- SSL/TLS details: ACM vs manual certs, SNI endpoints, any HSTS / security headers configured elsewhere (CDN/WAF).
Data & state
- Datastores & backups: Postgres plan/features (extensions, follower, PITR), Redis plan, backup/restore flows, retention.
- File storage: any use of ephemeral filesystem (tmp/uploads) that should be moved to object storage.
- Caches: in-memory assumptions, Redis usage patterns, cache invalidation strategy.
Observability & ops
- Logging drains: Papertrail/Datadog/Splunk drains + formats, PII controls.
- Metrics/APM: add-ons and config, alert rules, SLOs.
- Error tracking: Sentry/Rollbar, release tagging.
Delivery and environments
- Pipelines & review apps: how staging/prod are promoted, config var diffs, Heroku CI.
- GitHub integration: auto-deploy branches, required checks.
- Regions: app region + data region alignment, compliance constraints.
Access & governance
- SSO / permissions model: Enterprise features, team roles, API tokens in CI.
- Secrets handling: where secrets live (Heroku config vars, CI, Vault), rotation plan.
Project structure & build
- Framework + version (Next.js, Nuxt, SvelteKit, etc.) and Vercel build preset.
- Build & Output settings: build command, install command, output directory, ignored build step.
- Monorepo setup: root directory, workspace tool (pnpm/yarn/npm), Turbo/Nx config, package manager version.
- Node/runtime versions: Node version, edge runtime usage, any experimental flags.
Routing & edge behavior (must map cleanly)
- Rewrites/redirects/headers (
vercel.json / next.config): including regex rules and priority order.
- Middleware usage (Next.js middleware): what it does (auth, geo, A/B, rewrites).
- Edge Functions vs Serverless Functions: which endpoints run where and why.
- Regional settings: functions/edge regions and latency expectations.
Rendering modelFor Next.js especially, inventory exactly which parts are:
- SSG pages
- SSR pages
- ISR pages (revalidate timings, on-demand revalidation hooks)
- Route Handlers / API routes
- Image Optimization (
next/image) usage
This determines whether you need a long-running web process, background jobs, cache, and/or a CDN strategy.Environment variables & secrets
- Env vars per environment (Production / Preview / Development) and which are “encrypted” / “sensitive”.
- Preview env overrides: anything that only exists on preview deployments.
- Secret sources: Vercel integrations, Vercel-managed secrets, external vaults.
Storage & state (Vercel add-ons / managed services)
- Vercel Postgres / KV / Blob usage, connection methods, pooling, migrations.
- Any external DB (Supabase/RDS/etc): connection limits, pooling, IP allowlists.
- File uploads: where do uploads go (Blob/S3/etc)? Any code writing to local disk (won’t persist).
Scheduled and async work
- Vercel Cron Jobs: schedules, endpoints called, auth strategy.
- Background processing: do you rely on queueing elsewhere? Any long-running tasks incorrectly placed in serverless functions?
Limits & performance assumptions
- Function timeouts/memory assumptions (serverless) and payload size constraints.
- Caching strategy: Vercel/CDN cache headers, Next fetch caching, ISR cache behavior.
- Build cache expectations and cold-start sensitivity.
Domains, deploys, and release workflow
- Domains + redirects + canonical host rules, wildcard domains, multiple apps behind one domain.
- Preview deployments: how they’re used (QA, PR checks), who needs access, protected previews.
- Git integration: which repos/branches deploy, required checks, auto-promote patterns, deploy hooks/webhooks.
Observability & access
- Logs/analytics: Vercel Analytics, Speed Insights, log drains (if any), retention needs.
- Team access: members, roles, SSO, tokens used by CI/CD.
App/runtime/build
- Runtime & version: language and version in use (
railway variables to check), Node/Python/Ruby/Go/etc., package manager (npm/yarn/pnpm/pip).
- Nixpacks build config: any
nixpacks.toml or railway.toml customizing the build, install, or start commands.
- Build vs run separation: what gets generated at build time (assets, compiled bundles) vs what needs runtime write access.
Processes, jobs, scheduling
- Start command: defined in
railway.toml ([deploy] startCommand) or auto-detected by Nixpacks.
- Cron jobs: any Railway Cron services defined in your project.
- Workers: additional Railway services acting as background workers (check your project’s service list).
Networking, routing, and platform features
- Public domains: custom domains and Railway-generated
*.up.railway.app URLs.
- Private networking: any use of Railway’s private network (
*.railway.internal) between services.
- Port binding: Railway injects a
$PORT variable — confirm your app binds to it.
- TCP proxy: any services exposed via Railway’s TCP proxy.
Data & state
- Railway Postgres / MySQL / Redis / MongoDB: plugin services in your project — check the Railway dashboard.
- Connection strings: exported as environment variables (e.g.
DATABASE_URL, REDIS_URL).
- Volume mounts: any persistent volumes attached to services.
- File storage: any code writing to local disk (ephemeral on Railway — must be moved to a volume or object storage).
Observability & ops
- Metrics: Railway built-in CPU/memory/network metrics per service.
- Log retention: Railway log history and any external log drain integrations.
- Deployment history & rollbacks: rollback strategy currently in use.
Delivery and environments
- Environments: Railway environments in use (e.g.
production, staging) and their variable overrides.
- GitHub integration: auto-deploy branches, PR environments, required checks.
- Deploy triggers: webhooks or API-triggered deploys.
Access & governance
- Team members and roles: current team access in the Railway dashboard.
- Tokens: any Railway API tokens used in CI/CD pipelines.
- Secrets handling: where secrets live (Railway variables, GitHub secrets, external vaults).
How Upsun maps to your existing architecture
When migrating from another platform, map your existing components to Upsun concepts.
2. Clone your Git repository locally
As a start, you need to clone your source code locally to make it Upsun ready.
git clone <REPOSITORY_URL> your-app-directory
cd your-app-directory
3. Initialize Upsun configuration
A Upsun project is configured through the .upsun/config.yaml file.
This file defines your applications, services, routes, and runtime configuration.
Upsun provides a CLI project:init command.
This command initializes your Upsun project configuration.
If you choose With AI (automatic), it detects your app’s requirements and creates a config file for you.
4. Create a new Upsun project
Using the CLI
Using the Console
If you do not already have an organization created on Upsun, create one:Then run the following command to create a project:When prompted, fill in details like the project name, region, and the name of your organization. Create a new project from scratch.If you do not already have an organization created to put the project, you’ll first be instructed to create one.Once you have done so, select that organization from the dropdown, and select Create from scratch.In the form, fill in details like the project name and region.
You’ll be able to define resources for the project after your first push.
Before migrating your production environment, we recommend pushing to a separate
branch first. Upsun automatically creates a preview environment for
each branch, allowing you to validate your configuration, services, and data
before switching any production traffic.
In your Upsun project, you can add as many services as you need.
List your current services in use and add them in the configuration, following the corresponding Service page.
As an example, if your current project is using PostgreSQL and Redis,
edit your .upsun/config.yaml file and add the following:
Using default endpoints
Using explicit endpoints
applications:
myapp:
relationships:
database:
cache:
services:
database:
type: postgresql:16
cache:
type: redis:7.2
applications:
myapp:
relationships:
database:
service: postgresql_service
endpoint: postgresql
cache:
service: redis_service
endpoint: redis
services:
postgresql_service:
type: postgresql:16
redis_service:
type: redis:7.2
You need to declare your services in the services top YAML key and add a relationships for each service in your
myapp configuration. Service environment variables will be automatically injected in your myapp container to interact with.
From Self-Managed
From Heroku
From Vercel
From Railway
Find out how your current application is started
and convert it to an Upsun configuration.
- How you start your runtime should go in
myapp.web.commands.start
- How you start your worker should go in
myapp.workers.<WORKER_NAME>.commands.start
Example:applications:
myapp:
type: "python:3.12"
web:
commands:
start: "gunicorn app:app --workers 4 --bind 0.0.0.0:$PORT"
workers:
celery:
commands:
start: "celery -A tasks worker --loglevel=info"
Heroku applications typically define processes in a Procfile.On Upsun, these processes are defined in .upsun/config.yaml
using the web section for the HTTP entrypoint and
workers for background processes.Transform your process definitions. If your Procfile has:web: gunicorn app:app --workers 4
worker: celery -A tasks worker --loglevel=info
Your .upsun/config.yaml should include these commands in the commands.start section:applications:
myapp:
type: "python:3.12"
web:
commands:
start: "gunicorn app:app --workers 4 --bind 0.0.0.0:$PORT"
workers:
celery:
commands:
start: "celery -A tasks worker --loglevel=info"
Applications deployed on Vercel often rely on serverless functions and edge features.On Upsun, these workloads typically run as a persistent web process
inside an application container. Unlike serverless functions, the process stays alive
between requests, which changes how you think about memory, state, and concurrency.Identify the main entrypoint of your application.For example, a Next.js application usually runs with:You can define this command in .upsun/config.yaml:applications:
myapp:
type: "nodejs:20"
web:
commands:
start: "npm run start"
If your project includes background jobs or queue consumers, define them as workers:applications:
myapp:
workers:
jobs:
commands:
start: "node worker.js"
You may also need to review features that rely on Vercel platform behavior such as:
- Edge middleware
- Serverless API routes
- Incremental static regeneration (ISR)
- Vercel image optimization
These features may require application-level implementations when running on Upsun. Railway services are typically started via a command defined in railway.toml or auto-detected by Nixpacks.On Upsun, the start command is defined in .upsun/config.yaml under web.commands.start.If your railway.toml defines:[deploy]
startCommand = "node dist/server.js"
Your .upsun/config.yaml should include:applications:
myapp:
type: "nodejs:20"
web:
commands:
start: "node dist/server.js"
If you have additional Railway services acting as workers, define them as Upsun workers:applications:
myapp:
workers:
jobs:
commands:
start: "node worker.js"
Railway injects a $PORT variable for your app to bind to. Upsun works the same way — make sure your app listens on $PORT.
You may also need to review features that rely on Railway-specific behavior such as:
- Private networking between services (
*.railway.internal)
- TCP proxy endpoints
- Persistent volumes (map these to Upsun mounts)
- Railway Cron services (convert to Upsun crons)
If you have custom build steps, define them explicitly in the hooks.build and hooks.deploy section:
applications:
myapp:
hooks:
build: |
pip install -r requirements.txt
python manage.py collectstatic --noinput
deploy: |
python manage.py migrate --noinput
The app container is fully writable during the build hooks and is read-only during the deploy hooks.
During the deploy hooks, only defined mounts are writable.
7. Define mounts
Define your mounts (writable folders) in .upsun/config.yaml:
applications:
myapp:
mounts:
'web/uploads':
source: storage
source_path: uploads
'private':
source: instance
source_path: private
Refer to supported mount types for more information.
8. Define routes
If you are migrating a multi-application project, or want to customize how your application is served,
you need to configure routes.
9. Optional: Define a resource initialization strategy
By default, when you first deploy your project,
Upsun allocates default resources to each of your containers.
If you don’t want to use those default resources,
define your own resource initialization strategy before pushing your code.
Alternatively, you can amend those default container resources after your project is deployed.
10. Optional: Add environment variables
If your app requires environment variables to build properly, add them to your environment.
Extract existing environment variables from your project:
From Self-Managed
From Heroku
From Vercel
From Railway
SSH to your server and use the env CLI command to list all your existing environment variables: List your Heroku environment variables using the CLI: List your Vercel environment variables using the CLI:You can retrieve the value of a variable with:This command writes environment variables to a .env file locally. List your Railway environment variables using the CLI:You can also export them to a .env file:railway variables --json | jq -r 'to_entries[] | "\(.key)=\(.value)"' > .env
Or use the Railway dashboard: go to your service → Variables tab.Railway injects service-specific variables automatically (e.g. DATABASE_URL, REDIS_URL). You do not need to recreate those — Upsun injects equivalent service environment variables automatically once you define your services.
Review the variables and recreate them in Upsun using variable:create.
# Standard environment variables
upsun variable:create --level environment --name env:<KEY> --value <VALUE>
# Project-wide variables
upsun variable:create --level project --name env:<KEY> --value <VALUE>
# Application-specific variables
upsun variable:create --app-scope myapp --name env:<KEY> --value <VALUE>
For sensitive values, be sure to add the --sensitive true option to avoid exposing any confidential information.
11. Push your changes
The way to push your code to Upsun depends on
whether you’re hosting your code with a third-party service using a source integration.
If you aren’t, your repository is hosted in Upsun
and you can use the CLI or just Git itself.
At this stage, ensure you’ve already committed your updated files (mainly .upsun/config.yaml) in your Git history:git add .upsun/config.yaml
git commit -m "Add Upsun configuration file"
-
(Optional) Get your project ID by running the following command:
-
(Optional) Add Upsun as a remote repository by running the following command:
upsun project:set-remote <PROJECT_ID>
-
Push to the Upsun repository by running the following command:
When you try to push, any detected errors in your configuration are reported and block the push.
After any errors are fixed, a push creates a new environment.Set up the integration for your selected service:Then push code to that service as you do normally.
Pushing to a branch creates an environment from that branch.Note that the source integration doesn’t report any errors in configuration directly.
You have to monitor those in your project activities.
-
Add an SSH key.
-
In the Console, open your project and click Code.
-
Click Git.
-
From the displayed command, copy the location of your repository.
It should have a format similar to the following:
abcdefgh1234567@git.eu.upsun.com:abcdefgh1234567.git
-
Add Upsun as a remote repository by running the following command:
git remote add upsun <REPOSITORY_LOCATION>
-
Push to the Upsun repository by running the following command:
git push -u upsun <DEFAULT_BRANCH_NAME>
When you try to push, any detected errors in your configuration are reported and block the push.
After any errors are fixed, a push creates a new environment.
12. Test your production environment
When your Upsun project is successfully deployed:
- Verify service connections:
- Check database connectivity
- Verify cache operations
- Test background worker execution
- Load testing: Run a load test against your Upsun environment
before switching DNS to validate performance under realistic traffic.
Tools like k6, Locust, or
Gatling can be used for this purpose.
- Review logs and metrics:
upsun logs app
upsun metrics:all
- Verify functionality:
- Test critical user flows
- Confirm integrations working
- Check scheduled jobs executing
13. Enable maintenance mode
Before importing data and switching DNS, enable maintenance mode in your application if it supports it.
This prevents users from writing new data to your old environment during the cutover, avoiding data loss or inconsistencies.
Do not skip this step if your application is live and receiving traffic. Any writes to your old environment after the database export will be lost.
14. Import data
Once you have an environment ready and maintenance mode is enabled, import the data from your current provider.
The exact process depends on the database engine you use.
From Self-Managed
From Heroku
From Vercel
From Railway
Depending on the database engine you’re using, download a dump of your database into a SQL file locally
and import it into your application container.
Option 1: Export as SQL directly (recommended)DATABASE_URL=$(heroku config:get DATABASE_URL)
pg_dump -Fp --no-owner --clean --if-exists $DATABASE_URL > database.sql
Option 2: If you have an existing custom format backupheroku pg:backups:capture
heroku pg:backups:download
pg_restore -f database.sql --no-owner --clean --if-exists latest.dump
If your Vercel project uses an external database (for example Supabase, Neon, or PlanetScale),
export the database using the appropriate tool for your database engine.For PostgreSQL:pg_dump -Fp --no-owner --clean DATABASE_URL > database.sql
For MySQL:mysqldump --single-transaction DATABASE_NAME > database.sql
If your Railway project uses a Railway Postgres service, export the database using pg_dump with the connection string from your Railway variables:DATABASE_URL=$(railway variables --json | jq -r '.DATABASE_URL')
pg_dump -Fp --no-owner --clean --if-exists "$DATABASE_URL" > database.sql
For Railway MySQL, use the individual connection variables Railway exports:MYSQLHOST=$(railway variables --json | jq -r '.MYSQLHOST')
MYSQLPORT=$(railway variables --json | jq -r '.MYSQLPORT')
MYSQLUSER=$(railway variables --json | jq -r '.MYSQLUSER')
MYSQLPASSWORD=$(railway variables --json | jq -r '.MYSQLPASSWORD')
MYSQLDATABASE=$(railway variables --json | jq -r '.MYSQLDATABASE')
mysqldump --single-transaction -h "$MYSQLHOST" -P "$MYSQLPORT" -u "$MYSQLUSER" -p"$MYSQLPASSWORD" "$MYSQLDATABASE" > database.sql
For Railway Redis, no SQL dump is needed — if you need to migrate data, use redis-cli --rdb or simply let your application repopulate the cache on Upsun.
Then import the dump using the Upsun CLI:
upsun sql < <BACKUP_FILE_NAME>
If you have multiple database services, target a specific one with --relationship=<RELATIONSHIP_NAME>.
For any potential more details, see the specific service documentation.
15. Import files
Your app may include content files, meaning files that aren’t intended to be part
of your codebase and therefore aren’t tracked in Git.
You can upload such files to mounts you created.
Assuming you have the following mounts:
applications:
myapp:
mounts:
'web/uploads':
source: storage
source_path: uploads
'private':
source: instance
source_path: private
Upload each mount separately:
upsun mount:upload --mount web/uploads --source ./uploads
upsun mount:upload --mount private --source ./private
Alternatively, you can transfer files using an SSH client.
16. Configure your domain and update DNS records
Lower your TTL in advance
Before switching DNS, lower your domain’s TTL to 300 seconds (5 minutes) at least 24 hours in advance.
This reduces propagation delay during cutover and makes it easier to roll back quickly if needed.
Add your domain to Upsun:
upsun domain:add www.yourDomain.com
Get your Upsun target URL:
Update your DNS records
Add a CNAME record in your DNS provider pointing your domain to the URL returned above.
Once DNS has propagated, verify your domain resolves correctly and your application loads as expected before disabling maintenance mode.
DNS propagation can take up to 48 hours depending on your provider and previous TTL value.
You can monitor propagation using a tool like dnschecker.org.
Rollback plan
If something goes wrong after the DNS switch, you can revert quickly:
- Point your
CNAME back to your old provider’s URL
- Re-disable maintenance mode on your old environment
- Investigate the issue on Upsun before retrying
Keep your old environment live and intact until you have fully validated the migration.
17. Invite teams and users
You can either invite individual users to your project, or create teams and then invite users to teams (recommended).
Create a team and invite users
We recommend creating a team and inviting individual users to it, as you can manage access at the team level instead of one-by-one.
When creating a team for a project, a role is required:
- At the Project level:
admin or viewer
- For each environment type (
production, staging and development): admin, contributor or viewer
To create a team, you can either use the Console or the CLI:
upsun team:create --role viewer,production:viewer,staging:contributor,development:admin --label "My team"
To invite a user to a team, provide the user ID (if existing user) or the email address:
upsun team:user:add <USER_EMAIL_ADDRESS_OR_ID>
Each user invited to a team inherits the team’s role.
Invite individual users
When inviting individual users on a project, a role is required:
To invite individual users on a project, use the following CLI command:
upsun user:add --role viewer,production:viewer,staging:contributor,development:admin <USER_EMAIL_ADDRESS>
18. Monitor your application
Once your application is live, make sure to check out the continuous profiling feature
to ensure your application is running as expected.
Troubleshooting common migration issues
If you encounter any error during the migration, refer to the Troubleshoot page.