How to convert a docker run command to Docker Compose
By Flavio
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 run | compose.yaml |
|---|---|
--name postgres | the service name, lowercased |
-p 5432:5432 | ports |
-e KEY=value | environment with KEY: value |
-e KEY | environment with KEY: and no value |
--env-file .env | env_file |
-v pgdata:/data | volumes, plus the top-level volumes entry |
-v ./html:/usr/share/nginx/html | volumes (a bind mount) |
--restart, -w, -u, --entrypoint, --hostname | restart, working_dir, user, entrypoint, hostname |
-it | stdin_open: true and tty: true |
--network backend | networks, plus a top-level network marked external: true |
--network host | network_mode: host |
| anything after the image | command |
-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.
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 →
Related tools
- 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.
Want me to talk about your product? You can sponsor this site.