From docker run to docker-compose.yml: Converting One-Off Commands into Versioned Files
July 26, 2026 · DevTools
The Postgres-on-my-laptop command started simple — docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:16. Then someone wanted persistent data, so we added a volume. Then someone wanted a Redis sidecar. Then we wanted a network so they could talk. The shell history grew into a paragraph, nobody could remember the exact incantation, and onboarding a new developer meant copy-pasting a six-line command from a wiki page that was six months out of date.
The fix is to put it in a docker-compose.yml file. The question is how to get there without losing flags in the translation.
You can try every example in this guide with the Docker Run to Compose Converter. It parses entirely in your browser — no command is executed, nothing is uploaded.
Why a Compose file beats a one-liner
A long docker run command has the same problem as a long shell command in any language: it's not a program, it's a moment in time. It cannot be reviewed, version-controlled, diffed, branched, or rolled back. A Compose file fixes all of that:
- Diffable — the change "bump Postgres from 16 to 17" becomes one line in a diff
- Reproducible — a new developer runs
docker compose upand gets the same stack you have - Documentable — the file is the documentation; no separate wiki entry to keep in sync
- Composable — multiple services, shared networks, named volumes, all expressed once
- Shareable — checked into the repo, available to CI runners, packaged with the app
The shell-history approach works for the first container you ever run. By the third, the cost of not having a Compose file is bigger than the cost of writing one.
The modern Compose shape
The current Compose Specification dropped the version: key at the top of the file. You don't need it anymore, and Compose emits a warning if you include it. A minimal web + database stack looks like this:
services:
web:
image: nginx:latest
ports:
- "80:80"
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=app
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
Two top-level keys you'll see in every Compose file: services: (the containers you want to run) and the named-resource keys (volumes:, networks:, configs:, secrets:) at the bottom. The named-resource block at the bottom is required for any named volume or network you reference from a service — forgetting it is the most common reason a Compose file silently does the wrong thing.
The flag mapping
The converter is essentially a flag-by-flag translator. Most docker run flags map cleanly:
docker run flag | Compose key | Notes |
|---|---|---|
--name NAME | container_name: NAME | Pick a name per service, not just per container |
-p HOST:CONTAINER | ports: ["HOST:CONTAINER"] | Quote to avoid YAML's base-60 number trap |
-v HOST:CONTAINER | volumes: ["HOST:CONTAINER"] | Bind mounts |
-v VOLUME:CONTAINER | volumes: ["VOLUME:CONTAINER"] | Named volumes — declare in top-level volumes: |
-e KEY=VALUE | environment: ["KEY=VALUE"] or environment: { KEY: VALUE } | Both work |
--env-file FILE | env_file: FILE | Path relative to the Compose file |
--network NAME | networks: [NAME] | Top-level networks: required for named networks |
--restart POLICY | restart: POLICY | no, always, on-failure, unless-stopped |
-w DIR | working_dir: DIR | |
-u USER | user: USER | |
--entrypoint CMD | entrypoint: CMD | List form is more portable |
--label KEY=VALUE | labels: [KEY=VALUE] or labels: { KEY: VALUE } | |
--add-host HOST:IP | extra_hosts: [HOST:IP] |
A few flags don't translate because they're properties of the invocation, not the service definition:
docker run flag | Compose equivalent | Why |
|---|---|---|
--rm | (no equivalent) | Compose manages removal via the project lifecycle |
-d | implicit | Compose always runs services in detached mode |
-it / -i / -t | (no equivalent) | Interactivity isn't a service property |
--link | depends_on: + shared network | Links are deprecated; use networks |
--pid, --privileged | pid: host, privileged: true | These do have Compose equivalents, but think twice before using them |
A complete before-and-after
Realistic command — a Postgres with persistent storage, custom user, and a published port:
docker run -d \
--name my-postgres \
-p 5432:5432 \
-e POSTGRES_USER=app \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=mydb \
-v pgdata:/var/lib/postgresql/data \
--restart unless-stopped \
postgres:16
Translated into Compose form:
services:
postgres:
container_name: my-postgres
image: postgres:16
ports:
- "5432:5432"
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=mydb
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
Five things to notice:
--name my-postgresbecomescontainer_name: my-postgresinside the service block — the service name (postgres) is the key underservices:, andcontainer_nameis the explicit container name- The port mapping
"5432:5432"is quoted — YAML's bare5432:5432would be a base-60 number, and08:80would be an invalid octal. Always quote - The environment block uses the list form (
- KEY=VALUE) — the map form (POSTGRES_USER: app) is also valid; pick one and be consistent - The named volume
pgdatais declared at the top-levelvolumes:block. Without that declaration, Compose creates an anonymous volume, which is harder to back up and harder to share --restart unless-stoppedis the right default for databases and long-running services: survives crashes and reboots, respects a deliberatedocker compose stop
The converter handles all of this. You can paste the full command in, get the YAML out, and edit from there.
Gotchas worth knowing
Bind mounts vs. named volumes. They look identical in the flag list but behave differently:
volumes:
- ./html:/usr/share/nginx/html # bind mount — a host path
- pgdata:/var/lib/postgresql/data # named volume — managed by Docker
A bind mount maps a directory on your machine into the container — great for live-editing source code. A named volume is storage Docker manages for you, with the right lifecycle hooks for databases. Bind mounts are great for development; named volumes are usually the right choice for any data you want to keep across docker compose down.
depends_on isn't a health check. It controls start order, not readiness. Postgres may accept connections seconds after its container starts, but the database server may take longer. For that, you need a healthcheck:
services:
db:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
web:
depends_on:
db:
condition: service_healthy
If your app is retrying connections anyway, you can ignore this. If it's not, your "Postgres is ready but the app crashes on first connection" debugging session begins.
The Compose project name. When you run docker compose up, Compose creates a project named after the directory (myapp_web_1, myapp_db_1). Override it with -p or the name: top-level key. This matters when you have multiple copies of the same stack on one host.
The Compose file extension. .yml and .yaml both work. Pick one and stick with it; mixing both is fine for tooling but annoying for humans.
The version: key. Removed in the current Compose Specification. If you see older files with version: '3.8' at the top, leave them alone — they're still valid — but don't add it to new files.
From shell history to a checked-in file
The real workflow is:
- Paste your
docker runcommand into the Docker Run to Compose Converter - Get a starting Compose file back — review the service name, ports, volumes, environment
- Add a second service if you have a sidecar (Redis, an API, an admin UI) — this is where Compose starts to shine
- Validate the YAML with the YAML Validator — the indentation foot-guns are real
- Run
docker compose configlocally — it re-parses and re-formats your file, catching structural errors the validator might miss - Commit the file; throw away the shell-history paragraph
For the other direction — composing a new stack visually — the Docker Compose Builder is the inverse: a form for each service that emits clean YAML.
Everything stays in your browser
The Docker Run to Compose Converter does no execution. The command you paste in is parsed into tokens and translated into YAML structure — never run, never started, never logged. Useful when your command includes environment variables you don't want anyone else to see.
Next steps
- Compose a stack from scratch: Docker Compose Builder
- Build the Dockerfile your service runs in: Dockerfile Builder
- Validate the output: YAML Validator
- A primer on the Compose file shape: A Sane Starting Point for docker-compose.yml