From docker run to Docker Compose: Every Flag Mapped

How docker run flags become Compose keys, named volumes versus bind mounts, networks and service DNS, GPUs, health checks, the flags with no equivalent, the obsolete version key, and going back again.

ยท 11 min read

Illustration for the Docker Run to Compose Converter guide

Most container documentation starts with a docker run command, because it fits on one line. Most real deployments end up in a Compose file, because a file can be versioned, reviewed and started again after a reboot without anyone remembering twelve flags. Moving between the two is mostly mechanical, but a handful of flags change shape, a few have no equivalent, and Compose quietly renames volumes and networks in a way that matters if data already exists. This article walks through the mapping and the traps. The Docker Run to Compose Converter does the conversion in either direction and lists anything it could not carry over.

The shape of a Compose file

A Compose file is YAML following the Compose Specification. Its top level has services, and optionally volumes, networks, configs and secrets. Each service corresponds roughly to one docker run:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: change-me
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped
volumes:
  pgdata:

There is no version: line. The Compose Specification merged the old version 2 and 3 formats, and Docker Compose v2 ignores the key and prints a warning that it is obsolete. If you copy an old file that starts with version: "3.8", delete that line.

Flag by flag

Most docker run options have a direct key with the same idea and a slightly different name.

docker runCompose keyNotes
--name dbcontainer_name: dbAlso a good service name; fixed names prevent scaling
-p 8080:80ports: ["8080:80"]Quote ports in YAML; "22:22" could otherwise be read oddly by YAML 1.1 parsers
-e KEY=value, -e KEYenvironment: {KEY: value}A bare KEY passes the variable through from the shell
--env-file .envenv_file: [.env]Different from the .env Compose reads for interpolation
-v name:/pathvolumes: ["name:/path"]Named volume: also declare under top-level volumes
-v ./dir:/path:rovolumes: ["./dir:/path:ro"]Bind mount; relative to the compose file
--mount type=bind,...long volume syntaxtype, source, target, read_only
--restart unless-stoppedrestart: unless-stopped
--network mynetnetworks: [mynet]Declare the network at the top level
--network hostnetwork_mode: hostAlso none, bridge, container:name
-w, -u, -hworking_dir, user, hostname
--entrypoint shentrypoint: sh
args after the imagecommand: [...]A list avoids shell quoting surprises
-i, -tstdin_open: true, tty: true
-m 512m, --cpus 1.5mem_limit: 512m, cpus: 1.5
--health-cmd "..."healthcheck: {test: ["CMD-SHELL", "..."]}Plus interval, timeout, retries, start_period
--stop-timeout 30stop_grace_period: 30s
--ulimit nofile=1024:2048ulimits: {nofile: {soft: 1024, hard: 2048}}
--log-driver, --log-optlogging: {driver, options}
--cap-add, --device, --add-hostcap_add, devices, extra_hostsLists

The converter handles the shell side too: quoted values with spaces, backslash line continuations, --flag=value, -p8080:80, and combined boolean flags such as -dit.

Volumes: named or bind

The single most important distinction is between a named volume and a bind mount. In -v pgdata:/var/lib/postgresql/data, the source has no slash, so Docker treats it as the name of a volume it manages. In -v ./init:/docker-entrypoint-initdb.d:ro or -v /srv/www:/usr/share/nginx/html, the source is a path on the host. In Compose, a named volume must be declared in the top-level volumes section, while a bind mount needs nothing extra. Relative bind paths are resolved from the directory containing the compose file, which is usually what you want. In plain docker run, relative paths in -v are only accepted by Docker 23 and later.

Diagram showing how Compose names resources: in project shop, the service db with volume pgdata and the default network become container shop-db-1, volume shop_pgdata and network shop_default; a docker run that should reuse that data must mount shop_pgdata and join shop_default
Diagram showing how Compose names resources: in project shop, the service db with volume pgdata and the default network become container shop-db-1, volume shop_pgdata and network shop_default; a docker run that should reuse that data must mount shop_pgdata and join shop_default

Networks and service names

When you run docker compose up, Compose creates a network for the project and attaches every service to it, with the service name as a DNS alias. That is why a web service can connect to db:5432 without any links. With docker run, containers on the default bridge network cannot resolve each other by name, which is why older instructions use --link or ask you to create a network first.

A --network mynet flag implies the network already exists. The converter therefore declares it with external: true, which tells Compose to use an existing network instead of creating <project>_mynet. If you would rather Compose create and own it, remove external: true.

Project names change resource names

Compose prefixes everything it creates with the project name, which defaults to the name of the directory holding the file. In a folder called shop, the volume pgdata is really shop_pgdata, the default network is shop_default and containers are named like shop-db-1 unless container_name is set. You can override the project with -p or the top-level name: key.

This matters in two situations. Converting an existing docker run -v pgdata:... setup to Compose creates a new empty volume shop_pgdata, not the one with your data; declare the volume with name: pgdata (or external: true) to reuse the old one. And going the other way, a docker run that should see data Compose stored must mount shop_pgdata. The converter's Compose-to-run direction asks for the project name and generates docker volume create and docker network create commands with the prefixed names for exactly this reason.

GPUs

docker run --gpus all asks the NVIDIA Container Toolkit for every GPU. Compose expresses device requests as resource reservations:

deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: all
          capabilities: [gpu]

--gpus 2 becomes count: 2, and --gpus '"device=0,1"' becomes device_ids: ["0", "1"]. The toolkit must be installed on the host either way.

Flags with no Compose equivalent

A few options belong to the act of running a container from a shell, not to its definition:

  • -d is implied by docker compose up -d.
  • --rm removes the container when it exits. Compose removes containers with docker compose down, and docker compose run --rm service exists for one-off tasks.
  • -P publishes every exposed port to a random host port. Compose needs explicit ports.
  • --attach and --sig-proxy only affect the terminal session.

In the other direction, depends_on, profiles, build and extends have no docker run flag. Start containers in dependency order by hand, and build images with docker build -t name . first. The converter reports all of these as warnings rather than dropping them silently.

Worked example

A home server runs two containers started by hand:

docker run -d --name db -e POSTGRES_PASSWORD=change-me \
  -v pgdata:/var/lib/postgresql/data --restart unless-stopped postgres:16
docker run -d --name app -p 3000:3000 -e DATABASE_URL=postgres://postgres:change-me@db/postgres \
  --restart unless-stopped ghcr.io/example/app:1.4

The converter produces two services, db and app, with a top-level pgdata volume. Two things need a human decision. First, the app reaches the database as db, which works in Compose because both services share the project network; with the original commands it only worked if they shared a user-defined network. Second, the existing data lives in the volume pgdata, but in a folder named home Compose would create home_pgdata. Adding name: pgdata under the volume keeps the data. Finally, move the password into an .env file and add it to the repository's ignore list with the .gitignore Generator.

Tips

  • Pin image tags (postgres:16.4) so docker compose pull does not upgrade a database by surprise.
  • Use docker compose config to see the fully resolved file, with variables substituted, before starting.
  • Prefer command and entrypoint as lists to avoid shell parsing differences.
  • Remove container_name from services you may want to scale.
  • Health checks plus depends_on: {db: {condition: service_healthy}} give real start-up ordering in Compose.
  • If files in a bind mount come out owned by root, set user: and check permissions with the Chmod Calculator.

Sources

  • Docker documentation, docker container run CLI reference.
  • Compose Specification and Docker Docs, "Compose file reference" (services, volumes, networks, deploy).
  • Docker Docs, "Networking in Compose" and "Specify a project name".
  • Docker Docs, "GPU support in Docker Compose"; NVIDIA Container Toolkit documentation.
  • Docker Engine 23.0 release notes (relative bind mount paths in docker run).

Related searches

  • docker run to docker compose
  • docker compose equivalent of docker run flags
  • docker compose gpus
  • docker compose named volume
  • docker compose version obsolete
  • docker compose network external
  • docker run -it compose
  • docker compose healthcheck
  • compose to docker run
  • docker compose project name prefix

Open the Docker Run to Compose Converter