Chapter 73: DevOps Projects Part 3: Docker Websites, APIs, Compose, Registry and ECR
73.7 Project 23: Three-tier app with Docker Compose (frontend, API, database)
Friends, real apps are more than one container. Today we write a Compose file with frontend, API and Postgres, put them on one network, keep DB data in a named volume, and prove all three tiers with curl and psql.
मित्रांनो, खरे apps एका container पेक्षा जास्त असतात. आज Compose: frontend, API, Postgres, एक network, named volume, curl आणि psql ने तीनही tiers सिद्ध.
दोस्तों, असली apps एक container से ज़्यादा होते हैं. आज Compose: frontend, API, Postgres, एक network, named volume, curl और psql से तीनों tiers सिद्ध.
What you will build in this project
- frontend, api and db services.
- Named volume pgdata.
- Network shopnet.
- curl + psql proof.
1. Start from a real company story
At Zomato in Gurugram, each engineer ran frontend, API and MySQL with three different scripts. One person used the wrong DB password and wiped a shared laptop database.
A demo for investors failed live. Rough cost: about ₹8 lakh in delayed diligence and overtime.
Sara moved the stack into one compose.yaml with a named volume and service DNS names.
Today we build that three-tier Compose lab.
Ravindra Bagale's Tip
Many students put passwords in the compose file and commit them. For labs use a throwaway password; for work use secrets or an untracked env file. Keep that habit.
खूप students passwords compose file मध्ये टाकून commit करतात. Lab मध्ये throwaway; कामात secrets किंवा untracked env file. सवय ठेवा.
बहुत students passwords compose file में डाल commit करते हैं. Lab में throwaway; काम में secrets या untracked env file. आदत रखिए.
2. Start and prepare
Step 1: Write frontend
Why: UI needs a static root. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: index.html. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 1: fe
Type this:
mkdir -p /tmp/tier/frontend && printf '%s\n' '<!doctype html><title>shop</title><h1>shop ui</h1>' > /tmp/tier/frontend/index.html && cat /tmp/tier/frontend/index.html
You will see:
<!doctype html><title>shop</title><h1>shop ui</h1>
What it means: UI.
Example 2: df fe
Type this:
printf '%s\n' 'FROM nginx:1.27-alpine' 'COPY index.html /usr/share/nginx/html/index.html' > /tmp/tier/frontend/Dockerfile && cat /tmp/tier/frontend/Dockerfile
You will see:
FROM nginx:1.27-alpine
COPY ...
What it means: FE image.
Example 3: ls
Type this:
ls /tmp/tier/frontend
You will see:
Dockerfile
index.html
What it means: Ready.
Step 2: Write API stub
Why: API is the middle tier. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: tiny API container. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 4: api
Type this:
mkdir -p /tmp/tier/api && printf '%s\n' 'from http.server import BaseHTTPRequestHandler,HTTPServer' 'class H(BaseHTTPRequestHandler):' ' def do_GET(self):' ' self.send_response(200); self.end_headers(); self.wfile.write(b"{\"ok\":true}")' 'HTTPServer(("0.0.0.0",3000),H).serve_forever()' > /tmp/tier/api/app.py && wc -l /tmp/tier/api/app.py
You will see:
6 /tmp/tier/api/app.py
What it means: Tiny API.
Example 5: df api
Type this:
printf '%s\n' 'FROM python:3.12-alpine' 'WORKDIR /app' 'COPY app.py .' 'CMD ["python","app.py"]' > /tmp/tier/api/Dockerfile && head -2 /tmp/tier/api/Dockerfile
You will see:
FROM python:3.12-alpine
WORKDIR /app
What it means: API image.
Example 6: ls
Type this:
ls /tmp/tier/api
You will see:
Dockerfile
app.py
What it means: Ready.
Step 3: Write compose file
Why: Compose describes the whole stack. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: services: keys. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 7: compose
Type this:
printf '%s\n' 'services:' ' frontend:' ' build: ./frontend' ' ports: ["8080:80"]' ' depends_on: [api]' ' api:' ' build: ./api' ' ports: ["3000:3000"]' ' depends_on: [db]' ' environment:' ' DATABASE_URL: postgres://shop:shop@db:5432/shop' ' db:' ' image: postgres:16-alpine' ' environment:' ' POSTGRES_USER: shop' ' POSTGRES_PASSWORD: shop' ' POSTGRES_DB: shop' ' volumes: ["pgdata:/var/lib/postgresql/data"]' 'volumes:' ' pgdata:' 'networks:' ' default:' ' name: shopnet' > /tmp/tier/compose.yaml && wc -l /tmp/tier/compose.yaml
You will see:
24 /tmp/tier/compose.yaml
What it means: Compose written.
Example 8: grep services
Type this:
grep -E 'frontend|api|db:' /tmp/tier/compose.yaml | head -5
You will see:
frontend:/api:/db:
What it means: Three services.
Example 9: mode
Type this:
stat -c '%a' /tmp/tier/compose.yaml
You will see:
644
What it means: Safe.
3. Build the main pieces
Step 4: Add postgres service
Why: Database must persist. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: image postgres. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 10: db image
Type this:
grep postgres /tmp/tier/compose.yaml | head -1
You will see:
image: postgres:16-alpine
What it means: Pinned DB.
Example 11: env user
Type this:
grep POSTGRES_USER /tmp/tier/compose.yaml
You will see:
POSTGRES_USER: shop
What it means: User.
Example 12: no secret file
Type this:
echo 'Lab password is shop; real life uses secrets or env files not committed'
You will see:
Lab password is shop; real life uses secrets or env files not committed
What it means: Habit.
Step 5: Add volumes
Why: Volumes keep data. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: volumes: block. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 13: vol
Type this:
grep pgdata /tmp/tier/compose.yaml
You will see:
volumes: ["pgdata:..."] / volumes: / pgdata:
What it means: Named volume.
Example 14: why vol
Type this:
echo 'Without a volume, DB data dies when the container is removed'
You will see:
Without a volume, DB data dies when the container is removed
What it means: Reason.
Example 15: ls vol later
Type this:
echo 'docker volume ls | grep pgdata'
You will see:
docker volume ls | grep pgdata
What it means: Check later.
Step 6: Add network
Why: Network lets services talk by name. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: networks: block. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 16: net
Type this:
grep shopnet /tmp/tier/compose.yaml
You will see:
name: shopnet
What it means: Custom name.
Example 17: dns tip
Type this:
echo 'api reaches db by hostname db on the compose network'
You will see:
api reaches db by hostname db on the compose network
What it means: DNS.
Example 18: depends
Type this:
grep depends_on /tmp/tier/compose.yaml | head -2
You will see:
depends_on: [api]/depends_on: [db]
What it means: Order hint.
4. Automate and verify
Step 7: docker compose up
Why: up starts all. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: compose up -d. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 19: up
Type this:
cd /tmp/tier && docker compose -f compose.yaml up -d --build
You will see:
... Started ...
What it means: Stack up.
Example 20: ps
Type this:
cd /tmp/tier && docker compose -f compose.yaml ps
You will see:
NAME ... STATUS ... PORTS
What it means: Three rows.
Example 21: vol ls
Type this:
docker volume ls | grep pgdata || echo 'volume_pending'
You will see:
...pgdata
What it means: Volume.
Step 8: Check ps
Why: ps shows health. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: compose ps. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 22: ps fmt
Type this:
cd /tmp/tier && docker compose -f compose.yaml ps --format 'table {{.Name}} {{.Status}}' | head -5
You will see:
NAME STATUS
What it means: Statuses.
Example 23: count
Type this:
cd /tmp/tier && docker compose -f compose.yaml ps -q | wc -l
You will see:
3
What it means: Three containers.
Example 24: net ls
Type this:
docker network ls | grep shopnet
You will see:
shopnet
What it means: Network.
Step 9: curl frontend
Why: Browser path via curl. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: curl :8080. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 25: fe curl
Type this:
curl -s http://127.0.0.1:8080/
You will see:
<!doctype html><title>shop</title><h1>shop ui</h1>
What it means: UI.
Example 26: code
Type this:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/
You will see:
200
What it means: OK.
Example 27: title
Type this:
curl -s http://127.0.0.1:8080/ | grep -o shop
You will see:
shop
What it means: Body.
5. Harden and accept
Step 10: curl API
Why: API path. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: curl :3000. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 28: api curl
Type this:
curl -s http://127.0.0.1:3000/
You will see:
{"ok":true}
What it means: API.
Example 29: api code
Type this:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/
You will see:
200
What it means: OK.
Example 30: env
Type this:
cd /tmp/tier && docker compose -f compose.yaml exec -T api printenv DATABASE_URL
You will see:
postgres://shop:shop@db:5432/shop
What it means: API sees DB URL.
Step 11: DB exec check
Why: psql or exec proves DB. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: exec psql. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 31: db
Type this:
cd /tmp/tier && docker compose -f compose.yaml exec -T db psql -U shop -d shop -c 'select 1 as ok;'
You will see:
ok
--
1
What it means: DB answers.
Example 32: tables
Type this:
cd /tmp/tier && docker compose -f compose.yaml exec -T db psql -U shop -d shop -c '\dt' || echo 'no_tables_yet'
You will see:
no_tables_yet
What it means: Empty schema OK.
Example 33: logs db
Type this:
cd /tmp/tier && docker compose -f compose.yaml logs db --tail 3
You will see:
database system is ready
What it means: Ready.
Step 12: Acceptance checklist
Why: Sara signs off when all three tiers answer. We do this before the next step so mistakes stay small and easy to undo. Read the output carefully; the numbers and names tell you if the stack is healthy today.
What: All green. Follow the numbered examples below. Type each command, read the exact output, and match the meaning line before you move on. That is how real teams learn Compose and registries.
Example 34: down note
Type this:
echo 'docker compose -f compose.yaml down keeps the named volume by default'
You will see:
docker compose -f compose.yaml down keeps the named volume by default
What it means: Persist tip.
Example 35: all 200
Type this:
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/; echo; curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/
You will see:
200
200
What it means: Both.
Example 36: Sign-off
Type this:
echo 'Sara: PASS'
You will see:
Sara: PASS
What it means: Done.
Check yourself
Q1. Who fixed Zomato demos?
Sara
Q2. DB image?
postgres:16-alpine
Q3. UI host port?
8080
Q4. API host port?
3000
Q5. Volume name?
pgdata
Q6. Compose command to start?
docker compose up -d
Friends, you ran a three-tier Compose stack with a named volume. Did you understand? Let us try it once more. Add a second API replica and put a simple load balance in front.
मित्रांनो, तुम्ही named volume सह three-tier Compose चालवला. दुसरी API replica आणि साधा load balance लावा.
दोस्तों, आपने named volume के साथ three-tier Compose चलाया. दूसरी API replica और सादा load balance लगाएँ.
Lab: try at home
- Write frontend and API Dockerfiles.
- Write compose.yaml with db volume.
- compose up --build.
- curl UI and API.
- psql select 1.
Recap
- Compose lists services.
- depends_on is order not readiness.
- Named volumes keep DB data.
- Service names are DNS names.
- Never commit real passwords.