Ravindra BagaleCourses & study guides मराठी

Chapter 53: Docker Compose Tutorial: Services, depends_on Healthchecks, Env Files, Profiles, Overrides, Scaling and Every Command

53.8 Scaling, watch mode and the full 3-tier stack

Friends, so far we started one container per service. Real traffic needs more than one API copy, and real developers do not want to rebuild after every small code change. Today we scale a service, put nginx in front to spread the load, use Compose Watch to sync code live, and finally write the complete 3-tier stack: nginx, API, Postgres and Redis, in one file.

1. Start from a story

At Swiggy, the menu API ran as one container on a test server. Every Friday evening, when the QA team ran a load test, the single API container hit 100 percent CPU and requests started timing out.

Priya, the developer, first tried a bigger server. It helped a little, but one Node process uses only one CPU core, so most of the new cores sat idle.

Then she ran four copies of the API with one command, docker compose up --scale api=4, and put nginx in front. Each copy got about a quarter of the traffic and the timeouts disappeared.

The same week, she got tired of rebuilding the image after every one-line change, so she added a develop.watch block. Now saving a file updates the running container in about a second. Today we do exactly what Priya did, step by step.

Browsersends request to port 80nginxsingle entry pointDocker DNS: apireturns every api container IPapi-1 ... api-4each takes a share of requestsPostgres + Redisshared data, one copy each
Picture 1: one entry, many API copies

2. Scaling a service

Step 1: What scaling means

Why: More copies of the same service.

What: Scaling starts several identical containers from one service definition. Compose names them project-service-1, project-service-2 and so on.

Example 1: Small file

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n' > compose.yaml && docker compose up -d

You will see:

✔ Container shop-api-1  Started

What it means: One copy, the normal start.

Example 2: Scale to three

Type this:

docker compose up -d --scale api=3

You will see:

✔ Container shop-api-1  Running
✔ Container shop-api-2  Started
✔ Container shop-api-3  Started

What it means: Two new copies were added; the first kept running.

Example 3: See them

Type this:

docker compose ps --format '{{.Name}}'

You will see:

shop-api-1
shop-api-2
shop-api-3

What it means: Three containers, numbered.

Step 2: Scale down

Why: Remove extra copies safely.

What: Running up with a smaller number stops and removes the highest-numbered containers first.

Example 4: Down to one

Type this:

docker compose up -d --scale api=1

You will see:

✔ Container shop-api-3  Removed
✔ Container shop-api-2  Removed

What it means: Extra copies removed.

Example 5: Check

Type this:

docker compose ps -q api | wc -l

You will see:

1

What it means: Only one is left.

Example 6: Zero

Type this:

docker compose up -d --scale api=0 && docker compose ps -q api | wc -l

You will see:

0

What it means: Zero copies is allowed: the service is defined but not running.

Step 3: The scale command

Why: Change copies without up.

What: docker compose scale changes the count of running services directly.

Example 7: Scale command

Type this:

docker compose scale api=4

You will see:

✔ Container shop-api-4  Started

What it means: Same result as up --scale.

Example 8: Count

Type this:

docker compose ps api --format '{{.Name}}' | wc -l

You will see:

4

What it means: Four copies.

Example 9: In the file

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    deploy:\n      replicas: 2\n' > compose.yaml && docker compose up -d && docker compose ps -q | wc -l

You will see:

2

What it means: deploy.replicas sets the default count, so every up starts two.

Step 4: Port clash when scaling

Why: Only one container can own a host port.

What: If a service publishes a fixed host port like 3000:3000, the second copy fails because the port is already taken.

Example 10: Fixed port

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    ports: ["3000:80"]\n' > compose.yaml && docker compose up -d --scale api=2 2>&1 | tail -1

You will see:

Error: Bind for 0.0.0.0:3000 failed: port is already allocated

What it means: Second copy cannot use port 3000.

Example 11: Port range

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    ports: ["3000-3003:80"]\n' > compose.yaml && docker compose up -d --scale api=3 && docker compose ps --format '{{.Ports}}'

You will see:

0.0.0.0:3000->80/tcp
0.0.0.0:3001->80/tcp
0.0.0.0:3002->80/tcp

What it means: Each copy gets a port from the range.

Example 12: Better: no ports

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    expose: ["80"]\n' > compose.yaml && docker compose up -d --scale api=3 && echo no-clash

You will see:

no-clash

What it means: Do not publish api at all; put nginx in front. That is the next step.

Step 5: container_name blocks scaling

Why: Names must be unique.

What: If a service has container_name set, Compose cannot create a second container with the same name.

Example 13: Fixed name

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    container_name: api\n' > compose.yaml && docker compose up -d --scale api=2 2>&1 | tail -1

You will see:

WARNING: The "api" service is using the custom container name "api". Docker requires each container to have a unique name.

What it means: Compose refuses to scale.

Example 14: Remove it

Type this:

sed -i '/container_name/d' compose.yaml && docker compose up -d --scale api=2 && echo ok

You will see:

ok

What it means: Default names are unique.

Example 15: Talk by service name

Type this:

docker compose exec --index 2 api hostname

You will see:

3c1f9a2b7d10

What it means: Use --index to pick one copy; other services just use the name api.

3. Load balancing with nginx

Step 6: Docker DNS returns every copy

Why: The service name points to all containers.

What: Inside the project network, the name api resolves to the IP of every api container. Clients that look up the name again get spread across copies.

Example 16: Scale first

Type this:

docker compose up -d --scale api=3 && echo up

You will see:

up

What it means: Three copies.

Example 17: Look up

Type this:

docker run --rm --network shop_default busybox nslookup api | grep Address | tail -3

You will see:

Address: 172.20.0.3
Address: 172.20.0.4
Address: 172.20.0.5

What it means: One name, three IPs.

Example 18: Ask repeatedly

Type this:

for i in 1 2 3 4; do docker run --rm --network shop_default curlimages/curl:8.10.1 -s api | grep 'Server name'; done

You will see:

Server name: 3c1f9a2b7d10
Server name: 9ab02e11c4f5
Server name: 3c1f9a2b7d10
Server name: 77d1e0a9b2c3

What it means: Fresh containers each pick an address, so requests spread.

Step 7: nginx as the front door

Why: One public port, many backends.

What: nginx listens on port 80 and proxies to http://api. A resolver line makes nginx look up the name again, so new copies are found.

Example 19: Config

Type this:

printf 'server {\n  listen 80;\n  resolver 127.0.0.11 valid=10s;\n  set $up http://api:80;\n  location / { proxy_pass $up; }\n}\n' > nginx.conf && echo written

You will see:

written

What it means: 127.0.0.11 is Docker's built-in DNS.

Example 20: Compose

Type this:

printf 'services:\n  api:\n    image: nginxdemos/hello:plain-text\n    deploy: {replicas: 3}\n  web:\n    image: nginx:1.27-alpine\n    ports: ["8080:80"]\n    volumes: ["./nginx.conf:/etc/nginx/conf.d/default.conf:ro"]\n    depends_on: [api]\n' > compose.yaml && docker compose up -d && echo started

You will see:

started

What it means: Only web publishes a port.

Example 21: Test spread

Type this:

for i in 1 2 3 4 5 6; do curl -s localhost:8080 | grep 'Server name'; done | sort | uniq -c

You will see:

2 Server name: 3c1f9a2b7d10
2 Server name: 77d1e0a9b2c3
2 Server name: 9ab02e11c4f5

What it means: Traffic is shared by all three.

Step 8: Adding copies while running

Why: Grow without downtime.

What: Because nginx re-resolves every 10 seconds, scaling up adds new backends without restarting nginx.

Example 22: Grow

Type this:

docker compose up -d --scale api=5 --no-recreate

You will see:

✔ Container shop-api-4  Started
✔ Container shop-api-5  Started

What it means: --no-recreate leaves the old ones alone.

Example 23: Wait and test

Type this:

sleep 11 && for i in $(seq 10); do curl -s localhost:8080 | grep 'Server name'; done | sort -u | wc -l

You will see:

5

What it means: All five now get traffic.

Example 24: Shrink

Type this:

docker compose up -d --scale api=2 --no-recreate && sleep 11 && curl -s -o /dev/null -w '%{http_code}\n' localhost:8080

You will see:

200

What it means: Still serving after scale-down.

4. Compose Watch: live code sync

You save a filesrc/app.js changeddocker compose watchnotices the changeaction: synccopies file into containeraction: rebuildfor package.json changesBrowser refreshnew code is running
Picture 2: what watch does on save

Step 9: The develop.watch block

Why: Tell Compose what to watch.

What: Each rule has a path on your laptop, an action and, for sync, a target inside the container.

Example 25: Rules

Type this:

printf 'services:\n  api:\n    build: .\n    ports: ["3000:3000"]\n    develop:\n      watch:\n        - action: sync\n          path: ./src\n          target: /app/src\n        - action: rebuild\n          path: package.json\n' > compose.yaml && docker compose config -q && echo valid

You will see:

valid

What it means: Two rules: sync code, rebuild on dependency change.

Example 26: Start watch

Type this:

docker compose watch

You will see:

[+] Running 1/1
 ✔ Container shop-api-1  Started
Watch enabled

What it means: Builds, starts and keeps watching.

Example 27: Save a file

Type this:

printf 'console.log("v2")\n' >> src/app.js

You will see:

Syncing "api" after changes were detected

What it means: The file was copied into the running container.

Step 10: sync, sync+restart and rebuild

Why: Three actions for three cases.

What: sync copies files only, good for hot-reload tools. sync+restart copies and restarts the container, good for config files. rebuild builds a new image and replaces the container.

Example 28: sync+restart

Type this:

printf '        - action: sync+restart\n          path: ./nginx.conf\n          target: /etc/nginx/conf.d/default.conf\n' >> compose.yaml && echo added

You will see:

added

What it means: Config changes need a process restart.

Example 29: Rebuild trigger

Type this:

printf '{"name":"api","dependencies":{"express":"4.21.1"}}\n' > package.json

You will see:

Rebuilding service(s) ["api"] after changes were detected

What it means: New dependencies need a new image.

Example 30: Ignore files

Type this:

printf '          ignore:\n            - node_modules/\n            - "*.test.js"\n'

You will see:

ignore:
  - node_modules/
  - "*.test.js"

What it means: Skip folders you do not want synced.

Step 11: Watch with up

Why: One command for dev.

What: Modern Compose lets you pass --watch to up, so logs and watching share one terminal.

Example 31: up --watch

Type this:

docker compose up --watch

You will see:

api-1  | listening on 3000
⦿ Watch enabled

What it means: Logs plus watch.

Example 32: Only one service

Type this:

docker compose watch api

You will see:

Watch enabled

What it means: Watch just the API.

Example 33: Stop watching

Type this:

printf 'Press Ctrl+C in the watch terminal, then run: docker compose down\n'

You will see:

Press Ctrl+C in the watch terminal, then run: docker compose down

What it means: Watch stops; containers keep running until down.

5. The full 3-tier stack

Step 12: The complete compose.yaml

Why: Everything together.

What: nginx at the front, three API copies, Postgres with a healthcheck and volume, Redis for cache, and two networks so the database is never reachable from the front.

Example 34: Write it

Type this:

printf 'services:\n  web:\n    image: nginx:1.27-alpine\n    ports: ["80:80"]\n    volumes: ["./nginx.conf:/etc/nginx/conf.d/default.conf:ro"]\n    networks: [front]\n    depends_on: [api]\n  api:\n    build: ./api\n    deploy: {replicas: 3}\n    environment:\n      DATABASE_URL: postgres://shop:example-only@db:5432/shop\n      REDIS_URL: redis://cache:6379\n    networks: [front, back]\n    depends_on:\n      db: {condition: service_healthy}\n      cache: {condition: service_started}\n  db:\n    image: postgres:16-alpine\n    environment: {POSTGRES_USER: shop, POSTGRES_PASSWORD: example-only, POSTGRES_DB: shop}\n    volumes: ["dbdata:/var/lib/postgresql/data"]\n    healthcheck: {test: ["CMD-SHELL","pg_isready -U shop"], interval: 5s, retries: 10}\n    networks: [back]\n  cache:\n    image: redis:7-alpine\n    networks: [back]\nnetworks: {front: {}, back: {}}\nvolumes: {dbdata: {}}\n' > compose.yaml && docker compose config --services

You will see:

web
api
db
cache

What it means: Four services, two networks, one volume. The password is an example only.

Example 35: Bring it up

Type this:

docker compose up -d --wait

You will see:

✔ Container shop-db-1     Healthy
✔ Container shop-cache-1  Started
✔ Container shop-api-1    Started
✔ Container shop-api-2    Started
✔ Container shop-api-3    Started
✔ Container shop-web-1    Started

What it means: --wait returns only when everything is healthy or running.

Example 36: Test

Type this:

curl -s localhost/health

You will see:

{"status":"ok","db":"up","cache":"up"}

What it means: The request went nginx, then api, then db and cache.

Step 13: Check the network split

Why: The front cannot see the database.

What: web is only on front, db is only on back, and api sits on both as the bridge.

Example 37: From web

Type this:

docker compose exec web ping -c1 -W1 db 2>&1 | tail -1

You will see:

ping: bad address 'db'

What it means: nginx cannot even resolve db.

Example 38: From api

Type this:

docker compose exec --index 1 api sh -c 'nc -z db 5432 && echo reachable'

You will see:

reachable

What it means: API reaches the database.

Example 39: Inspect

Type this:

docker network inspect shop_back --format '{{range .Containers}}{{.Name}} {{end}}'

You will see:

shop-db-1 shop-cache-1 shop-api-1 shop-api-2 shop-api-3

What it means: Only back-end members.

Step 14: Day-two commands for the stack

Why: Run it like a pro.

What: Logs, stats, scaling under load and a clean shutdown that keeps the data.

Example 40: Follow API logs

Type this:

docker compose logs -f --tail 5 api

You will see:

api-2  | GET /health 200 3ms
api-1  | GET /menu 200 12ms

What it means: Prefix shows which copy answered.

Example 41: Live usage

Type this:

docker compose stats --no-stream --format '{{.Name}} {{.CPUPerc}}'

You will see:

shop-api-1 18.2%
shop-api-2 17.9%
shop-api-3 18.5%

What it means: Load is even.

Example 42: Stop, keep data

Type this:

docker compose down && docker volume ls --format '{{.Name}}' | grep dbdata

You will see:

shop_dbdata

What it means: down keeps named volumes; only down -v deletes them.

6. Common mistakes and how Priya avoids them

Mistake one: publishing a fixed port on a scaled service. Priya publishes only nginx and uses expose for the API.

Mistake two: container_name on a service she wants to scale. She removes it and lets Compose number the containers.

Mistake three: storing state inside API containers, such as uploaded files or sessions in memory. With four copies, a user lands on a different copy each time. Priya keeps sessions in Redis and files in a volume or S3.

Mistake four: nginx without a resolver line. nginx then remembers old IPs forever and never sees new copies.

Mistake five: using watch in production. Watch is a developer tool; production uses fixed image tags.

Check yourself

Q1. How do you run four API copies?

docker compose up -d --scale api=4, or deploy.replicas: 4.

Q2. Why does a fixed host port break scaling?

Only one container can bind a host port.

Q3. What does action: sync do?

Copies changed files into the running container.

Q4. Why is db only on the back network?

So the public-facing nginx can never reach it.

Friends, scale with one flag, balance with nginx, develop with watch, and keep the database behind a private network. Did you understand? Let us try it once more. Build the 3-tier stack, scale the API to five and watch the requests spread.

Ravindra Bagale's Tip

Before a load test, scale the API and check docker compose stats. If one copy is much busier than the others, nginx is not re-resolving: add the resolver line.

Lab: try at home

  1. Scale a service to three and back to one.
  2. Break scaling with a fixed port, then fix it with expose.
  3. Put nginx in front and count which copy answers.
  4. Add a develop.watch block and edit a file.
  5. Write the full 3-tier compose.yaml and bring it up with --wait.
  6. Prove nginx cannot reach the database.

Recap

  • --scale and deploy.replicas run many copies.
  • Fixed host ports and container_name block scaling.
  • Docker DNS returns every copy; nginx spreads the load.
  • Compose Watch syncs or rebuilds on save.
  • The 3-tier stack uses front and back networks.
  • down keeps volumes; down -v deletes them.

Ravi Sir's Cards for this lesson

मराठी cards