Migrating Docker Compose to WSL containers
There is no wslc compose. This is the single largest gap between
WSL containers and Docker Desktop, and no flag or setting works around it. What
you can do is translate a Compose file mechanically into wslc commands, because most
of what Compose does is declarative sugar over exactly the primitives wslc already has: networks,
volumes, environment variables and published ports.
The parts that do not translate are the parts Compose implements itself — dependency ordering, healthchecks and restart policies. Those you rebuild in a script. This guide covers both halves.
On this page
The mental model
A Compose file is three things stacked together: a set of container definitions, a network and volume topology, and a small orchestrator that starts things in order and watches their health. wslc gives you the first two directly. The third is yours to write.
So every migration follows the same four steps:
- Create the user-defined network once, so containers can resolve each other by name.
- Create every named volume up front.
- Start each service as a
wslc run, using the Compose service name as--nameso DNS still works. - Add explicit readiness waits where Compose had
depends_onorhealthcheck.
Naming discipline matters more than it did with Compose. Compose prefixed container names with the project name and created an implicit default network. With wslc, the container name is the DNS name, and there is no implicit network. Keep the service names identical to your Compose file and your application connection strings will not need to change.
Field-by-field translation table
| Compose field | wslc equivalent | Status |
|---|---|---|
image: | positional image argument | Direct |
container_name: | --name | Direct |
ports: | -p host:container | Direct |
environment: | -e KEY=value | Direct |
env_file: | --env-file | Direct |
volumes: (named) | wslc volume create + -v name:/path | Direct |
volumes: (bind) | -v C:/host/path:/path | VirtioFS perms |
networks: | wslc network create + --network | Bridge only |
command: | trailing command arguments | Direct |
entrypoint: | --entrypoint | Direct |
working_dir: | -w | Direct |
user: | -u | Direct |
build: | wslc build -t as a separate step | Two steps |
build.platform: | — | None |
depends_on: | ordering in your script | Manual |
healthcheck: | readiness poll in your script | Manual |
restart: | — | None |
deploy: | — | None |
profiles: | script parameters | Manual |
secrets: / configs: | mount a file, or -e | Manual |
extends: | — | None |
The command converter applies the direct rows automatically and flags the manual ones, which is faster than working through this table by hand for a large file.
Worked example: web + API + Postgres
Here is a typical three-service development stack:
# docker-compose.yml
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: devpass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
api:
build: ./api
environment:
DATABASE_URL: postgres://postgres:devpass@db:5432/appdb
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./web/nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
volumes:
pgdata: And the wslc translation, as a PowerShell script:
# up.ps1
$ErrorActionPreference = "Stop"
$NET = "appnet"
# 1. topology
wslc network create $NET 2>$null
wslc volume create pgdata 2>$null
# 2. build what needs building
wslc build -t app-api:dev ./api
# 3. db first
wslc run -d --name db --network $NET `
-e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=appdb `
-v pgdata:/var/lib/postgresql/data `
postgres:16-alpine
# 4. replace healthcheck + depends_on with an explicit wait
Write-Host "waiting for db..." -NoNewline
for ($i = 0; $i -lt 30; $i++) {
wslc exec db pg_isready -U postgres *> $null
if ($LASTEXITCODE -eq 0) { Write-Host " ready"; break }
Write-Host "." -NoNewline
Start-Sleep -Seconds 2
}
if ($LASTEXITCODE -ne 0) { throw "db failed to become ready" }
# 5. api
wslc run -d --name api --network $NET -p 3000:3000 `
-e DATABASE_URL="postgres://postgres:devpass@db:5432/appdb" `
app-api:dev
# 6. web — bind mount uses forward slashes
wslc run -d --name web --network $NET -p 8080:80 `
-v "$($PWD.Path -replace '\\','/')/web/nginx.conf:/etc/nginx/conf.d/default.conf:ro" `
nginx:alpine
wslc container list
Four details in that script are the whole point of this guide. The network is created explicitly
because wslc has no implicit default network. The --name values match the Compose
service names so db still resolves from the API container. The healthcheck became a
poll loop with a hard failure after 60 seconds. And the bind mount converts Windows backslashes to
forward slashes, because the wslc argument parser treats a backslash as an escape character.
Progress output is not decoration. A migration script that runs silently for a minute while an image pulls looks identical to a hung script. Print a line before every long-running step and a dot per poll iteration — you will save yourself far more debugging time than the three lines of code cost.
Replacing depends_on and healthcheck
Compose's depends_on: condition: service_healthy is the feature people miss most, and
it is worth being precise about what it actually did: it blocked the dependent container's start
until the dependency's own healthcheck passed. Two mechanisms, one outcome.
In wslc you rebuild the outcome. The pattern is always the same — run the dependency's health
probe from outside via wslc exec, poll on an interval, and fail hard on timeout:
function Wait-Ready($name, $probe, $timeoutSec = 60) {
$deadline = (Get-Date).AddSeconds($timeoutSec)
while ((Get-Date) -lt $deadline) {
wslc exec $name @probe *> $null
if ($LASTEXITCODE -eq 0) { return }
Start-Sleep -Seconds 2
}
throw "$name not ready after ${timeoutSec}s"
}
Wait-Ready "db" @("pg_isready","-U","postgres")
Wait-Ready "redis" @("redis-cli","ping")
Wait-Ready "api" @("curl","-fsS","http://localhost:3000/health")
The equivalent probes for common images: pg_isready -U postgres for Postgres,
mysqladmin ping for MySQL and MariaDB, redis-cli ping for Redis,
mongosh --eval "db.runCommand('ping')" for MongoDB, and
curl -fsS localhost:PORT/health for anything with an HTTP endpoint.
Note the hard throw on timeout. A silent continue means the next service starts
against a database that is not up, and you debug a confusing application error instead of a clear
infrastructure one.
Service discovery between containers
Containers on the same user-defined wslc network resolve each other by container name, exactly as
they did on a Compose network. postgres://db:5432 works unchanged provided the
container is named db and both containers joined the same
--network appnet.
Two failure modes to know. First, containers on the default network do not get name
resolution — forgetting --network on one service produces a
"could not resolve host" that looks like a DNS bug but is a topology bug. Second,
--network host is meaningless here: wslc already routes container traffic through the
Windows host network stack, so there is no separate host mode to opt into, and passing it is
silently ignored.
That host-stack routing is also a genuine upgrade over Docker Desktop for corporate machines. Container traffic inherits your VPN routes, proxy settings and firewall rules automatically, so the usual dance of configuring proxy environment variables inside every container mostly disappears.
Teardown and rebuild scripts
Compose gave you down, down -v and up --build for free.
Write them once per project and they stay useful:
# down.ps1 — stop and remove containers, keep data
$services = @("web","api","db")
foreach ($s in $services) {
wslc stop $s 2>$null
wslc container remove $s 2>$null
}
Write-Host "stopped: $($services -join ', ')"
# down.ps1 -Volumes — also destroy data
if ($Volumes) {
wslc volume remove pgdata 2>$null
wslc network remove appnet 2>$null
Write-Host "removed volumes and network"
}
Stop in reverse dependency order (web, api, db) so the application layer does not spend its last
seconds throwing connection errors into your logs. Keep the volume removal behind an explicit flag
— the equivalent of -v on compose down — so a routine restart
never destroys your development database.
When not to migrate
Translation is mechanical but it is not free, and there are stacks where the honest answer is to stay on Docker Desktop:
- More than about six services. The script becomes a small orchestrator you now maintain. That is real ongoing cost against a Compose file that already works.
- Compose profiles driving several environments. Reimplementing profile selection in script parameters is doable and tedious.
- Testcontainers or CI depending on a Docker socket. wslc exposes no Docker-compatible socket, so these do not work at all.
- Multi-arch builds. No
buildx, noplatform. - Containers expected to survive reboot. No restart policies.
A reasonable middle path: migrate the single-container projects to wslc, keep Docker Desktop installed but not starting on login, and launch it only for the Compose stacks. You get most of the idle-memory and file-I/O benefit without rewriting orchestration you already trust. The comparison guide covers the coexistence trade-offs in more detail.
FAQ
- Does wslc support docker compose?
- No. The wslc public preview has no Compose runtime and no wslc compose subcommand. Multi-service stacks must be translated into individual wslc run commands, usually driven by a PowerShell or bash script, or kept on Docker Desktop.
- How do I run a docker-compose.yml with WSL containers?
- Translate it manually: create a user-defined network with wslc network create, create named volumes with wslc volume create, then start each service as a wslc run command in dependency order, using the service name as the container name so DNS resolution between containers works.
- Which Compose fields have no wslc equivalent?
- deploy, restart, healthcheck, depends_on with condition, profiles, configs, secrets, extends and build.platform have no direct equivalent. depends_on ordering must be enforced by your script, and healthchecks must be replaced with a readiness poll loop.
- Can I keep using Compose alongside wslc?
- Yes. Docker Desktop and wslc use separate utility VMs and image stores, so you can keep Docker installed for Compose projects and use wslc for single-container work. The cost is duplicated images and disk usage.