Cloud Explainers

Docker vs Docker Compose: Which One Should You Use?

Docker10 min videoPublished Nov 9, 2026

Docker runs containers; Docker Compose tells Docker what to run, from one file. We started the same small tea shop (a Python app and PostgreSQL) both ways, for real: by hand with docker commands, then with docker compose. What Compose adds (a network with names, start order, health checks, only changing what changed), what down keeps, where plain docker is simpler, and which one to pick.

Docker vs Docker Compose: Which One Should You Use?, the full picture: diagram of docker, docker compose, Docker Engine, app, db, Ana's tea shop
The whole picture, from the video's recap.

One shop, two ways to start it

Your app and its database run in two containers, and every tutorial starts that pair a different way. Some type docker run, some type docker compose. Here's Ana's tea shop by hand: the database, a network, then the app, with every setting typed out. And the same shop with Compose: one short command.

laptop: Ana's terminal
docker run -d --name db -e POSTGRES_PASSWORD=tea-lab-only -v dbdata:/var/lib/postgresql postgres:18-alpinebe01785e244b4fe499c3c6c7a4ac1a7782e5f0b5e677ffa899b60b529a264507
laptop: Ana's terminal
docker network create shop-net685f3c3de940a5aab6c5e2a57b6fe822bed678a81e899edc21c4425bcf31f08f
laptop: Ana's terminal
docker run -d --name app --network shop-net -p 8080:8000 -e DB_HOST=db -e DB_PASSWORD=tea-lab-only shop-app7e98a160772acce78db3d5ee074ade192c0bc1234a9b1a4c2228bb17dc2acf0d
laptop: Ana's terminal
docker compose up -d Container shop-db-1  Recreate
 Container shop-db-1  Recreated
 Container shop-db-1  Starting
 Container shop-db-1  Started
 Container shop-db-1  Waiting
 Container shop-db-1  Healthy
 Container shop-app-1  Starting
 Container shop-app-1  Started

Docker runs containers. Compose is a tool that tells Docker what to run, from one file. They aren't rivals. By the end you'll know which one to reach for, why Ana's app crashed even with Compose, and what happens to her orders when she tears it all down.

Someone asked exactly this on Stack Overflow in 2016: what's the difference between docker and docker compose? It has been viewed more than a hundred and seventy thousand times. Let's answer it with a real run.

A real question, from Stack Overflow: cards of Asked in 2016, "The difference between, 170,000+ views, Our answer

Starting it by hand

What does docker do on its own? A background program, the Docker Engine, runs every container. The docker command is only a client: each command sends one request, start this, stop that, and then it's done. It remembers nothing about her shop.

docker: one command, one request: diagram of docker, Engine, docker run db, started, docker stop app, stopped, No memory of the shop: each command stands alone.

By hand, Ana builds her app's image, then starts the database: a name, a password, a volume for its files, and the image. Then the app: its port, and where to find the database, the host called db.

laptop: Ana's terminal
docker build -q -t shop-app .sha256:c25c61591130ba4dee6660c1e9057f50cf7e0d2f2b88e5031cae4cfb10169119
laptop: Ana's terminal
docker run -d --name app -p 8080:8000 -e DB_HOST=db -e DB_PASSWORD=tea-lab-only shop-app8b3c96d61b99756930c24fa0bade75ab76cc02af4e88c4dce8ab289fd63b925d

One of those settings matters later. Dash v, dbdata, colon, then a path. It gives the database a named volume, dbdata, mounted where PostgreSQL keeps its files. A container can be deleted and made again; the volume, and the orders in it, stay until someone deletes the volume itself.

Where the database keeps its data: diagram of -v dbdata:/var/lib/postgresql, -v = a volume, dbdata = its name, /var/lib/postgresql = path inside the container, Delete the container: the volume stays.

Both containers started. Guess first: can the app reach the database by its name? No. The app exited, and its log says: name or service not known.

laptop: Ana's terminal
docker ps -a --format 'table {{.Names}}\t{{.Status}}'NAMES     STATUS
app       Exited (1) 2 seconds ago
db        Up 7 seconds
laptop: Ana's terminal
docker logs app 2>&1 | tail -1psycopg.OperationalError: [Errno -2] Name or service not known

Why? Plain docker run puts containers on Docker's default network, and there, Docker's docs say, containers can only reach each other by IP address. A network you create yourself has a built-in name lookup, so db becomes a name the app can find.

The default network has no names: diagram of app, db, Name lookup

So Ana creates a network, plugs the database into it, removes the broken app, and runs it again on that network.

laptop: Ana's terminal
docker network connect shop-net db
laptop: Ana's terminal
docker rm appapp

Now the shop answers: zero orders. Three test orders later, three. And if she types the database command again? A conflict: that name is already in use.

laptop: Ana's terminal
curl localhost:8080Ana's tea shop: 0 orders
laptop: Ana's terminal
for i in 1 2 3; do curl -s -X POST localhost:8080/order; doneorder 1 saved
order 2 saved
order 3 saved
laptop: Ana's terminal
curl localhost:8080Ana's tea shop: 3 orders
laptop: Ana's terminal
docker run -d --name db -e POSTGRES_PASSWORD=tea-lab-only -v dbdata:/var/lib/postgresql postgres:18-alpinedocker: Error response from daemon: Conflict. The container name "/db" is already in use by container "be01785e244b4fe499c3c6c7a4ac1a7782e5f0b5e677ffa899b60b529a264507". You have to remove (or rename) that container to be able to reuse that name.

Run 'docker run --help' for more information

Count what it takes to start one small shop by hand, done right the first time: four commands, 291 characters, typed the same way, in the same order, on every laptop that runs it.

Starting it by hand: figure of 291 characters typed to start the shop by hand, source: the four commands from our run, without the mistakes

The same shop in one file

Compose moves all of that into a file. compose.yaml lists two services, db and app, with the same image, password, volume and port she typed by hand. depends_on says: start the database first.

laptop: Ana's terminal
cat compose.yamlservices:
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_PASSWORD: tea-lab-only
    volumes:
      - dbdata:/var/lib/postgresql
  app:
    image: shop-app
    ports:
      - "8080:8000"
    environment:
      DB_HOST: db
      DB_PASSWORD: tea-lab-only
    depends_on:
      - db
volumes:
  dbdata:

Compose is part of the docker command itself: docker compose, with a space. Old tutorials write it with a dash: that's version one, written in Python, no longer supported, and not even installed here. One command, and Compose makes the network, the volume, and both containers.

laptop: Ana's terminal
docker compose versionDocker Compose version 2.40.3+ds1-0ubuntu1~24.04.1
laptop: Ana's terminal
docker-compose version-bash: docker-compose: command not found
laptop: Ana's terminal
docker compose up -d Network shop_default  Creating
 Network shop_default  Created
 Volume shop_dbdata  Creating
 Volume shop_dbdata  Created
 Container shop-db-1  Creating
 Container shop-db-1  Created
 Container shop-app-1  Creating
 Container shop-app-1  Created
 Container shop-db-1  Starting
 Container shop-db-1  Started
 Container shop-app-1  Starting
 Container shop-app-1  Started

So one file became four things on the same engine: a network named after the project, a volume for the database, and two containers, started in order. Ana didn't type a single docker run.

Started is not ready

But the app exited again. Not a name problem this time: connection refused. Compose did start the database first. So why?

laptop: Ana's terminal
docker compose ps -a --format 'table {{.Service}}\t{{.Status}}'SERVICE   STATUS
app       Exited (1) 2 seconds ago
db        Up 3 seconds
laptop: Ana's terminal
docker compose logs app | tail -2app-1  | psycopg.OperationalError: connection failed: connection to server at "172.19.0.2", port 5432 failed: Connection refused
app-1  | 	Is the server running on that host and accepting TCP/IP connections?

Started isn't ready. A brand-new PostgreSQL spends a few seconds setting up its files before it accepts connections. depends_on, on its own, only waits for the container to be running. Docker's docs say it plainly: Compose doesn't wait until a container is ready.

depends_on alone: started, not ready: diagram of Compose, db, app, start db, start app, connect: refused, Running is not the same as accepting connections.

So Ana adds a health check: pg_isready, every two seconds. And the app now waits for the condition service_healthy.

laptop: Ana's terminal
cat compose.yamlservices:
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_PASSWORD: tea-lab-only
    volumes:
      - dbdata:/var/lib/postgresql
    healthcheck:
      test: pg_isready -U postgres
      interval: 2s
  app:
    image: shop-app
    ports:
      - "8080:8000"
    environment:
      DB_HOST: db
      DB_PASSWORD: tea-lab-only
    depends_on:
      db:
        condition: service_healthy
volumes:
  dbdata:

Now Compose keeps asking the database: are you ready? While PostgreSQL is still setting up, the answer is no, and Compose waits. Once the answer is yes, the database counts as healthy, and only then does Compose start the app.

A health check: wait for ready: diagram of Compose, db, app, pg_isready?, no, yes: healthy, start app, Healthy first, then the app.

This time Compose waits: database healthy, then the app starts. Zero orders, three test orders, three.

laptop: Ana's terminal
curl localhost:8080Ana's tea shop: 0 orders
laptop: Ana's terminal
for i in 1 2 3; do curl -s -X POST localhost:8080/order; doneorder 1 saved
order 2 saved
order 3 saved
laptop: Ana's terminal
curl localhost:8080Ana's tea shop: 3 orders

Is it a different Docker?

So is Compose a different Docker? No. Plain docker ps lists both containers, named after the folder, shop, then the service, then a number. Compose only adds a label with its project name, so it can find them again.

laptop: Ana's terminal
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'NAMES        IMAGE                STATUS
shop-db-1    postgres:18-alpine   Up 3 seconds (healthy)
shop-app-1   shop-app             Up Less than a second
laptop: Ana's terminal
docker inspect shop-app-1 --format '{{index .Config.Labels "com.docker.compose.project"}}'shop

Every name Compose gives follows one pattern: the project, which is the folder's name, then the service from the file, then a number, because a service can run more than one copy.

How Compose names a container: diagram of shop-app-1, shop = the project, app = the service, 1 = the copy, Its network follows the project too: shop_default.

So the layers look like this. compose.yaml at the top. The docker compose plugin reads it and sends the same kind of requests you'd send by hand to the same Docker Engine, which runs the containers.

Compose sits on top of Docker: diagram of compose.yaml, docker compose, Docker Engine, Containers

And the network she had to make by hand? Compose made one for the project, shop_default, and every service registers its name there. Asked from inside the app, db answers with its address, 172.19.0.2.

The project also gets its own commands. docker compose ps lists only this project's containers. logs shows a service's output, exec runs a command inside one, and down removes the project. By hand, you track every container name yourself.

Commands for the whole project: cards of docker compose ps, docker compose logs app, docker compose exec app, docker compose down

This isn't only for small shops. Sentry, the error-tracking company, ships its self-hosted version as a Docker Compose project. Its install guide asks for Compose 2.32.2 or newer, and starts everything with one command: docker compose up, wait.

Compose in production: Sentry self-hosted: cards of Sentry, Docker + Compose, Compose 2.32.2+, docker compose up --wait

Run it again, change one line

By hand, running a command twice gave a conflict. So what if Ana runs compose up again, with nothing changed? Both containers: running. Nothing was restarted.

laptop: Ana's terminal
docker compose up -d Container shop-db-1  Running
 Container shop-app-1  Running
 Container shop-db-1  Waiting
 Container shop-db-1  Healthy

Next, Ana wants the shop on a different port. A port line has two sides: 8080 is the door on her laptop, 8000 is where the app listens inside its container. She only needs to change the laptop side.

One port line, two sides: diagram of 8080:8000, 8080 = on the laptop, 8000 = inside the container, Change the left side, the app inside stays the same.

So she moves the shop to port 8081, one line in the file, and runs the same command. The database keeps running. Only the app is recreated, and it still has three orders.

laptop: Ana's terminal
sed -i 's/8080:8000/8081:8000/' compose.yaml && grep 8081 compose.yaml      - "8081:8000"
laptop: Ana's terminal
docker compose up -d Container shop-db-1  Running
 Container shop-app-1  Recreate
 Container shop-app-1  Recreated
 Container shop-db-1  Waiting
 Container shop-db-1  Healthy
 Container shop-app-1  Starting
 Container shop-app-1  Started
laptop: Ana's terminal
curl localhost:8081Ana's tea shop: 3 orders

That's the real difference. A docker run command says: do this, now. A compose file says: this is how the shop should look. Each up compares the file with what's running, and changes only what's different.

An order, or a description: comparison table of docker run vs compose.yaml, Says, Run it twice, Change a port

Tearing it all down

Done for the day, Ana runs compose down. It stops and removes both containers, and the network. Guess first: are her three orders gone? No.

laptop: Ana's terminal
docker compose down Container shop-app-1  Stopping
 Container shop-app-1  Stopped
 Container shop-app-1  Removing
 Container shop-app-1  Removed
 Container shop-db-1  Stopping
 Container shop-db-1  Stopped
 Container shop-db-1  Removing
 Container shop-db-1  Removed
 Network shop_default  Removing
 Network shop_default  Removed

Why not? down removes what Compose can rebuild in a second from the file: the containers and the network. It keeps what it can't rebuild: the named volume holding PostgreSQL's files, and the three orders inside them.

What compose down removes, and keeps: diagram of shop-app-1, shop-db-1, shop_default, shop_dbdata

No containers left, but the volume, shop_dbdata, is still there. Up again, and the shop still has three orders. Only down with dash v removes the volume too.

laptop: Ana's terminal
docker ps -a --format '{{.Names}}' | wc -l0
laptop: Ana's terminal
docker volume ls --format '{{.Name}}'shop_dbdata
laptop: Ana's terminal
docker compose up -d Network shop_default  Creating
 Network shop_default  Created
 Container shop-db-1  Creating
 Container shop-db-1  Created
 Container shop-app-1  Creating
 Container shop-app-1  Created
 Container shop-db-1  Starting
 Container shop-db-1  Started
 Container shop-db-1  Waiting
 Container shop-db-1  Healthy
 Container shop-app-1  Starting
 Container shop-app-1  Started
laptop: Ana's terminal
curl localhost:8081Ana's tea shop: 3 orders
laptop: Ana's terminal
docker compose down -v Container shop-app-1  Stopping
 Container shop-app-1  Stopped
 Container shop-app-1  Removing
 Container shop-app-1  Removed
 Container shop-db-1  Stopping
 Container shop-db-1  Stopped
 Container shop-db-1  Removing
 Container shop-db-1  Removed
 Volume shop_dbdata  Removing
 Network shop_default  Removing
 Volume shop_dbdata  Removed
 Network shop_default  Removed

Where plain docker is simpler

So is Compose always better? No. For a one-off command, plain docker is simpler: run a container, get the answer, and dash dash rm deletes it afterwards. No file needed.

laptop: Ana's terminal
docker run --rm python:3.13-slim python --versionPython 3.13.18

And Compose has a boundary: it runs everything on the one Docker Engine it talks to. Spreading containers across many machines, and moving them when a machine dies, is a job for an orchestrator such as Kubernetes.

Each tool has its job: cards of One-off command, An app with parts, Many machines, e.g. Kubernetes

Which one to pick

So which one should you use? With plain docker, the settings live in commands you retype, you make the network yourself, and running twice is a conflict. With Compose, the settings live in a file, the network comes with it, and up changes only what's different. In one line: docker is one command, one container; Compose is one file, the whole app.

docker or Docker Compose?: comparison table of docker vs Docker Compose, Settings, Network, Run again, Best for, In one line

Use plain docker to try an image, build one, or run a one-off tool. Use Compose as soon as your app has two parts that talk to each other: the file becomes the setup, and anyone on your team starts it with one command. Underneath, it's the same Docker.

Every command and the lab files, compose.yaml included, are linked in the description, so you can run the tea shop both ways yourself.

Run it yourself: cards of compose.yaml, app.py, Dockerfile, Every command

Recap

So: docker sends one request at a time and remembers nothing. Compose keeps the whole shop in one file, makes its network, waits for the health checks you add, and down keeps your volumes. Same engine underneath.

If this cleared up docker versus Compose for you, a like helps other beginners find it, and subscribing brings you the next one.

Every container here ran for real on one small Ubuntu laptop. Thanks for watching Cloud Explainers.

Sources

Demos: one container on our own Mac standing in for Ana's Ubuntu laptop, with Docker running inside it; the tea shop is made up. Watch the full video on YouTube.

More videos

Linux9 min

Linux Makefiles Will Finally Make Sense After This

Auth (OAuth, JWT)9 min

SAML vs OAuth vs OpenID Connect: What's the Difference?

Python7 min

How to Fix Python cron Jobs That Never Run

Linux22 min

Linux Bash Scripting: Full Tutorial for Beginners

Networking10 min

How PXE Network Boot Works

Active Directory9 min

How Active Directory Works

Linux10 min

How to Fix a Linux systemd Service That Won't Start

Linux9 min

Linux Logs: syslog vs journald

Auth (OAuth, JWT)9 min

JWT vs Session Cookies: Which One Should You Use?

Docker9 min

How to Fix Docker Compose Containers That Can't Talk to Each Other

Django (Python web framework)22 min

Django Full Tutorial for Beginners

Linux9 min

How Linux systemd Works

Networking8 min

How to Fix 'Windows Cannot Access' Network Share Errors (SMB)

APIs10 min

REST vs gRPC: Which One Should You Use?

Certificates & PKI (ADCS)9 min

How to Fix SSL Certificate Chain Errors (Missing Intermediate)

Python9 min

Every Python Concept for Beginners in 10 Minutes

Databases & SQL9 min

Every SQL Join Type in 10 Minutes

OpenRouter (one API for many AI models)9 min

Why OpenRouter Free Models Hit Their Limit So Fast

Gemini API & Google AI Studio9 min

Why Your Gemini API Free Tier Runs Out So Fast

OpenRouter (one API for many AI models)10 min

OpenRouter Will Finally Make Sense After This

Caching & Redis10 min

How Redis Caching Works

Gemini API & Google AI Studio10 min

Gemini API vs Vertex AI: Which One Should You Use?

Gemini API & Google AI Studio8 min

How to Fix a Leaked Gemini API Key

Go (Golang)9 min

Why Golang Uses Goroutines Instead of Threads

Gemini API & Google AI Studio9 min

How to Fix Gemini API Error 429: Resource Exhausted

Claude & the Claude Developer Platform10 min

Claude Token Pricing Will Finally Make Sense After This

Python11 min

Why Python Web Scraping Returns Nothing (and the Fix)

Python10 min

Python uv vs pip: Which One Should You Use?

Python22 min

Python Automation: Full Tutorial for Beginners

Auth (OAuth, JWT)9 min

How OAuth Login Works

Gemini API & Google AI Studio10 min

Google AI Studio Will Finally Make Sense After This

Docker10 min

What Happens When You Run a Docker Container

Claude & the Claude Developer Platform8 min

How to Fix Claude Code Billing Your API Key Instead of Your Plan

Auth (OAuth, JWT)9 min

Why Passkeys Can't Be Phished Like Passwords

PowerShell10 min

PowerShell vs Bash: Which One Should You Learn?

Linux10 min

The Linux Roadmap for Beginners

Django (Python web framework)9 min

Django vs FastAPI: Which One Should You Use?

Databases & SQL10 min

Why SQLite Is the Most Used Database in the World

Databases & SQL11 min

How a NoSQL Database Like MongoDB Works

Python11 min

How to Fix Python pip error: externally-managed-environment

Databases & SQL9 min

How to Fix MySQL Too Many Connections Database Errors

Go (Golang)9 min

Golang Concurrency Will Finally Make Sense After This

AWS10 min

How Amazon S3 Works

Kubernetes10 min

How Kubernetes Works

Git10 min

How Git Works

Docker10 min

How to Fix Docker Port Already Allocated Errors

PostgreSQL10 min

How PostgreSQL Works

Git8 min

How to Fix Git Merge Conflicts

Linux9 min

How to Fix No Space Left on Device in Linux

Networking10 min

How the Internet Works

Databases & SQL9 min

SQL vs NoSQL: When Each One Wins

Auth (OAuth, JWT)8 min

How to Fix JWT Token Expired Errors (401 Unauthorized)

Claude & the Claude Developer Platform9 min

Claude API vs Claude Pro: Why Your Subscription Doesn't Include It

AI agent skills9 min

Why AI Agent Skills Beat Giant Prompts

Linux10 min

Linux Cron Jobs Will Finally Make Sense After This

Cybersecurity10 min

Security Logs Will Finally Make Sense After This

Cybersecurity8 min

How to Fix a Security Leak: An API Key Pushed to GitHub

DNS9 min

DNS Will Finally Make Sense After This

Cybersecurity9 min

Never Ignore a Security Update

Auth (OAuth, JWT)9 min

Two-Factor Authentication Will Finally Make Sense

Networking8 min

Never Expose a Database to the Internet

Cybersecurity10 min

SSH Keys vs Passwords: Which Is Safer for Security?

Linux10 min

The 5 Levels of Linux Boot: From Power On to Login

Networking12 min

The 5 Levels of Network Firewalls

Cybersecurity10 min

Password Security: Why Hackers Crack Weak Passwords

Linux9 min

Why Your Linux Server Runs Out of Memory

Networking10 min

HTTP vs HTTPS: What Actually Changes on Your Network

Linux12 min

Every Linux File Permission You Need to Know

Linux10 min

Linux Processes vs Threads: When Each One Wins

Networking11 min

The OSI Model Will Finally Make Sense After This

Git11 min

The 7 Levels of Git

Networking10 min

Every Network Troubleshooting Command You Need

Cybersecurity10 min

Hashing vs Encryption in Cybersecurity: When Each One Wins

Networking11 min

Why the Internet Ran Out of IP Addresses

Linux11 min

Linux Package Managers: apt vs Snap vs Flatpak

Python9 min

Why Python Is Slow (and When It Doesn't Matter)

Networking11 min

TCP vs UDP: When Each One Wins

Linux8 min

Stop Running Linux as Root (Do This Instead)

Linux11 min

Linux Users and Groups Will Finally Make Sense

Cybersecurity9 min

Why Phishing Beats Good Security (and How to Spot It)

Docker12 min

The 7 Levels of Docker

AI agents & MCP12 min

AI Agents Will Finally Make Sense After This

Git8 min

Stop Using git push --force (Use This Instead)

AI agent skills10 min

Agent Skills vs MCP: When Each One Wins

Networking12 min

Networking Will Finally Make Sense After This

Linux12 min

Every Linux Command You Actually Need

Cybersecurity12 min

The 7 Levels of Cybersecurity

System design12 min

Every System Design Concept You Need, in One Diagram

PostgreSQL10 min

Why PostgreSQL Ignores Your Index