Vercel runs Docker container images as request-driven Vercel Functions on Fluid compute, so the HTTP surface of a Dockerized monolith deploys to Vercel while its always-on worker processes need a different shape. Container images on Vercel scale out with traffic and scale down when traffic stops, which means a worker loop that never receives an HTTP request has nothing keeping it running.
Most worker types have direct replacements on Vercel: queue consumers become Vercel Queues push consumers, multi-step jobs become Vercel Workflows, and scheduled tasks become Cron Jobs. Every pull request then gets a preview deployment that includes all services and their own isolated queue topics.
Processes that must run continuously can stay on Render, Railway, Amazon ECS, or Kubernetes and exchange work with Vercel through a shared queue such as Amazon SQS or Redis.
Copy link to headingOverview
In this guide, you'll learn how to:
- Decide which processes in a Dockerized monolith fit Vercel and which need another home
- Deploy the web and API process as a container image service
- Replace queue consumers, multi-step jobs, and scheduled tasks with Vercel Queues, Vercel Workflows, and Cron Jobs
- Keep preview deployments isolated for every pull request, including background jobs and data
- Connect always-on workers on another platform in a hybrid architecture
Copy link to headingPrerequisites
Before you begin, make sure you have:
- A Dockerized monolith in a Git repository connected to a Vercel project, or the Vercel CLI installed
- A list of the monolith's process types, usually found in a
Procfile,compose.yaml,supervisord.conf, or Kubernetes manifests - The Docker CLI and a running Docker daemon, which
vercel devneeds to run container images locally - A plan for persistent state, such as a Postgres or Redis database from the Vercel Marketplace
Container images and Vercel Services are in beta and available on all plans. Vercel Queues is also in beta and available on all plans.
Copy link to headingCan a Dockerized monolith run on Vercel?
A Dockerized monolith runs on Vercel when its runtime entrypoint is an HTTP server. Vercel builds a Dockerfile.vercel (or Containerfile.vercel) into an OCI image, stores it in Vercel Container Registry, and runs it as a Vercel Function that scales with traffic. Each container instance is stateless, so persistent state has to live in backing services such as a Marketplace database or Vercel Blob. Worker processes that never receive an HTTP request don't run on Vercel as-is. You decompose them into Vercel Queues consumers, Vercel Workflows, or Cron Jobs, or you keep them on a platform built for always-on processes.
Copy link to headingHow Vercel runs container images
Vercel treats a container image as a Vercel Function with the same limits and Active CPU pricing as any other function. The behavior that matters for a monolith is:
- HTTP only: The container must open an HTTP server. Vercel routes traffic to port
80by default, and you can override it by setting thePORTenvironment variable in your project settings. - Scale to zero: Instances with no traffic for 5 minutes in production scale down automatically. In preview environments, the threshold is 30s.
- Graceful shutdown: On scale-down, the container receives a
SIGTERMsignal with a 30s grace period before Vercel terminates it. - Stateless instances: Nothing written to the local filesystem or process memory survives between instances.
- Duration limits: Each request is bound by the function's
maxDuration, which defaults to 300s and goes up to 800s on Pro and Enterprise. The 1800s extended maximum beta only covers specific Node.js, Bun, and Python runtime versions, and container images aren't on that list, so plan for 800s. - Networking gaps: Secure Compute and Static IPs aren't supported with container images yet.
Copy link to headingWhy an always-on worker loop doesn't fit container images
A typical worker command, such as celery worker, sidekiq, or a BullMQ Worker, opens a connection to a broker and loops forever waiting for jobs. That process never serves an HTTP request, so Vercel has no request to route to it and no traffic to keep the instance alive. A worker started as a background loop inside the web container also stops working, because the instance scales down after 5 minutes without HTTP traffic in production and 30s in preview. Vercel inverts the model instead. Vercel Queues holds the work and invokes a private consumer function for each message, so compute only runs when there's a job to process.
Copy link to headingWhich Vercel runtime each monolith process maps to
Map every process type in your monolith to one row of this table before changing any code. The worker type, not the fact that it's a worker, decides where it runs.
| Monolith process | Example start command | Where it runs | Vercel primitive |
|---|---|---|---|
| Web server or API | node src/server.js, gunicorn app.wsgi, rails server | Vercel | Container image in a Vercel Services project |
| Fire-and-forget jobs (emails, PDFs, webhooks) | celery worker, sidekiq, BullMQ Worker | Vercel | Vercel Queues push consumer |
| Multi-step or long-waiting jobs (onboarding, billing runs, approvals) | Chained jobs with manual state tracking | Vercel | Vercel Workflows |
| Scheduled tasks | cron, celery beat, whenever | Vercel | Cron Jobs that enqueue work or start a workflow |
| Always-on daemons (persistent broker or socket connections, local disk, privileged host access) | Custom daemon or sidecar | Another platform | Render, Railway, Amazon ECS, or Kubernetes, connected through a shared queue such as Amazon SQS or Redis |
Vercel Queues and Vercel Workflows solve different problems. Vercel Queues gives you direct control over topics, messages, and consumer groups, which fits one-message-one-job work. Vercel Workflows builds on Vercel Queues and adds durable steps, sleep, and hooks, so a job can run across many function invocations and pause for minutes to months without a duration limit.
Copy link to headingSteps
The steps below use a Node.js Express monolith with a web process, a job worker, and a scheduler as the running example. The same decomposition applies to Python, Ruby, Go, and Java monoliths. Python projects can use the Celery and Dramatiq integrations, which compile existing workers into private queue-triggered functions. These integrations run on the Python runtime and declare their workers in pyproject.toml, so put the worker code in a Python service next to the container image rather than inside it. Service bindings aren't available yet for services on the Go or Rust runtime, so build a Go service as a container image if it needs to call another service.
Copy link to heading1. Inventory the monolith's process types
List every process the monolith starts in production. A Procfile for the example monolith looks like this:
For each process, record what triggers it, how long one unit of work takes, and whether it depends on local disk or a persistent connection. Then assign it a row from the mapping table above. In the example, web becomes a container image service, worker becomes a Vercel Queues consumer, and scheduler becomes a Cron Job that starts a Vercel Workflow.
A unit of work that regularly runs longer than your function's maxDuration belongs in Vercel Workflows, where you split it into steps that each finish within the limit.
Copy link to heading2. Move durable state out of the container
Container instances on Vercel keep nothing between requests, so every piece of state the monolith writes locally needs a backing service. Replace each local dependency before you deploy:
- Database containers: Attach a managed database from the Vercel Marketplace, which injects its connection string as an environment variable. For example, run
vercel install neonfor Postgres orvercel install upstashfor Redis. - Uploaded or generated files: Move them to Vercel Blob instead of a mounted volume.
- Sessions and caches: Store them in Redis or your database instead of process memory.
- Job progress: Persist it in the database so a restarted consumer can resume or skip completed work.
Your application code keeps its existing Postgres or Redis client and reads the connection string from the injected environment variable.
Copy link to heading3. Deploy the web and API process as a container image
Move the monolith into a web/ directory and add a Dockerfile.vercel next to its package.json. The image only needs to start the HTTP server:
The server listens on PORT when you set it and falls back to Vercel's default port 80. It also closes cleanly on SIGTERM so in-flight requests finish during the 30s grace period:
Remove any code that starts the worker loop or scheduler from inside the web process. Those processes move to the jobs service in the next steps.
Copy link to heading4. Add a jobs service and route traffic between services
Create a jobs/ directory with a Next.js app that will hold queue consumers and workflows. The Vercel Queues docs configure push consumers on framework function paths, such as a Next.js route handler, so the consumers live in a framework service next to the container image.
Declare both services in a vercel.json at the repository root:
This configuration does four things:
- Public traffic: The rewrite sends every public request to the
webcontainer. Thejobsservice has no rewrite, so it receives no public traffic. - Internal calls: The binding on
webinjects thejobsservice's internal URL asJOBS_INTERNAL_URL. Traffic over a binding stays on Vercel's internal network, and the injected URL is deployment-aware, so a preview'swebalways calls the same preview'sjobs. A binding grants reachability but doesn't authenticate the caller, so keep authorization between services in your code if it matters. - Queue consumer: The
queue/v2betatrigger makessend-invoice/route.tsa private consumer of theinvoicestopic.maxDeliveriescaps retries for a message that keeps failing. - Schedule: The cron entry calls
/api/cron/nightly-reconcileon the production deployment at 03:00 UTC every day. The cron request is an HTTPGETto that path on the production URL, which the catch-all rewrite sends toweb, where step 6 adds the handler. Step 6 shows how to confirm this on your first production deployment. Vercel doesn't retry a failed cron invocation, which is another reason to keep the cron handler thin.
Enable Vercel Workflows in the jobs service by installing workflow and @vercel/queue, then wrapping the Next.js config:
Without withWorkflow(), the 'use workflow' and 'use step' directives compile to no-ops.
Copy link to heading5. Convert the first queue consumer
Pick one low-risk job, such as sending an invoice email, and move it off the old worker. The web process publishes work through the jobs service, and Vercel Queues invokes the consumer for each message.
Add an internal route in the jobs service that publishes to the invoices topic. The idempotencyKey drops duplicate publishes for the same invoice during the message's retention period:
Add the consumer. Vercel acknowledges the message when the handler returns and redelivers it when the handler throws:
The retry callback drops a message after 5 failed deliveries. The trigger's maxDeliveries: 10 from step 4 is a backstop for failures the callback never sees, such as a function timeout or crash, where the handler doesn't get to run its retry logic.
Replace the old enqueue call in the web process with a call to the jobs service over the binding:
Deploy and send a request to the new endpoint:
The response includes a messageId, and the jobs service's runtime logs show the consumer processing the message. Publishing through the jobs service keeps every Vercel Queues call in a framework service, where the SDK's credential handling is documented.
Copy link to heading6. Convert scheduled and multi-step jobs
Scheduled jobs work best as thin triggers that start durable work and return. A cron request is bound by the function's maxDuration, while a workflow can run across many invocations with no duration limit.
Define the nightly reconciliation as a workflow in the jobs service. Each 'use step' function is persisted, so a failure retries only the failed step:
Add an internal route that starts a run:
Add the cron route to the web container. It checks the CRON_SECRET environment variable, starts the workflow over the binding, and returns within seconds:
Set CRON_SECRET to a random string of at least 16 characters in every environment that serves this route, including Preview. The handler rejects all requests when the variable is missing, and a failed workflow start returns a non-2xx status so it shows up in the cron logs.
Cron delivery is best effort. A scheduled run can occasionally be skipped or invoked more than once, so make the work safe to repeat. reconcileAccount should check whether an account was already reconciled for date before doing anything, and listAccountsToReconcile should return all outstanding work since the last successful run so a missed night catches up the next day.
After the next production deployment, trigger the cron job manually to confirm it reaches the web container instead of waiting for 03:00 UTC:
vercel crons is in beta. It reads cron definitions from your deployed production project, so the cron entry must already be in production. Check the result under Settings, then Cron Jobs, then View Logs, and open your project's Observability tab and select Workflows to see each run, its steps, and any retries.
On the Hobby plan, cron jobs can run at most once per day, and Vercel may invoke them at any point within the scheduled hour, so the 0 3 * * * schedule can run anytime between 03:00 and 03:59 UTC. On other plans, jobs run within the specified minute.
Copy link to heading7. Validate the preview deployment
Open the pull request's preview deployment and confirm each part of the monolith works before you retire the old workers:
- The web container responds at the preview URL, including
/healthz - Publishing a message from the preview produces a consumer log in the same preview deployment
- Calling the cron route manually with the preview's
CRON_SECRETstarts a workflow run - The preview reads and writes only preview data, never production data
Run vercel dev from the repository root to run every service locally with the same routing and bindings. Cron schedules don't fire locally, so call /api/cron/nightly-reconcile directly with your local CRON_SECRET to test the scheduled path.
Copy link to headingHow preview deployments isolate web, jobs, and data
Every commit to a pull request creates a preview deployment with its own URL, and every service in the project deploys atomically on that deployment's shared domain. A preview of the example monolith therefore includes the web container, the jobs service, and its queue consumers at the same commit. Services also roll back together, so an Instant Rollback returns web and jobs to a matching version. Cron jobs are the exception. An Instant Rollback doesn't update active cron jobs, which keep running on their current schedule until you disable or update them.
Copy link to headingQueue topics are isolated per deployment
Vercel Queues partitions topics by deployment ID by default, and in push mode it delivers messages back to the same deployment that published them. A preview deployment's consumers only process messages that the preview published, so preview jobs never consume production messages and schema changes in a pull request never break production consumers. You get a preview worker for every pull request without provisioning anything.
Vercel Workflows runs on Vercel Queues and keeps each run on the deployment that created it by default, so a workflow run started from a preview executes against that preview's code, and a new production deployment doesn't change runs already in progress.
Copy link to headingCron Jobs only run in production
Vercel triggers cron jobs by sending an HTTP GET request to the production deployment URL, so schedules never fire on preview deployments. To test scheduled work in a preview, call the cron route yourself:
The x-vercel-protection-bypass header is only needed when Deployment Protection covers preview URLs. Create the secret under Protection Bypass for Automation in your project's Deployment Protection settings. Calls over service bindings skip the public request pipeline that Deployment Protection runs in, and queue consumers are invoked by Vercel's internal queue infrastructure rather than through a public URL, so neither needs the bypass header.
Copy link to headingPreview data needs its own database or branch
Preview deployments share whatever database their environment variables point to. Set the Preview environment's variables to a separate database, or use a Marketplace provider that branches the database per preview deployment. The Neon integration creates a database branch named preview/<git-branch> when you enable Create Database Branch for Deployment for Preview. Every preview deployment of that Git branch shares the same database branch, so data written by one commit's preview is still there for the next. Neon injects the branch's connection string into each preview deployment at deploy time. It overrides the Preview environment variables for that deployment and doesn't appear in your project's environment variable settings. To keep the schema in step with the code, run your migrations in the build command, for example npx prisma migrate deploy && npm run build. Without that separation, a preview's jobs write to the same tables as production.
Copy link to headingStale previews can keep processing messages
Messages stay pinned to the deployment that published them, so an old preview keeps receiving retries until its messages are acknowledged or expire, which takes 24 hours by default. Delete preview deployments you no longer need to stop their consumers immediately.
Copy link to headingRun always-on workers on another platform
Some processes don't decompose into queue messages or workflow steps. Keep those workers on a platform that runs always-on containers and connect them to the Vercel-hosted web tier through a shared queue or database.
Copy link to headingWorkers that should stay always-on
Keep a worker on Render, Railway, Amazon ECS, or Kubernetes when it:
- Holds a persistent connection it can't give up, such as a stream consumer or a long-lived socket to a third-party system
- Depends on local persistent disk or large in-memory state that's expensive to rebuild
- Needs privileged host access, custom kernels, or host networking
- Needs a fixed outbound IP address for allowlisting, since container images don't support Static IPs yet
- Runs a single unit of work that can't be split into steps and exceeds your function's
maxDuration
Copy link to headingConnect external workers through a shared queue
An external worker needs a queue that both platforms can authenticate to. The web tier on Vercel publishes jobs, and the worker consumes them with the client and credentials it already uses on its own platform. Amazon SQS works well for this, because code running on Vercel, including container images, can assume an IAM role over OpenID Connect (OIDC) federation instead of storing AWS access keys.
Publish from the web container with the AWS SDK and the Vercel OIDC credentials provider:
The worker on Amazon ECS, Kubernetes, Render, or Railway keeps its existing SQS consumer. Only the producer moves to Vercel.
Setting this up takes three pieces of configuration:
- OIDC identity provider: Register Vercel as an OpenID Connect provider in AWS IAM, then create a role whose trust policy admits your Vercel project. The OIDC federation docs and Migrate self-hosted Next.js and containers from AWS to Vercel walk through the trust policy.
- Pinned region: Set
AWS_REGIONexplicitly in your project's environment variables. Vercel setsAWS_REGIONto the region the function runs in, so an unpinned client can address a queue in a region where it doesn't exist. - Per-environment values: Set
AWS_ROLE_ARNandTRANSCODE_QUEUE_URLseparately for the Production and Preview environments, as described in the next section.
Vercel Queues also supports consumers outside Vercel through poll mode, which is designed for long-running services, on-premise workers, and other clouds. Python projects using Celery can point an external celery worker at the same queues with the vercel-poll:// broker. Every Vercel Queues API request requires a Vercel OIDC token in the Authorization header, so confirm how your external worker will obtain and refresh that token before choosing this path. If that isn't settled, a broker your worker already authenticates to, such as Amazon SQS or Redis, is the lower-risk choice.
Copy link to headingKeep your existing queue and move only the web tier
The lowest-change hybrid keeps the monolith's existing queue library. The web container on Vercel enqueues jobs into your existing broker, such as Redis from the Vercel Marketplace or a broker you already run, and the unchanged worker process keeps running on its current platform. Use this path to move the web tier first and convert workers later, one job type at a time.
Copy link to headingPreview workers in a hybrid setup
External workers don't get the per-deployment isolation that Vercel Queues provides, so separate preview traffic yourself. Point the Preview environment's TRANSCODE_QUEUE_URL at a preview queue, and give previews their own IAM role. Scope each role's trust policy to one project and one environment, such as owner:[TEAM_SLUG]:project:[PROJECT_NAME]:environment:preview, so preview deployments can never publish to the production queue. Avoid a wildcard that matches environment:preview across every project on your team, because anyone who can push a branch to any of those projects could then assume the role.
Then choose how preview jobs get processed:
- Shared preview worker: Run one external worker that consumes the preview queue for all pull requests. It's the lowest-cost option, but every preview shares one worker and one set of preview data.
- Per-pull-request worker: Create a queue and worker per pull request on the external platform. Tear both down when the pull request closes, because the external platform won't clean them up when Vercel removes the preview.
Copy link to headingChoose a Vercel-only, hybrid, or container-platform architecture
Use the process inventory from step 1 to pick the architecture. The deciding question is whether any process must stay running when there's no work to do.
| Architecture | Fits when | Previews for every pull request |
|---|---|---|
| Vercel-only | Every process is an HTTP server, a queue consumer, a multi-step job, or a scheduled task | Automatic for web, API, and queue consumers, with per-deployment topic isolation |
| Hybrid | Web and API fit Vercel, but one or more workers need an always-on process | Automatic for Vercel services; external workers need a shared preview worker or per-pull-request provisioning |
| Container platform | The monolith must stay one image with private services, attached disks, and always-on workers cloned into every preview | Handled by the container platform's preview environments |
Many teams start hybrid, moving the web tier and the easiest job types to Vercel first, then converting the remaining workers as they're rewritten. A container platform remains the better fit when most of the monolith's value sits in stateful, always-on processes. For a feature-by-feature comparison of the compute models, see Running Docker on Vercel vs Render.
Copy link to headingTroubleshooting
Copy link to headingThe deployment ignores the services configuration
Vercel switches a project into services mode when vercel.json defines a services key. Confirm that vercel.json sits at the repository root and contains the services block. In services mode, build and runtime keys such as functions, buildCommand, and framework aren't valid at the top level, so move them into the relevant service, then redeploy.
Copy link to headingThe deployment builds but requests fail or time out
The container isn't listening where Vercel sends traffic. Bind the server to 0.0.0.0, not localhost, and listen on PORT with a fallback to 80. Confirm the startup command in Dockerfile.vercel starts the HTTP server rather than a worker.
Copy link to headingBackground work stops after a few minutes
A worker loop started inside the web container dies when the instance scales down, which happens after 5 minutes without traffic in production and 30s in preview. Move that loop to a Vercel Queues consumer, a Vercel Workflow, or an external worker.
Copy link to headingFiles or cached values disappear between requests
Container instances are stateless, and each request can land on a different instance. Move files to Vercel Blob and caches or sessions to Redis or your database.
Copy link to headingA job runs more than once
Vercel Queues delivers at least once, so a message can arrive again after a consumer timeout or an infrastructure event. Make handlers idempotent by recording completed work in the database, and use idempotencyKey when publishing to drop duplicate publishes. Cron Jobs can also invoke the same scheduled run more than once, so apply the same idempotency to work started from a cron route.
Copy link to headingAn old deployment keeps running consumers after a rollback
Messages stay pinned to the deployment that published them, so promoting or rolling back doesn't stop the old deployment's consumers. Delete the old deployment to stop its consumers immediately, and set maxDeliveries on triggers to bound retries. The Observability Query tab can show function duration grouped by deployment to find which deployment is consuming compute.
Copy link to headingA request returns a 504 FUNCTION_INVOCATION_TIMEOUT error
The request ran longer than the function's maxDuration. Return quickly from HTTP routes and move the work to a queue consumer, or split it into Vercel Workflow steps that each finish within the limit.
Copy link to headingNext steps
- Compare the Vercel and Render compute models in Running Docker on Vercel vs Render
- Translate a full
compose.yamlwith How Docker Compose concepts map to Vercel - Configure images, ports, and scale-down behavior in the Container Images documentation
- Route traffic and bind services in the Vercel Services documentation
- Learn delivery, retries, and deployment isolation in Queues concepts and Queues pricing and limits
- Build durable multi-step jobs with Vercel Workflows
- Schedule work with Cron Jobs
- Reach AWS queues and databases from Vercel without access keys in Migrate self-hosted Next.js and containers from AWS to Vercel
- Convert Python workers with Celery on Vercel or Dramatiq on Vercel