Yakuniy loyiha — to'liq stack ni konteynerlash va deploy

~90 daq · Docker · Keyingi qadam

Muammodan boshlaylik

Siz 44 dars davomida alohida qismlarni o'rgandingiz: image, Dockerfile, volume, tarmoq, Compose, registry, xavfsizlik, CI/CD, deploy, monitoring. Haqiqiy ishda ular birga ishlaydi. Bu yakuniy loyiha — hammasini bitta tizimga yig'ish.

Vazifa: "Dokon" onlayn do'koni uchun to'liq stek:

  • Frontend — React (Vite), Nginx orqali
  • API — Node.js (Express yoki NestJS)
  • Baza — MySQL
  • Kesh — Redis
  • Navbat ishlovchisi — buyurtma emaillarini yuboradi
  • Reverse proxy — Caddy, HTTPS bilan
  • CI/CD — GitHub Actions → GHCR → VPS

Har bir bosqichda — qaysi darsga tayanish kerakligi ko'rsatilgan. Avval o'zingiz yozing, keyin yechimni oching.


Loyiha tuzilishi

dokon/
├── frontend/
│   ├── Dockerfile
│   ├── nginx.conf
│   └── src/...
├── api/
│   ├── Dockerfile
│   ├── .dockerignore
│   └── src/...
├── deploy/
│   ├── compose.yaml          # serverga ko'chiriladi
│   └── Caddyfile
├── compose.yaml              # ishlab chiqish uchun asos
├── compose.override.yaml     # dev: watch, portlar
├── .env.example
└── .github/workflows/deploy.yml

Arxitektura:

              Internet
                 │ 80/443
            ┌────▼────┐
            │  caddy  │  ← HTTPS (Let's Encrypt)
            └─┬─────┬─┘
     /api/*   │     │   /*
        ┌─────▼─┐ ┌─▼──────┐
        │  api  │ │  web   │     tarmoq: frontend
        └─┬───┬─┘ └────────┘
──────────┼───┼─────────────────────────────────
        ┌─▼─┐ ┌▼─────┐  ┌────────┐  tarmoq: backend (internal)
        │ db│ │redis │◄─┤ worker │
        └───┘ └──────┘  └────────┘

1-bosqich: API Dockerfile

Darslar: 12–17, 33

Talablar:

  • Multi-stage: deps, dev, build, production
  • production da faqat production bog'liqliklar, root siz foydalanuvchi
  • Healthcheck
  • node to'g'ridan-to'g'ri (npm orqali emas)
  • BuildKit kesh mount
Yechim
# syntax=docker/dockerfile:1
FROM node:22-alpine AS base
WORKDIR /app

FROM base AS deps
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

FROM deps AS dev
COPY . .
CMD ["npm", "run", "dev"]

FROM deps AS build
COPY . .
RUN npm run build

FROM base AS production
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=15s --retries=3 \
  CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

.dockerignore:

node_modules
dist
.git
.env*
!.env.example
*.log
coverage

Worker — xuddi shu image, command: ["node", "dist/worker.js"].


2-bosqich: Frontend Dockerfile

Darslar: 16, 36

Talablar:

  • Node da build, Nginx da berish
  • SPA routing (try_files)
  • /assets/ uzoq keshlanadi, index.html — yo'q
  • API manzili — nisbiy /api (bitta image hamma muhitda)
Yechim
# syntax=docker/dockerfile:1
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm 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
EXPOSE 80

nginx.conf:

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

    location / {
        try_files $uri $uri/ /index.html;
    }
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
    location = /index.html {
        add_header Cache-Control "no-cache";
    }
    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}

Frontend kodida: fetch('/api/mahsulotlar').


3-bosqich: Ishlab chiqish muhiti

Darslar: 26–31

Talablar:

  • docker compose up --watch — hammasi ko'tariladi, kod o'zgarishi darhol ko'rinadi
  • Baza va Redis healthcheck bilan, API ular tayyor bo'lgandan keyin
  • Baza ma'lumotlari volume da
  • Adminer — faqat --profile tools bilan
Yechim

compose.yaml:

name: dokon

services:
  api:
    build:
      context: ./api
      target: production
    environment:
      DATABASE_URL: mysql://dokon:${DB_PASSWORD:?}@db:3306/dokon
      REDIS_URL: redis://redis:6379
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_healthy }
    networks: [frontend, backend]

  worker:
    build:
      context: ./api
      target: production
    command: ["node", "dist/worker.js"]
    environment:
      DATABASE_URL: mysql://dokon:${DB_PASSWORD}@db:3306/dokon
      REDIS_URL: redis://redis:6379
    depends_on:
      redis: { condition: service_healthy }
    networks: [backend]

  web:
    build: ./frontend
    networks: [frontend]

  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?}
      MYSQL_DATABASE: dokon
      MYSQL_USER: dokon
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
      interval: 5s
      retries: 20
    networks: [backend]

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
    networks: [backend]

  adminer:
    image: adminer
    profiles: [tools]
    ports: ["127.0.0.1:8081:8080"]
    networks: [backend]

networks:
  frontend:
  backend:

volumes:
  db-data:

compose.override.yaml (avtomatik qo'llanadi):

services:
  api:
    build:
      target: dev
    ports: ["3000:3000"]
    develop:
      watch:
        - action: sync
          path: ./api/src
          target: /app/src
        - action: rebuild
          path: ./api/package.json

  web:
    build:
      target: build
    command: npm run dev -- --host 0.0.0.0
    ports: ["5173:5173"]
    develop:
      watch:
        - action: sync
          path: ./frontend/src
          target: /app/src

  db:
    ports: ["127.0.0.1:3306:3306"]

Vite dev serverida /api ni API ga proxy qiling (vite.config.js → server.proxy: { '/api': 'http://api:3000' }).

cp .env.example .env
docker compose up --watch
docker compose --profile tools up -d adminer

Dev da backend internal emas — lokal portlar kerak. Production da (5-bosqich) — internal: true.


4-bosqich: CI/CD

Darslar: 38, 39, 41

Talablar:

  • PR — testlar
  • main — testlar → ikki image (api, web) GHCR ga → Trivy → deploy
  • Teglar: sha-... va latest
Yechim
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: ./api
          target: dev
          load: true
          tags: dokon-api:test
          cache-from: type=gha,scope=api
          cache-to: type=gha,mode=max,scope=api
      - run: docker run --rm dokon-api:test npm test

  build:
    needs: test
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    strategy:
      matrix:
        include:
          - name: api
            context: ./api
            target: production
          - name: web
            context: ./frontend
            target: ""
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: ${{ matrix.context }}
          target: ${{ matrix.target }}
          push: true
          tags: |
            ghcr.io/${{ github.repository }}-${{ matrix.name }}:sha-${{ github.sha }}
            ghcr.io/${{ github.repository }}-${{ matrix.name }}:latest
          cache-from: type=gha,scope=${{ matrix.name }}
          cache-to: type=gha,mode=max,scope=${{ matrix.name }}
      - uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: ghcr.io/${{ github.repository }}-${{ matrix.name }}:sha-${{ github.sha }}
          severity: CRITICAL
          exit-code: "1"
          ignore-unfixed: true

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: deploy
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /opt/dokon
            export TAG=sha-${{ github.sha }}
            echo "TAG=$TAG" > .tag
            docker compose --env-file .env --env-file .tag pull
            docker compose --env-file .env --env-file .tag up -d
            docker image prune -f
  • matrix — bitta job ta'rifi, ikki image
  • scope — har image uchun alohida kesh
  • .tag fayl — joriy versiya serverda saqlanadi (keyingi up -d larda ham, rollback uchun ham)

5-bosqich: Production server

Darslar: 25, 37, 39, 40, 42

Talablar:

  • Faqat Caddy tashqariga ochiq; backend tarmoq internal
  • Barcha xizmatlarda restart, resurs chegaralari
  • API: read_only, cap_drop, no-new-privileges
  • Sirlar — .env (chmod 600)
  • Log hajmi cheklangan
Yechim

deploy/compose.yaml (serverda /opt/dokon/compose.yaml):

name: dokon

x-xavfsiz: &xavfsiz
  restart: unless-stopped
  read_only: true
  tmpfs: [/tmp]
  cap_drop: [ALL]
  security_opt: [no-new-privileges:true]
  logging:
    driver: local
    options: { max-size: "10m", max-file: "3" }

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports: ["80:80", "443:443", "443:443/udp"]
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    networks: [frontend]

  web:
    image: ghcr.io/firdavsdev07/dokon-web:${TAG:-latest}
    restart: unless-stopped
    deploy:
      resources:
        limits: { memory: 64M }
    networks: [frontend]

  api:
    <<: *xavfsiz
    image: ghcr.io/firdavsdev07/dokon-api:${TAG:-latest}
    environment:
      DATABASE_URL: mysql://dokon:${DB_PASSWORD:?}@db:3306/dokon
      REDIS_URL: redis://redis:6379
      NODE_OPTIONS: --max-old-space-size=384
    deploy:
      resources:
        limits: { memory: 512M, cpus: "1.0" }
    depends_on:
      db: { condition: service_healthy }
      redis: { condition: service_healthy }
    networks: [frontend, backend]

  worker:
    <<: *xavfsiz
    image: ghcr.io/firdavsdev07/dokon-api:${TAG:-latest}
    command: ["node", "dist/worker.js"]
    environment:
      DATABASE_URL: mysql://dokon:${DB_PASSWORD}@db:3306/dokon
      REDIS_URL: redis://redis:6379
      SMTP_URL: ${SMTP_URL:?}
    deploy:
      resources:
        limits: { memory: 256M }
    networks: [backend, egress]

  db:
    image: mysql:8.4
    restart: unless-stopped
    command: --innodb-buffer-pool-size=512M
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?}
      MYSQL_DATABASE: dokon
      MYSQL_USER: dokon
      MYSQL_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    deploy:
      resources:
        limits: { memory: 1G }
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "--silent"]
      interval: 5s
      retries: 20
    networks: [backend]

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --maxmemory 100mb --maxmemory-policy allkeys-lru
    deploy:
      resources:
        limits: { memory: 128M }
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
    networks: [backend]

networks:
  frontend:
  backend:
    internal: true
  egress:                 # worker email yuborishi uchun internetga chiqish

volumes:
  caddy-data:
  caddy-config:
  db-data:
  • x-xavfsiz + <<: *xavfsiz — YAML anchor: umumiy sozlamalar bir marta yoziladi (31-dars)
  • backend: internal — baza va Redis internetga umuman chiqa olmaydi
  • worker email yuborishi uchun — alohida egress tarmog'i (internal tarmoq tashqariga chiqishni ham yopadi)
  • api frontend tarmog'i orqali Caddy bilan, backend orqali baza bilan

deploy/Caddyfile:

dokon.uz {
    encode gzip zstd
    handle /api/* {
        reverse_proxy api:3000
    }
    handle {
        reverse_proxy web:80
    }
}

www.dokon.uz {
    redir https://dokon.uz{uri} permanent
}

/api/* uchun handle (handle_path emas) — prefiks saqlanadi, chunki API marshrutlari /api/... bilan boshlanadi.


6-bosqich: Zaxira va monitoring

Darslar: 21, 42, 43

Talablar:

  • Har kecha baza zaxirasi, 14 kun saqlash, boshqa joyga nusxa
  • Tashqi uptime monitoring + Telegram ogohlantirish
  • Tiklash sinovi
Yechim

/opt/dokon/backup.sh:

#!/bin/sh
set -e
cd /opt/dokon
SANA=$(date +%F_%H%M)
FAYL=backups/dokon_$SANA.sql.gz

docker compose exec -T db sh -c \
  'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines dokon' \
  | gzip > "$FAYL"

# Bo'sh yoki buzilgan zaxirani aniqlash
gzip -t "$FAYL"
[ "$(stat -c%s "$FAYL")" -gt 1000 ] || { echo "Zaxira juda kichik!"; exit 1; }

rclone copy "$FAYL" b2:dokon-backups/
find backups -name "*.sql.gz" -mtime +14 -delete
# crontab -e
0 3 * * * /opt/dokon/backup.sh >> /var/log/dokon-backup.log 2>&1

Tiklash sinovi (oyiga bir marta, alohida konteynerda):

docker run -d --name tiklash-sinov -e MYSQL_ROOT_PASSWORD=sinov mysql:8.4
sleep 30
gunzip -c backups/dokon_2026-11-06_0300.sql.gz \
  | docker exec -i tiklash-sinov sh -c 'mysql -u root -psinov -e "CREATE DATABASE dokon" && mysql -u root -psinov dokon'
docker exec tiklash-sinov mysql -u root -psinov -e "SELECT COUNT(*) FROM dokon.buyurtmalar"
docker rm -f tiklash-sinov

Monitoring: UptimeRobot / Better Stack (yoki boshqa serverdagi Uptime Kuma) da https://dokon.uz/api/health ni har daqiqada tekshirish + Telegram bildirishnomasi.


Yakuniy tekshiruv

Ishlab chiqish
[ ] git clone → cp .env.example .env → docker compose up --watch → ishlaydi
[ ] Kod o'zgarishi bir necha soniyada ko'rinadi

Image lar
[ ] API image < 200 MB, web image < 60 MB
[ ] Root siz (docker run --rm ... whoami)
[ ] docker history da sirlar yo'q
[ ] Trivy: CRITICAL yo'q

CI/CD
[ ] PR → faqat testlar
[ ] main → GHCR da sha-... teglari → serverda yangi versiya
[ ] Rollback: oldingi TAG bilan up -d — ishlaydi

Production
[ ] https://dokon.uz — yashil qulf, http → https
[ ] Sahifani /mahsulot/42 da yangilash — 404 emas
[ ] Faqat 80/443 ochiq (tashqaridan nmap bilan tekshiring)
[ ] Server qayta yuklandi (reboot) → hammasi o'zi ko'tarildi
[ ] docker stop api → 1-2 soniyada to'xtaydi
[ ] Zaxira har kuni, boshqa joyda, tiklash sinalgan
[ ] Sayt o'chirilsa — Telegram ga xabar keladi

Nima uchun shunday?

Nima uchun bu loyihada shuncha qatlam — oddiy docker run lar bilan ham sayt ishlardi-ku?

Ishlardi — birinchi kun. Lekin production — bu birinchi kun emas, keyingi 1000 kun: yuzlab deploy, server qayta yuklanishlari, xotira oqishlari, xavfsizlik zaifliklari, yangi jamoa a'zolari, va bir kuni — disk buzilishi. Bu loyihadagi har bir qatlam ana shu kunlardan biriga javob: CI — "kim nimani deploy qildi", aniq teglar — "qanday qaytaramiz", chegaralar — "nega hammasi qulab tushdi", internal tarmoq — "baza qanday sizdi", zaxira — "ma'lumotlar qayerda", monitoring — "nega mijozdan bildik". Docker bularning hammasini kod sifatida yozish imkonini beradi — takrorlanadigan, ko'rib chiqiladigan va ishonchli.


Xulosa

Siz Docker kursini tugatdingiz! Endi siz:

  • Image va konteyner qanday ishlashini, qatlamlar va keshni tushunasiz
  • Har qanday til uchun samarali, xavfsiz, multi-stage Dockerfile yoza olasiz
  • Volume, bind mount va tarmoqlar bilan ma'lumotlar va xizmatlarni boshqarasiz
  • Compose bilan ko'p xizmatli loyihani bitta buyruq bilan ishga tushirasiz
  • Node.js, PHP, Python va frontend loyihalarini konteynerlashtirasiz
  • Registry, CI/CD va VPS orqali production ga chiqarasiz
  • Xavfsizlik, resurslar, loglar va monitoringni sozlaysiz
  • Kubernetes ga o'tish uchun poydevorga egasiz

Keyingi qadam

  • Shu loyihani haqiqatan VPS ga chiqaring — eng yaxshi o'rganish usuli
  • O'z mavjud loyihalaringizdan birini konteynerlashtiring
  • Kubernetes: kind bilan lokal klaster, rasmiy "Kubernetes Basics" qo'llanmasi
  • Rasmiy hujjatlar: docs.docker.com

Omad! 🐳