You can move a FastAPI, Flask, or Django API from Railway to Edgenivo's native Python runtime without a Dockerfile when its dependencies fit the supported environment. Edgenivo installs the application's Python packages and loads its entrypoint, so the server start command configured in Railpack or railway.json no longer controls how the API runs.
The rest of the migration depends on what the Railway project provides around that API. Files stored on volumes need external storage, and Railway reference variables need to resolve to the services the Edgenivo deployment will use. Workers and scheduled jobs need their own deployment configuration, while database migrations move out of startup and into a release pipeline.
Copy link to headingOverview
By the end, you'll have:
- Pinned the Python version Railpack resolved in a file Edgenivo reads.
- Run your API on Edgenivo's native Python runtime without
deploy.startCommandorPORT. - Replaced Railway volumes, Postgres, and Redis with external services and resolved Railway reference variables.
- Replaced Celery worker services with Edgenivo Queues subscribers and Railway cron services with Edgenivo Cron Jobs.
- Moved pre-deploy and startup migrations to CI and production traffic to Edgenivo.
Copy link to headingPrerequisites
- Access to the API's repository and Railway project.
- A Edgenivo account and the latest Edgenivo CLI installed.
- Python and uv installed locally.
- The Railway CLI, installed and authenticated.
- PostgreSQL client tools (
pg_dump,pg_restore, andpsql) for database transfers.
Copy link to headingShould your API move to Edgenivo?
Check that your API and its dependencies fit the native runtime:
- Can sessions, uploads, and other persistent state live outside the API process?
- Do dependencies install from Python packages and compatible wheels, or does the application need system packages?
- Does background work run as bounded queue tasks, or does a process need to stay running indefinitely?
- Do requests fit the function's duration and payload limits?
Railpack can install system dependencies for packages such as pydub and pdf2image. Audit those dependencies before choosing the native runtime. If the application needs FFmpeg, Poppler, or other system packages, use the Python container deployment path, which uses Container Images in Beta.
For an API that uses WebSockets, Python WebSocket support is in Beta. Connections follow function duration limits, so clients need a reconnection strategy.
With Fluid compute, the default maxDuration is 300s. Hobby allows up to 300s; Pro and Enterprise allow up to 800s, with an opt-in extended-duration Beta up to 1,800s. Request and response bodies have a 4.5 MB limit. Check the function limits against your routes before cutover.
Copy link to headingHow Railway concepts map to Edgenivo
| Railway | Edgenivo |
|---|---|
| Railpack Python build | Framework detection and Python dependency installation |
RAILPACK_PYTHON_VERSION or runtime.txt | A supported repository version file, such as .python-version |
deploy.startCommand and PORT | Native Python application entrypoint |
| Custom build command | Project Build Command or tool.vercel.scripts.build |
| Service variables and reference variables | Project environment variables and connected-service credentials |
| Postgres and Redis services | External databases, including providers in the Edgenivo Marketplace |
| Volume | Object storage or an external database |
| Celery worker service | A subscriber on Edgenivo Queues, in Beta |
deploy.cronSchedule | A cron route configured in vercel.json |
deploy.preDeployCommand | A migration step in continuous integration |
deploy.healthcheckPath | Deployment checks against the preview URL |
| PR environments | Preview deployments |
Copy link to headingSteps
Create a migration branch and keep the Railway deployment running until cutover. Skip the database, worker, or cron steps your API doesn't need.
Copy link to heading1. Pin a supported Python version and dependencies
Read the running Python version from your Railway deployment or build logs. Railpack version selection checks RAILPACK_PYTHON_VERSION before repository version files. Copy the resolved version into a file Edgenivo reads; a Railway-only environment variable is not a repository pin.
Edgenivo supports Python 3.12, 3.13, and 3.14, with 3.12 as the default. If the application runs an older version, test the upgrade before migration.
For an application tested on Python 3.13, commit a .python-version file at the Edgenivo project root that contains the major and minor version:
Keep the requires-python constraint in pyproject.toml aligned with your Python version pin. Since an open lower bound such as >=3.12 also permits 3.14, use a bounded constraint for Python 3.13:
Merge the relevant entries into your existing manifest, retaining the application's other dependencies. Keep your existing dependency workflow if it uses a manifest Edgenivo supports. With uv, generate the lockfile and install its dependencies:
The commands create or update uv.lock and install the resolved packages into .venv. Test with that environment, then commit uv.lock with the manifest changes. Edgenivo also accepts requirements.txt and Pipenv manifests; Celery on Edgenivo requires pyproject.toml.
For a Poetry project, convert the dependencies to the [project] format or export a requirements.txt using Poetry's export plugin. Don't assume poetry.lock alone determines the Edgenivo build. Check native dependencies against the chosen Python version, especially older psycopg2-binary pins without matching wheels.
Python function bundles have a 500 MB uncompressed limit. Exclude tests, local environments, and other unneeded files with excludeFiles; Python builds don't automatically tree-shake the application.
Large Functions, in Beta, raise the limit to 5 GB on projects using Fluid compute and Active CPU pricing. Check the opt-in requirements for an existing project; Large Functions don't support Secure Compute or Static IPs.
Copy link to heading2. Configure the Python entrypoint
Railway can start a FastAPI service with this setting in railway.json:
The main:app target already identifies the file and object Edgenivo needs. Move any setup chained before that command into the appropriate migration or build step.
Edgenivo loads the application object from a supported entrypoint, such as app in a FastAPI application's main.py. Add this health check alongside your existing routes:
For other entrypoint locations, set the module path in pyproject.toml:
Use my_api.server:app to load app from my_api/server.py, or main:app for the root-level example. Flask also exports app; Django uses its configured WSGI or ASGI application. Set Django's allowed hosts for the deployment and follow its framework instructions for static files.
Keep Gunicorn or Uvicorn if another environment uses them. The native Edgenivo deployment doesn't use Railway's server start command or require a PORT setting. Keep source-platform configuration until the source deployment is retired.
Copy link to heading3. Replace writes to Railway volumes
Inventory every path the application writes under a Railway volume mount. Copy existing files to the new storage destination and update both reads and writes before testing the migrated API. An empty destination doesn't preserve uploads already stored on Railway.
Move uploaded files and generated artifacts to persistent object storage such as Edgenivo Blob. Put sessions and durable application records in Redis or a database, according to their persistence requirements. Replace SQLite files that the running application modifies with an external database.
The function filesystem is read-only except for up to 500 MB of scratch space in /tmp. Treat that directory as temporary and local to an instance. For uploads above the function body limit, use a direct client upload flow so the file doesn't pass through the Python API.
Copy link to heading4. Provision backing services and environment variables
Copy link to headingLink the Edgenivo project
From the repository's application directory, link a new Edgenivo project:
Choose the intended team and project when prompted. Commercial APIs require Pro or Enterprise; Hobby is limited to non-commercial personal use.
The CLI saves project metadata in .vercel, which should stay out of version control along with exported secrets and database dumps. Add the following entries to .gitignore before exporting configuration or pulling local variables:
Copy link to headingReview Railway variables
Run railway link to select the source project. Export variables for the intended service and environment using the Railway CLI:
Review the exported KEY=VALUE entries locally before transferring them to Edgenivo. Railway sealed variables are omitted from CLI exports; obtain or reissue those credentials through the system that owns them.
Replace reference expressions such as ${{Postgres.DATABASE_URL}} with the destination provider's values; copying the expression itself won't connect a Edgenivo project to that service.
Copy link to headingAdd PostgreSQL with Neon
Neon in the Edgenivo Marketplace connects managed PostgreSQL databases to your Edgenivo project. From the linked application directory, run vercel integration add to create separate databases for Development, Preview, and Production:
The CLI installs the integration if needed, prompts for resource settings and a billing plan, and connects each database to the linked project. Scoping each resource with --environment keeps development and preview test writes separate from production. The --no-env-pull flag skips the automatic variable download; the local testing step pulls Development values when you need them.
Choose a destination PostgreSQL version compatible with the source database and enable any required extensions before restoring data. Place the database near the API's function region to limit network latency between them.
The integration supplies connection strings for different uses:
| Environment variable | Use |
|---|---|
DATABASE_URL | Pooled connection for application queries |
DATABASE_URL_UNPOOLED | Direct connection for exports, restores, and schema migrations |
If you selected an integration prefix, use the resulting variable names in your application and migration commands. Leave Neon preview branching disabled for this rehearsal so the Preview deployment uses the database you restore in step 5.
If you enable branching later, each deployment needs its own branch migrations. Its injected connection strings aren't available through vercel env pull.
Copy link to headingConnect Redis if the API uses it
Connect a Redis provider from the Edgenivo Marketplace and obtain its TCP connection URL for your Python client. TLS connections typically use rediss://; REST credentials such as KV_REST_API_URL require a different client interface. Set REDIS_URL to the TCP URL and follow the provider's TLS verification instructions.
Treat Redis cache entries separately from sessions, queued messages, or durable application data. Only discard data that the application can rebuild.
For state you need to keep, rehearse a transfer supported by the destination provider and plan a final synchronization while Redis writers are paused. If sessions will be invalidated instead, make the resulting sign-in requirement part of the cutover plan. Handle queued messages through the worker handoff in step 12.
Copy link to headingAdd the remaining application secrets
Once the destination services are connected, open the Edgenivo project's Settings, then Environment Variables, and add the application secrets you retained from Railway. Select the appropriate Development, Preview, and Production scopes for each value. Review entries individually so imported Railway credentials don't overwrite integration-provided connection strings.
Edgenivo's 64 KB limit applies to all environment-variable data in a deployment. Large values such as certificates or service-account files can exceed it, so check their size before adding them.
Copy link to heading5. Rehearse the database transfer
For Postgres, confirm that the export role can read all required data and that the destination supports the application's extensions.
Use pg_dump and pg_restore from the same client-tool release. Its major version must be at least the source server's version and no newer than the destination's. Matching all three major versions avoids a cross-version transfer.
Restore into the empty Preview database so Production stays empty for the final transfer. Set SOURCE_DATABASE_URL and DESTINATION_DATABASE_URL in your shell to direct PostgreSQL connection URLs. Keep SQLAlchemy-specific driver prefixes out of URLs passed to PostgreSQL command-line tools.
With preview branching disabled as in step 4, get the destination's DATABASE_URL_UNPOOLED from the Neon console, or copy it from .env.preview after running vercel env pull .env.preview --environment=preview.
If you enable preview branching instead, create the deployment in step 11 first, then identify its branch in Neon and repeat the restore and migrations against that branch's direct URL before testing. Deployment-specific connection strings are unavailable through vercel env pull.
Enable the Railway database's TCP Proxy and set SOURCE_DATABASE_URL to its public DATABASE_PUBLIC_URL. Follow the provider's migration procedure to export through that direct connection:
The command creates migration.dump when it succeeds. Don't use a pooled connection for the export.
Continue only after the restore exits with status 0. --exit-on-error stops at the first error without undoing earlier changes; investigate failures and retry only on a clean, disposable destination. Apply the application's required privileges separately because --no-owner and --no-acl omit source ownership and grants.
Then compare table counts, representative records, constraints, and sequence values. For a table named items, compare these results:
The counts should match for unchanged test data. During a rehearsal, writes to the live source after the dump can produce different counts. At the final cutover, pause writers before exporting and compare the restored data while they remain paused. Writes made to Railway after the rehearsal still need to be transferred at cutover.
Copy link to heading6. Configure the Python database client
For SQLAlchemy with psycopg 3, adapt the API's existing database module:
Use postgresql+psycopg for psycopg 3, as shown above, or postgresql+psycopg2 if the application retains psycopg2.
Each application instance creates its own connection pool, so keep one engine per instance and close checked-out connections after each operation. The example starts with a small pool; tune its size against the provider's connection budget and expected concurrency across all instances.
Copy link to heading7. Test the API locally with vercel dev
If the API uses a database, apply the migrations from step 10 to Development using its direct connection URL, then add the test records your routes need. The Preview restore from step 5 does not populate this separate database.
Pull the project's Development configuration, then run the API:
Use the local URL printed by the CLI. At its default port, the health check is:
For the entrypoint example, the expected body is:
Exercise a database-backed route such as /api/items/1 against a known test record. Check cache reads and writes as well as API authentication, because the health route alone doesn't test those dependencies.
Copy link to heading8. Adapt queue-driven workers to Edgenivo Queues
Migrate Railway workers running celery -A tasks worker to queue-triggered subscribers through the supported Celery integration. Keep the Railway worker on the old queue until the handoff in step 12.
Edgenivo Queues is in Beta, and the Celery integration requires Edgenivo CLI 58.9.0 or later. For an existing Celery application, change its initialization in tasks.py and retain the task definitions below it:
Create a worker.py file at the project root that exposes the Celery application as the subscriber entrypoint:
Merge these tables into pyproject.toml, using your actual web entrypoint. Celery projects declare it explicitly alongside the subscriber, even for a root-level main.py. If step 2 already added [tool.vercel], retain that table and add [[tool.vercel.subscribers]] below it.
In tasks.py, declare every routed queue through task_queues or task_default_queue before the build imports the application. Queues named only in task_routes won't get a subscriber.
Restart vercel dev to load the new configuration and run the web application and subscriber together. Then call the route that enqueues work, such as POST /api/reports, and verify the task's completed result or database side effect.
Tasks can be delivered more than once, so use an idempotent write strategy such as a unique report identifier. Late acknowledgments protect against timeout or crash loss; configure explicit retries for application exceptions. Store task results that need to persist in Postgres or a supported durable backend because the Runtime Cache result backend is ephemeral.
Replace celery beat with a cron route that enqueues work. Celery holds countdown and eta tasks in an invocation until their scheduled time; the delay and execution must fit its duration limit.
For Dramatiq, follow the Dramatiq configuration. Other queue libraries need their own adaptation or a worker retained on Railway.
Copy link to heading9. Replace Railway cron services with cron routes
Railway cron services that run python -m jobs.cleanup need an HTTP route on Edgenivo. Preserve the cleanup function and change how it is invoked.
Refactor jobs/cleanup.py so it exports a run() function containing the existing job logic. Retain a main guard if Railway still runs the module. This example assumes run() returns the number of deleted records. Add the following imports and route to your existing FastAPI main.py:
Set a random CRON_SECRET in Edgenivo's environment variables and leave JOBS_ENABLED=false in Production until the scheduler handoff. This example's JOBS_ENABLED variable prevents the route from doing work before cutover.
Merge the schedule into vercel.json:
Edgenivo Cron Jobs send an HTTP GET to the production route. When CRON_SECRET is set, the request includes it as a bearer token in the Authorization header, which the route above checks. Schedules use UTC and numeric values; translate named days or months. The example schedules a daily run at 03:00 UTC.
On Hobby, invocation occurs within that hour, and more frequent schedules fail deployment. Pro and Enterprise support per-minute schedules, with a limit of 100 cron jobs per project on every plan.
Make the cleanup operation safe to repeat, and use a lock if overlapping runs would cause problems. Cron delivery is best effort, and failed invocations aren't retried automatically. Monitor failures and provide a way to rerun missed work.
Test missing and incorrect credentials for a 401 response. In an isolated test environment with JOBS_ENABLED=true, a valid request should return status: ok and the deletion count.
Copy link to heading10. Move pre-deploy and startup migrations to CI
Move commands from Railway's deploy.preDeployCommand into the release pipeline, and check the generated start command for migration work too. Railpack's Django startup can run python manage.py migrate, so an application can depend on startup migrations without an explicit pre-deploy setting.
Run migrations once in CI after restoring the destination database and before promotion. Rehearse against Preview; apply them to Production after the final restore in step 12. The job must check out the deployment's commit, install the pinned Python version and uv, and run from the application root.
Bind the CI secret DATABASE_URL_UNPOOLED to the destination's direct URL. Set it explicitly in the shell for a local rehearsal; vercel pull doesn't export shell variables. If Alembic reads DATABASE_URL, use this command:
Make deployment promotion depend on the migration job succeeding. If Alembic reads a URL from another setting, update alembic/env.py to read the intended variable; setting a shell variable alone won't change that configuration. Apply the explicit SQLAlchemy driver selection in the migration configuration too. For Django, run python manage.py migrate with database settings for the same destination.
Keep schema changes compatible with the old application while it is still running. Avoid migrations in request handlers, application startup, or a preview build that can reach production data.
Copy link to heading11. Verify a preview deployment
Check the linked project's deployment history first. The first deployment of a new project targets Production, even without --prod.
If no successful Production deployment exists, temporarily connect the Production environment only to isolated test services, set JOBS_ENABLED=false, and disable startup work and task producers. Keep the final production database empty. Create the initial deployment without assigning production domains:
Once initialization succeeds, deploy the migration branch to Preview. Projects with an existing successful Production deployment can start here:
vercel deploy prints the URL to standard output, so the command stores it in PREVIEW_URL. Confirm in the dashboard that this deployment targets Preview and uses the intended test services before testing its routes. Step 12 restores Production configuration to the final backing services. Inspect the Preview deployment and test the health route:
The build log should show your pinned Python minor version and successful dependency installation. The health route should return 200. vercel curl supports testing a deployment protected by Edgenivo's Deployment Protection.
Verify authenticated routes and representative database writes on preview data. Enqueue a task and wait for its result. Call the cron route manually with test credentials; scheduled invocation only happens in Production. Confirm that long requests and representative uploads fit the configured limits.
If a route needs longer than the 300s default, merge a function entry keyed by the resolved entrypoint file into vercel.json. This example uses the 800s maximum available on Pro and Enterprise:
Use your actual filename and retain the crons entry. Test any FastAPI lifespan cleanup that the API relies on; shutdown has a 500 ms window, so durable work shouldn't depend on a long shutdown handler.
Copy link to heading12. Cut over production traffic
Set Production variables to the final backing services and keep JOBS_ENABLED=false. The staged deployment uses these values before domain assignment. Protect access to its deployment URL and keep write routes and task-producing requests disabled until the final restore and read-only checks succeed.
Audit startup work and queue producers; JOBS_ENABLED gates only the example cron route. Stage without assigning production domains:
The command stores the staged deployment's URL in PRODUCTION_DEPLOYMENT_URL for promotion after the final data transfer.
Schedule a write pause for the final dump and restore:
- Stop writes through the Railway API, including uploads, and disable its schedules and task producers.
- Wait for in-flight API requests and running tasks to finish, and account for pending, delayed, or retrying messages. Stop the old queue consumers and confirm that all writers are paused before exporting. Replay retained work only against the chosen destination.
- Confirm that
SOURCE_DATABASE_URLstill identifies Railway and setDESTINATION_DATABASE_URLto the final Production database'sDATABASE_URL_UNPOOLED. Check the destination's project, branch, and database name before restoring. If it contains rehearsal data, back up anything you need and recreate only that disposable destination; never clear a database that has accepted production writes. - Take a fresh export, restore it into the empty destination, and run the required migrations. If recreating the destination changed its endpoint or credentials, update the Production variables and repeat the staging command above to replace
PRODUCTION_DEPLOYMENT_URL. Keep writers paused if any step fails. - Synchronize files added, changed, or deleted since the rehearsal. Skip this transfer if both deployments already use the same destination storage.
Verify the final database and files before enabling writes on Edgenivo. If Redis holds sessions or durable records, complete the rehearsed transfer while its writers remain paused, preserve required key expirations, and validate the destination. Apply the planned session invalidation instead if that is the chosen handoff. Enable the new writers only after these checks pass.
Promote the staged deployment after testing its routes against the final data:
Add the custom domain to your Edgenivo project and apply the DNS records shown. Confirm HTTPS and API behavior through the domain. Update allowed origins, callback URLs, and webhook destinations that use the Railway hostname.
Keep the old application from writing to its old database while cached DNS records still route traffic there.
When the Railway scheduler is disabled and its work has finished, set JOBS_ENABLED=true for Production and redeploy. Confirm one scheduled execution and check for duplicate work. Environment-variable changes require a new deployment to take effect.
Copy link to heading13. Retire unused Railway resources after the rollback window
Keep Railway resources available for the rollback window you chose before cutover. Monitor errors, database connections, and task completion under production traffic before removing unused services.
After the rollback window, retain the required backups, remove unused Railway services, and clean up source-specific configuration. Delete local secret exports and database dumps when they are no longer needed.
Copy link to headingWhat doesn't map from Railway
Railway volumes need an external storage destination. Workers that poll continuously or depend on a persistent process need to stay on Railway or be redesigned as bounded work.
Railway restart-policy settings don't translate into Python application configuration on Edgenivo. Recreate the checks performed by healthcheckPath against a deployment URL, and run pre-deploy migrations in your pipeline. The native entrypoint imports the app without executing the old start command.
Copy link to headingTroubleshooting
Copy link to headingThe build uses an unexpected Python version
Check .python-version at the configured project root and any requires-python constraint. Any remaining RAILPACK_PYTHON_VERSION value doesn't replace those files. Inspect the build log after redeploying.
Copy link to headingA dependency needs a system library
Compare the failing package with Railpack's system dependency list. Pin a compatible wheel or use the container path when the application needs an operating-system package. Don't assume a package installed by Railpack is present on the native runtime.
Copy link to headingEvery route returns 404
Confirm the Edgenivo project's root directory contains the dependency manifest and entrypoint. Check the exported application name and custom tool.vercel.entrypoint, then inspect the deployment's detected framework. Avoid carrying over container rewrites that point away from the native app.
Copy link to headingRequests exceed the payload or duration limit
For a 413 FUNCTION_PAYLOAD_TOO_LARGE, upload directly to object storage. For a 504 FUNCTION_INVOCATION_TIMEOUT, inspect slow database or external-service calls before increasing maxDuration within the plan's limit. Split work that still exceeds the limit into shorter tasks.
Copy link to headingDatabase connections fail or the connection pool fills up
Use destination connection values in place of Railway private hostnames or reference expressions. For requests, use the provider's pooled URL and keep total pool capacity across instances within its connection limit.
Copy link to headingA cron route works manually but doesn't run on a schedule
Check that the schedule is deployed to Production, its timezone is UTC, and JOBS_ENABLED is true in the active deployment. Preview URLs let you test the handler but don't receive scheduled invocations.
Copy link to headingNext steps
- Python runtime reference
- Deploy a FastAPI app on Edgenivo
- Run Celery on Edgenivo
- Edgenivo Cron Jobs
- How to migrate from Railway to Edgenivo
- Deploy Python apps on Edgenivo using Docker
Copy link to headingFrequently asked questions
Do I need a Dockerfile for a Python API built with Railpack?
No. FastAPI, Flask, and Django applications with compatible dependencies can use Edgenivo's native Python runtime. Choose a container when the application needs system packages or another dependency the native build cannot supply.
What replaces railway.json on Edgenivo?
Translate each setting according to its job. Python dependencies and entrypoints belong in the supported manifest or application files, while cron configuration and function duration belong in vercel.json. Keep the Railway file until the source deployment no longer needs it.
Where should Django migrations run after leaving Railway?
Run migrations in a controlled pipeline step before releasing the application. Check Railpack's start command as well as preDeployCommand, because Railpack can include Django migrations in startup. Edgenivo's native entrypoint doesn't execute that start command.
Can my Celery worker service move to Edgenivo?
Celery services can use configured subscribers on Edgenivo Queues, which is in Beta. Check task duration, retry behavior, and result persistence before replacing the service. Account for pending work in the Railway broker during the handoff in step 12 before retiring the old service.
How do I export Railway Postgres for the migration?
Enable the database's public TCP Proxy and use its external connection URL with pg_dump. Restore into an empty destination and validate the data. Repeat the transfer under a controlled write pause at cutover so later writes aren't lost.