docker
docker run to docker-compose: Convert Any Command, Flag by Flag
A practical guide to converting a docker run command into a docker-compose.yaml file — what each flag maps to (-p, -e, -v, --restart, --name, --network), with copy-paste examples for Postgres, Redis, and Nginx.
You found a docker run one-liner in a README, it works, and now you want it in a docker-compose.yaml so it’s version-controlled, self-documenting, and starts with one command. The translation is mechanical once you know which flag becomes which Compose key. This guide is that mapping — plus three real examples you can copy straight into a file.
The flag-to-Compose cheat sheet
Every common docker run flag has a direct home in Compose:
docker run flag | Compose key | Notes |
|---|---|---|
--name web | container_name: web | Usually unnecessary — the service name already names it |
-p 8080:80 | ports: ["8080:80"] | host:container |
-e KEY=val | environment: { KEY: val } | Or an env_file |
-v /data:/var/lib | volumes: ["/data:/var/lib"] | Bind mount or named volume |
--restart unless-stopped | restart: unless-stopped | Same values |
--network mynet | networks: [mynet] | Define the network at the bottom |
-d | (nothing) | Compose runs detached with up -d |
--rm | (nothing) | Not a Compose concept — it’s for one-off runs |
-it | stdin_open: true + tty: true | Rarely needed for services |
image:tag (last arg) | image: image:tag | The image is a key, not positional |
Two of these trip people up: -d and --rm have no Compose equivalent because they describe how you invoke a run, not what the service is. You get detached mode from docker compose up -d at the command line.
Worked example 1: Postgres
docker run --name db -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=app \
-p 5432:5432 -v pgdata:/var/lib/postgresql/data \
--restart unless-stopped -d postgres:16
becomes:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
restart: unless-stopped
volumes:
pgdata:
Note the named volume pgdata has to be declared in the top-level volumes: block — that’s the one thing the CLI creates implicitly that Compose makes you spell out.
Worked example 2: Redis
docker run --name cache -p 6379:6379 --restart always -d redis:7-alpine
becomes:
services:
cache:
image: redis:7-alpine
ports:
- "6379:6379"
restart: always
Worked example 3: Nginx with a config bind mount
docker run --name proxy -p 80:80 -p 443:443 \
-v ./nginx.conf:/etc/nginx/nginx.conf:ro \
-v ./certs:/etc/nginx/certs:ro \
--restart unless-stopped -d nginx:1.27
becomes:
services:
proxy:
image: nginx:1.27
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
restart: unless-stopped
The :ro suffix (read-only) carries over unchanged — it’s part of the volume string in both syntaxes.
Gotchas worth knowing
- Port order is
host:containerin both — get it backwards and the mapping silently does the wrong thing. - Named volumes must be declared; bind mounts (a path starting with
.or/) don’t. - Multiple
-eflags collapse into oneenvironment:map. For long lists, pointenv_file:at a.envfile instead — noting that Compose’s.envdialect interpolates${VAR}and other loaders don’t, one of several disagreements between .env parsers. --linkis dead — don’t translate it. Services on the same Compose network reach each other by service name automatically.
Do it automatically
Once you’ve seen the pattern a few times you’ll do simple commands in your head — but for anything with a dozen flags, paste the whole docker run command into the Docker Run → Compose converter and get the docker-compose.yaml instantly. It maps ports, env, volumes, name, restart, and networks for you, and it runs entirely in your browser — nothing is sent anywhere.
And while you’re tidying the run half of the workflow, the build half has a footgun of its own: .dockerignore doesn’t match patterns the way .gitignore does, and the .dockerignore generator writes a stack-aware starting point.