---
id: platform/migrate-from-docker-compose
title: Migrate from Docker Compose
description: Map Docker Compose services, ports, datastores and volumes to a Bex render.yaml, and see which Compose features have no Bex equivalent.
keywords: [bex, migrate, docker compose, docker-compose.yml, render.yaml, migration guide]
last_updated: 2026-10-05
---

A Docker Compose file describes containers on one host. A Bex
[render.yaml](./app-resource.md) describes managed services, datastores and
disks. Most of a Compose file maps, but host-level features such as bind mounts,
networks and start order do not, and moving a file moves no data.

Bex is in active development and its upstream project does not yet recommend
production workloads. Begin with a non-production rehearsal and check
[compatibility](./compatibility.md) and required capabilities before proceeding.

## Use the converter

Paste your `docker-compose.yml`, and optionally the `.env` its services read,
into the [Docker Compose converter](/tools/docker-compose-to-render-yaml). It is
a browser starting point: it drafts a render.yaml by the rules below, lists
every element it could not map with the reason, and checks the draft with the
[render.yaml checker](/tools/render-yaml-checker). `bex blueprints validate`
remains the authoritative check. Nothing you paste leaves your browser.

## How Compose maps to Bex

| Compose | Bex |
| --- | --- |
| Service with a published TCP port (`ports`) | Web service; the first TCP port becomes `PORT` unless it is 3000 |
| Service with only `expose`, or referenced by other services | Private service (`pserv`) |
| Any other long-running service | Background worker |
| Container port below 1024 | Not mapped: reconfigure the app to listen on `$PORT` |
| Extra published ports, UDP ports | Not mapped: one HTTP port per service |
| `build` | Docker runtime (`dockerContext`, `dockerfilePath`) |
| `image` without `build` | Image runtime; the image's own command runs |
| `command`, `entrypoint` | `dockerCommand` (Docker runtime only) |
| `environment`, `env_file` | Environment variables; secrets become `sync: false` |
| `deploy.replicas` | `numInstances` |
| Official `postgres` image | Bex Postgres, wired with `fromDatabase` (major 13 or later) |
| Official `redis` or `valkey` image | Bex Key Value, wired with `fromService` |
| Named volume used by one service | A disk (paid plan, single instance, choose a size) |
| Shared named volume, bind mount, tmpfs | Not mapped |
| HTTP `healthcheck` on a web service | `healthCheckPath` |
| `depends_on`, `networks`, `profiles`, `restart`, `cap_add`, `privileged` | Not mapped, with the reason |

`${VAR}` substitution in the Compose file is not evaluated: resolve it and
enter the value yourself.

## Ports and PORT

A web service must listen on `0.0.0.0` and the port Bex injects as `PORT`. The
default container port is 3000, and Bex does not detect ports, so a service that
listens on another port needs `PORT` set to match
([web services](./web-services.md)). Tenant containers drop Linux capabilities,
so an app on port 80 or 443 must be reconfigured to an unprivileged port such as
8080 ([Docker deploys](./docker-deploys.md)). Bex routes one HTTP port per
service.

## Build or image

A service with `build` uses the Docker runtime: set `dockerContext` and
`dockerfilePath` relative to the repository root, and put dependency
installation in the Dockerfile. A service with only `image` uses the image
runtime and runs the image's own command; bex refuses `dockerCommand` and
repository fields on image services. A private registry needs a
[registry credential](./docker-deploys.md).

## Datastores

Containers from the official `postgres` image become [Bex Postgres](./postgres.md),
and other services' connection URLs are rewired with `fromDatabase`. Postgres
majors below 13 are not supported. Containers from the official `redis` or
`valkey` images become [Bex Key Value](./key-value.md), managed Valkey, wired with
`fromService` and `type: keyvalue`. The managed datastores start empty: their
container volumes, environment and ports do not carry over. Move data with a
tested transfer, not by copying volumes.

## Volumes and disks

A named volume used by one service can become a [persistent disk](./persistent-disks.md).
There is one disk per paid web service, private service or background worker, a
service with a disk runs a single instance, and Compose volumes have no size, so
choose one. Shared named volumes, bind mounts and tmpfs mounts have no Bex
equivalent: bake files into the image or keep shared state in a datastore.

## What does not map

Compose `depends_on`, `networks`, `profiles`, `restart`, `cap_add`,
`privileged` and non-HTTP health checks have no Bex setting. Bex services share a
[private network](./private-network.md), restart automatically, and should retry
connections instead of relying on start order.

## Move data and traffic

Rehearse the data transfer and the cutover with steps 4 and 5 of
[Migrate from Render](./migrate-from-render.md); they apply to any source host.
