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.
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:
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
EOFEach 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:
docker compose up -d --quiet-pull 2>&1 | grep -E ' Started$| Network .* Created$'
sleep 2
curl -s http://localhost:8080 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:
docker compose ps --format 'table {{.Service}}\t{{.Image}}\t{{.State}}\t{{.Ports}}'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/tcpServices can reach each other#
Compose puts all the services on one network, so that they find each other by service name:
docker compose exec web ping -c 1 cache | head -n 1
docker compose exec cache redis-cli pingPING cache (172.18.0.2): 56 data bytes
PONGEveryday commands#
Logs#
docker compose logs --no-log-prefix --tail 1 web 2>&1 | cut -c1-60172.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:
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:8081200Building your own image#
build tells Compose to build from a Dockerfile, instead of pulling an image:
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<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:
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 1 web
3 workerStop everything#
docker compose down > /dev/null 2>&1
docker compose ps -q | wc -l0Common 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
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-dataPONG
0Key takeaway#
Next: An App with a Database.
Finished this lesson?
Related lessons
Recommended Docker and Kubernetes resources
- Docker Deep Dive by Nigel Poulton
A practical guide to how Docker works, for readers who have finished the basics.
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.