Ravindra BagaleCourses & study guides मराठी

Chapter 57: Docker Projects Part 1: Websites, Databases, LAMP, LEMP, WordPress, 3-Tier and Every Language with MySQL and MongoDB

57.16 Project 16: 3-tier app: React frontend, Node.js API and PostgreSQL

Why: the problem

Suppose we build the events app for a college fest in Pune, like a small BookMyShow: students see events and register for them. Most real web apps have three tiers: a frontend in the browser (React), a backend API (Node.js), and a database (PostgreSQL). Without Docker the frontend developer runs Node 22, the backend developer runs Node 18, Postgres is installed differently on each laptop, and the server needs all of them again. React also has a trap: in development it runs on a Node dev server, but in production it is only static HTML, JS and CSS files that any web server can serve. With Docker we build React in a multi-stage image (Node builds, nginx serves), run the API in its own small image, and keep Postgres in a container with a volume. nginx also proxies /api to the backend, so the browser talks to one address and we avoid CORS problems. This is the same shape companies like Swiggy and Zomato use, at a much bigger scale.

What: every term in plain English

  • 3-tier — Presentation (React), logic (API) and data (database), each in its own layer and container.
  • React — A JavaScript library for building user interfaces from components.
  • Vite — A fast build tool for React. npm run build makes the static files in dist/.
  • Multi-stage build — Stage 1 uses Node to build; stage 2 copies only dist/ into nginx. Node never ships.
  • Reverse proxy /api — nginx sends /api/... requests to the api container, so the browser uses one origin.
  • CORS — A browser rule that blocks calls to another origin. Same-origin through nginx avoids it.
  • SPA fallback — try_files ... /index.html so React routes like /events/3 work on refresh.
  • pg — The Node.js PostgreSQL client library.
Write the Node API with Postgres. Build the API image. Write the React app. Build React in multi-stage. Compose all three tiers. Develop with hot reload. Clean up.
React in nginx, Node API and Postgres on two networks

How: step by step

  1. Write the Node API with Postgres. Express + pg, health check.
  2. Build the API image. npm ci, non-root, HEALTHCHECK.
  3. Write the React app. Vite, fetch /api/events.
  4. Build React in multi-stage. Node build, nginx serve.
  5. Compose all three tiers. Networks: front and back.
  6. Develop with hot reload. compose.dev.yaml.
  7. Clean up. down -v.

Architecture

Browser :8080 web: nginx + React dist (fest-web:1.0, front network) /api/ proxied to api:3000 api: Node 22 + Express (front + back networks) db: PostgreSQL 16 (back network, pg-data volume)
3-tier app architecture

Folder layout

three-tier/
  compose.yaml
  compose.dev.yaml
  .env
  db/init.sql
  api/
    Dockerfile
    package.json
    server.js
  web/
    Dockerfile
    .dockerignore
    nginx.conf
    index.html
    package.json
    vite.config.js
    src/main.jsx

Example 1: Tier 2 and 3: the Node API and PostgreSQL

Goal. Tier 2 and 3: the Node API and PostgreSQL.

api/package.json

{
  "name": "fest-api",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "start": "node server.js" },
  "dependencies": { "express": "^4.21.1", "pg": "^8.13.0" }
}

api/server.js

import express from 'express';
import pg from 'pg';

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
const app = express();
app.use(express.json());

app.get('/api/health', async (req, res) => {
  try { await pool.query('SELECT 1'); res.json({ status: 'ok' }); }
  catch (e) { res.status(503).json({ status: 'db down' }); }
});

app.get('/api/events', async (req, res) => {
  const { rows } = await pool.query('SELECT id, title, venue, seats_left FROM events ORDER BY id');
  res.json(rows);
});

app.post('/api/events/:id/register', async (req, res) => {
  const { name } = req.body || {};
  if (!name) return res.status(400).json({ error: 'name is required' });
  const r = await pool.query(
    'UPDATE events SET seats_left = seats_left - 1 WHERE id = $1 AND seats_left > 0 RETURNING seats_left',
    [req.params.id]
  );
  if (r.rowCount === 0) return res.status(409).json({ error: 'event full or not found' });
  await pool.query('INSERT INTO registrations (event_id, name) VALUES ($1, $2)', [req.params.id, name]);
  res.status(201).json({ ok: true, seats_left: r.rows[0].seats_left });
});

const server = app.listen(3000, () => console.log('api on 3000'));
process.on('SIGTERM', () => server.close(() => pool.end()));

api/Dockerfile

FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.js .
USER node
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \
  CMD wget -qO- http://localhost:3000/api/health || exit 1
CMD ["node", "server.js"]

db/init.sql

CREATE TABLE events (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  venue TEXT NOT NULL,
  seats_left INT NOT NULL CHECK (seats_left >= 0)
);
CREATE TABLE registrations (
  id SERIAL PRIMARY KEY,
  event_id INT REFERENCES events(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO events (title, venue, seats_left) VALUES
  ('Robotics Workshop', 'Lab 2', 40),
  ('Battle of Bands', 'Main Ground', 500),
  ('Hackathon', 'Library Hall', 2);

What each line does

  • pg.Pool max: 10 — Keeps up to 10 database connections open and reuses them.
  • $1, $2 — Postgres placeholders; values are sent apart from the SQL, which stops SQL injection.
  • WHERE seats_left > 0 RETURNING — Takes one seat in one atomic SQL step, so two students can never get the last seat at the same time.
  • 409 — Conflict: the event is full.
  • SIGTERM handler — docker stop sends SIGTERM; we finish requests and close the pool cleanly.
  • npm ci --omit=dev — Installs exact versions from package-lock.json, without dev tools.
  • USER node — The official Node image has a non-root user called node.
  • CHECK (seats_left >= 0) — The database itself refuses negative seats.

Green output

$ docker run --rm -v "$PWD/api":/app -w /app node:22-alpine npm install
added 78 packages, and audited 79 packages in 4s
$ docker build -t fest-api:1.0 api
 => naming to docker.io/library/fest-api:1.0
$ docker images fest-api
REPOSITORY   TAG   SIZE
fest-api     1.0   161MB

Expected result. package-lock.json is created by a throwaway Node container, then the API image is built from it.

Example 2: Tier 1: the React app built with Vite

Goal. Tier 1: the React app built with Vite.

web/package.json

{
  "name": "fest-web",
  "version": "1.0.0",
  "type": "module",
  "scripts": { "dev": "vite --host 0.0.0.0", "build": "vite build" },
  "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1" },
  "devDependencies": { "vite": "^5.4.10", "@vitejs/plugin-react": "^4.3.3" }
}

web/vite.config.js

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: { proxy: { '/api': process.env.API_URL || 'http://localhost:3000' } },
});

web/index.html

<!doctype html>
<html lang="en">
  <head><meta charset="UTF-8" /><title>Pune College Fest</title></head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

web/src/main.jsx

import React, { useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';

function App() {
  const [events, setEvents] = useState([]);
  const [msg, setMsg] = useState('');

  const load = () => fetch('/api/events').then(r => r.json()).then(setEvents);
  useEffect(() => { load(); }, []);

  async function register(id) {
    const r = await fetch(`/api/events/${id}/register`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: 'Student' }),
    });
    const body = await r.json();
    setMsg(r.ok ? `Registered. Seats left: ${body.seats_left}` : body.error);
    load();
  }

  return (
    <main>
      <h1>Pune College Fest</h1>
      <p>{msg}</p>
      <ul>
        {events.map(e => (
          <li key={e.id}>
            {e.title} at {e.venue} ({e.seats_left} seats)
            <button disabled={e.seats_left === 0} onClick={() => register(e.id)}>Register</button>
          </li>
        ))}
      </ul>
    </main>
  );
}

createRoot(document.getElementById('root')).render(<App />);

What each line does

  • vite build — Makes dist/ with index.html and hashed JS and CSS files.
  • server.proxy '/api' — In development, Vite forwards /api to the API, the same way nginx does in production.
  • fetch('/api/events') — A relative URL: the browser calls the same origin, so there is no CORS and no hard-coded host.
  • useState / useEffect — React hooks: keep the events list and load it once when the page opens.
  • disabled={e.seats_left === 0} — The button turns off when an event is full.
  • createRoot(...).render — Puts the App component into the div with id root.

Green output

$ docker run --rm -v "$PWD/web":/app -w /app node:22-alpine sh -c 'npm install && npm run build'
vite v5.4.10 building for production...
dist/index.html                  0.38 kB
dist/assets/index-DkT3a9Qe.js  143.21 kB | gzip: 46.10 kB
built in 1.62s

Expected result. Only static files come out of the build. That is all nginx needs.

Example 3: Multi-stage React image with nginx, and Compose for all three tiers

Goal. Multi-stage React image with nginx, and Compose for all three tiers.

web/Dockerfile

FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
HEALTHCHECK --interval=10s --timeout=3s CMD wget -qO- http://localhost/ > /dev/null || exit 1

web/nginx.conf

server {
    listen 80;
    root /usr/share/nginx/html;

    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location /api/ {
        proxy_pass http://api:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location / {
        try_files $uri $uri/ /index.html;
    }
}

web/.dockerignore

node_modules
dist

compose.yaml

services:
  web:
    build: ./web
    image: fest-web:1.0
    ports: ["8080:80"]
    networks: [front]
    depends_on:
      api: { condition: service_healthy }
    restart: unless-stopped
  api:
    build: ./api
    image: fest-api:1.0
    environment:
      DATABASE_URL: postgres://fest:${DB_PASS}@db:5432/fest
    networks: [front, back]
    depends_on:
      db: { condition: service_healthy }
    restart: unless-stopped
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: fest
      POSTGRES_PASSWORD: ${DB_PASS}
      POSTGRES_DB: fest
    volumes:
      - pg-data:/var/lib/postgresql/data
      - ./db/init.sql:/docker-entrypoint-initdb.d/01.sql:ro
    networks: [back]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U fest -d fest"]
      interval: 5s
      retries: 20
    restart: unless-stopped
networks:
  front:
  back:
volumes:
  pg-data:

.env

DB_PASS=change-me-db

What each line does

  • FROM node AS build ... FROM nginx — The final image is nginx plus about 150 KB of files. Node, npm and node_modules stay in the build stage.
  • COPY --from=build /app/dist — Takes only the built files from stage 1.
  • expires 1y, immutable — Vite file names have a hash, so they can be cached for a year; a new build makes new names.
  • proxy_pass http://api:3000 — No path after the port, so /api/events goes to the API as /api/events.
  • try_files ... /index.html — React handles its own routes; refresh on /events/3 still loads the app.
  • networks front and back — web reaches api, api reaches db, but web can never reach db directly.
  • Only web has ports — The API and Postgres are private. All traffic comes through nginx on 8080.

Green output

$ docker compose up -d --build --wait
 Container three-tier-db-1   Healthy
 Container three-tier-api-1  Healthy
 Container three-tier-web-1  Healthy
$ docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep fest
fest-web:1.0 48.7MB
fest-api:1.0 161MB
$ curl -s localhost:8080/api/events
[{"id":1,"title":"Robotics Workshop","venue":"Lab 2","seats_left":40},{"id":2,"title":"Battle of Bands","venue":"Main Ground","seats_left":500},{"id":3,"title":"Hackathon","venue":"Library Hall","seats_left":2}]
$ docker compose exec web wget -qO- -T 2 db:5432 || echo "web cannot reach db"
wget: bad address 'db:5432'
web cannot reach db

Expected result. The full app runs on one port; the React image is under 50 MB, and the network split keeps the database away from the web tier.

Example 4: Development mode with hot reload, and the last-seat test

Goal. Development mode with hot reload, and the last-seat test.

compose.dev.yaml

services:
  web:
    build: !reset null
    image: node:22-alpine
    working_dir: /app
    command: sh -c "npm install && npm run dev"
    environment:
      API_URL: http://api:3000
    volumes:
      - ./web:/app
      - web-modules:/app/node_modules
    ports: ["5173:5173"]
  api:
    build: !reset null
    image: node:22-alpine
    working_dir: /app
    command: sh -c "npm install && node --watch server.js"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"]
      interval: 5s
      start_period: 30s
    volumes:
      - ./api:/app
      - api-modules:/app/node_modules
volumes:
  web-modules:
  api-modules:

race test

for i in 1 2 3; do
  curl -s -X POST localhost:8080/api/events/3/register -H 'Content-Type: application/json' -d '{"name":"S'$i'"}' &
done
wait
docker compose exec db psql -U fest -d fest -c 'SELECT seats_left FROM events WHERE id = 3'

What each line does

  • compose.dev.yaml — Overrides web and api with plain Node images and our code mounted live.
  • npm run dev — Vite dev server on 5173 with hot reload: save a .jsx file and the browser updates.
  • node --watch — Node 22 restarts the API when server.js changes.
  • web-modules volume — node_modules stays inside a volume, so Linux packages do not clash with a Mac or Windows host.
  • API_URL — Tells the Vite proxy to send /api to the api service.
  • Three requests at once (&) — Hackathon has 2 seats; the atomic UPDATE lets exactly two win.

Green output

$ docker compose -f compose.yaml -f compose.dev.yaml up -d
# Browser: http://localhost:5173  -> edit web/src/main.jsx title, page updates in under 1 s
$ docker compose -f compose.yaml -f compose.dev.yaml down
$ docker compose up -d --wait
$ (race test)
{"ok":true,"seats_left":1}
{"ok":true,"seats_left":0}
{"error":"event full or not found"}
 seats_left
------------
          0

Expected result. Hot reload for daily coding, and proof that the last seat is never sold twice.

Every command, step by step (plain docker and Compose)

  1. Make folders

    mkdir -p three-tier/api three-tier/web/src three-tier/db && cd three-tier
    

    What it does: One folder per tier.

    (no output)
    
  2. Add files

    find . -type f -not -path '*/node_modules/*' | sort
    

    What it does: Copy every file from the examples.

    ./.env
    ./api/Dockerfile
    ./api/package.json
    ./api/server.js
    ./compose.yaml
    ./db/init.sql
    ./web/Dockerfile
    ./web/index.html
    ./web/nginx.conf
    ./web/package.json
    ./web/src/main.jsx
    ./web/vite.config.js
    
  3. Make lock files

    docker run --rm -v "$PWD/api":/app -w /app node:22-alpine npm install
    docker run --rm -v "$PWD/web":/app -w /app node:22-alpine npm install
    

    What it does: npm ci in the Dockerfiles needs package-lock.json.

    added 78 packages
    
  4. Build and start

    docker compose up -d --build --wait
    

    What it does: Builds fest-web and fest-api, then starts all three.

    Container three-tier-web-1 Healthy
    
  5. Open the app

    echo http://localhost:8080
    

    What it does: The React page lists three events.

    http://localhost:8080
    
  6. Call the API through nginx

    curl -s localhost:8080/api/events
    

    What it does: Same origin as the page.

    [{"id":1,"title":"Robotics Workshop",...}]
    
  7. Register

    curl -s -X POST localhost:8080/api/events/1/register -H 'Content-Type: application/json' -d '{"name":"Asha"}'
    

    What it does: Takes one seat.

    {"ok":true,"seats_left":39}
    
  8. SPA refresh

    curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/events/3
    

    What it does: try_files returns index.html, not 404.

    200
    
  9. Check private tiers

    docker compose port api 3000 || echo private
    

    What it does: The API has no host port.

    private
    
  10. Logs per tier

    docker compose logs api --tail 3
    

    What it does: Each tier logs on its own.

    api-1  | api on 3000
    
  11. Data survives

    docker compose down && docker compose up -d --wait && curl -s localhost:8080/api/events | grep -o '"seats_left":39'
    

    What it does: pg-data keeps registrations.

    "seats_left":39
    
  12. Clean up

    docker compose down -v
    docker image rm fest-web:1.0 fest-api:1.0
    

    What it does: Removes containers, volume and images.

    Volume three-tier_pg-data Removed
    

When it is red 1

502 Bad Gateway (on /api/events)

Cause. nginx cannot reach api:3000: the API crashed or is not on the front network.

Fix, step by step

  1. docker compose logs api.
  2. Make sure api is in networks: [front, back].

When it is red 2

Access to fetch at 'http://localhost:3000/api/events' from origin 'http://localhost:8080' has been blocked by CORS policy

Cause. The React code calls the API on another port.

Fix, step by step

  1. Use a relative URL: fetch('/api/events').
  2. Let nginx proxy /api, so both share one origin.

When it is red 3

404 Not Found (refresh on /events/3)

Cause. nginx looked for a file called events/3.

Fix, step by step

  1. Add try_files $uri $uri/ /index.html; in location /.

When it is red 4

npm ERR! The `npm ci` command can only install with an existing package-lock.json

Cause. package-lock.json was never created.

Fix, step by step

  1. Run npm install once in a node container to make it.
  2. Commit package-lock.json with the code.

When it is red 5

Error: connect ECONNREFUSED 172.20.0.2:5432

Cause. The API started before Postgres was ready.

Fix, step by step

  1. Keep depends_on db: condition: service_healthy.
  2. pg.Pool also retries on the next request.

Practice tasks

Easy Task 1: API

Why: Tier 2.

What to do: Build fest-api:1.0 and run it with only Postgres.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose up -d db api
docker compose exec api wget -qO- localhost:3000/api/health

Easy Task 2: Build

Why: Tier 1.

What to do: Build the React dist folder in a node container.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker run --rm -v "$PWD/web":/app -w /app node:22-alpine sh -c 'npm install && npm run build'

Easy Task 3: Size

Why: Multi-stage.

What to do: Compare fest-web:1.0 with node:22-alpine size.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker images | grep -E 'fest-web|node'

Medium Task 4: Proxy

Why: Same origin.

What to do: Call /api/events through port 8080 only.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
curl -s localhost:8080/api/events

Medium Task 5: Networks

Why: Isolation.

What to do: Prove web cannot resolve db but api can.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose exec web nslookup db || echo blocked
docker compose exec api nslookup db

Medium Task 6: Race

Why: Atomic SQL.

What to do: Send 5 registrations at once to the 2-seat Hackathon.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
for i in 1 2 3 4 5; do curl -s -X POST localhost:8080/api/events/3/register -H 'Content-Type: application/json' -d '{"name":"x"}' & done; wait

Medium Task 7: Dev

Why: Hot reload.

What to do: Run compose.dev.yaml and change the page title.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose -f compose.yaml -f compose.dev.yaml up -d

Hard Task 8: Cache

Why: Headers.

What to do: Show Cache-Control on an assets file and none on index.html.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
curl -sI localhost:8080/ | grep -i cache
curl -sI localhost:8080/assets/$(docker compose exec web ls /usr/share/nginx/html/assets | head -1) | grep -i cache

Hard Task 9: Scale

Why: API.

What to do: Scale the API to 3 and reload nginx.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose up -d --scale api=3
docker compose exec web nginx -s reload

Easy Task 10: Clean

Why: Habit.

What to do: Remove everything.

How:
  1. Use a VM or laptop with Docker and Compose (2 GB RAM is enough).
  2. Create the three-tier folder with the files from this lesson.
  3. Run the steps and compare with the solution.
Solution
docker compose down -v
docker image rm fest-web:1.0 fest-api:1.0

Homework: try at home

Add a login page to the React app and a POST /api/login in the API that returns a JWT (secret from .env), protect the register endpoint with it, and add a Redis container to rate-limit registrations to 5 per minute per user.

Recap

  • Three tiers, three containers: web, api, db.
  • React is built by Node and served by nginx in a multi-stage image.
  • nginx proxies /api so there is one origin and no CORS.
  • Two networks keep the database away from the web tier.
  • Atomic SQL stops double booking.
  • A dev override gives hot reload with the same Compose project.

FAQ

Q. Can I use Next.js instead of React with Vite?

Yes, but Next.js with server rendering needs a Node container at run time, not only nginx.

Q. Where should the API URL be set for React?

Use relative /api paths; values baked at build time cannot change per server.

Q. Can I put the React files on S3 and CloudFront?

Yes. That is common in production; the API stays in containers. See Project 58.13.

Q. Why is fest-api bigger than fest-web?

It has Node and node_modules; fest-web is only nginx and static files.

Q. Should Postgres run in a container in production?

Many teams use Amazon RDS instead. Project 58.1 shows the same API with RDS.

Q. How do I add HTTPS?

Put Traefik or certbot in front of the web container, as in Projects 4 and 58.7.

Interview questions

  1. Describe a 3-tier architecture in Docker.

    Model answer: A web tier (nginx with built frontend), an app tier (API container) and a data tier (database with a volume), on separate networks with only the web tier published.

  2. How do you containerise a React app for production?

    Model answer: Multi-stage: build with Node, copy dist into nginx, add SPA fallback and cache headers.

  3. How do you avoid CORS between frontend and API?

    Model answer: Serve both from one origin with a reverse proxy, or set exact CORS headers on the API.

  4. How do you stop two users booking the last seat?

    Model answer: Use one atomic UPDATE with a condition, or a transaction with row locks.

  5. How do you run the same stack for development and production?

    Model answer: One base compose.yaml plus an override file for dev with bind mounts and hot reload.

Quick quiz

Q1. What are the three tiers in this project?

React frontend (in nginx), Node.js API and PostgreSQL.

Q2. Why build React in a multi-stage Dockerfile?

Node is needed only to build; the final image is just nginx with static files.

Q3. Why does nginx proxy /api?

So the browser talks to one origin and CORS is not needed.

Q4. Why two networks, front and back?

So the web tier cannot reach the database directly.

Q5. What does try_files ... /index.html do?

It sends unknown paths to React so client routes work on refresh.

Samajla ka?

Lab

  1. Finish every project in this chapter on one Ubuntu VM or laptop.
  2. Keep each project in its own folder.
  3. Run docker compose down -v after each project.
  4. Write the commands you typed in a notebook.
  5. Break one thing on purpose in each project and fix it.

Practice task

Pick any two projects and combine them behind one nginx container on ports 80 and 8080.

Ravindra Bagale's Tip

Project kelyashivay Docker yet nahi. Roj ek project kara, aani interview madhe tyacha demo dakhva.

In short

  • Every project follows the same order: code, Dockerfile, compose, run, test, clean.
  • Data lives in named volumes.
  • Containers talk by service name on a user-defined network.
  • Secrets come from .env files that never go to Git.

Ravi Sir's Cards for this lesson

मराठी cards