Migration from Docker Compose¶
Audience: Users familiar with
docker composewho want to run their stacks with Container-Compose on macOS.Platform note: Container-Compose targets macOS with
apple/containeras its runtime. Linux containers are run in lightweight VMs per container. Some Docker Compose features depend on Linux kernel primitives (cgroups, namespaces, block I/O controllers) that do not have a direct equivalent in this model.
Quick start¶
If your stack uses only the common Compose primitives, it may work with no changes:
The compose file name (compose.yml, compose.yaml, docker-compose.yml,
docker-compose.yaml) and the --file / -f flag work the same way.
COMPOSE_PROJECT_NAME and COMPOSE_FILE environment variables are respected.
What is unchanged:
- image:, build:, ports:, volumes: (bind mounts and named), environment:
- depends_on: (including condition: service_healthy and
condition: service_completed_successfully)
- networks: (with bridge driver; driver is mapped to container-network-vmnet)
- configs: and secrets: with file: sources
- Multi-file include: support
Supported features¶
The full field-by-field coverage matrix lives in
docs/feature-parity.md. The summary by tier:
| Tier | What it means | User impact |
|---|---|---|
| Supported | Field is decoded and passed to the runtime | Works as expected |
| Tier 1 — Wireable | Runtime support exists, field is parsed but not fully applied | Limited functionality; see workarounds below |
| Tier 3 — Upstream FR | Feature requires apple/container changes |
Not available; field is warn-skipped |
| Tier 4 — Won't do | Linux/Windows-specific or Swarm-only | Not applicable on macOS; field is silently skipped |
Key supported highlights:
- Port publishing (-p / ports:)
- Named volumes (local driver) and bind mounts
- Build contexts including dockerfile_inline: and target:
- Multi-service dependency ordering with depends_on
- Environment variable substitution and .env files
- Configs and secrets (file-sourced; bind-mounted into containers)
- DNS configuration (dns:, dns_search:, dns_opt:)
- Capabilities (cap_add:, cap_drop:)
- CPU and memory limits (deploy.resources.limits.cpus/memory,
mem_limit:, cpus:)
Workarounds for partial features¶
Networks: driver options, IPAM, IPv6, aliases¶
What does not work: networks.<n>.driver_opts, networks.<n>.ipam.*,
networks.<n>.enable_ipv6, and network-level aliases: are all warn-skipped
(CHAOS-1334 — blocked on apple/container upstream).
Workaround: For service discovery, containers on the same bridge network
can reach each other by container name. The --alias flag is not supported, so
cross-service references must use the compose service name, which Container-Compose
maps directly to the container id (<project>-<service>).
# This works for basic inter-service communication:
services:
app:
image: myapp:latest
networks:
- backend
db:
image: postgres:16
networks:
- backend
networks:
backend:
driver: bridge # mapped to container-network-vmnet automatically
Static IP assignment (networks.<n>.ipv4_address,
networks.<n>.ipv6_address) produces runtime errors — the --ip and --ip6
flags are not accepted by apple/container. Remove these fields or replace
them with service-name-based discovery.
Secrets and configs: runtime APIs vs. file-sourced¶
What works today: secrets: and configs: with file: sources are
fully supported — the file contents are bind-mounted into the container at the
specified target path.
secrets:
db_password:
file: ./secrets/db_password.txt
services:
app:
secrets:
- db_password
# /run/secrets/db_password is available inside the container
What does not work: The POST /secrets REST API (runtime-managed secrets
without a file source) is not implemented in either production backend — see
RuntimeError.notSupported under
docs/guides/error-codes.md.
External secret backends (secrets.<n>.external: true) are not supported.
Workaround: For external secrets, read the secret from your secrets manager in an entrypoint script and write it to the expected path at container start. Or use environment variables (less secure but simpler):
Named volumes: non-local drivers¶
What works: Named volumes with the local driver (the default) are
fully supported via container volume create:
What does not work: Non-local volume drivers (nfs, tmpfs, plugin
drivers) fall back to a local directory. The driver_opts: field is parsed
but not wired to container volume create --opt yet (CHAOS-1335 open).
Workaround: For NFS-backed data, bind-mount the NFS share directly from the macOS host rather than using a volume driver:
For tmpfs mounts, use the tmpfs: field directly (supported):
Volume modes: read-only and selinux labels¶
The compose :ro volume modifier is parsed and passed to the runtime.
Docker-specific selinux labels (:z, :Z) are not supported by
apple/container and should be removed from your compose file.
Resource limits: unsupported fields¶
deploy.resources.limits.cpus and deploy.resources.limits.memory (and their
top-level equivalents cpus:, mem_limit:) are supported.
Many granular resource flags accepted by Docker are not supported by
apple/container and are warn-skipped by Container-Compose. Setting them in
your compose file produces a console warning but does not cause failure:
shm_size:,pids_limit:,mem_swappiness:,memswap_limit:cpu_shares:,cpuset:,cpu_period:,cpu_quota:oom_kill_disable:,oom_score_adj:
These are all Tier 0 or Tier 3 items in
docs/feature-parity.md. The fields are decoded and
a warning is printed; the container starts without the constraint.
Action: Remove these fields from your compose file to suppress the warnings. The container will start but without the resource constraint applied.
blkio_config: is a guarded fork feature: Container-Compose emits
--blkio-weight, --blkio-weight-device, and the per-device bandwidth / IOPS
flags only when the installed container run / container create supports
them. Older or upstream-only runtimes still warn-skip the field.
Healthcheck: declaration vs. enforcement¶
healthcheck: is fully decoded and the depends_on: condition: service_healthy
flow reads health status via the fork's ContainerSnapshot.health extension
(CHAOS-1319). Container-Compose emits --health-cmd / --health-* only when
the installed full-chaos/container fork advertises those flags. Older
runtimes are warn-skipped, so the healthcheck subprocess loop is not executed
by the runtime itself in that environment.
Practical impact: If your depends_on chain relies on health checks passing, you may see the dependent service start before the upstream service is truly ready if the upstream service's health check implementation is not done in the entrypoint.
Workaround: Use condition: service_started (default) or
condition: service_completed_successfully for init containers instead of
relying on service_healthy for readiness gating.
Restart policies¶
restart: always, restart: on-failure, restart: unless-stopped, and
restart: no are decoded and emitted as --restart to container run. This
flag exists only in the Container-Compose-pinned fork of apple/container
(full-chaos/container branch tier2-fork-patches) and is not in upstream
apple/container. It will be warn-skipped once the fork is retired.
Logging: logging: block¶
logging.driver and logging.options are warn-skipped. apple/container
uses a fixed log layout and does not support external log drivers. Container
logs remain available via compose logs.
Real-world examples¶
Postgres + Redis (stateful services)¶
A standard Postgres + Redis stack translates directly:
# docker-compose.yml (Docker version)
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
cache:
image: redis:7-alpine
volumes:
- redisdata:/data
ports:
- "6379:6379"
healthcheck:
test: "redis-cli ping"
interval: 5s
retries: 20
volumes:
pgdata:
redisdata:
This compose file works with Container-Compose without any changes.
The named volumes pgdata and redisdata are created as local volumes
via container volume create. The healthcheck: block is decoded but not
executed by the runtime — Redis will report ready immediately after start.
Run it:
WordPress + MySQL¶
WordPress and MySQL compose files frequently use restart: always and
depend on MySQL being ready before WordPress starts.
# docker-compose.yml (original)
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootsecret
MYSQL_DATABASE: wordpress
MYSQL_USER: wpuser
MYSQL_PASSWORD: wpsecret
volumes:
- dbdata:/var/lib/mysql
restart: always
wordpress:
image: wordpress:latest
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wpuser
WORDPRESS_DB_PASSWORD: wpsecret
WORDPRESS_DB_NAME: wordpress
depends_on:
- db
restart: always
volumes:
dbdata:
What to adjust:
-
restart: always— currently works via the fork's--restartflag but will become a warning when the fork is retired. Acceptable for now. -
depends_on: dbusescondition: service_started(the default). MySQL takes a few seconds to initialize; WordPress may connect before MySQL is ready. Add a retry loop in your WordPress entrypoint or use a wait script:
Then in your WordPress startup, implement retry logic for the database
connection (e.g., using wait-for-it.sh or the built-in WordPress connection
retry).
- Service discovery:
WORDPRESS_DB_HOST: db— this relies on the service namedbresolving to the MySQL container. Container-Compose names containers<project>-<service>(e.g.myproject-db). Check whether your network configuration routes the short namedbor the full container name. If short-name resolution does not work, override with the full container id or use the IP address discovered at runtime.
Simple web app with build + Postgres¶
# Original docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
DATABASE_URL: "postgres://appuser:apppass@db:5432/appdb"
depends_on:
db:
condition: service_started
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: apppass
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
This works as-is. The build: block uses the standard Dockerfile in the
current directory. Note:
- If your Dockerfile uses
RUN --mount=type=cacheBuildKit cache directives, these work becausecontainer buildsupports BuildKit. build.cache_fromandbuild.cache_toare warn-skipped (CHAOS-1377 / CHAOS-1397). Remove these from thebuild:block to suppress warnings.build.sshis warn-skipped. Use multi-stage builds with baked-in SSH keys or pass credentials via--secretinstead.
Build the images first, then start:
Troubleshooting¶
"unknown option --\<flag>" at runtime¶
This means Container-Compose is passing a flag to container run that
apple/container does not accept. This is a Tier 0 issue documented in
docs/feature-parity.md.
Common culprits and fixes:
| Compose field | Symptom | Fix |
|---|---|---|
security_opt: |
unknown option --security-opt |
Remove field (CHAOS-1371) |
userns_mode: |
unknown option --userns |
Remove field |
ipc: |
unknown option --ipc |
Remove field (CHAOS-1372) |
pid: |
unknown option --pid |
Remove field |
uts: |
unknown option --uts |
Remove field |
devices: |
unknown option --device |
Remove field (CHAOS-1373) |
sysctls: |
unknown option --sysctl |
Remove field |
mac_address: |
unknown option --mac-address |
Remove field (CHAOS-1374) |
networks.<n>.ipv4_address: |
unknown option --ip |
Remove field |
networks.<n>.ipv6_address: |
unknown option --ip6 |
Remove field |
For a complete list see docs/feature-parity.md §3.
Service <name> must define either 'image' or 'build'¶
This is ComposeError.imageNotFound. See
error-codes.md.
External volume '<name>' was not found¶
This is ComposeError.externalVolumeNotFound. Create the volume before
running compose:
See error-codes.md.
Container exits immediately on compose up¶
- Check logs:
container-compose logs <service> - Run the container directly to see its output:
container-compose run --no-deps <service> - Verify the entrypoint/command: common issue is a Docker-specific entrypoint
that references
/docker-entrypoint.shpaths not present in the image variant.
compose up fails with "No such container"¶
A dependency service was removed or never started. Run:
If the error persists, check for container id conflicts with container ps -a.
Named volume data is missing after compose down¶
compose down does not remove named volumes by default (same behavior as
Docker Compose). Pass --volumes to also remove volumes:
To inspect or back up volume data before removal, the volume's data is stored
in the apple/container volume registry. Use container volume inspect <name>
to find the host path.
Timed out after <N>s waiting for container '<name>'¶
This is ComposeWaitError.timeout. See
error-codes.md.
A service is not starting in time. Check:
1. Is the upstream service actually starting? compose logs <upstream>
2. Is the condition reachable? service_healthy requires a healthcheck;
service_completed_successfully requires a zero exit code.
REST API returns 501 for create/network/secret operations¶
If you are calling the Container REST API directly (not the compose CLI), you
may hit .notSupported errors for operations the Bridge backend cannot handle.
See the Runtime Protocol Contract
for which operations are affected and why. Use the CLI compose commands for
these workflows instead.
Feature support summary¶
The following table summarizes the most common Docker Compose fields and their
status in Container-Compose. For the full matrix, see
docs/feature-parity.md.
| Field | Status | Notes |
|---|---|---|
image: |
Supported | Auto-qualifies short names to docker.io/library/<name> |
build: |
Supported | cache_from, cache_to, ssh, network are warn-skipped |
ports: |
Supported | |
environment:, env_file: |
Supported | |
volumes: bind mounts |
Supported | File-level bind mounts not supported (directory only) |
volumes: named volumes |
Supported | local driver only; driver_opts not wired |
depends_on: |
Supported | service_healthy, service_completed_successfully supported |
networks: |
Supported | driver: bridge → container-network-vmnet; driver_opts, IPAM blocked |
configs:, secrets: (file) |
Supported | external: true not supported |
healthcheck: |
Partial | Decoded; emitted only when the installed fork supports --health-cmd / --health-*; depends_on reads health status |
restart: |
Partial | Fork-only; will become warn-skip when fork is retired |
logging: |
Warn-skipped | External log drivers not supported |
security_opt:, userns_mode: |
Warn-skipped | apple/container has no equivalent |
devices:, sysctls: |
Warn-skipped | Kernel-level features |
ipc:, pid:, uts: |
Warn-skipped | Linux namespace modes |
shm_size:, pids_limit: |
Warn-skipped | cgroup features not exposed |
cpu_shares:, cpuset:, etc. |
Warn-skipped | Fine-grained CPU controls not exposed |
blkio_config: |
Partial | Emitted only when the installed fork supports --blkio-* / --device-*-* |
deploy.replicas, deploy.placement |
Skipped | Swarm-only fields |
service.links, volumes_from: |
Skipped | Deprecated by compose-spec |
See also¶
- Error Codes Reference — meaning and resolution for every error
- Runtime Protocol Contract — how the runtime layer works and its gaps
- Feature Parity Inventory — the canonical field-by-field coverage matrix
- Reviews — code review notes and context