6-bo'lim mashqlari — Node.js + MySQL + Redis

~45 daq · Docker · Docker Compose

Vazifa

To'liq ishlab chiqish muhitini Compose bilan quramiz. 18-darsdagi kitoblar API ni kengaytiramiz:

  1. Kitoblar MySQL da saqlansin; jadval va test ma'lumotlari init skript bilan
  2. GET /kitoblar natijasi Redis da 30 soniya keshlansin
  3. POST /kitoblar — yangi kitob qo'shish, keshni tozalash
  4. API baza sog'lom bo'lgandan keyin ishga tushsin
  5. Ishlab chiqishda — hot reload (Compose Watch)
  6. Adminer — tools profilida
  7. Parollar — .env da, Git dan tashqarida
  8. Production uchun alohida override

Avval o'zingiz yozing. Quyida — yechim.


Tuzilma

kitoblar-api/
├── compose.yaml
├── compose.override.yaml
├── compose.prod.yaml
├── .env.example
├── .env                   ← Git da emas
├── .dockerignore
├── Dockerfile
├── db/init/
│   └── 01-kitoblar.sql
├── package.json
└── src/
    └── index.ts
npm install express mysql2 redis
npm install -D typescript tsx @types/express @types/node

db/init/01-kitoblar.sql
CREATE TABLE kitoblar (
    id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    nom        VARCHAR(200) NOT NULL,
    muallif    VARCHAR(100) NOT NULL,
    yil        SMALLINT UNSIGNED NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) DEFAULT CHARSET = utf8mb4;

INSERT INTO kitoblar (nom, muallif, yil) VALUES
    ('O''tkan kunlar', 'Abdulla Qodiriy', 1926),
    ('Kecha va kunduz', 'Cho''lpon', 1936),
    ('Sariq devni minib', 'Xudoyberdi To''xtaboyev', 1968);
src/index.ts
import express from 'express';
import mysql from 'mysql2/promise';
import { createClient } from 'redis';

const app = express();
app.use(express.json());

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  connectionLimit: 10,
});

const redis = createClient({ url: process.env.REDIS_URL });
redis.on('error', (e) => console.error('Redis:', e.message));
await redis.connect();

const KESH_KALIT = 'kitoblar:hammasi';

app.get('/health', async (_req, res) => {
  try {
    await pool.query('SELECT 1');
    await redis.ping();
    res.json({ holat: 'ok' });
  } catch {
    res.status(503).json({ holat: 'xato' });
  }
});

app.get('/kitoblar', async (_req, res) => {
  const kesh = await redis.get(KESH_KALIT);
  if (kesh) return res.set('X-Kesh', 'HIT').json(JSON.parse(kesh));

  const [rows] = await pool.query('SELECT id, nom, muallif, yil FROM kitoblar ORDER BY id');
  await redis.set(KESH_KALIT, JSON.stringify(rows), { EX: 30 });
  res.set('X-Kesh', 'MISS').json(rows);
});

app.post('/kitoblar', async (req, res) => {
  const { nom, muallif, yil } = req.body ?? {};
  if (!nom || !muallif) return res.status(422).json({ xato: 'nom va muallif kerak' });

  const [r] = await pool.execute(
    'INSERT INTO kitoblar (nom, muallif, yil) VALUES (?, ?, ?)',
    [nom, muallif, yil ?? null]
  );
  await redis.del(KESH_KALIT);
  res.status(201).json({ id: (r as mysql.ResultSetHeader).insertId });
});

const PORT = Number(process.env.PORT ?? 3000);
const server = app.listen(PORT, '0.0.0.0', () => console.log(`API ${PORT}-portda`));

process.on('SIGTERM', async () => {
  server.close();
  await pool.end();
  await redis.quit();
  process.exit(0);
});

package.json da "type": "module" va tsconfig.json da "module": "nodenext" (top-level await uchun), skriptlar: "dev": "tsx watch src/index.ts", "build": "tsc", "start": "node dist/index.js".

Dockerfile
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./

FROM base AS dev
RUN npm ci
COPY . .
CMD ["npm", "run", "dev"]

FROM base AS build
RUN npm ci
COPY . .
RUN npm run build

FROM base AS production
ENV NODE_ENV=production
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
compose.yaml — umumiy asos
name: kitoblar

services:
  api:
    build:
      context: .
      target: production
    restart: unless-stopped
    environment:
      DB_HOST: baza
      DB_NAME: kitoblar
      DB_USER: app
      DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD kerak}
      REDIS_URL: redis://kesh:6379
    depends_on:
      baza:
        condition: service_healthy
      kesh:
        condition: service_healthy

  baza:
    image: mysql:8.4
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?DB_ROOT_PASSWORD kerak}
      MYSQL_DATABASE: kitoblar
      MYSQL_USER: app
      MYSQL_PASSWORD: ${DB_PASSWORD}
      TZ: Asia/Tashkent
    volumes:
      - baza-data:/var/lib/mysql
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 30s

  kesh:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      retries: 10

  adminer:
    image: adminer
    ports:
      - "127.0.0.1:8080:8080"
    profiles: [tools]

volumes:
  baza-data:
  redis-data:
compose.override.yaml — ishlab chiqish
services:
  api:
    build:
      target: dev
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: development
    develop:
      watch:
        - action: sync
          path: ./src
          target: /app/src
        - action: rebuild
          path: package.json

  baza:
    ports:
      - "127.0.0.1:3306:3306"
compose.prod.yaml va .env.example
# compose.prod.yaml
services:
  api:
    ports:
      - "127.0.0.1:3000:3000"
    deploy:
      resources:
        limits:
          memory: 512M
    logging:
      driver: json-file
      options: { max-size: "10m", max-file: "3" }
# .env.example
DB_ROOT_PASSWORD=
DB_PASSWORD=

Sinov

cp .env.example .env               # parollarni to'ldiring
docker compose watch               # yoki: docker compose up --watch

Boshqa terminalda:

curl -i localhost:3000/kitoblar    # X-Kesh: MISS
curl -i localhost:3000/kitoblar    # X-Kesh: HIT

curl -X POST localhost:3000/kitoblar -H "Content-Type: application/json" \
  -d '{"nom":"Ufq","muallif":"Said Ahmad","yil":1976}'

curl -i localhost:3000/kitoblar    # X-Kesh: MISS — kesh tozalangan, yangi kitob bor

Tekshiring:

  • src/index.ts ni o'zgartiring — API rebuild siz qayta yuklanadi
  • docker compose down → up — kitoblar saqlangan (volume)
  • docker compose --profile tools up -d → http://localhost:8080 — Adminer (server: baza)
  • docker compose ps — hammasi (healthy)
  • .env dan DB_PASSWORD ni olib tashlang — docker compose up aniq xato bilan to'xtaydi
  • Production konfiguratsiya: docker compose -f compose.yaml -f compose.prod.yaml config — dev sozlamalari yo'q

Qo'shimcha vazifalar

  1. ⭐⭐ migrate xizmati — service_completed_successfully bilan (28-dars)
  2. ⭐⭐ worker xizmati — xuddi shu image, boshqa command (27-dars)
  3. ⭐⭐⭐ Tarmoqlarni ajrating: baza va kesh — internal: true tarmoqda

O'zingizni tekshiring

  • Oddiy depends_on nima uchun yetmaydi?
  • .env va env_file farqi?
  • compose.override.yaml qachon o'qiladi, qachon yo'q?
  • docker compose down va down -v farqi?
  • Compose Watch ning bind mount dan afzalligi?
  • profiles nima uchun kerak?

Keyingi bo'lim

7-bo'lim: Real loyihalar — Node.js/NestJS, PHP/Laravel, Python, frontend va reverse proxy.