Skip to content
Docs

How Docker Compose concepts map to Vercel

Translate your Docker Compose file to Vercel: Compose services become Vercel Services, networks become bindings, and volumes become Marketplace databases and Vercel Blob.

Ben SabicContent Engineer

Your compose.yaml defines services, networks, and volumes, and Vercel does not read any of it. There is no compose up equivalent, so the translation is manual, but every concept in that file has a destination on the platform. Compose services become Vercel Services, service-name DNS becomes service bindings, and volumes decompose into Marketplace databases, Vercel Blob, and environment configuration. Use the mapping table below to find where each line of your Compose file goes, then follow the worked example to translate a complete five-service stack.

Copy link to headingOverview

In this guide, you'll learn:

  • Which Vercel primitive replaces each Docker Compose concept
  • How service-to-service networking changes from DNS names to bindings
  • Where volume-backed state goes, whether that's a Marketplace database, Vercel Blob, or Global Config
  • Which Compose patterns don't translate, and what to do instead

Copy link to headingThe short answer

Each row links a Compose concept to its Vercel destination. The sections that follow explain each mapping with a code pairing.

Compose conceptWhat it doesVercel equivalent
services (app containers)Defines each container to build and runVercel Services, declared under the services key in vercel.json
Service-name DNS (http://api:3000)Containers reach each other by name on the default networkService bindings, which inject an internal URL environment variable
portsPublishes a container port to the hostTop-level rewrites in vercel.json targeting a service
networks (isolation)Controls which services can reach each otherInternal-by-default services plus explicit bindings
volumes for database dataPersistent storage for Postgres, Redis, or Mongo containersMarketplace databases such as Neon, Supabase, and Upstash
volumes for file uploadsShared filesystem for user filesVercel Blob
volumes and configs for config filesRuntime configuration mounted as filesEnvironment variables, or Global Config for read-heavy config
environment, env_filePer-service variablesPer-environment environment variables, pulled locally with vercel env pull
depends_on, healthcheckStartup ordering and readinessNo direct equivalent. Services deploy atomically and scale independently
deploy.replicas, restartManual scaling and restart policyFluid compute autoscaling
Worker services (queue consumers)Long-running background containersVercel Queues, Vercel Workflow, or Cron Jobs, depending on the job
docker compose upRuns the whole stack locallyvercel dev runs every service locally
docker compose logs -f appStreams one service's logsThe Logs UI filters by individual service

Vercel Services and container images are both in beta, available on all plans.

Copy link to headingServices: containers become Vercel Services

A Compose services block maps to the services key in vercel.json.

Each Vercel Service is an independently built unit with its own root directory, and a service that builds from a Dockerfile sets its entrypoint to a Dockerfile.vercel path, so a Compose service defined by build: maps one to one.

compose.yaml
services:
web:
build: ./web
api:
build: ./api
vercel.json
{
"services": {
"web": { "root": "web/" },
"api": { "root": "api/", "runtime": "container", "entrypoint": "Dockerfile.vercel" }
}
}

The deployment model is an upgrade over compose up. All services in a project deploy atomically, stay in sync on a shared domain, and roll back together, which replaces the best-effort startup coordination you get from Compose.

Copy link to headingNetworking: DNS names become bindings

Compose and Vercel invert each other's defaults. On Compose's default network every service can reach every other service by name, and ports opts a service into public exposure. On Vercel every service is internal by default, and a top-level rewrite opts a service into public traffic. No rewrite means no public traffic.

Internal calls use bindings instead of DNS names. A binding is declared on the calling service, names the target, and injects the target's URL as an environment variable at runtime:

vercel.json
{
"services": {
"web": {
"root": "web/",
"bindings": [
{ "type": "service", "service": "api", "format": "url", "env": "API_INTERNAL_URL" }
]
},
"api": { "root": "api/" }
},
"rewrites": [{ "source": "/(.*)", "destination": { "service": "web" } }]
}

Where Compose code calls http://api:3000/items/123, Vercel code calls the injected URL:

await fetch(new URL('items/123', process.env.API_INTERNAL_URL));

Traffic over a binding stays on Vercel's internal network and never routes through the public internet. Bindings aren't available yet for services on the Go or Rust runtime; build those services as container images instead, which can use bindings.

Copy link to headingState: volumes decompose by role

On Vercel you attach managed state instead of running stateful containers. A Compose volume serves three different jobs, and each job has a different destination.

Copy link to headingDatabase data moves to Marketplace databases

Don't translate your postgres or redis service. Replace it with a managed database from the Vercel Marketplace, which provisions the resource and injects its credentials as environment variables:

# compose.yaml: a service and volume you delete
services:
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
# Vercel: attach a managed database instead
vercel install neon # Postgres
vercel install upstash # Redis

This is the biggest mental shift in the migration. Your application code keeps its Postgres or Redis client and reads the connection string from the injected environment variable.

Copy link to headingFile uploads move to Vercel Blob

A volume that holds user uploads or generated assets becomes Vercel Blob, an object store for images, videos, and other files. There are no mounted paths; code writes and reads blobs through the SDK, authenticated with OIDC rather than a stored secret.

Copy link to headingConfig files become environment variables

A volume or configs entry that mounts configuration files becomes environment variables. For configuration that's read on many requests and updated rarely, such as feature flags or redirect maps, use Global Config, a globally replicated store with reads of roughly 1 ms in every region.

Copy link to headingBackground work: worker containers decompose by job type

A Compose worker service is one long-running container that does whatever jobs you feed it. On Vercel that container decomposes by job type. Use Vercel Queues for background jobs processed off the request path, Vercel Workflow for durable multi-step processes that survive suspensions, and Cron Jobs for scheduled work. On the Hobby plan, cron jobs are limited to daily schedules.

Copy link to headingConfiguration and local development

Per-service environment and env_file entries become Vercel environment variables, scoped per environment across production, preview, and development. Run vercel env pull to download them for local work, alongside the variables Vercel generates automatically for services and bindings.

The local loop also translates directly. vercel dev replaces docker compose up and runs every service in the project locally for a production-like environment. Services built from container images require the Docker CLI and a running Docker daemon on your machine. For observability, the Logs UI filters by individual service, replacing docker compose logs -f app, and the Deployments panel visualizes the services graph.

Copy link to headingWhat doesn't translate

Some Compose patterns have no Vercel mapping. Name them now to avoid surprises mid-migration.

  • Self-managed stateful containers: You can't run your own Postgres or Redis container with local disk. Container-based functions are stateless; each instance keeps nothing between requests. Attach a Marketplace database instead. Vercel has announced durable storage attached to containers as upcoming, but it has not shipped.
  • Host bind mounts: There is no host filesystem to mount from. Move files to Vercel Blob or bake them into the container image.
  • depends_on startup ordering: Services deploy atomically and scale independently, with no startup ordering or readiness gating between them. Design each service to tolerate any startup order, for example by retrying dependent calls.
  • Privileged containers: Workloads that need privileged host access, custom kernels, or host networking aren't supported.

Copy link to headingWorked example: translate a five-service stack

This canonical Compose stack covers the full mapping: a frontend, an API, two stateful services, and a worker.

compose.yaml
services:
web:
build: ./web
ports:
- "3000:3000"
environment:
- API_URL=http://api:8000
depends_on:
- api
api:
build: ./api
depends_on:
- db
- redis
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7
worker:
build: ./worker
depends_on:
- redis
volumes:
pgdata:

Five Compose services become two Vercel Services, two Marketplace databases, and a queue consumer.

1. Attach the databases. The db and redis services disappear. Provision managed replacements, which inject connection strings as environment variables:

vercel install neon
vercel install upstash

2. Define the app services. web and api become Vercel Services. The ports mapping on web becomes the public rewrite, and the API_URL environment variable becomes a binding:

vercel.json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"services": {
"web": {
"root": "web/",
"bindings": [
{ "type": "service", "service": "api", "format": "url", "env": "API_INTERNAL_URL" }
]
},
"api": {
"root": "api/",
"runtime": "container",
"entrypoint": "Dockerfile.vercel"
}
},
"rewrites": [{ "source": "/(.*)", "destination": { "service": "web" } }]
}

api has no rewrite, so it receives no public traffic. web reaches it through process.env.API_INTERNAL_URL. The container image listens on $PORT, which defaults to 80. The depends_on entries have no translation; the deployment is atomic and each service must tolerate any startup order.

3. Replace the worker. The worker service becomes a Vercel Queues consumer. Instead of a container polling Redis, the producer sends messages to a queue and Vercel invokes your consumer function to process each one off the request path.

Run vercel dev to bring the translated stack up locally, then vercel deploy to ship it.

More Vercel Container Registry guides