Skip to content
Docs

Migrate Fly.io worker processes to Edgenivo Daemons

Migrate Fly.io workers to Edgenivo Daemons with your existing Dockerfile. Map process settings, preserve queue state, and plan cutover and rollback.

Ben SabicContent Engineer

To migrate a Fly.io worker process group to Edgenivo Daemons, reuse its Dockerfile, include any required image entrypoint in the daemon command, and connect it to a queue both platforms can reach. Deploy with zero instances, test the worker, then transfer processing from Fly Machines to Edgenivo.

The Rails example below has three process groups in fly.toml: web serves requests, worker runs Sidekiq, and cron runs Supercronic. The workers share application code with the web group, but their commands, shutdown behavior, and queue connections need separate configuration.

Edgenivo Daemons is in Public Beta on all plans.

Copy link to headingPrerequisites

  • Access to the Fly.io app and flyctl, signed in to its organization.
  • Edgenivo project with the repository as its root directory. Subfolder roots such as apps/web aren't supported during the beta.
  • Permission to deploy to production, the only environment that starts daemons.
  • Edgenivo CLI 64.3.0 or later for the vercel daemons commands.
  • Docker and separate Redis, database, and application credentials for local testing.
  • Enough daemon quota for the workload. Hobby allows 5 Active CPU-hours per month; reaching that quota stops the team's daemons until the next month or an upgrade.

Run CLI commands from the repository root containing fly.toml and vercel.json. Use vercel link to connect that directory to the intended Edgenivo project before adding variables or deploying.

Copy link to headingShould your process group become a daemon?

Check these requirements before moving a process group:

  • Does it pull its own work? Queue consumers such as Sidekiq, Celery, and BullMQ fit this model. Check for direct connections over Fly's private network as well as [http_service] and [[services]]. HTTP handlers need a Edgenivo Function or Service; raw TCP listeners need a host that supports inbound TCP.
  • Is it meant to keep running? Edgenivo restarts an instance even after exit code 0. Fly's default on-failure policy leaves a cleanly exited Machine stopped. Run-once tasks need a separate trigger, such as a cron job calling a Function.
  • Can it lose its local disk? Daemon storage is ephemeral. Move persistent data from Fly Volumes to a database or Edgenivo Blob, and update code that expects a mounted filesystem.
  • Does the image build for Linux amd64? Test that architecture locally before deploying.
  • Does each job fit within 8 GiB and 4 vCPUs? This is the largest daemon size during the beta. More instances help only when the work can be divided among them.

If you prefer Function-based processing, Edgenivo Queues and Workflows provide alternatives that require adapting how jobs run. Daemons let you retain the worker process while updating its platform-specific configuration.

Copy link to headingHow fly.toml worker settings map to Edgenivo

Each worker process group becomes a daemons entry in vercel.json. Reuse its Dockerfile and translate the settings below. Both platforms need access to the same queue before Edgenivo workers begin processing the backlog.

Fly.ioEdgenivo
Process group that needs no inbound trafficNamed entry under daemons
The group's command, which replaces CMD but keeps ENTRYPOINTcommand, which replaces both ENTRYPOINT and CMD
[build] dockerfileentrypoint, the Dockerfile path relative to the daemon's root
fly scale count worker=2 --region iadinstances: { "iad1": 2 }, a count per Edgenivo region
[[vm]] with processes = ["worker"]resources.requests.memory: 1, 2, 4, or 8 GiB, with 1 to 4 vCPUs
kill_signal, SIGINT by defaultAlways SIGTERM
kill_timeout, 5 seconds by default, up to 300terminationGracePeriodSeconds, 30 by default, up to 3,600
[[restart]] policy, on-failure by defaultAlways restarts instances that exit or crash
fly secrets set and [env]Project environment variables in the Production scope
.internal and .flycast addresses, Upstash Redis and Managed Postgres on the private networkEndpoints reachable over the internet, such as Marketplace Redis and Postgres
Fly Volumes in [mounts]Edgenivo Blob or a database
Supercronic process groupOne configured daemon instance with protection against duplicate schedules, or cron jobs
Scheduled Machines from fly machine run --scheduleCron jobs that call a route
release_commandA CI step before production promotion
fly logsLogs filtered by daemon:<name>
fly scale count from the CLIvercel daemons scale

Because daemons run only in production, deploy with zero instances so Edgenivo can build and register the daemon before it processes jobs.

Copy link to headingSteps

Keep the Fly worker group available until the handoff. Treat the queue and database changes in step 2 as separate production migrations, with their own maintenance window and rollback plan.

Copy link to heading1. Inventory the process groups

Start from fly.toml. The Rails app in this guide looks like this:

fly.toml
app = "orders"
primary_region = "iad"
[processes]
web = "./bin/rails server -b [::] -p 8080"
worker = "bundle exec sidekiq"
cron = "supercronic /rails/config/crontab"
[http_service]
internal_port = 8080
processes = ["web"]
[[vm]]
size = "shared-cpu-1x"
memory = "1gb"
processes = ["web", "cron"]
[[vm]]
size = "performance-1x"
processes = ["worker"]

Check every group's inbound connections, running Machine count, and regions:

fly scale show
fly machine list

Estimate worker capacity from the started Machine count. By default, Fly creates one running Machine and one stopped standby for a group without services. Groups with mounted volumes receive one Machine instead. Record standby IDs so you can verify they remain stopped during cutover.

Process groupWhat it doesDestination on Edgenivo
webRails behind [http_service]Container Function
workerSidekiq, no serviceDaemon
cronSupercronic, no serviceDaemon with duplicate protection, or cron jobs

Copy link to heading2. Prepare shared data and queue access

Before moving the web group, check that Fly workers can reach the same Redis and database. The Fly.io migration guide covers the web group, database, and volumes. Keep connection settings and job formats compatible across platforms.

Fly's .internal and .flycast endpoints require access to its private network. Redis provisioned through Fly and Managed Postgres use private connectivity too. This migration uses authenticated endpoints reachable from both platforms, such as databases from the Edgenivo Marketplace. If your existing provider already supplies a suitable endpoint, you can keep the same database.

If Redis must move, complete that migration before the worker handoff:

  1. Pause every producer, including web requests that enqueue jobs and recurring schedulers. Let active jobs finish, then stop the consumers before copying queue data.
  2. Transfer the remaining queue state with a migration method supported by both Redis providers. Include delayed jobs and retries, retained failed jobs, and any scheduler or uniqueness data your application uses. An empty ready queue does not mean Redis has no pending work.
  3. Confirm that the destination preserves this state, then update every producer and consumer to use it before resuming them. If the providers cannot transfer the required state, keep the old queue available under a separately tested drain plan.

For the Fly app, store the new REDIS_URL=... value in .env.redis-migration, excluded through both .gitignore and .dockerignore, with file permissions restricted to your user. Stage it only when you're ready to switch:

fly secrets import --stage < .env.redis-migration
fly secrets deploy

Staging defers the deployment, but any Machine started or updated afterward receives the new value. Keep producers paused through the deployment and verify each group's Redis connection before resuming work. Delete the temporary secret file when the change is complete.

Configure Sidekiq's Redis as a persistent store with maxmemory-policy set to noeviction. Account for Sidekiq's polling when choosing a plan with per-command billing. Confirm that new jobs complete and scheduled jobs and retries remain available on the destination before proceeding.

Copy link to heading3. Point the daemon at your existing Dockerfile

Reuse the Dockerfile that contains the application and worker dependencies. Keep that file for Fly and the daemons, and give the web Function its own copy:

orders/
├── vercel.json
├── Dockerfile # daemons and fly deploy build from this
├── Dockerfile.vercel # copy for the web Function
├── fly.toml
├── app/
└── config/

Keep the files in sync. Dockerfile remains available for Fly during the rollback window; Dockerfile.vercel serves the web Function. Set PORT to 8080 for this Rails server, as described in the web migration guide.

Use Dockerfile for the daemon so the root-level Dockerfile.vercel remains available for web-app detection. In this example, set root to ./ and entrypoint to Dockerfile. The Dockerfile's directory becomes the build context. Files beneath a daemon's root aren't published as static assets, and the app's output directory must be separate.

Check Fly-specific build arguments and dependencies before reuse. Even if the image deploys on Fly, verify that Edgenivo can rebuild it for Linux amd64.

Copy link to heading4. Keep the image's entrypoint script

Fly's process-group command replaces CMD but preserves ENTRYPOINT. Edgenivo's command replaces both, so include any required entrypoint setup.

Build the image and inspect its entrypoint:

docker build --platform linux/amd64 -t orders .
docker inspect --format '{{json .Config.Entrypoint}}' orders

If the entrypoint is ["/rails/bin/docker-entrypoint"], use this daemon command:

vercel.json
{
"command": ["/rails/bin/docker-entrypoint", "bundle", "exec", "sidekiq"]
}

Retain any additional entrypoint arguments. Edgenivo doesn't insert a shell, so invoke one explicitly for shell expansion or pipelines.

Read the script before reuse: Rails-generated scripts commonly prepare the database only for server commands, while custom scripts may run migrations for every process. Move unconditional migrations to the CI release step, and use exec to pass control to Sidekiq so it receives shutdown signals.

Copy link to heading5. Switch the shutdown signal to SIGTERM

Fly and Edgenivo stop a process in different orders:

  1. Fly sends kill_signal, which defaults to SIGINT, waits kill_timeout (5 seconds by default), then sends SIGTERM.
  2. Edgenivo sends SIGTERM first, waits terminationGracePeriodSeconds (30 seconds by default), then force-stops the instance.

If a custom worker handles only SIGINT, add a SIGTERM handler before moving it.

On SIGTERM, Sidekiq stops fetching jobs, waits up to its -t timeout for active jobs, then tries to requeue unfinished jobs. Allow at least five more seconds for shutdown: with -t 25, set terminationGracePeriodSeconds to 30 or more.

SettingOn Fly.ioOn Edgenivo
Signalkill_signal = "SIGINT"SIGTERM
Sidekiq timeout-t 25-t 25
Wait before a forced stopkill_timeout = 30terminationGracePeriodSeconds: 30

Test signal handling with Docker in step 9. Entrypoint scripts must pass signals to the worker, usually by ending with exec "$@".

Jobs must tolerate retries without repeating an external effect such as charging a card. Sidekiq doesn't guarantee exactly-once execution, and a forced stop can lose active jobs with its default fetch strategy. Keep job payloads and database migrations compatible with the previous release because old and new instances overlap during a Edgenivo rollout.

Copy link to heading6. Declare the daemon in vercel.json

Add a daemons key next to the rest of your configuration. Start at zero instances; step 11 raises the count during the handoff:

vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"daemons": {
"sidekiq": {
"root": "./",
"entrypoint": "Dockerfile",
"command": ["/rails/bin/docker-entrypoint", "bundle", "exec", "sidekiq", "-t", "25"],
"instances": { "iad1": 0 },
"resources": { "requests": { "memory": "2Gi" } },
"terminationGracePeriodSeconds": 30
}
}
}

Choose the region based on Redis and database location, then check latency during handoff. The table lists candidate Edgenivo regions:

Fly.io regionCandidate Edgenivo region
iad Ashburniad1 Washington, D.C.
ewr Secaucusiad1 Washington, D.C.
ord Chicagocle1 Cleveland
dfw Dallascle1 or iad1
yyz Torontocle1 Cleveland or yul1 Montréal
sjc San Josesfo1 San Francisco
lax Los Angelessfo1 San Francisco
lhr Londonlhr1 London
cdg Pariscdg1 Paris
ams Amsterdamfra1 Frankfurt or lhr1 London
fra Frankfurtfra1 Frankfurt
arn Stockholmarn1 Stockholm
jnb Johannesburgcpt1 Cape Town
nrt Tokyohnd1 Tokyo
sin Singaporesin1 Singapore
syd Sydneysyd1 Sydney
gru São Paulogru1 São Paulo

Choose the instance size by measured memory use; Edgenivo allocates CPU with that size. Daemon names use lowercase letters with supported separators: sidekiq and celery-beat are valid, but worker2 isn't.

Fly.io [[vm]]Daemon resources.requests.memory
shared-cpu-1x with memory = "1gb" (the size defaults to 256 MB)"1Gi", 1 vCPU (smallest daemon size)
performance-1x, 2 GB"2Gi", 1 vCPU
performance-2x, 4 GB"4Gi", 2 vCPUs
performance-4x, 8 GB"8Gi", 4 vCPUs

Fly's shared CPUs run at a small baseline with bursts, so a shared-CPU worker isn't directly comparable to a daemon vCPU. Size by the memory the worker uses, then check the CPU chart after the handoff.

Copy link to heading7. Rebuild secrets as environment variables

Use fly secrets list to inventory names and digests, then retrieve values from your secret manager or the service that issued them. Fly doesn't expose plaintext values through its secrets API. Avoid printing credentials from a running Machine into terminal output; command-history cleanup doesn't erase that output.

Review [env] in fly.toml alongside the secrets. Replace private hostnames and Fly-specific values before adding them to Edgenivo's Production environment:

fly secrets list
vercel env add RAILS_MASTER_KEY production --sensitive
vercel env add REDIS_URL production --sensitive

Add the database connection and any other settings the worker needs. Daemons use the project's environment variables, so the web Function and workers must agree on shared services. Replace uses of FLY_REGION with VERCEL_REGION and update any comparisons against Fly's region codes. If the app needs a primary-region setting, configure one explicitly with the intended Edgenivo region.

Environment variables are also available to daemon builds as build arguments. Keep secrets out of Dockerfile ARG instructions and image layers. For cloud authentication, use the short-lived OIDC token available through VERCEL_OIDC_TOKEN_FILE where the provider supports it.

Copy link to heading8. Move schedulers and release_command

Supercronic can run as a daemon with one configured instance in one region. Add cron alongside sidekiq in the existing daemons object. The excerpt below shows the new entry at zero instances while the Fly scheduler remains active:

vercel.json
{
"daemons": {
"cron": {
"root": "./",
"entrypoint": "Dockerfile",
"command": ["/rails/bin/docker-entrypoint", "supercronic", "/rails/config/crontab"],
"instances": { "iad1": 0 },
"resources": { "requests": { "memory": "1Gi" } }
}
}
}

Edgenivo briefly runs old and new instances during deployments, even when the configured count is one. Protect each scheduled occurrence against duplicate effects, for example with a database uniqueness constraint keyed by the schedule and intended run time. Test this protection before enabling the daemon. Give Supercronic enough shutdown time for any jobs it runs directly, or have it enqueue work for Sidekiq.

Other scheduling mechanisms need different handling:

  • Scheduled Machines created with fly machine run --schedule run on a schedule and exit. Move suitable tasks to cron jobs that call protected Function routes, subject to the Function's runtime limits.
  • Sidekiq-Cron coordinates scheduling across processes that share Redis. Preserve its job names and namespaces when moving it. Check other scheduler libraries individually; Redis coordination isn't a property of every scheduler embedded in Sidekiq.

During the initial handoff, stop Fly's standalone scheduler before starting its Edgenivo replacement. Check for missed schedule occurrences during that gap and reconcile them using the same duplicate protection.

Move Fly's release_command to a CI step that must pass before production promotion. Keep migrations compatible with workers running during rollout.

Copy link to heading9. Test the worker locally with Docker

Run the production image against nonproduction Redis and a nonproduction database, with test credentials for any external services. Docker tests the process locally; production networking still needs verification during the handoff. Daemons don't run in preview deployments or vercel dev during the beta.

Create .env.docker with one unquoted KEY=value per line. Exclude it through both .gitignore and .dockerignore. Replace the example endpoints and add any test-only Rails credential key the app requires:

.env.docker
REDIS_URL=rediss://default:[email protected]:6379
DATABASE_URL=postgresql://dev-user:[email protected]:5432/orders_test
RAILS_ENV=production
SECRET_KEY_BASE=development-only-value

Run the daemon's command with those values:

docker run --rm --platform linux/amd64 --name sidekiq --env-file .env.docker \
--entrypoint /rails/bin/docker-entrypoint orders bundle exec sidekiq -t 25

In another terminal, open a console inside the same container and enqueue a known test job:

docker exec -it sidekiq ./bin/rails console

Confirm that it completes against the test services. Then start a job that will still be running when you exercise shutdown:

docker stop --signal SIGTERM --timeout 30 sidekiq

The explicit signal overrides the image's STOPSIGNAL; the timeout enforces the 30-second shutdown window. Check logs and job state to confirm that the active job finished or was requeued before the container stopped. Restart the container and verify that retries complete without repeating external effects.

Copy link to heading10. Deploy to production with zero instances

The first production deploy proves the image builds and registers the daemon. With zero instances, the command itself first runs in step 11:

vercel --prod
vercel daemons ls
vercel daemons inspect sidekiq

If the build fails here, the troubleshooting section covers the common causes. The Fly worker group is still processing every job.

Copy link to heading11. Hand off the queue

Both platforms can consume the shared Redis queue while you move capacity. Keep them on compatible application versions and queue settings. Sidekiq supports multiple consumers, but retries can still execute a job more than once.

Start one Edgenivo instance without a deployment:

vercel daemons scale sidekiq iad1=1
vercel logs --query 'source:daemon'

Filter the project's Logs page by daemon:sidekiq. Confirm successful Redis and database connections, then check that representative jobs complete.

Increase Edgenivo's capacity while watching queue latency and downstream connection limits. The example uses two instances. Stop Fly workers one at a time, allowing Sidekiq its full shutdown window. Replace WORKER_MACHINE_ID with an ID from the inventory:

vercel daemons scale sidekiq iad1=2
fly machine stop WORKER_MACHINE_ID --signal SIGTERM --timeout 30

Check job outcomes and verify that every Fly worker Machine, including any standby, is stopped:

fly machine list

For Supercronic, stop every Fly cron Machine and confirm shutdown before enabling one Edgenivo instance:

vercel daemons scale cron iad1=1

Check the expected schedule occurrences and duplicate protection from step 8. Once processing is stable, record both final counts in vercel.json and deploy. Clear the overrides after that deployment so future scaling follows the file:

vercel daemons scale sidekiq --reset
vercel daemons scale cron --reset

Omit the cron commands if the app has no standalone scheduler.

Copy link to heading12. Verify under real load

Watch the daemon for at least one full business cycle:

  • Instances. Run vercel daemons instances ls sidekiq and inspect logs for repeated restarts. Check startup failures, missing configuration, and memory pressure.
  • Throughput. Compare queue latency and completion rates with the Fly baseline. Before increasing instances or concurrency, check Redis latency and database connection limits.
  • Resources. Review CPU and memory on the daemon's dashboard page. Adjust the instance size or concurrency to the constraint you observe.

Check a controlled restart with a job whose outcome you can verify. Replace INSTANCE_ID with an ID from the instance list:

vercel daemons instances restart INSTANCE_ID

Confirm that the job finishes or returns to the queue and completes after restart. This checks graceful shutdown; crash recovery depends on the Sidekiq fetch strategy and must be tested separately with nonproduction jobs.

Copy link to heading13. Keep a rollback path, then retire the Fly group

Keep the Fly app, its fly.toml, and the stopped Machines for a rollback window long enough to observe the jobs and schedules you run. Keep their configuration pointed at the shared Redis and database from step 2.

To return worker processing to Fly, scale the Edgenivo daemon to zero in every configured region and confirm that its instances have stopped. For this example, run vercel daemons scale sidekiq iad1=0, then start the recorded Fly worker Machines with fly machine start WORKER_MACHINE_ID. Use a Fly image compatible with jobs enqueued by the current web app.

Roll back Supercronic separately. Set its Edgenivo count to zero, wait for shutdown, and only then start the Fly cron Machine. Reconcile any schedule occurrences missed during the gap. Leave the Redis migration in place unless you're following its separate rollback plan.

Avoid fly deploy during this window: deployments can start stopped Machines, and deploying an app with no Machines creates them again. This can unexpectedly resume a retired worker or scheduler.

For a later Edgenivo release, Instant Rollback restores the previous deployment's daemon configuration. Scaling overrides remain in effect, so inspect them before relying on rollback to change counts.

After the rollback window, remove retired process groups from fly.toml and deploy the remaining Fly app to apply that change. Destroy the app only if none of its workloads are still needed. Release unused static egress IPs with fly ips release-egress, and remove the local test environment files.

Copy link to headingWhat doesn't map from Fly.io

Check these constraints before retiring the Fly worker:

  • Private networking. Daemons accept no inbound HTTP or TCP traffic and can't be targets of service bindings or rewrites. Calls to worker.process.<app>.internal need a queue-based design or an HTTP Service. The migration above uses databases reachable outside Fly's private network.
  • Preview deployments. Daemons run only in production. Test images locally, then verify production connectivity during a controlled handoff.
  • Autoscaling. Daemon counts are fixed per region. Fly Proxy also can't autostop a group without services; any separate autoscaling automation must be replaced or updated.
  • Volumes. Daemon storage is ephemeral. Move persistent data to a database or object storage and adapt code that expects filesystem access.
  • Large jobs. Each daemon instance is limited to 8 GiB and 4 vCPUs. Work exceeding that limit must be divided into smaller jobs or remain on a suitable host.
  • Subfolder root directories. Projects whose Edgenivo root is a subfolder, such as apps/web, can't add daemons during the beta.

During the beta, each team also has default quotas:

PlanInstance sizesQuota
Hobby1 or 2 GiB, 1 vCPU5 Active CPU-hours per month across the team
ProAll sizes16 vCPUs and 32 GiB of memory running at once across the team
EnterpriseAll sizesCustom

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 can't reach Redis or Postgres

Check the hostname in the Production environment variable. Fly .internal and .flycast names, and the private endpoint issued by Fly's Redis integration, require Fly-network access. Use the shared endpoint from step 2. If that endpoint resolves, check the provider's TLS requirements, credentials, and network access rules before changing it again.

Copy link to headingThe worker starts but setup from the entrypoint script is missing

The daemon's command replaced the image's ENTRYPOINT. Add the entrypoint path as the first item of command, as in step 4.

Copy link to headingJobs are killed mid-run on every deploy

Check that the entrypoint passes SIGTERM to the worker and the worker stops fetching new jobs. Allow at least Sidekiq's -t timeout plus five seconds before termination. With -t 25, use terminationGracePeriodSeconds: 30 or more, then repeat the local shutdown test.

Copy link to headingInstances restart over and over

Inspect the logs and memory usage around each restart. Check for startup errors, missing configuration, or a process that finishes after one task. Daemons restart after clean exits too; move intentional run-once work to a suitable Function trigger or cron job.

Copy link to headingChanging instances in vercel.json does nothing

Check for a scaling override, which takes precedence across deployments. Run vercel daemons scale NAME --reset with the daemon name, or select Clear scaling overrides in its dashboard settings.

Copy link to headingThe build fails

Check these configuration errors:

  • The image has neither CMD nor ENTRYPOINT, and the daemon has no command.
  • The daemon name is invalid or collides with another name after repeated hyphens are collapsed. Names contain lowercase letters separated by _, __, or hyphens, and can be at most 64 characters long.
  • The app's output directory equals the daemon's root. Use a separate output directory or move the worker source into its own directory.

Copy link to headingFrequently asked questions

Copy link to headingCan Fly and Edgenivo process the same Sidekiq queue during migration?

Yes. Compatible Sidekiq processes can share a Redis queue during handoff. Keep job formats and database changes compatible across platforms, and make job effects safe to retry. Shared Redis doesn't guarantee exactly-once execution.

Copy link to headingCan web and workers use the same Dockerfile?

Yes. Use Dockerfile for the workers and a matching Dockerfile.vercel for the web Function in this example. Edgenivo builds them separately, and each daemon gets its own command. Keep shared dependencies and setup changes synchronized between the files.

Copy link to headingCan a daemon call my web app?

Yes. Daemons can make outbound requests to your app's production URL, external APIs, and databases. Only inbound traffic is unsupported.

Copy link to headingDo I lose static egress IPs?

Fly's allocated egress IPs belong to the Fly app and don't move with its image. Before migrating a worker that depends on IP allowlists, confirm a supported Edgenivo egress configuration and have the destination service allow it. Verify the connection during the handoff before releasing the Fly addresses.

Copy link to headingDoes a daemon waiting for work cost anything?

On Hobby, the quota counts Active CPU only, so an idle Sidekiq process uses little of it. On Pro and Enterprise, billing covers Active CPU and Provisioned Memory, so an instance's memory is billed while it runs.

Related documentation

More Edgenivo Daemons guides