To move always-on Railway workers to Edgenivo Daemons, package each worker with a Dockerfile, declare it in vercel.json, and deploy to production. Start with zero instances, test shutdown behavior, and move processing gradually while Railway and Edgenivo use the same queue broker.
Consider an orders project on Railway. Its Next.js app adds jobs to Redis, and a BullMQ worker processes payments and sends receipts. Separate cron jobs clean up abandoned carts. The worker can move to a daemon while the Railway migration guide covers the web app, databases, and DNS.
Edgenivo builds the worker image and maintains the instance count you configure in each region. Daemons pull work through outbound connections and restart after the process exits, including a clean exit.
Copy link to headingPrerequisites
- Access to the Railway project and the Railway CLI, linked to it.
- Edgenivo project whose root directory is the repository root. Daemons don't support subfolder roots such as
apps/webduring the beta. - Permission to deploy to production, since that's the only environment that starts daemons.
- Edgenivo CLI 64.3.0 or later, linked to the intended Edgenivo project for the
vercel daemonscommands. - Docker, to build and test worker images locally.
- Team capacity within the Daemons quotas. Hobby allows 5 Active CPU-hours per month across the team; assess actual CPU usage and instance size rather than elapsed running time.
Copy link to headingShould your worker become a daemon?
Check these requirements before moving a Railway worker:
- Pulls its own work. Queue consumers and polling workers can fit this model. Move inbound HTTP handlers to a Edgenivo Function or Service; other inbound protocols need a host that supports them.
- Stays running. Daemons restart after any exit. Scheduled tasks that finish and exit belong in cron jobs.
- Tolerates ephemeral storage. Move durable files from Railway volumes to a database or Edgenivo Blob, and update the worker to use that storage.
- Builds for Linux
amd64. Supply aDockerfileorContainerfileand compatible dependencies. - Fits the available resources. During the beta, an instance can have up to 8 GiB of memory and 4 vCPUs. Add instances when your workload supports parallel processing.
For tasks that fit within Function duration limits, Edgenivo Queues can deliver messages to a Function. Daemons let you retain a continuously running worker and its queue library, with changes to networking, storage, and shutdown.
Copy link to headingHow Railway worker concepts map to Edgenivo
Most of a Railway worker's settings have a direct home in the daemon's entry in vercel.json. The rest move to project settings or to a Marketplace provider.
Both platforms must reach the same broker for the gradual worker handoff in this guide. Moving the broker is a separate data migration, covered in step 2.
| Railway | Edgenivo |
|---|---|
| Worker service with no public domain | A named entry under daemons in vercel.json |
| Railpack or Nixpacks build | A Dockerfile or Containerfile in the daemon's root (required) |
Same repo, different startCommand | command, which overrides the image's ENTRYPOINT and CMD |
| Replicas | instances, a count per region from 0 to 100 |
| Multi-region replicas | instances keyed by Edgenivo region ID, such as { "iad1": 2, "cdg1": 1 } |
| CPU and memory limits in service settings | resources.requests.memory: 1, 2, 4, or 8 GiB, with 1 to 4 vCPUs |
| Restart policy | Built in: Edgenivo restarts instances that exit or crash |
| Deployment draining time | terminationGracePeriodSeconds, 30 by default, up to 3,600 |
| Service and reference variables | Project environment variables in the Production scope |
Private networking at *.railway.internal | Connection strings from your providers, such as Marketplace Redis and Postgres |
| Redis, Postgres, or RabbitMQ services | Managed providers from the Marketplace or elsewhere |
| Volumes | Edgenivo Blob or a database |
| Cron service | A cron job that calls a route; an in-process scheduler needs protection against overlapping instances |
| PR environments for the worker | None during the beta; test the image locally with Docker |
| Service logs and metrics | Logs filtered by source:daemon, plus CPU and memory charts on the daemon's page |
| Scaling replicas from the dashboard | Scaling overrides in the dashboard or vercel daemons scale |
Daemons accept no inbound connections and run only in production deployments during the beta. Replace Railway private-network dependencies before starting a daemon, and test the image locally before its first production run.
Copy link to headingSteps
Keep the Railway worker available through validation and the rollback window. Coordinate production app, database, and broker changes separately; deploying with zero daemon instances postpones only the Edgenivo worker's start.
Copy link to heading1. Inventory the Railway project
List every service and decide where it lands before you change any code. For the orders project, the inventory looks like this:
| Railway service | What it does | Destination on Edgenivo |
|---|---|---|
web | Next.js app, public domain | Framework detection, per the Railway migration guide |
worker | BullMQ consumer, no public domain | A daemon |
cleanup | Cron service that runs and exits | A cron job |
Redis | Queue broker | A Redis provider from the Marketplace |
Postgres | Orders database | A Postgres provider from the Marketplace |
For each worker, write down its start command, replica count and regions, CPU and memory limits, the variables it reads, and any path it writes to on disk. Export its variables with the Railway CLI so you have them in one place:
Keep .env.railway out of version control and delete it when the migration ends. Railway omits sealed variables from this export, so collect those from the system that issued them.
Copy link to heading2. Prepare a shared broker and migrate its state
Use the Railway migration guide for the web app, Postgres, and volumes. Before moving the worker, make sure the production app and both worker deployments can reach the same broker. Railway's *.railway.internal hostnames resolve only within its private network.
If the broker must move, choose a provider that supports your queue library. BullMQ needs a Redis connection compatible with ioredis, including its blocking commands. Configure persistence for your durability requirements and use maxmemory-policy=noeviction so Redis does not evict queue data.
Complete the broker migration before the worker handoff. Pause producers and schedulers while you account for the old broker's work. Drain pending jobs with Railway workers or use a tested procedure to transfer queue state.
For a state transfer, stop the old consumers after their active jobs finish and before copying broker state. Include delayed jobs, scheduled retries, waiting dependencies, and recurring schedules; an empty waiting queue alone does not establish that the broker is ready to retire.
After checking that the destination contains the required state, connect the Railway worker and the production app to it. Resume producers and schedules, then confirm jobs complete. Keep the old broker available until you have reconciled its remaining work and tested the rollback plan.
Copy link to heading3. Give each worker a Dockerfile
Daemons build from a Dockerfile or Containerfile. For a worker previously built with Railpack, add a Dockerfile alongside its code and dependency files:
Keep bullmq and ioredis in the worker's production dependencies and commit its lockfile. The JavaScript example below uses ES modules; set "type": "module" in worker/package.json. It assumes orders.js exports your existing processOrder function.
Use this worker/Dockerfile:
Use worker/.dockerignore to exclude local dependencies and environment files from the build context:
With the daemon's root set to worker/, the image can copy only files from that directory. If your Railway worker shares the web app's codebase and uses a different start command, choose one of these layouts:
- Move the worker's dependencies into
worker/, or into a shared package it installs. - Build from the repository root with
Dockerfile.worker,root: "./", andentrypoint: "Dockerfile.worker". Use the suffixed filename so Edgenivo doesn't treat it as the web app's Dockerfile. Put the.dockerignoreat the repository root for this layout, including the exclusions for.env.railwayand.env.docker.
Python workers that share a backend image keep sharing it. Point the daemon at the backend's Dockerfile and override the start command, as step 5 shows.
Copy link to heading4. Make the worker stop cleanly
Edgenivo sends SIGTERM before replacing or removing an instance and waits 30 seconds by default before force-stopping it. In BullMQ, worker.close() stops fetching jobs and waits for active processors to finish or fail. It has no timeout of its own, so allow enough termination time for the worker and its connections to close.
Adapt worker/worker.js to your existing handler:
If an instance stops before BullMQ records completion, another worker can process the stalled job again. Make processOrder safe to repeat after partial progress. Use the payment provider's idempotency mechanism for charges and persist enough order state to avoid repeating completed actions.
BullMQ's jobId prevents adding a duplicate while that ID remains in the queue; it does not make the handler's side effects idempotent.
Keep message formats and database changes compatible with both worker versions during rollout and rollback. For other queue libraries, check when messages are acknowledged and what happens after forced termination. Celery, for example, acknowledges tasks before execution by default; its retry and acknowledgment settings need a separate review.
Copy link to heading5. Declare the daemon in vercel.json
Add a top-level daemons key next to anything already in vercel.json. Start with zero instances; step 10 raises the count when you're ready to hand off the queue:
Translate the Railway settings you recorded in step 1:
- Replicas become
instances. Two Railway replicas in one region become{ "iad1": 2 }. Choose the Edgenivo region closest to your broker and database, since every job makes round trips to both. - Resource limits become
resources.requests.memory. Pick 1, 2, 4, or 8 GiB; Edgenivo assigns 1, 1, 2, or 4 vCPUs to match. - Draining time becomes
terminationGracePeriodSeconds. Set it a little above your longest job, up to 3,600 seconds. - The name must be lowercase letters with separators.
orders-workerandemail_senderwork;worker1andWorkerfail the build.
For a Celery worker that shares a backend image, use this alternative vercel.json entry. Its command replaces the image's ENTRYPOINT and CMD. Keep the count at zero until handoff, and substitute celery for orders-worker in later CLI commands:
Edgenivo doesn't run command through a shell, so $VARIABLES stay literal. Wrap the command in ["sh", "-c", "exec ..."] when you need expansion.
Copy link to heading6. Move the worker's environment variables
Daemon instances receive the project's environment variables. Use .env.railway as an inventory, then add the application variables the worker needs in the Production scope. Replace old broker and database URLs with the destination values from step 2, and omit Railway-provided variables. Check existing project values before changing variables shared with the web app:
Railway reference variables such as ${{Redis.REDIS_URL}} don't resolve outside Railway. Replace each with the real value from the destination provider; Marketplace integrations add theirs to the project for you.
Edgenivo also supplies project variables as image build arguments. Keep runtime credentials out of Dockerfile ARG and ENV instructions because those values can persist in image metadata or layers. If the build needs private-package credentials, verify a supported secret-delivery method before deploying that image.
For providers that support OIDC, replace long-lived cloud keys with federation. Each instance's VERCEL_OIDC_TOKEN_FILE holds the path to its short-lived token.
Copy link to heading7. Recreate cron services and schedulers
Railway cron services start on a schedule and exit after completing their task. Move suitable tasks to a Edgenivo cron job that calls a protected route within Function limits. Longer tasks can be enqueued by that route for a daemon to process. Disable the Railway schedule before enabling its replacement so both platforms don't trigger the same work.
BullMQ repeatable jobs and Job Schedulers remain with their Redis queue state. When both deployments use that same state, either worker can process the scheduled jobs. If you moved Redis in step 2, verify that recurring schedules and delayed jobs moved with it before enabling them on the destination.
Celery beat and node-cron need a separate scheduler plan. Even with one configured daemon instance, old and new processes can overlap during a deployment. Use coordination that allows only one scheduler to enqueue each occurrence, or move the schedule to a cron route. Job handlers still need protection against repeated execution.
Celery beat's default scheduler stores last-run times in the local celerybeat-schedule file. Store that state durably before moving beat to an environment with ephemeral storage. Database-backed storage preserves it across instance replacement, but does not elect a single active scheduler.
Copy link to heading8. Test the worker locally with Docker
Daemons don't run in preview deployments or vercel dev during the beta. Test the image locally using a development broker and test credentials. Create .env.docker at the project root with one unquoted KEY=value per line, and exclude it from version control and Docker build contexts:
From the project root, build and run the Linux amd64 image:
Enqueue a test job against the development broker. While it is running, open another terminal and stop the container with the configured grace period:
Check for SIGTERM received in the logs, verify that BullMQ records the job as completed, and confirm the container exits before the timeout. In a separate local test, interrupt a job before completion and verify stalled-job recovery. Repeat the same logical job to check that payments and other side effects are not duplicated.
Copy link to heading9. Deploy to production with zero instances
Check that every daemon entry has zero instances and no existing scaling override raises its count. Then deploy the project to build and register the images. This is a production app deployment too, so review any web-app changes included in it:
Confirm Edgenivo knows about the daemon:
If the image build fails, use the troubleshooting section. Confirm that desired and running daemon counts are zero while the Railway worker processes jobs.
Copy link to heading10. Hand off the queue
For the BullMQ example, keep both deployments on the same Redis database, queue name, and BullMQ prefix. Use compatible worker versions and payload formats, and check that their combined concurrency stays within the broker's and downstream services' limits.
Start one Edgenivo instance without redeploying by applying a scaling override:
Watch its logs until it has completed real jobs:
To follow only this daemon, filter your project's Logs page by daemon:orders-worker.
Increase Edgenivo capacity as you reduce Railway replicas. Compare completion rate, queue age, failures, and resource use at each change; equal replica counts can provide different throughput.
Allow Railway workers to finish active jobs, then stop their deployment after Edgenivo is handling the load. Disable automatic Railway redeployments during the rollback window so a push cannot restart the old workers.
Set instances in vercel.json to the count you tested, deploy to production, and clear the override so later deployments use the file:
Check the resulting instance count. If the two platforms still use different brokers, stop here and complete the state migration in step 2 before retiring the Railway worker. Scaling workers does not transfer jobs between brokers.
Copy link to heading11. Verify under real load
Observe the daemon through a representative workload cycle, including scheduled tasks and peak traffic:
- Instances. Use
vercel daemons instances ls orders-workerto compare running instances with the intended count. Investigate unexpected restarts using the logs and memory charts. - Job processing. Compare completion rate, queue age, and failed or stalled jobs with the Railway baseline. Check broker connectivity and downstream errors or rate limits before adding concurrency.
- Resources. Review CPU, memory, running instances, and data transfer on the daemon's dashboard page. Investigate sustained memory growth and memory-related exits before increasing the instance size.
Restart one instance while processing a controlled test job to check graceful replacement. Replace your_instance_id with an ID from the instance list:
Verify the job's recorded result and side effects. This checks a managed restart; use the local forced-stop test from step 8 to check interrupted-job recovery.
Copy link to heading12. Keep a rollback path, then retire Railway
Keep the Railway worker's code and settings available for the agreed rollback window, with automatic deployments disabled. To return processing to Railway, restore a compatible worker against the same broker and verify that it can complete jobs. Then scale the Edgenivo daemon to zero in every active region and confirm its instances have stopped. For the single-region example, use vercel daemons scale orders-worker iad1=0.
If the Edgenivo worker is causing harmful side effects, stop it first and allow jobs to wait while you restore Railway. In either sequence, confirm the old worker still understands the current job payloads and database schema. Jobs remain in the shared broker; a rollback to a different broker requires its own state migration.
Instant Rollback restores the earlier deployment's daemon definitions, images, and configured instance counts with the app. Existing scaling overrides remain in force, so inspect them before relying on the restored configuration.
After the rollback window and queue-state checks pass, delete the Railway worker service and remove .env.railway and .env.docker from your machine. Keep broker and database resources that other services still use.
Copy link to headingWhat doesn't map from Railway
Plan for these Daemons beta limits:
- Preview environments. Railway PR environments can run a copy of the worker; Edgenivo previews don't start daemons. Test images locally with Docker.
- Inbound traffic. Daemons cannot be reached over HTTP or TCP, a service binding, or a rewrite. If your Railway worker also served a health check, a Prometheus
/metricsendpoint, or a queue dashboard such as Bull Board, move the dashboard to your app and push metrics instead of scraping them. - Autoscaling. Instance counts are fixed per region. Change them in
vercel.json, the dashboard, or the CLI. - Persistent disk. Instance storage is ephemeral. Volumes move to Edgenivo Blob or a database.
- Subfolder root directories. Monorepos whose Edgenivo project root is a subfolder, such as
apps/web, can't add daemons yet. - Non-
amd64images. Edgenivo builds for Linuxamd64. Binaries you copy into the image must match.
During the beta, each team also has default quotas:
| Plan | Instance sizes | Quota |
|---|---|---|
| Hobby | 1 or 2 GiB, 1 vCPU | 5 Active CPU-hours per month across the team |
| Pro | All sizes | 16 vCPUs and 32 GiB of memory running at once across the team |
| Enterprise | All sizes | Custom |
Each project environment runs at most 50 daemon instances across all its daemons and regions. On Pro and Enterprise, daemons are billed for Active CPU and Provisioned Memory; see Pricing for rates.
Copy link to headingTroubleshooting
Copy link to headingThe worker logs ENOTFOUND redis.railway.internal
Check the Production environment variables for *.railway.internal hostnames. Replace them with the destination provider's reachable connection string, including its required TLS settings, and redeploy.
Copy link to headingInstances restart over and over
Read the logs before each restart and check memory usage. Look for missing variables, startup errors, dependency failures, or memory exhaustion. If the process exits successfully, confirm whether it is intended to stay running. Scheduled tasks that complete and exit belong in a cron job; a worker may instead be exiting because its polling loop stopped.
Copy link to headingChanging instances in vercel.json does nothing
Check for a scaling override, which persists across deployments. Clear it with vercel daemons scale orders-worker --reset, replacing orders-worker with your daemon's name, or select Clear scaling overrides in the dashboard.
Copy link to headingJobs run twice after a deploy
Check whether the same job was retried after losing its lock or stopping before completion, or whether two schedulers enqueued separate jobs. Make side effects idempotent and prevent overlapping schedulers from producing the same occurrence. If active jobs exceed the termination grace period, increase it within the supported limit or break the work into smaller jobs.
Copy link to headingThe daemon didn't start on a preview deployment
That's expected during the beta. Only production deployments start daemons.
Copy link to headingThe build fails
Check the build logs for these configuration problems:
- No command to run. Add
CMDorENTRYPOINTto the Dockerfile, or setcommand. - Colliding names. Repeated hyphens collapse when Edgenivo names the image repository, so
a-banda--bclash. Rename one. - Output directory equals the daemon's
root. Keep worker code in its own directory, such asworker/.
Copy link to headingFrequently asked questions
Copy link to headingDo I still need the main Railway migration guide?
Yes, for everything around the worker. How to migrate from Railway to Edgenivo covers the web app, databases, volumes, and DNS. It points continuous workers to Daemons; this guide walks through that move step by step.
Copy link to headingShould my Celery worker be a daemon or a Edgenivo Queues subscriber?
Use a daemon when your Celery deployment needs a continuously running worker and an external broker. Review its acknowledgment behavior, shutdown time, and any beat scheduler before migrating. Celery on Edgenivo Queues runs tasks as Functions and supports preview deployments; choose it when your tasks and configuration fit that integration's limits.
Copy link to headingCan a daemon call my app or API?
Yes. Daemons can make outbound connections to your app's production URL, external APIs, and databases, subject to those services' authentication and network rules. Daemons do not expose an inbound HTTP or TCP endpoint.
Copy link to headingHow many workers can one project run?
You can define up to 100 daemons per project and configure up to 100 instances per daemon in a region. The project-environment limit of 50 running instances and your team's quota constrain the total you can run.
Copy link to headingDoes a daemon waiting for work cost anything?
Hobby's monthly quota measures Active CPU usage. Waiting for work does not consume CPU at the same rate as processing jobs, but measure your worker's usage against that quota. On Pro and Enterprise, billing includes Active CPU and Provisioned Memory, so memory is billed while an instance runs even when its queue is empty.
Copy link to headingRelated resources
- The Complete Guide to Edgenivo Daemons for a first daemon from scratch
- How to migrate from Railway to Edgenivo for the web app, data, and DNS
- Migrate a Python API from Railway to Edgenivo for FastAPI, Flask, Django, and cron routes
- How to choose between Functions, containers, and Daemons on Edgenivo
- Edgenivo Queues poll mode, for a broker that lives on Edgenivo
- Edgenivo Daemons is now in public beta on the changelog