Put plainly, docker compose vs docker run explained comes down to this: docker run creates and starts one container from command-line flags, while Docker Compose declares containers, networks and volumes in a YAML file you start with docker compose up. For anything you keep running on a home server, use Compose.
Neither one is a different runtime. Both talk to the same Docker Engine, and a container started either way gets the same isolation, the same restart policies and the same mounts. The difference is where the configuration lives. With docker run it lives in a command you typed once and in a shell history that eventually rotates. With Compose it lives in a file you can read, diff, commit to git and back up next to the data it describes. Nearly every practical argument between the two comes back to that.
What is the actual difference between docker run and Docker Compose?
docker run is an imperative, one-container command. Docker’s run reference describes it as “Create and run a new container from an image,” with every setting passed as a flag. Compose is declarative: you describe the desired state of a whole project in compose.yaml, and Compose creates, updates or removes containers to match.
Docker’s Compose application model docs say Compose configures an application’s services from a YAML file and groups each deployment under a project name, which by default comes from the directory holding the file. Day to day, the two compare like this:
docker run | docker compose | |
|---|---|---|
| Scope | One container per command | A project of one or more services |
| Where config lives | Flags in your shell history | compose.yaml on disk |
| Default network | Default bridge, no name resolution | <project>_default, service names resolve |
| Changing a setting | Stop, remove, re-run with every flag | Edit the file, run docker compose up -d |
| Start order | Whatever order you type | depends_on, optionally gated on a healthcheck |
| Teardown | docker stop and docker rm per container | docker compose down |
| Best at | Throwaway and one-off containers | Anything you want running next month |
The same container, written both ways
Vaultwarden is a handy Rosetta stone because its official README publishes both forms of the same deployment. The docker run version:
docker run --detach --name vaultwarden \
--env DOMAIN="https://vw.domain.tld" \
--volume /vw-data/:/data/ \
--restart unless-stopped \
--publish 127.0.0.1:8000:80 \
vaultwarden/server:latest
And the compose.yaml version:
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vw.domain.tld"
volumes:
- ./vw-data/:/data/
ports:
- 127.0.0.1:8000:80
Almost every flag maps one-to-one onto a key. Two exceptions catch people. First, --detach has no key; it becomes the -d on docker compose up -d. Second, the volume paths differ. The run command uses an absolute /vw-data/, while the Compose file uses ./vw-data/, which the Compose services reference resolves from the Compose file’s parent directory. Convert carelessly and the new container starts against an empty folder, and Vaultwarden cheerfully creates a fresh, empty vault.
docker run flag | Compose key |
|---|---|
--name | container_name |
-e, --env | environment |
--env-file | env_file |
-v, --volume | volumes |
-p, --publish | ports |
--restart | restart |
--network | networks, or network_mode: host |
--device | devices |
--shm-size | shm_size |
| Arguments after the image | command |
devices is the key that trips people up when moving a media server over; the Jellyfin hardware transcoding guide shows it in a working file. Host mode has its own trade-offs, covered in Docker Compose host networking.
Why does Compose pull ahead at two containers?
Networking. Containers started with plain docker run land on the default bridge network, where Docker’s bridge driver docs say they “can only access each other by IP addresses.” Compose creates a per-project network on which every service resolves by name, with no extra commands and nothing to remember.
To get that with docker run, you create a user-defined network and attach every container to it yourself. Here is Postgres plus Adminer, adapted from the official Postgres image docs:
docker network create pgdemo
docker run -d --name db --network pgdemo \
--restart always --shm-size 128m \
-e POSTGRES_PASSWORD=example \
-v pgdata:/var/lib/postgresql/data \
postgres:16
docker run -d --name adminer --network pgdemo \
--restart always -p 8080:8080 \
adminer
The same stack in Compose:
services:
db:
image: postgres:16
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
adminer:
image: adminer
restart: always
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
Save it as compose.yaml, run docker compose up -d, open port 8080, pick PostgreSQL, then log in with server db, user postgres and password example. Per the Compose networking docs, “Each service registers its name with an internal DNS server,” so db resolves. The file also does something a pile of run commands can’t: condition: service_healthy holds Adminer back until Postgres passes its healthcheck. Change example before this goes anywhere real. The Docker Compose networking guide covers external networks and why two projects can’t see each other by default.
Updating and changing config: where docker run hurts
A container’s configuration is fixed when it is created. To change one environment variable on a docker run container, or move it to a newer image, you stop it, remove it and run it again with every original flag, retyped correctly. The classic rebuild discovery is that the only record of those flags was a shell history line that rolled over months ago.
Compose turns that into an edit and two commands. The compose up reference says that when a service’s configuration or image has changed, Compose picks it up “by stopping and recreating the containers (preserving mounted volumes).” Untouched services are left alone:
docker compose pull
docker compose up -d
Teardown has one trap. docker compose down removes the project’s containers and the networks it created, but named volumes survive unless you add -v. Typing down -v out of habit deletes your Postgres data. Bind mounts like ./vw-data are host folders and neither command touches them.
The file is also what you back up. A directory holding compose.yaml, an .env file and the bind-mounted data is a complete, restorable service. It’s the same format Portainer stacks use, so moving between the CLI and a web UI costs nothing.
When is docker run still the right tool?
docker run is still right for anything you plan to throw away within the hour: testing an image before committing to it, running a CLI tool once, or opening a shell in a scratch container. Add --rm, which the run reference says removes the container “and its associated anonymous volumes when it exits.”
# Scratch shell, gone on exit
docker run --rm -it alpine sh
If you do keep a docker run container around, never skip --restart. Docker’s restart policy docs list no as the default, so the container stays down after a reboot. Omitting restart: in Compose has the same effect.
For one-off jobs that need a project’s environment and volumes, such as a database migration, use docker compose run --rm <service> <command>. It reuses the service definition but “does not create any of the ports specified in the service configuration” unless you pass --service-ports, so it won’t collide with the running copy.
Should you type docker-compose or docker compose?
Type docker compose, with a space. The hyphenated docker-compose is Compose v1, a Python tool first released in 2014 that Docker’s deprecation page says “is no longer maintained.” Compose v2 is written in Go and runs as a Docker CLI plugin, and the Compose history page says the 2025 v5 release “is functionally identical to Compose v2.”
Two consequences for old tutorials. The top-level version: key they open with is ignored by v2, so delete it. The preferred file name is now compose.yaml, though docker-compose.yml still works for backward compatibility. If docker compose version errors on a Linux box, install the docker-compose-plugin package from Docker’s repository.
Converting an existing docker run container to Compose
Recover the real flags, write them into compose.yaml, remove the old container, then start the service with Compose. Data survives as long as it lives in a bind mount or a volume you point back at, because the container itself is disposable. The step that loses data is skipping the inspection.
- Run
docker inspect <name>and readConfig.Env,HostConfig.PortBindings,HostConfig.RestartPolicyandMounts. That’s the actual configuration, whatever you remember typing. - Translate with the flag table above, keeping host paths exactly as
Mountsshows them. - Watch named volumes. Compose scopes the volumes it creates to the project, so
pgdatabecomes something likemyproject_pgdata, a new and empty volume. The volumes reference coversexternal: true, which tells Compose to reuse the existing one. - Stop and remove the old container without
-v, then rundocker compose up -d. - Confirm the app’s data is there before you delete anything else.
Starting fresh instead? The first self-hosted stack walkthrough builds a multi-service file from scratch.
FAQ
is docker compose better than docker run
For long-running services, yes. Compose keeps the whole configuration in a file you can version and back up, gives every service name-based DNS on a project network, and recreates only what changed during updates. docker run remains the better fit for throwaway containers, quick image tests and one-shot CLI tools you won’t start again.
should i use docker compose for a single container
Yes, Compose is worth it even for one container. The file records the image, ports, volumes and restart policy in one place, docker compose pull followed by up -d handles updates, and adding a second service later is a few lines rather than a rebuild. The only cost is one directory and one short YAML file.
does docker compose use more resources than docker run
No. Compose is a client-side CLI plugin that sends requests to the same Docker Engine that docker run uses, and a detached up -d exits when it finishes, so no extra daemon sits in memory. The containers it creates carry exactly the same overhead. A small mini-PC running ten services through Compose pays only for those ten containers.
can docker compose containers talk to docker run containers
Yes, as long as they share a user-defined network. Create one with docker network create, attach the docker run container using --network, and declare the same network in compose.yaml with external: true. Containers left on the default bridge network cannot resolve each other by name, so a shared named network is the fix.