Skip to content
Laviyx Learn

Try

Docker and Kubernetes course menu

Docker Compose Tutorial: Run Multi-Container Apps

Define and run multi-container apps with Docker Compose: write compose.yaml, use up, down, ps and logs, build images, publish ports, and scale services.

Intermediate8 min readUpdated
On this page

Real apps have several parts: a web server, an API, a database and a cache. Typing a long docker run for each one is slow and error-prone. Docker Compose describes all of them in one file, and starts them with one command.

compose.yaml#

Create a project with a web server and a Redis cache:

Terminal
mkdir demo
cd demo
mkdir html
echo "<h1>Hello from Compose</h1>" > html/index.html
cat > compose.yaml <<'EOF'
services:
  web:
    image: nginx:1.30-alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro

  cache:
    image: redis:8.6-alpine
EOF

Each entry under services is a container. YAML uses spaces to show structure, so keep the indentation exact.

Start it#

-d runs it in the background:

Terminal
docker compose up -d --quiet-pull 2>&1 | grep -E ' Started$| Network .* Created$'
sleep 2
curl -s http://localhost:8080
Output
 Network demo_default  Created
 Container demo-cache-1  Started
 Container demo-web-1  Started
<h1>Hello from Compose</h1>

Compose created a network and both containers, named after the folder. See them with:

Terminal
docker compose ps --format 'table {{.Service}}\t{{.Image}}\t{{.State}}\t{{.Ports}}'
Output
SERVICE   IMAGE               STATE     PORTS
cache     redis:8.6-alpine    running   6379/tcp
web       nginx:1.30-alpine   running   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Services can reach each other#

Compose puts all the services on one network, so that they find each other by service name:

Terminal
docker compose exec web ping -c 1 cache | head -n 1
docker compose exec cache redis-cli ping
Output
PING cache (172.18.0.2): 56 data bytes
PONG

Everyday commands#

CommandWhat it does
docker compose up -dCreate and start everything
docker compose up -d --buildRebuild images first
docker compose psList the services
docker compose logs -f webFollow a service's logs
docker compose exec web shOpen a shell in a service
docker compose restart webRestart a service
docker compose stopStop, and keep the containers
docker compose downStop and remove containers and networks
docker compose down -vAlso delete volumes

Logs#

Terminal
docker compose logs --no-log-prefix --tail 1 web 2>&1 | cut -c1-60
Output
172.18.0.1 - - [05/Nov/2026:07:16:47 +0000] "GET / HTTP/1.1"

Changing the file#

Edit compose.yaml and run up -d again, and Compose changes only what is different. Change the port:

Terminal
sed -i 's/8080:80/8081:80/' compose.yaml
docker compose up -d > /dev/null 2>&1
sleep 2
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081
Output
200

Building your own image#

build tells Compose to build from a Dockerfile, instead of pulling an image:

Terminal
cat > Dockerfile <<'EOF'
FROM nginx:1.30-alpine
COPY html /usr/share/nginx/html
EOF
cat > compose.yaml <<'EOF'
services:
  web:
    build: .
    ports:
      - "8080:80"
EOF
docker compose up -d --build --remove-orphans > /dev/null 2>&1
sleep 2
curl -s http://localhost:8080
Output
<h1>Hello from Compose</h1>

Scale a service#

Run several copies. A fixed host port can be used only once, so scaling is for services without one:

Terminal
cat >> compose.yaml <<'EOF'
  worker:
    image: redis:8.6-alpine
EOF
docker compose up -d --scale worker=3 --quiet-pull > /dev/null 2>&1
docker compose ps --format '{{.Service}}' | sort | uniq -c
Output
      1 web
      3 worker

Stop everything#

Terminal
docker compose down > /dev/null 2>&1
docker compose ps -q | wc -l
Output
0

Common mistakes#

Practice#

Practice exercise

Write a compose.yaml in a new folder with one service, db, using redis:8.6-alpine, with a named volume db-data at /data. Start it, check it with redis-cli ping, then remove it with its volume.

Show solution
Terminal
mkdir ~/redis-lab && cd ~/redis-lab
cat > compose.yaml <<'EOF'
services:
  db:
    image: redis:8.6-alpine
    volumes:
      - db-data:/data

volumes:
  db-data:
EOF
docker compose up -d --quiet-pull > /dev/null 2>&1
sleep 1
docker compose exec db redis-cli ping
docker compose down -v > /dev/null 2>&1
docker volume ls -q | grep -c db-data
Output
PONG
0

Key takeaway#

Next: An App with a Database.

Finished this lesson?

Some links are affiliate links. If you buy through them, Laviyx Learn may earn a commission at no extra cost to you. As an Amazon Associate, we earn from qualifying purchases.