Skip to content
Advertisement

docker run to Compose Converter

Turn a docker run command into a Compose service — with every flag either translated or listed with the reason it could not be.

Converter Config

11 lines · 280 chars

service “postgres” · postgres:16-alpine

1 flag not carried over

  • --detach

    Detaching is decided when you bring the stack up (docker compose up -d), so it is not a service key.

3 things worth checking

  • Declared the named volume “pgdata” at the top level. Compose refuses to start a service that mounts a volume the file never declares.
  • container_name pins the container to one name, so the service can never be scaled past a single replica. Remove it unless something outside Compose looks the container up by name.
  • Resource limits are written under deploy.resources. Compose v2 applies them locally; older docker-compose v1 ignored everything under deploy unless you were deploying to Swarm.

Settings

How docker run to Compose Converter works

A `docker run` invocation and a Compose service describe the same container, but they are shaped for different moments. The command line is imperative and disposable: it exists in your shell history, it applies once, and the knowledge of why each flag is there lives in whoever typed it. A Compose service is declarative and reviewable: it sits in a file, it goes through the same pull request as the code, and it can be applied again on a machine that has never seen the original command. Converting between them is mostly renaming, since Compose grew out of the run flags — but not entirely, and the gaps are where the interesting part lives.

Roughly three quarters of the flags are a direct rename. `--publish` becomes `ports`, `--volume` becomes `volumes`, `--workdir` becomes `working_dir`, `--restart` keeps its own name and its own vocabulary. Repeatable flags become sequences in the order they were written, which matters more than it sounds: published ports are matched positionally by nobody, but environment variables and capability changes are read top to bottom by people, and a shuffled list is a diff that reviewers cannot follow.

Some flags change shape rather than name. Docker takes a health check as a shell string and Compose takes a list whose first element declares how to interpret the rest, so `--health-cmd "curl -f localhost"` has to become `["CMD-SHELL", "curl -f localhost"]`. Memory and CPU ceilings move down two levels into `deploy.resources.limits`. A `--ulimit nofile=1024:2048` splits into a mapping of a soft and a hard bound. And `--network` lands in one of two entirely different keys depending on its argument: the four reserved modes are `network_mode`, while any other name is an entry in the service’s `networks` list that the file then has to declare at the top level, because Compose refuses to start against a network or volume it has never heard of.

The rest have no Compose equivalent at all, and this is the category that decides whether a converter is trustworthy. `-d` describes how you invoked the command, not what the container is, so it belongs to `docker compose up` rather than to any service key. `--rm` contradicts the entire premise of a file that expects to be brought up and down repeatedly. `--network-alias` is real in Compose but lives under the network entry rather than on the service. A converter that drops these without a word hands back a file that starts a subtly different container and looks complete while doing it, which is worse than one that refuses — so every flag here ends up either translated or named in a list with the reason it was not.

Reference

  • docker run [flags] IMAGE [command] → services.<name> in the Compose Spec
  • --publish → ports · --volume/--mount → volumes · --env → environment · --env-file → env_file
  • --memory, --cpus → deploy.resources.limits · --gpus → deploy.resources.reservations.devices
  • --health-cmd C → healthcheck.test: ["CMD-SHELL", C]
  • --network host|none|bridge|container:X → network_mode; any other name → networks + a top-level declaration
  • No top-level version: key — it has been obsolete since Compose v2 and only earns a warning

How to use this converter

  1. Paste the command exactly as you ran it

    Backslash-continued lines, quoted values and a leading sudo are all fine, and so is podman or the longer docker container run spelling. The command is split the way a POSIX shell would split it, so quoting is respected rather than guessed at.

  2. Read the generated service

    Keys are ordered by what a person looks for first — what the container is, then what it runs, then what it talks to, then how it is constrained — rather than by the order the flags happened to arrive in.

  3. Work through the flags that were not carried over

    Each one is listed with the reason. Some are genuinely run-time concerns you can forget about; others, like a static IP or a network alias, need a few lines added by hand in a place the service key cannot reach.

  4. Check the things worth checking

    Named volumes and external networks that were added for you, shell expansions that were resolved before Docker ever saw them, and any variable passed through from your own environment are all called out separately from the errors.

  5. Decide about container_name

    It is emitted by default because the original command asked for it, but it pins the service to a single replica forever. Turn it off unless something outside Compose finds the container by that exact name.

Worked examples

A published port survives its own YAML

Given
docker run -p 22:22 -p 8080:80 alpine
Result
ports: - "22:22" - "8080:80"

Both are quoted deliberately. Written bare, 22:22 is a base-60 integer to any reader still on YAML 1.1 — 1342 — which is why Compose’s own documentation says to quote every port mapping.

A named volume brings a declaration with it

Given
docker run -v pgdata:/var/lib/postgresql/data postgres:16
Result
volumes: - pgdata:/var/lib/postgresql/data (plus a top-level) volumes: pgdata:

Docker creates a missing named volume on the spot; Compose treats an undeclared one as a mistake and refuses to start. The source is classified as a path or a name using the same rule Docker uses.

A health check changes shape

Given
--health-cmd "pg_isready -U postgres" --health-interval 10s
Result
healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 10s

CMD-SHELL runs the string through a shell, matching what Docker did with it. A bare list without that first element would be read as an exec-form argv and would not find the shell operators.

A flag with nowhere to go

Given
docker run -d --rm --network-alias db postgres:16
Result
Three entries in the "not carried over" list, and nothing invented in the file

-d belongs to the up command, --rm contradicts a persistent stack, and --network-alias is set under the network entry rather than on the service. All three are named rather than dropped.

The shell got there first

Given
docker run -v $PWD:/app --name build-$(date +%s) node:22
Result
The literal text is preserved, and both expansions are flagged

$PWD and the date substitution were resolved by your shell before Docker was invoked. Compose expands ${VAR} from the environment or a .env file, but never runs a command substitution.

When to use it

  • Turning a container someone has been starting by hand on a staging box into a file that lives in the repository and survives that person going on holiday.
  • Recovering a service definition from shell history or a runbook, when the original compose file was lost or never existed.
  • Translating a vendor’s "quick start" one-liner into something you can review, diff and pin before it goes anywhere near production.
  • Checking what a long command actually asks for — the key ordering makes it far easier to read forty flags as YAML than as one wrapped line.
  • Building the first service of a new stack from the container you have already been debugging interactively.
  • Auditing a command for flags that grant privileges, by seeing security_opt, cap_add and privileged pulled out into their own keys.

Things to watch out for

  • The conversion is one-way on purpose. Compose can express things `docker run` has no flag for at all — build contexts, depends_on, profiles, several services and the relationships between them — so a faithful reverse is a separate design problem rather than this one read backwards.
  • An unrecognised flag is reported, never guessed at. Most run flags take a value, so an unknown one is assumed to as well, except when doing so would consume the image name. Either way it appears in the list with whatever text was skipped alongside it.
  • Everything under deploy is applied by Compose v2 on a single machine, but the older docker-compose v1 ignored that whole section unless you were deploying to Swarm. If a memory limit seems to have no effect, check which binary is reading the file.
  • A network is written as external: true, matching what the original command required — `docker run --network backend` needs backend to exist already. Remove that line if you would rather Compose create the network as part of the stack.
  • Values are quoted wherever YAML would otherwise change them. That covers ports, restart: "no", and any environment value that reads as a boolean, a version number or a base-60 time. Everything on a command line is a string, and the output says so.
  • Nothing is executed and nothing is contacted. The command is text, parsed in this browser, so pasting one that carries a password or an internal hostname does not send it anywhere.

Frequently asked questions

Why is there no version key at the top of the file?

Because the Compose Spec removed it. Compose v2 ignores the field and prints a warning when it finds one, so emitting it would add noise to every file for the benefit of a binary that has been unsupported for years.

Can it convert a compose file back into a docker run command?

Not in this direction. Compose describes multi-service stacks, build steps and dependencies that no single run command can express, so the reverse needs its own scope rather than being the same table read upside down.

What happens to flags with no Compose equivalent?

They are listed underneath the output, each with the reason it could not be carried over. Nothing is dropped silently, because a file that quietly loses a security or networking flag starts a different container than the command you gave it.

Why did my named volume get an extra entry at the bottom?

Compose requires every named volume the file mounts to be declared at the top level, and refuses to start otherwise. Docker instead creates one on demand, which is the difference that makes the extra block necessary.

Should I keep container_name in the output?

Usually not. It fixes the container to one name, so the service can never be scaled beyond a single instance and a second stack on the same host will collide with it. Keep it only when something external looks the container up by name.

Does it handle multi-line commands copied from a terminal?

Yes. Backslash-continued lines are joined the way a shell joins them, quoted values holding spaces stay in one piece, and a trailing comment on any line is ignored rather than being read as an argument.

All devops tools