Utilo

Turn a docker run command into Compose without losing the flags that matter

How ports, env, volumes, restart policy, and networks move from a docker run line into compose.yaml, which flags do not survive, and when the env file should become a Kubernetes Secret instead.

· 5 min read

A container usually starts as one line. Someone runs docker run with a port, then adds an environment variable, then a volume, then --restart unless-stopped because the process died over lunch. The line is now a deploy, and it lives in a shell history. Compose is the point where that line becomes a file you can review.

The docker run ↔ Compose converter parses the command locally and writes a service, or reads a Compose file and writes the docker run you would need to poke one service. This note is about which flags have a honest mapping and which ones you should not trust a mechanical conversion to invent.

A line and the service it should become

Take a command that is already doing real work:

docker run -d --name billing \
  -p 8080:8080 \
  -e DATABASE_URL=postgres://billing:secret@db:5432/billing \
  -e LOG_LEVEL=info \
  -v billing-data:/var/lib/billing \
  --restart unless-stopped \
  ghcr.io/example/billing:1.4.2

The Compose service is not a string rewrite. It is a different data model:

  • The image is ghcr.io/example/billing:1.4.2. The tag is part of the image, not a separate field you can forget.
  • 8080:8080 is a published port. In Compose that is ports: ["8080:8080"]. The left side is the host, the right side is the container. Swapping them is how a service looks healthy inside the network and refuses connections from your laptop.
  • --restart unless-stopped becomes restart: unless-stopped. always and on-failure are different policies. on-failure can take a retry count. Dropping the policy because the converter did not recognize the flag means the process will not come back after a reboot, which is the bug you wrote the flag to prevent.
  • The named volume billing-data must appear both on the service and, if you want the name preserved, in the top-level volumes: map. A bind mount such as ./data:/var/lib/billing is a host path. A named volume is a Docker-managed directory. Conflating them changes where the data lives.
  • -d and --name are about the client invocation. Detach has no Compose equivalent because docker compose up -d is the client's choice. The container name can be set, but Compose already prefixes the project name. Forcing container_name: billing makes the file unable to run twice on one machine.

The environment block is the sensitive part. DATABASE_URL in the example contains a password. The converter does not need a server to see it, and you should not need one either. Prefer an env file over a password baked into the Compose file you commit.

Flags that do not mean what a quick conversion implies

--rm deletes the container when it exits. Compose does not have that switch. A converted file that silently drops --rm will leave containers around. That is usually what you want for a service and the wrong behavior for a one-shot migration container. Say so in the file, with a comment or a profiles entry, instead of assuming the flag survived.

-it allocates a terminal and keeps stdin open. Compose can set stdin_open and tty. A service that is a web server does not need either. A service that is a shell does. Converting a debug command (docker run -it --rm ubuntu) into a long-running service is how you get a container that exits immediately and a restart policy that starts it again.

--network host is not a network name. It is a mode. Compose represents it as network_mode: host. Treating it as a user-defined network called host creates a different network and the process binds on the bridge, not on the host interfaces. Port publishing then means something again, which is the opposite of why someone passed --network host.

--link is obsolete. A converted file should use a Compose network and DNS, where the service name is the hostname. Keeping --link in a new file copies a feature Docker has been removing.

A health check (--health-cmd, --health-interval) has a Compose block. If the converter keeps the command and drops the interval, you get Docker's defaults, which may mark a slow starter as unhealthy and, with a restart policy, kill it in a loop. Read the healthcheck after the conversion. Do not assume the defaults match the flags you typed.

deploy.resources limits in Compose are for Swarm and for some Compose implementations. A --memory and --cpus on docker run apply to a local container. Putting those numbers only under deploy means docker compose up on a developer machine may ignore them. The converter's output is a draft. The limits are worth reading twice if the process is known to leak.

Environment files are a second tool

Once the service exists, the variables are the part that leaks. A Compose file with env_file: .env still contains no secret if .env is gitignored. A Kubernetes deploy does not read that .env. It wants a Secret or a ConfigMap.

That translation is the .env converter: .env to a Secret manifest, or to JSON or YAML, and a comparison of two env files so a missing production key shows up before the pod crashes. The Compose converter does not know which keys are secret. The env converter does not know your Compose service name. Use them in that order. Generate the service, move the variables out of the committed file, then produce the manifest when the same variables have to run on Kubernetes.

A Secret manifest stores values as Base64. That encoding is not a lock. Anyone who can kubectl get secret -o yaml can decode it. The reason to generate the manifest locally is so the values are not pasted into a website that offers "env to Kubernetes" as a form. The cluster will still store the Secret. That is a different trust boundary from the conversion step.

The reverse direction

Paste a Compose file and ask for docker run when you want to reproduce one service outside the project. The reverse mapping drops project-level networks, depends_on ordering, and most profiles. depends_on does not become a flag. You start the dependency yourself. A converted docker run that omits the database is not a bug in the parser. Compose was expressing an order that a single docker run cannot.

Multiple services become multiple commands. A volume declared at the top of the file has to be created before the command runs, or you pass the volume name and let Docker create it. A relative bind mount is resolved against the Compose file's directory, not against whatever directory you are in when you later paste the docker run line. If the converted command contains ./data, run it from that directory or rewrite the path.

What to check before you commit the file

Read the image tag. Read every published port and which interface it binds. Read the environment block and move secrets to an env file. Read restart and healthcheck together. Read network_mode if it says host. Then run the file, or run the generated docker run, against a throwaway database. A conversion that parses is not a conversion that preserves the data directory.

Both tools stay in the browser. The command you paste can contain a registry password, a cloud token, or a database URL. The page that rewrites it does not need a copy.

Related guides