There is no 1:1 mapping from docker run flags to Compose service keys. Some flags map cleanly (-p → ports, -e → environment). Some map to a key that looks right but behaves differently (--restart is not deploy.restart_policy). And some have no Compose equivalent at all because the behavior belongs to the CLI invocation, not the service definition (--rm, -d, -it). This guide maps 31 flags a typical docker run command might use, cites Docker's own CLI reference and the Compose Specification for every claim, and runs real commands through our free Docker Run to Compose converter to show what it actually outputs today — including three parsing bugs we found while writing this.
Use the method below if you want to convert by hand, use the converter for anything with the flags it supports, and read the "what the converter gets wrong" section before you trust its output on a command with --rm, -it, or --privileged sitting right before the image name.
How to convert a docker run command to Compose
Split the command into one flag (or flag pair) at a time — do not try to convert the whole line in your head.
Look up each flag in the reference table below to find its Compose service key, or confirm it has none.
For anything with no listed key (
--rm,-d,-it,--pull), decide whether the behavior belongs indocker compose run/upflags instead of the YAML.Paste the result into the converter to catch formatting mistakes, then diff it against your hand conversion with the diff checker if the two disagree.
The full flag-to-Compose-key reference
Compose key names and defaults below are quoted from the Compose Specification services reference unless noted otherwise.[2] CLI flag defaults are from the docker container run reference.[1]
| docker run flag | Compose key | Notes |
|---|
| -p, --publish | ports | Short form [HOST:]CONTAINER[/PROTOCOL]. No host IP means it binds 0.0.0.0 — reachable from outside, in both the flag and the key. |
| -v, --volume | volumes (short form) | Same syntax both places. A missing host directory is created automatically by both the CLI and Compose's short/long bind syntax. |
| --mount | volumes (long form, type: bind) | Different defaults — see "Short volumes vs. --mount" below. |
| -e, --env | environment | Map or array form. -e NAME with no = copies the value from your shell at runtime — Compose resolves it the same way, and unsets it if the variable isn't exported. |
| --env-file | env_file | Compose processes multiple files top-down (last one wins) and defaults required: true — a missing file is an error unless you set required: false. |
| --name | container_name | Must match [a-zA-Z0-9][a-zA-Z0-9_.-]+. Setting it stops Compose from scaling that service past one container. |
| --restart | restart | Same four values as the CLI (no, always, on-failure[:max-retries], unless-stopped). Not the same key as deploy.restart_policy — see below. |
| --network, --net | networks | A service with no networks: joins the implicit default network instead of none. |
| --network-alias | networks.<name>.aliases | Nested under the network entry, not a top-level key. |
| -u, --user | user | Same name|uid[:group|gid] format. Defaults to the image's user (commonly root) if unset. |
| -w, --workdir | working_dir | Creates the directory inside the container if the image doesn't already have it. |
| --entrypoint | entrypoint | A non-null value here makes Compose ignore the image's default CMD, same as the CLI flag ignores it. |
| trailing command/args | command | Overrides image CMD. Unlike a Dockerfile's CMD, Compose does not run it through the image's SHELL automatically. |
| --cap-add / --cap-drop | cap_add / cap_drop | Lists of capability strings, same names both places. |
| --device | devices | HOST_PATH:CONTAINER_PATH[:CGROUP_PERMISSIONS] in both. Under --privileged/privileged: true, only the specified cgroup permissions are ignored — the device mapping itself still applies, and the container gets every other host device too. |
| --init | init | Boolean in Compose (init: true) vs. a bare flag on the CLI. |
| --read-only | read_only | Boolean in Compose (read_only: true) vs. a bare flag on the CLI. |
| --tmpfs | tmpfs | A path, or a list of paths/path:options. Compose's options are mode, uid, gid. |
| --health-cmd / --health-interval / --health-timeout / --health-retries / --health-start-period | healthcheck.test / .interval / .timeout / .retries / .start_period | Compose's docs say it "has the same default values" as a Dockerfile HEALTHCHECK, without restating the numbers. The CLI defaults it does list: --health-interval, --health-timeout and --health-start-period all default to 0s. |
| --memory, -m / --cpus | deploy.resources.limits.memory / .cpus | The Compose Specification defines this key with no Swarm-only wording — what that does and doesn't prove is below.[3] A flatter mem_limit: / top-level cpus: pair is also valid and must stay consistent with the deploy values if both are present. |
| -l, --label | labels | Map or array of key=value. The com.docker.compose prefix is reserved — using it in your own label is a Compose error. |
| -h, --hostname | hostname | Must be a valid RFC 1123 hostname in both. |
| --add-host | extra_hosts | HOSTNAME=IP short form (also accepts : since Compose 2.24.1). host-gateway resolves to the host's internal IP either way.[1] |
| --privileged | privileged | Boolean in Compose. Docker's own wording: "extended privileges," disables the default seccomp/AppArmor profile — the CLI docs warn "a container with --privileged is not a securely sandboxed process." |
| --shm-size | shm_size | Byte-value string, same format. |
| --ulimit | ulimits | CLI format is type=soft[:hard]; Compose takes an integer (single limit) or a {soft, hard} map per ulimit type. |
| --log-driver / --log-opt | logging.driver / logging.options | Defaults are platform-specific on both sides — neither doc lists a universal default driver. |
| -i/--interactive, -t/--tty (-it) | stdin_open, tty | Two separate booleans — the Compose docs say stdin_open is "the same as running a container with the -i flag" and tty is "the same as running a container with the -t or --tty flag." No combined -it-style shorthand key. |
| --rm | none | No service key. See below. |
| -d, --detach | none (CLI flag on up/run) | Not a service key. docker compose up -d has the flag; the YAML itself has nothing to set. |
| --platform | platform | os[/arch[/variant]], e.g. linux/arm64/v8. CLI flag needs daemon API 1.32+. |
| --pull | pull_policy | CLI only has always/missing/never (default missing). Compose's pull_policy adds build, daily, weekly, every_<duration>, and if_not_present as an alias for missing. |
No Compose key: --rm, -d, -it
Three of the most common flags in any docker run line have nothing to write in the YAML, because they describe how you want to run the container right now, not how the service is defined.
--rm has no service key because docker compose up doesn't have a --rm flag at all — it isn't in the command's options table.[6] When the up command exits, Docker's docs say only that "all containers are stopped," not removed.[6] docker compose run does have --rm, documented as "Automatically remove the container when it exits," but it's opt-in — every example in the docs adds it explicitly, the same as plain docker run.[7] If you relied on --rm to keep a host clean of one-off containers, that's a habit to carry into whichever Compose subcommand you actually use, not a line to add to compose.yaml.
-d/--detach is the same shape of answer: it's a flag on docker compose up (and run), not a service key.[6] docker compose up -d gets you the same background-and-print-ID behavior you'd get from docker run -d.
-it (short for -i -t) is the one case where the CLI flag does have Compose keys — stdin_open: true and tty: true — but they only matter if you're running docker compose run or attaching interactively; they're inert for a background docker compose up service that nothing ever attaches to.
Short volume syntax vs. --mount: who creates the directory
Here's a divergence that bites people the other direction from what they expect. Docker's bind-mount docs are explicit: -v/--volume "automatically creates the directory on the host for you" if it's missing, while "--mount does not automatically create a directory if the specified mount path does not exist on the host" — you get an error instead, unless you add the separate bind-create-src option.[8]
You'd expect Compose's long type: bind syntax, which maps to --mount, to inherit that fail-on-missing-path default. It doesn't. The Compose Specification sets bind.create_host_path to default to true: "Creates a directory at the source path on host if there is nothing present."[2] So a typo'd host path under Compose's short or long volume syntax silently becomes an empty directory instead of an error — the opposite of what the raw --mount flag it's modeled on would do. If you want the CLI's fail-closed behavior inside Compose, set create_host_path: false explicitly on the long form.
--restart is not deploy.restart_policy
The plain restart: key takes the same four values as docker run --restart — no, always, on-failure[:max-retries], unless-stopped — and the Compose docs point straight at the docker run --restart reference for what they mean.[2] That's the key our converter uses, and it's the right one for a direct flag conversion.
deploy.restart_policy is a different, Swarm-service-shaped schema entirely: condition (none/on-failure/any, defaulting to any), delay (default 0), max_attempts (default unlimited) and window, the evaluation period before Swarm decides a restart succeeded.[3] If you're copying a block from a Swarm stack file into a non-Swarm compose.yaml and expect it to behave like --restart, don't — it's a different four fields with no window or condition equivalent on the plain restart: key, and the Compose docs themselves note that when deploy.restart_policy isn't set, Compose falls back to the service's restart field.[3]
--memory and --cpus: what deploy.resources.limits actually traces back to
The common advice online is that the entire deploy: section — including resources.limits — is a Swarm-only artifact that plain docker compose up silently ignores unless you pass --compatibility. That advice has one real citation behind it, and it's more than eight years old: a GitHub thread opened February 17, 2018, about the original Python docker-compose v1, not the Go-based Compose v2 plugin you almost certainly have installed now (the one invoked as docker compose, no hyphen).
Here's exactly what that thread shows, because it gets misquoted a lot: a Docker maintainer, in a July 31, 2018 comment, ran docker-compose --compatibility up -d — the legacy v1 binary, with the compatibility flag explicitly set — against a file with resources.limits.cpus: '0.50' and a 50M memory limit. docker inspect on the resulting container reported "Memory": 52428800 and "NanoCpus": 500000000, matching the YAML.[9] That proves the translation mechanism existed in 2018 — for v1, with the flag set. It says nothing about v2 with no flag. The same maintainer's own framing in that thread is that deploy options are "scheduling options for swarm-mode services," and Compose "deploys local containers," so there's no orchestrator to apply them to on its own.[9] An October 3, 2023 reply in the same thread adds only that v2 "now uses the compose-specification file format which does not use a version" — it doesn't say whether resources.limits applies without Swarm or without --compatibility today.[9]
What we can confirm from Docker's current docs: the Compose Specification defines resources.limits as a plain key with no Swarm-only or version-gating language anywhere on the page — a real difference from the versioned v3 schema that 2018 thread was arguing about.[3] The docker compose up command reference has no --compatibility flag at all; it isn't in the options table.[6] What we could not confirm anywhere on docs.docker.com is whether a plain docker compose up, no flags, actually enforces resources.limits on a single engine today. Don't take our converter's deploy.resources.limits output on faith: run docker compose up -d on your own file and check docker inspect <container> for Memory/NanoCpus against the YAML before you rely on it in production.
Worked example: a Node API, by hand
Here's a realistic docker run line and its hand-written Compose equivalent, using the mappings above:
docker run -d --name api \
-p 3000:3000 \
-v ./logs:/app/logs \
-v appdata:/data \
-e NODE_ENV=production \
-e DATABASE_URL=postgres://admin:secret@db:5432/myapp \
--network app-net \
--restart unless-stopped \
--memory 512m --cpus 0.5 \
node:20-alpine node server.js
services:
api:
image: node:20-alpine
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./logs:/app/logs
- appdata:/data
environment:
NODE_ENV: production
DATABASE_URL: "postgres://admin:secret@db:5432/myapp"
networks:
- app-net
deploy:
resources:
limits:
memory: 512m
cpus: "0.5"
command: node server.js
volumes:
appdata:
networks:
app-net:
-d doesn't appear anywhere in the YAML — you'd run this with docker compose up -d instead, as covered above.
We ran that exact command through our own converter
Rather than hand-wave that our Docker Run to Compose converter "handles this," we ran it. The tool runs entirely in the browser, so we extracted its parsing logic unmodified and ran it against the command above, pasted as a single line. Here is the actual output, byte for byte:
services:
api:
image: node:20-alpine
restart: unless-stopped
ports:
- "3000:3000"
volumes:
- ./logs:/app/logs
- appdata:/data
environment:
NODE_ENV: production
DATABASE_URL: "postgres://admin:secret@db:5432/myapp"
networks:
- app-net
deploy:
resources:
limits:
memory: 512m
cpus: "0.5"
command: node server.js
volumes:
appdata:
networks:
app-net:
That matches the hand conversion exactly, as long as the command lands in the textarea as one line. For the flags it supports — -p, -v, -e, --name, --restart, --network, --memory, --cpus, and a trailing command — the converter is reliable on single-line input. (-d is also recognized, but produces no YAML either way, same as covered above — it's a CLI flag, not a service key.) "As long as it's one line" turns out to matter a lot, which is the first bug below.
Three bugs we found while testing it
Bug 1: its own placeholder example breaks the tool
The converter's input box ships with this exact placeholder text, shown in gray before you type anything:
docker run -d --name nginx \
-p 80:80 -p 443:443 \
-v /data/nginx:/etc/nginx \
--restart always \
nginx:alpine
Paste that in verbatim — line breaks and trailing backslashes included, exactly as it's formatted in the placeholder — and the actual output is four parse errors plus one "parsed" service that isn't nginx at all:
Could not parse: " -p 80:80 -p 443:443 \..."
Could not parse: " -v /data/nginx:/etc/nginx \..."
Could not parse: " --restart always \..."
Could not parse: " nginx:alpine..."
The one "parsed" service that results has image: \ — a lone backslash as the image name, with no quotes around it (the converter's YAML writer never quotes image). The root cause: the component splits the textarea's contents into lines on every \n before handing each line to the parser, and treats every line as an independent docker run invocation. The parser itself does have code to collapse a trailing \ plus newline into a space — but by the time a line reaches that code, the newlines are already gone, so that collapse logic never has anything to do. A multi-line, backslash-continued command, which is how Docker's own docs and most READMEs format anything with more than two or three flags, has to be joined into one line before you paste it in. The box's own example doesn't follow that rule.
Bug 2: --rm immediately before the image fails outright
Everything outside the supported flag set goes through a generic "skip this flag, and skip the next token too if it doesn't look like another flag" fallback. That fallback assumes every unrecognized flag takes exactly one value. Boolean flags that take no value break it, and two of the most common ones are --rm and -it.
Feed the converter docker run --rm nginx:alpine on its own and it fails outright:
Could not parse: "docker run --rm nginx:alpine..."
What happens: the parser sees --rm, doesn't recognize it, advances past it, and then — because the next token (nginx:alpine) doesn't start with a dash — treats the image name as --rm's value and skips it too. There's nothing left to become the image, so the whole line errors.
Bug 3: -it before the image silently produces the wrong service
This one is worse than Bug 2, because it doesn't error — it silently produces the wrong service. The cause is -it alone. Feed the converter docker run -it ubuntu:24.04 bash and the real output is:
services:
app:
image: bash
-it is an unrecognized flag, so the generic fallback skips it, then checks whether the next token looks like another flag. ubuntu:24.04 doesn't, so the fallback swallows it as -it's "value" too, discarding the real image entirely. The next bare token, bash — which was supposed to be the command — gets assigned as the image instead, because it's the first non-flag token the parser ever reaches. The output has no error, looks plausible, and is completely wrong. Putting --rm in front of it, as in docker run --rm -it ubuntu:24.04 bash, doesn't change the outcome: -it starts with a dash, so --rm's own fallback skips only itself, leaving -it to eat ubuntu:24.04 exactly as before. We confirmed a different failure mode with --privileged and --init placed directly before an image with no trailing command: both make the converter report "Could not parse" for the same reason --rm alone does in Bug 2 — there's no bare token left over to become the image.
The practical rule until this is fixed: never put a zero-argument flag — --rm, -it, --privileged, --init, --read-only, or any other flag that takes no value — directly before the image. Put it anywhere else in the flag list, then check the image name in the tool's "Parsed N services" summary before you trust the YAML.
Flags the converter silently drops today
Everything in the reference table that isn't -d, -p, -v, -e, --name, --restart, --network/--net, --memory/-m, or --cpus gets silently discarded — no error, no warning, just absent from the output. We confirmed this by running a command with sixteen of them at once, pasted as a single line (multi-line input here hits the same parsing failure as Bug 1 above):
docker run --user 1000:1000 -w /app --entrypoint /bin/sh --cap-add SYS_PTRACE --device /dev/snd:/dev/snd --health-cmd "curl -f http://localhost/ || exit 1" --health-interval 30s --health-retries 3 --label com.example.app=web --hostname web1 --add-host db.local:10.0.0.5 --platform linux/amd64 --pull always --mount type=bind,source=/data,target=/data --env-file ./app.env --network-alias web myorg/app:1.2.3
Actual output:
services:
app:
image: myorg/app:1.2.3
Every one of those sixteen flags — --user, -w, --entrypoint, --cap-add, --device, all three health flags we passed, --label, --hostname, --add-host, --platform, --pull, --mount, --env-file, and --network-alias — vanished. None of these happened to collide with the image-eating bug above (each one here is immediately followed by its own value, not by the image), so you get a clean, valid, silently-incomplete compose.yaml rather than an error or a wrong image. Add the keys for these by hand from the reference table above — they're the right Compose keys, the tool just doesn't write them yet.
Converting .env files along the way
If your docker run command used --env-file app.env, the Compose key is env_file: [app.env] — a one-line swap, since Compose reads the same VAR=value format. Where it gets messier is when a long -e/--env list should become an env_file instead for readability, or vice versa. Our free .env file converter handles reshaping between .env, JSON, and shell-export formats if you're restructuring environment variables as part of the migration rather than just converting flag syntax.
FAQ
Does docker compose up remove containers the way docker run --rm does?
No. docker compose up has no --rm flag — Docker's own reference for the command confirms it isn't in the options table — and stopping the command only stops containers, it doesn't remove them.[6] docker compose run has --rm, but like plain docker run, it's opt-in, not default.[7]
Why did my converted compose.yaml create an empty folder instead of erroring?
Because both Compose's short and long bind-mount syntax default to creating a missing host directory (bind.create_host_path: true), the same way -v does on the CLI.[2] If you want a typo to fail loudly instead, add create_host_path: false to the long volume form.
Will deploy.resources.limits actually limit my container without Swarm?
We couldn't confirm that from Docker's own docs either way. The Compose Specification defines resources.limits with no Swarm-only language,[3] but the only runtime demonstration we found is more than eight years old and used the legacy v1 CLI with --compatibility explicitly set — not today's v2 plugin with no flag.[9] Check it yourself: run docker compose up -d and compare docker inspect's Memory/NanoCpus fields against your YAML.
Can I just copy my --restart value into deploy.restart_policy?
No — they're different schemas. restart: takes the same four values as the CLI flag. deploy.restart_policy uses condition/delay/max_attempts/window, a Swarm-service shape with no 1:1 mapping back to the simple flag.[3]
What should I do with flags the converter drops?
Add them by hand using the Compose key from the reference table above. None of the dropped flags in our test run broke the output — they're just missing from it, so the fix is additive, not corrective.
Once you have a complete compose.yaml, the next step is making it production-ready — health check ordering, secrets, and resource limits that actually hold under load are covered in our Docker Compose for production guide. Paste your own command into the Docker Run to Compose converter, then check anything with a zero-argument flag before the image against the "Parsed N services" summary it shows you.
Sources
docker container run, Docker CLI reference, Docker Inc. (accessed November 5, 2026)
Services top-level element, Compose Specification, Docker Inc. (accessed November 5, 2026)
Deploy top-level element, Compose Specification, Docker Inc. (accessed November 5, 2026)
Volumes top-level element, Compose Specification, Docker Inc. (accessed November 5, 2026)
Networks top-level element, Compose Specification, Docker Inc. (accessed November 5, 2026)
docker compose up, Docker CLI reference, Docker Inc. (accessed November 5, 2026)
docker compose run, Docker CLI reference, Docker Inc. (accessed November 5, 2026)
Bind mounts, Docker Engine storage docs, Docker Inc. (accessed November 5, 2026)
9. docker/compose pull request #5684, maintainer comment thread, GitHub (accessed November 5, 2026)
Originally published at wowhow.cloud
Top comments (0)