How to convert a docker run command to Docker Compose

By

Turn a docker run command into a compose.yaml file with a free converter, then fix the parts that need you: named volumes, secrets, networks and limits.

~~~

Free toolThis tutorial uses the Docker Run to Compose Converter. It runs in your browser, so open it in another tab and follow along.

To convert a docker run command to Docker Compose, paste it into the docker run to Compose converter, copy the YAML it generates into a compose.yaml file, check it with docker compose config, and start it with docker compose up -d.

A docker run with eight flags is hard to remember and only lives in your shell history. A Compose file goes in the repository next to the code, and the next person starts the same container with one command.

Convert a docker run command

Here’s a Postgres container started the usual way:

docker run -d \
  --name postgres \
  --restart unless-stopped \
  -p 5432:5432 \
  -e POSTGRES_USER=flavio \
  -e POSTGRES_PASSWORD=change-me \
  -e POSTGRES_DB=blog \
  -v pgdata:/var/lib/postgresql/data \
  postgres:17

Open the converter and paste the command into Input, backslashes and all. The Compose output box shows this:

# Generated from docker run — review before use.
services:
  postgres:
    image: postgres:17
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: flavio
      POSTGRES_PASSWORD: change-me
      POSTGRES_DB: blog
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pgdata:

The --name became the service name. Every -p, -e and -v landed in its own list, and --restart kept its value.

Look at the last two lines. pgdata is a named volume, not a folder on your disk, and Compose wants every named volume declared in a top-level volumes block. The converter adds it for you.

There’s also a Warnings box with one line about -d. Compose has no field for detached mode, because you pass -d to docker compose up instead.

Click Copy YAML and save it as compose.yaml in an empty folder.

Check the file and start it

Before starting anything, ask Compose to read the file:

docker compose config

It prints the file the way Compose understands it, with defaults filled in, or stops with an error. If the top-level volumes block were missing, this is where you’d find out:

service "postgres" refers to undefined volume pgdata: invalid compose project

When config is happy, start the container in the background and check it’s running:

docker compose up -d
docker compose ps
docker compose logs postgres

docker compose down stops and removes the container. The pgdata volume stays, so your data survives. Add -v only when you want to delete it too.

How flags map to Compose

This is what the converter does with the flags it knows:

docker runcompose.yaml
--name postgresthe service name, lowercased
-p 5432:5432ports
-e KEY=valueenvironment with KEY: value
-e KEYenvironment with KEY: and no value
--env-file .envenv_file
-v pgdata:/datavolumes, plus the top-level volumes entry
-v ./html:/usr/share/nginx/htmlvolumes (a bind mount)
--restart, -w, -u, --entrypoint, --hostnamerestart, working_dir, user, entrypoint, hostname
-itstdin_open: true and tty: true
--network backendnetworks, plus a top-level network marked external: true
--network hostnetwork_mode: host
anything after the imagecommand

-d, --rm and -P show up as warnings, since they control how you start the container, not what it is.

Add your app next to it

Compose gets more useful once you add a second service. Here I add an app that talks to Postgres:

services:
  postgres:
    image: postgres:17
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: flavio
      POSTGRES_PASSWORD:
      POSTGRES_DB: blog
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

  app:
    build: .
    ports:
      - 3000:3000
    environment:
      DATABASE_URL: postgres://flavio:${POSTGRES_PASSWORD}@postgres:5432/blog
    depends_on:
      - postgres

volumes:
  pgdata:

The app reaches the database at postgres:5432. Compose puts both services on the same network, and each service name works as a hostname there. depends_on starts Postgres before the app, but it doesn’t wait until Postgres accepts connections, so the app should retry its first connection.

Notice that POSTGRES_PASSWORD has no value anymore. That’s the next section.

From docker run to a running Compose projectPaste the docker run command into the converter. Read the warnings and the unsupported flags, and translate the unsupported ones by hand. Save the output as compose.yaml and run docker compose config. If it reports an error, fix the file and run config again. When it passes, start the project with docker compose up -d.YesNoYesNodocker run commandPaste it in theconverterUnsupported flagslisted?Translate them byhandSave as compose.yamldocker composeconfigError?Fix the filedocker compose up -d

What to watch out for

Don’t commit the password

The generated file has the password in clear text, because it was in the command. Keep the variable and drop the value. If you run the converter on -e POSTGRES_PASSWORD with no =, it writes POSTGRES_PASSWORD: with nothing after it, and adds a warning.

With no value, Compose reads the variable from your shell or from a .env file in the same folder as compose.yaml. Put it in .env and add .env to .gitignore:

POSTGRES_PASSWORD=change-me

The ${POSTGRES_PASSWORD} in the app’s DATABASE_URL reads the same file.

env_file has to exist

--env-file .env becomes env_file. Compose stops with an error like env file /home/flavio/blog/.env not found when the file is missing, so create it before you run config.

Relative paths start from compose.yaml

In a Compose file, a bind mount like ./html:/usr/share/nginx/html is relative to the folder that contains compose.yaml, not to the folder you run the command from. Keep the file at the root of the project and the paths keep working.

External networks must exist

--network backend becomes a network marked external: true. Compose doesn’t create external networks, and it returns an error when you start the project and the network isn’t there. Create it once with docker network create backend, or delete the networks lines and let Compose use its default network.

Some flags need a human

Memory and CPU limits, devices, health checks and capabilities are listed under Unsupported flags. Compose has fields for them (mem_limit, cpus, devices, healthcheck, cap_add), but the shape is different, so the converter leaves them to you.

If the converter stops with Unknown option --log-driver. Its value cannot be separated safely from the image name., write the flag with an equals sign, --log-driver=json-file. The converter can’t know whether the word after an unknown flag is its value or the image name. With =, the flag goes to the unsupported list and the rest converts. That one maps to Compose’s logging field.

To start a Compose file from scratch instead, the Docker Compose generator builds one for an app plus a database. My introduction to Docker Compose explains the file in depth, and the free Docker course has lessons on describing services with Compose and choosing volumes or bind mounts.

Try the Docker Run to Compose Converter

Convert common docker run flags, volumes, ports, and environment variables into Compose YAML.

Open the Docker Run to Compose Converter →

  • Docker Compose Generator: Generate a compose.yaml for your Node.js, Astro, or Next.js app with Postgres, MySQL, Redis — healthchecks wired correctly.
  • Dockerfile Generator: Generate Dockerfiles for Node.js, Next.js, Astro, Bun, and static sites — with .dockerignore and build commands.
  • Container Base Image Sizes: Compare Docker base images — alpine, slim, distroless, scratch — by pull size, shell, and libc, with a 3-question picker.

More tool tutorials →

Want me to talk about your product? You can sponsor this site.